mirror of
https://github.com/Azure/k8s-deploy.git
synced 2026-06-21 18:59:27 +08:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2de818915 | |||
| 2ee6236ebc | |||
| bba74ad3b5 | |||
| 4e60e959ea | |||
| 497ce6351c | |||
| 6ecb006985 | |||
| d7506e9702 | |||
| 4ebf668e6f | |||
| 15920eb094 | |||
| 507f2d4fc7 | |||
| 06a06b13b9 | |||
| fa093f2922 | |||
| aabcfcba3e | |||
| fd893fd074 | |||
| 659e414483 | |||
| 1e490c6238 | |||
| 75cb5d47f7 | |||
| bcdb90f36f | |||
| ee3c5aed75 | |||
| 961b316a51 | |||
| 4810ff9a3e | |||
| ca8d2604ac | |||
| 5cbd4acaca | |||
| 18ff12030c | |||
| 8898d95f4f | |||
| 33608d18f7 | |||
| acd12a4705 | |||
| 81557b8633 | |||
| e1b9842236 | |||
| 26cb2cdb5f | |||
| 5ebbfbbefe | |||
| ac49626466 | |||
| d332939666 | |||
| 2577009bcb | |||
| a58ad23e7f | |||
| ee4b5d33e0 | |||
| 625898f6eb | |||
| 8d257fed50 | |||
| 4f6b70e29a | |||
| 202bacc71b | |||
| 2c09684db9 | |||
| 282a81e1fc | |||
| ce7c8f066f | |||
| 25ded46b9d | |||
| 56e4abca5e | |||
| 4bd69f56a9 | |||
| 04921d7d06 | |||
| 51b95a5ca2 | |||
| 49257c6f33 | |||
| 895952654c | |||
| 0fd84a1b0d | |||
| d35174fe93 | |||
| f80ed6c460 | |||
| 72bc167726 | |||
| 21d3af2857 | |||
| a80355209a | |||
| 92589546e8 | |||
| b9146889f3 |
@@ -0,0 +1 @@
|
||||
* @Azure/aks-atlanta
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Bug Report
|
||||
description: File a bug report specifying all inputs you provided for the action, we will respond to this thread with any questions.
|
||||
title: 'Bug: '
|
||||
labels: ['bug', 'triage']
|
||||
assignees: '@Azure/aks-atlanta'
|
||||
body:
|
||||
- type: textarea
|
||||
id: What-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Tell us what happened and how is it different from the expected?
|
||||
placeholder: Tell us what you see!
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: Version
|
||||
attributes:
|
||||
label: Version
|
||||
options:
|
||||
- label: I am using the latest version
|
||||
required: true
|
||||
- type: input
|
||||
id: Runner
|
||||
attributes:
|
||||
label: Runner
|
||||
description: What runner are you using?
|
||||
placeholder: Mention the runner info (self-hosted, operating system)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: Logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Run in debug mode for the most verbose logs. Please feel free to attach a screenshot of the logs
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,6 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: GitHub Action "k8s-deploy" Support
|
||||
url: https://github.com/Azure/k8s-deploy
|
||||
security: https://github.com/Azure/k8s-deploy/blob/main/SECURITY.md
|
||||
about: Please ask and answer questions here.
|
||||
@@ -0,0 +1,13 @@
|
||||
name: Feature Request
|
||||
description: File a Feature Request form, we will respond to this thread with any questions.
|
||||
title: 'Feature Request: '
|
||||
labels: ['Feature']
|
||||
assignees: '@Azure/aks-atlanta'
|
||||
body:
|
||||
- type: textarea
|
||||
id: Feature_request
|
||||
attributes:
|
||||
label: Feature request
|
||||
description: Provide example functionality and links to relevant docs
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,32 +0,0 @@
|
||||
token=$1
|
||||
commit=$2
|
||||
repository=$3
|
||||
prNumber=$4
|
||||
frombranch=$5
|
||||
tobranch=$6
|
||||
|
||||
getPayLoad() {
|
||||
cat <<EOF
|
||||
{
|
||||
"event_type": "K8sDeployActionPR",
|
||||
"client_payload":
|
||||
{
|
||||
"action": "CreateSecret",
|
||||
"commit": "$commit",
|
||||
"repository": "$repository",
|
||||
"prNumber": "$prNumber",
|
||||
"tobranch": "$tobranch",
|
||||
"frombranch": "$frombranch"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
response=$(curl -X POST -H "Authorization: token $token" https://api.github.com/repos/Azure/azure-actions-integration-tests/dispatches --data "$(getPayLoad)")
|
||||
|
||||
if [ "$response" == "" ]; then
|
||||
echo "Integration tests triggered successfully"
|
||||
else
|
||||
echo "Triggering integration tests failed with: '$response'"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,52 +1,51 @@
|
||||
name: "Code scanning - action"
|
||||
name: 'Code scanning - action'
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 19 * * 0'
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 19 * * 0'
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
CodeQL-Build:
|
||||
# CodeQL runs on ubuntu-latest and windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# CodeQL runs on ubuntu-latest and windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
name: setting-default-labels
|
||||
|
||||
# Controls when the action will run.
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0/3 * * *'
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
name: Setting issue as idle
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is idle because it has been open for 14 days with no activity.'
|
||||
stale-issue-label: 'idle'
|
||||
days-before-stale: 14
|
||||
days-before-close: -1
|
||||
operations-per-run: 100
|
||||
exempt-issue-labels: 'backlog'
|
||||
|
||||
- uses: actions/stale@v3
|
||||
name: Setting PR as idle
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-pr-message: 'This PR is idle because it has been open for 14 days with no activity.'
|
||||
stale-pr-label: 'idle'
|
||||
days-before-stale: 14
|
||||
days-before-close: -1
|
||||
operations-per-run: 100
|
||||
@@ -1,23 +0,0 @@
|
||||
name: "Trigger Integration tests"
|
||||
on:
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
# - 'releases/*'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- 'releases/*'
|
||||
jobs:
|
||||
trigger-integration-tests:
|
||||
name: Trigger Integration tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: IntegrationTests
|
||||
|
||||
- name: Trigger Test run
|
||||
run: |
|
||||
bash ./IntegrationTests/.github/workflows/TriggerIntegrationTests.sh ${{ secrets.L2_REPO_TOKEN }} ${{ github.event.pull_request.head.sha }} ${{ github.repository }} ${{ github.event.pull_request.number }} ${{ github.event.pull_request.head.ref }} ${{ github.event.pull_request.base.ref }}
|
||||
@@ -0,0 +1,18 @@
|
||||
name: 'Run prettify'
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
prettier:
|
||||
name: Prettier Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Enforce Prettier
|
||||
uses: actionsx/prettier@v2
|
||||
with:
|
||||
args: --check .
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Create release PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
description: 'Define release version (ex: v1, v2, v3)'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
release-pr:
|
||||
uses: OliverMKing/javascript-release-workflow/.github/workflows/release-pr.yml@main
|
||||
with:
|
||||
release: ${{ github.event.inputs.release }}
|
||||
@@ -0,0 +1,215 @@
|
||||
name: Minikube Integration Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- 'releases/*'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- 'releases/*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
run-integration-test:
|
||||
name: Run Minikube Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
KUBECONFIG: /home/runner/.kube/config
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
rm -rf node_modules/
|
||||
npm install
|
||||
|
||||
- name: Install ncc
|
||||
run: npm i -g @vercel/ncc
|
||||
- name: Build
|
||||
run: ncc build src/run.ts -o lib
|
||||
|
||||
- name: Set name of ns
|
||||
run: echo "::set-output name=name::$(echo `date +%Y%m%d%H%M%S`)"
|
||||
shell: bash
|
||||
id: ns
|
||||
|
||||
- uses: Azure/setup-kubectl@v1
|
||||
name: Install Kubectl
|
||||
|
||||
- id: setup-minikube
|
||||
name: Setup Minikube
|
||||
uses: manusa/actions-setup-minikube@v2.4.2
|
||||
with:
|
||||
minikube version: 'v1.24.0'
|
||||
kubernetes version: 'v1.17.8'
|
||||
driver: 'none'
|
||||
timeout-minutes: 3
|
||||
|
||||
- name: Create namespace to run tests
|
||||
run: kubectl create ns test-${{ steps.ns.outputs.name }}
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
name: Install Python
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Cleaning any previously created items
|
||||
run: |
|
||||
python test/integration/k8s-deploy-delete.py 'Service' 'nginx-service' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-delete.py 'Service' 'nginx-service-green' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-delete.py 'Deployment' 'nginx-deployment-green' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-delete.py 'Deployment' 'nginx-deployment' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-delete.py 'Ingress' 'nginx-ingress' ${{ steps.ns.outputs.name }}
|
||||
|
||||
- name: Executing deploy action
|
||||
uses: ./
|
||||
with:
|
||||
namespace: test-${{ steps.ns.outputs.name }}
|
||||
images: nginx:1.14.2
|
||||
manifests: |
|
||||
test/integration/manifests/test-service.yml
|
||||
strategy: blue-green
|
||||
route-method: service
|
||||
action: deploy
|
||||
|
||||
- name: Checking if deploments and services were created with green labels
|
||||
run: |
|
||||
python test/integration/k8s-deploy-test.py 'Deployment' 'nginx-deployment-green' 'green' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Service' 'nginx-service' 'green' ${{ steps.ns.outputs.name }}
|
||||
|
||||
- name: Executing promote action
|
||||
uses: ./
|
||||
with:
|
||||
namespace: test-${{ steps.ns.outputs.name }}
|
||||
images: nginx:1.14.2
|
||||
manifests: |
|
||||
test/integration/manifests/test-service.yml
|
||||
strategy: blue-green
|
||||
route-method: service
|
||||
action: promote
|
||||
|
||||
- name: Checking if deploments and services were created with none labels after promote
|
||||
run: |
|
||||
python test/integration/k8s-deploy-test.py 'Deployment' 'nginx-deployment' 'None' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Service' 'nginx-service' 'None' ${{ steps.ns.outputs.name }}
|
||||
|
||||
- name: Executing deploy action on
|
||||
uses: ./
|
||||
with:
|
||||
namespace: test-${{ steps.ns.outputs.name }}
|
||||
images: nginx:1.19.1
|
||||
manifests: |
|
||||
test/integration/manifests/test-service.yml
|
||||
strategy: blue-green
|
||||
route-method: service
|
||||
action: deploy
|
||||
|
||||
- name: Checking if deploments and services were created with green labels, and old workloads persist on deploy
|
||||
run: |
|
||||
python test/integration/k8s-deploy-test.py 'Deployment' 'nginx-deployment-green' 'green' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Service' 'nginx-service' 'green' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Deployment' 'nginx-deployment' 'None' ${{ steps.ns.outputs.name }}
|
||||
|
||||
- name: Executing reject action
|
||||
uses: ./
|
||||
with:
|
||||
namespace: test-${{ steps.ns.outputs.name }}
|
||||
images: nginx:1.19.1
|
||||
manifests: |
|
||||
test/integration/manifests/test-service.yml
|
||||
strategy: blue-green
|
||||
route-method: service
|
||||
action: reject
|
||||
|
||||
- name: Checking if deploments and services were routed back to none labels after reject
|
||||
run: |
|
||||
python test/integration/k8s-deploy-test.py 'Deployment' 'nginx-deployment' 'None' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Service' 'nginx-service' 'None' ${{ steps.ns.outputs.name }}
|
||||
|
||||
- name: Cleaning up current set up
|
||||
run: |
|
||||
python test/integration/k8s-deploy-delete.py 'Service' 'nginx-service' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-delete.py 'Deployment' 'nginx-deployment' ${{ steps.ns.outputs.name }}
|
||||
|
||||
- name: Executing deploy action for ingress
|
||||
uses: ./
|
||||
with:
|
||||
namespace: test-${{ steps.ns.outputs.name }}
|
||||
images: nginx:1.14.2
|
||||
manifests: |
|
||||
test/integration/manifests/test-ingress.yml
|
||||
strategy: blue-green
|
||||
route-method: ingress
|
||||
action: deploy
|
||||
|
||||
- name: Checking if deploments, services and ingresses were created with green labels
|
||||
run: |
|
||||
python test/integration/k8s-deploy-test.py 'Deployment' 'nginx-deployment-green' 'green' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Service' 'nginx-service-green' 'green' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Ingress' 'nginx-ingress' 'green' ${{ steps.ns.outputs.name }}
|
||||
|
||||
- name: Executing promote action for ingress
|
||||
uses: ./
|
||||
with:
|
||||
namespace: test-${{ steps.ns.outputs.name }}
|
||||
images: nginx:1.14.2
|
||||
manifests: |
|
||||
test/integration/manifests/test-ingress.yml
|
||||
strategy: blue-green
|
||||
route-method: ingress
|
||||
action: promote
|
||||
|
||||
- name: Checking if deploments, services and ingresses were created with none labels after promote
|
||||
run: |
|
||||
python test/integration/k8s-deploy-test.py 'Deployment' 'nginx-deployment' 'None' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Service' 'nginx-service' 'None' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Ingress' 'nginx-ingress' 'None' ${{ steps.ns.outputs.name }}
|
||||
|
||||
- name: Executing deploy action for ingress
|
||||
uses: ./
|
||||
with:
|
||||
namespace: test-${{ steps.ns.outputs.name }}
|
||||
images: nginx:1.19.1
|
||||
manifests: |
|
||||
test/integration/manifests/test-ingress.yml
|
||||
strategy: blue-green
|
||||
route-method: ingress
|
||||
action: deploy
|
||||
|
||||
- name: Checking if deploments, services and ingresses were created with green labels after deploy, and old deployment persists
|
||||
run: |
|
||||
python test/integration/k8s-deploy-test.py 'Deployment' 'nginx-deployment-green' 'green' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Deployment' 'nginx-deployment' 'None' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Service' 'nginx-service' 'None' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Service' 'nginx-service-green' 'green' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Ingress' 'nginx-ingress' 'green' ${{ steps.ns.outputs.name }}
|
||||
|
||||
- name: Executing reject action for ingress
|
||||
uses: ./
|
||||
with:
|
||||
namespace: test-${{ steps.ns.outputs.name }}
|
||||
images: nginx:1.19.1
|
||||
manifests: |
|
||||
test/integration/manifests/test-ingress.yml
|
||||
strategy: blue-green
|
||||
route-method: ingress
|
||||
action: reject
|
||||
|
||||
- name: Checking if deploments, services and ingresses were created with none labels after reject
|
||||
run: |
|
||||
python test/integration/k8s-deploy-test.py 'Deployment' 'nginx-deployment' 'None' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Service' 'nginx-service' 'None' ${{ steps.ns.outputs.name }}
|
||||
python test/integration/k8s-deploy-test.py 'Ingress' 'nginx-ingress' 'None' ${{ steps.ns.outputs.name }}
|
||||
|
||||
- if: ${{ always() }}
|
||||
name: Delete created namespace
|
||||
run: kubectl delete ns test-${{ steps.ns.outputs.name }}
|
||||
|
||||
- if: ${{ always() }}
|
||||
name: Posting result back to PR
|
||||
run: |
|
||||
if [ '${{ steps.job-type.outputs.type }}' == 'pr' ]; then ruby postStatus.rb ${{github.event.client_payload.repository}} ${{github.event.client_payload.commit}} ${{secrets.L2_REPO_TOKEN}} ${{job.status}} ${{github.run_id}} ${{matrix.os}} false ${{ secrets.L2_REPO_USER }}; fi
|
||||
shell: bash
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Tag and create release draft
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- releases/*
|
||||
|
||||
jobs:
|
||||
tag-and-release:
|
||||
uses: OliverMKing/javascript-release-workflow/.github/workflows/tag-and-release.yml@main
|
||||
@@ -1,20 +0,0 @@
|
||||
name: "build-test"
|
||||
on: # rebuild any PRs and main branch changes
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- 'releases/*'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 'releases/*'
|
||||
|
||||
jobs:
|
||||
build: # make sure build/ci works properly
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- run: |
|
||||
npm install
|
||||
npm build
|
||||
npm test
|
||||
@@ -0,0 +1,19 @@
|
||||
name: 'Run unit tests.'
|
||||
on: # rebuild any PRs and main branch changes
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- 'releases/*'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'releases/*'
|
||||
|
||||
jobs:
|
||||
build: # make sure build/ci works properly
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- run: |
|
||||
npm install
|
||||
npm test
|
||||
+3
-327
@@ -1,329 +1,5 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
.DS_Store
|
||||
.idea
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
coverage/
|
||||
@@ -0,0 +1,4 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
coverage
|
||||
/lib
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"semi": false,
|
||||
"tabWidth": 3,
|
||||
"singleQuote": true,
|
||||
"printWidth": 80
|
||||
}
|
||||
+9
-9
@@ -1,9 +1,9 @@
|
||||
# Microsoft Open Source Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
|
||||
Resources:
|
||||
|
||||
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
|
||||
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
|
||||
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
|
||||
# Microsoft Open Source Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
|
||||
Resources:
|
||||
|
||||
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
|
||||
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
|
||||
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
|
||||
|
||||
@@ -1,330 +1,477 @@
|
||||
|
||||
# Deploy manifests action for Kubernetes
|
||||
|
||||
This action can be used to deploy manifests to Kubernetes clusters.
|
||||
|
||||
This action requires that the cluster context be set earlier in the workflow by using either the [Azure/aks-set-context](https://github.com/Azure/aks-set-context/tree/releases/v1) action or the [Azure/k8s-set-context](https://github.com/Azure/k8s-set-context/tree/releases/v1) action.
|
||||
|
||||
If you are looking to automate your workflows to deploy to [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/) and [Azure Web App for Containers](https://azure.microsoft.com/en-us/services/app-service/containers/), consider using [`Azure/webapps-deploy`](https://github.com/Azure/webapps-deploy) action.
|
||||
|
||||
## Action capabilities
|
||||
|
||||
Following are the key capabilities of this action:
|
||||
|
||||
- **Artifact substitution**: The deploy action takes as input a list of container images which can be specified along with their tags or digests. The same is substituted into the non-templatized version of manifest files before applying to the cluster to ensure that the right version of the image is pulled by the cluster nodes.
|
||||
|
||||
- **Object stability checks**: Rollout status is checked for the Kubernetes objects deployed. This is done to incorporate stability checks while computing the action status as success/failure.
|
||||
|
||||
- **Secret handling**: The secret names specified as inputs in the action are used to augment the input manifest files with imagePullSecrets values before deploying to the cluster. Also, checkout the [Azure/k8s-create-secret](https://github.com/Azure/k8s-create-secret) action for creation of generic or docker-registry secrets in the cluster.
|
||||
|
||||
- **Deployment strategy** The action supports canary and blue-green deployment strategies:
|
||||
- **Canary strategy**: Choosing canary strategy with this action leads to creation of workloads suffixed with '-baseline' and '-canary'. There are two methods of traffic splitting supported in the action:
|
||||
- **Service Mesh Interface**: Service Mesh Interface abstraction allows for plug-and-play configuration with service mesh providers such as Linkerd and Istio. Meanwhile, this action takes away the hard work of mapping SMI's TrafficSplit objects to the stable, baseline and canary services during the lifecycle of the deployment strategy. Service mesh based canary deployments using this action are more accurate as service mesh providers enable granular percentage traffic split (via service registry and sidecar containers injected into pods alongside application containers).
|
||||
- **Only Kubernetes (no service mesh)**: In the absence of service mesh, while it may not be possible to achieve exact percentage split at the request level, it is still possible to perform canary deployments by deploying -baseline and -canary workload variants next to the stable variant. The service routes requests to pods of all three workload variants as the selector-label constraints are met (KubernetesManifest will honor these when creating -baseline and -canary variants). This achieves the intended effect of routing only a portion of total requests to the canary.
|
||||
- **Blue-Green strategy**: Choosing blue-green strategy with this action leads to creation of workloads suffixed with '-green'. There are three route-methods supported in the action:
|
||||
|
||||
*Terminolgy: An **identified** service is one that is supplied as part of the input manifest(s) and targets a workload in the supplied manifest(s).
|
||||
- **Service route-method**: **Identified** services are configured to target the green deployments.
|
||||
- **Ingress route-method**: Along with deployments, new services are created with '-green' suffix (for **identified** services), and the ingresses are in turn updated to target the new services.
|
||||
- **SMI route-method**: A new [TrafficSplit](https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md) object is created for each **identified** service. The TrafficSplit object is updated to target the new deployments. **Note** that this works only if SMI is set up in the cluster.
|
||||
|
||||
Traffic is routed to the new workloads only after the time provided as `version-switch-buffer` input has passed. `promote` action creates workloads and services with new configurations but without any suffix. `reject` action routes traffic back to the old workloads and deletes the '-green' workloads.
|
||||
|
||||
|
||||
## Action inputs
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action inputs</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr>
|
||||
<td><code>namespace</code><br/>Namespace</td>
|
||||
<td>(Optional) Namespace within the cluster to deploy to.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>manifests</code><br/>Manifests</td>
|
||||
<td>(Required) Path to the manifest files to be used for deployment</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>images</code><br/>Images</td>
|
||||
<td>(Optional) Fully qualified resource URL of the image(s) to be used for substitutions on the manifest files. This multiline input accepts specifying multiple artifact substitutions in newline separated form. For example - <br>images: |<br>  contosodemo.azurecr.io/foo:test1<br>  contosodemo.azurecr.io/bar:test2<br>In this example, all references to contosodemo.azurecr.io/foo and contosodemo.azurecr.io/bar are searched for in the image field of the input manifest files. For the matches found, the tags test1 and test2 are substituted.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>imagepullsecrets</code><br/>Image pull secrets</td>
|
||||
<td>(Optional) Multiline input where each line contains the name of a docker-registry secret that has already been setup within the cluster. Each of these secret names are added under imagePullSecrets field for the workloads found in the input manifest files</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>strategy</code><br/>Strategy</td>
|
||||
<td>(Optional) Deployment strategy to be used while applying manifest files on the cluster. Acceptable values: none/canary/blue-green. none - No deployment strategy is used when deploying. canary - Canary deployment strategy is used when deploying to the cluster. blue-green - Blue-Green deployment strategy is used when deploying to cluster.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traffic-split-method</code><br/>Traffic split method</td>
|
||||
<td>(Optional) Acceptable values: pod/smi; Default value: pod <br>SMI: Percentage traffic split is done at request level using service mesh. Service mesh has to be setup by cluster admin. Orchestration of <a href="https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md" data-raw-source="TrafficSplit](https://github.com/deislabs/smi-spec/blob/master/traffic-split.md)">TrafficSplit</a> objects of SMI is handled by this action. <br>Pod: Percentage split not possible at request level in the absence of service mesh. So the percentage input is used to calculate the replicas for baseline and canary as a percentage of replicas specified in the input manifests for the stable variant.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>percentage</code><br/>Percentage</td>
|
||||
<td>(Required if strategy == canary) Percentage used to compute the number of replicas of '-baseline' and '-canary' varaints of the workloads found in manifest files. For the specified percentage input, if (percentage * numberOfDesirerdReplicas)/100 is not a round number, the floor of this number is used while creating '-baseline' and '-canary'<br/>Example: If Deployment hello-world was found in the input manifest file with 'replicas: 4' and if 'strategy: canary' and 'percentage: 25' are given as inputs to the action, then the Deployments hello-world-baseline and hello-world-canary are created with 1 replica each. The '-baseline' variant is created with the same image and tag as the stable version (4 replica variant prior to deployment) while the '-canary' variant is created with the image and tag corresponding to the new changes being deployed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>baseline-and-canary-replicas</code><br/>Baseline and canary replicas</td>
|
||||
<td>(Optional; Relevant only if trafficSplitMethod == smi) When trafficSplitMethod == smi, as percentage traffic split is controlled in the service mesh plane, the actual number of replicas for canary and baseline variants could be controlled independently of the traffic split. For example, assume that the input Deployment manifest desired 30 replicas to be used for stable and that the following inputs were specified for the action - <br> strategy: canary<br> trafficSplitMethod: smi<br> percentage: 20<br> baselineAndCanaryReplicas: 1<br> In this case, stable variant will receive 80% traffic while baseline and canary variants will receive 10% each (20% split equally between baseline and canary). However, instead of creating baseline and canary with 3 replicas, the explicit count of baseline and canary replicas is honored. That is, only 1 replica each is created for baseline and canary variants.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>route-method</code><br/>Route Method</td>
|
||||
<td>(Optional; Relevant only if strategy==blue-green) Default value: service. Acceptable values: service/ingress/smi. Traffic is routed based on this input.
|
||||
<br>Service: Service selector labels are updated to target '-green' workloads.
|
||||
<br>Ingress: Ingress backends are updated to target the new '-green' services which in turn target '-green' deployments.
|
||||
<br>SMI: A <a href="https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md" data-raw-source="TrafficSplit](https://github.com/deislabs/smi-spec/blob/master/traffic-split.md)">TrafficSplit</a> object is created for each required service to route traffic to new workloads.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>version-switch-buffer</code><br/>Version Switch Buffer</td>
|
||||
<td>(Optional; Relevant only if strategy==blue-green and action == deploy) Default value: 0. Acceptable values: 1-300. Waits for the given input in minutes before routing traffic to '-green' workloads.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>action</code><br/>Action</td>
|
||||
<td>(Required) Default value: deploy. Acceptable values: deploy/promote/reject. Promote or reject actions are used to promote or reject canary/blue-green deployments. Sample YAML snippets are provided below for guidance on how to use the same.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>kubectl-version</code><br/>Kubectl version</td>
|
||||
<td>(Optional) Version of kubectl client to be used for deploying the manifest to the cluster. If this input is left unspecified, latest version is used.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>force</code><br/>Force</td>
|
||||
<td>(Optional) Deploy when a previous deployment already exists. If true then '--force' argument is added to the apply command. Using '--force' argument is not recommended in production.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Examples YAML snippets
|
||||
|
||||
### Basic deployment (without any deployment strategy)
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
kubectl-version: 'latest'
|
||||
```
|
||||
|
||||
### Deployment Strategies - Canary deployment without service mesh
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
strategy: canary
|
||||
percentage: 20
|
||||
```
|
||||
|
||||
### To promote/reject the canary created by the above snippet, the following YAML snippet could be used:
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
strategy: canary
|
||||
action: promote # substitute reject if you want to reject
|
||||
```
|
||||
|
||||
### Deployment Strategies - Canary deployment based on Service Mesh Interface
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
strategy: canary
|
||||
traffic-split-method: smi
|
||||
percentage: 20
|
||||
baseline-and-canary-replicas: 1
|
||||
```
|
||||
### To promote/reject the canary created by the above snippet, the following YAML snippet could be used:
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }} '
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
strategy: canary
|
||||
traffic-split-method: smi
|
||||
action: reject # substitute reject if you want to reject
|
||||
```
|
||||
### Deployment Strategies - Blue-Green deployment with different route methods
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
ingress.yml
|
||||
strategy: blue-green
|
||||
route-method: ingress # substitute with service/smi as per need
|
||||
version-switch-buffer: 15
|
||||
```
|
||||
|
||||
### **To promote/reject the green workload created by the above snippet, the following YAML snippet could be used:**
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
ingress-yml
|
||||
strategy: blue-green
|
||||
route-method: ingress # should be the same as the value when action was deploy
|
||||
action: promote # substitute reject if you want to reject
|
||||
```
|
||||
|
||||
## End to end workflows
|
||||
|
||||
Following are a few examples of not just this action, but how this action could be used along with other container and k8s related actions for building images and deploying objects onto k8s clusters:
|
||||
|
||||
### Build container image and deploy to Azure Kubernetes Service cluster
|
||||
|
||||
```yaml
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: Azure/docker-login@v1
|
||||
with:
|
||||
login-server: contoso.azurecr.io
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- run: |
|
||||
docker build . -t contoso.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
docker push contoso.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
|
||||
# Set the target AKS cluster.
|
||||
- uses: Azure/aks-set-context@v1
|
||||
with:
|
||||
creds: '${{ secrets.AZURE_CREDENTIALS }}'
|
||||
cluster-name: contoso
|
||||
resource-group: contoso-rg
|
||||
|
||||
- uses: Azure/k8s-create-secret@v1
|
||||
with:
|
||||
container-registry-url: contoso.azurecr.io
|
||||
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
secret-name: demo-k8s-secret
|
||||
|
||||
- uses: Azure/k8s-deploy@v1
|
||||
with:
|
||||
manifests: |
|
||||
manifests/deployment.yml
|
||||
manifests/service.yml
|
||||
images: |
|
||||
demo.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
imagepullsecrets: |
|
||||
demo-k8s-secret
|
||||
```
|
||||
|
||||
### Build container image and deploy to any Azure Kubernetes Service cluster
|
||||
|
||||
```yaml
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: Azure/docker-login@v1
|
||||
with:
|
||||
login-server: contoso.azurecr.io
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- run: |
|
||||
docker build . -t contoso.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
docker push contoso.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
|
||||
- uses: Azure/k8s-set-context@v1
|
||||
with:
|
||||
kubeconfig: ${{ secrets.KUBE_CONFIG }}
|
||||
|
||||
- uses: Azure/k8s-create-secret@v1
|
||||
with:
|
||||
container-registry-url: contoso.azurecr.io
|
||||
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
secret-name: demo-k8s-secret
|
||||
|
||||
- uses: Azure/k8s-deploy@v1
|
||||
with:
|
||||
manifests: |
|
||||
manifests/deployment.yml
|
||||
manifests/service.yml
|
||||
images: |
|
||||
demo.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
imagepullsecrets: |
|
||||
demo-k8s-secret
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
|
||||
|
||||
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
|
||||
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
|
||||
provided by the bot. You will only need to do this once across all repos using our CLA.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
# Deploy manifests action for Kubernetes
|
||||
|
||||
This action is used to deploy manifests to Kubernetes clusters. It requires that the cluster context be set earlier in the workflow by using either the [Azure/aks-set-context](https://github.com/Azure/aks-set-context/tree/releases/v1) action or the [Azure/k8s-set-context](https://github.com/Azure/k8s-set-context/tree/releases/v1) action. It also requires Kubectl to be installed (you can use the [Azure/setup-kubectl](https://github.com/Azure/setup-kubectl) action).
|
||||
|
||||
If you are looking to automate your workflows to deploy to [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/) and [Azure Web App for Containers](https://azure.microsoft.com/en-us/services/app-service/containers/), consider using [`Azure/webapps-deploy`](https://github.com/Azure/webapps-deploy) action.
|
||||
|
||||
This action requires the following permissions from your workflow:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
actions: read
|
||||
```
|
||||
|
||||
## Action capabilities
|
||||
|
||||
Following are the key capabilities of this action:
|
||||
|
||||
- **Artifact substitution**: Takes a list of container images which can be specified along with their tags or digests. They are substituted into the non-templatized version of manifest files before applying to the cluster to ensure that the right version of the image is pulled by the cluster nodes.
|
||||
|
||||
- **Object stability checks**: Rollout status is checked for the Kubernetes objects deployed. This is done to incorporate stability checks while computing the action status as success/failure.
|
||||
|
||||
- **Secret handling**: The secret names specified as inputs in the action are used to augment the input manifest files with imagePullSecrets values before deploying to the cluster. Also, checkout the [Azure/k8s-create-secret](https://github.com/Azure/k8s-create-secret) action for creation of generic or docker-registry secrets in the cluster.
|
||||
|
||||
- **Deployment strategy** Supports both canary and blue-green deployment strategies
|
||||
|
||||
- **Canary strategy**: Workloads suffixed with '-baseline' and '-canary' are created. There are two methods of traffic splitting supported:
|
||||
- **Service Mesh Interface**: Service Mesh Interface abstraction allows for plug-and-play configuration with service mesh providers such as [Linkerd](https://linkerd.io/) and [Istio](https://istio.io/). Meanwhile, this action takes away the hard work of mapping SMI's TrafficSplit objects to the stable, baseline and canary services during the lifecycle of the deployment strategy. Service mesh based canary deployments using this action are more accurate as service mesh providers enable granular percentage traffic split (via service registry and sidecar containers injected into pods alongside application containers).
|
||||
- **Only Kubernetes (no service mesh)**: In the absence of service mesh, while it may not be possible to achieve exact percentage split at the request level, it is still possible to perform canary deployments by deploying -baseline and -canary workload variants next to the stable variant. The service routes requests to pods of all three workload variants as the selector-label constraints are met (KubernetesManifest will honor these when creating -baseline and -canary variants). This achieves the intended effect of routing only a portion of total requests to the canary.
|
||||
- **Blue-Green strategy**: Choosing blue-green strategy with this action leads to creation of workloads suffixed with '-green'. An identified service is one that is supplied as part of the input manifest(s) and targets a workload in the supplied manifest(s). There are three route-methods supported in the action:
|
||||
|
||||
- **Service route-method**: Identified services are configured to target the green deployments.
|
||||
- **Ingress route-method**: Along with deployments, new services are created with '-green' suffix (for identified services), and the ingresses are in turn updated to target the new services.
|
||||
- **SMI route-method**: A new [TrafficSplit](https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md) object is created for each identified service. The TrafficSplit object is updated to target the new deployments. This works only if SMI is set up in the cluster.
|
||||
|
||||
Traffic is routed to the new workloads only after the time provided as `version-switch-buffer` input has passed. The `promote` action creates workloads and services with new configurations but without any suffix. `reject` routes traffic back to the old workloads and deletes the '-green' workloads.
|
||||
|
||||
## Action inputs
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action inputs</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>action </br></br>(Required)</td>
|
||||
<td>Acceptable values: deploy/promote/reject.</br>Promote or reject actions are used to promote or reject canary/blue-green deployments. Sample YAML snippets are provided below for guidance.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>manifests </br></br>(Required)</td>
|
||||
<td>Path to the manifest files to be used for deployment. These can also be directories containing manifest files, in which case, all manifest files in the referenced directory at every depth will be deployed, or URLs to manifest files (like https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/controllers/nginx-deployment.yaml). Files and URLs not ending in .yml or .yaml will be ignored.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>strategy </br></br>(Required)</td>
|
||||
<td>Acceptable values: basic/canary/blue-green. <br>
|
||||
Default value: basic
|
||||
<br>Deployment strategy to be used while applying manifest files on the cluster.
|
||||
<br>basic - Template is force applied to all pods when deploying to cluster. NOTE: Can only be used with action == deploy
|
||||
<br>canary - Canary deployment strategy is used when deploying to the cluster.<br>blue-green - Blue-Green deployment strategy is used when deploying to cluster.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>namespace </br></br>(Optional)
|
||||
<td>Namespace within the cluster to deploy to.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>images </br></br>(Optional)</td>
|
||||
<td>Fully qualified resource URL of the image(s) to be used for substitutions on the manifest files. This multiline input accepts specifying multiple artifact substitutions in newline separated form. For example:<br>
|
||||
<code><br>images: |<br>  contosodemo.azurecr.io/foo:test1<br>  contosodemo.azurecr.io/bar:test2<br></code><br>
|
||||
In this example, all references to contosodemo.azurecr.io/foo and contosodemo.azurecr.io/bar are searched for in the image field of the input manifest files. For the matches found, the tags test1 and test2 are substituted.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>imagepullsecrets </br></br>(Optional)</td>
|
||||
<td>Multiline input where each line contains the name of a docker-registry secret that has already been setup within the cluster. Each of these secret names are added under imagePullSecrets field for the workloads found in the input manifest files</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>pull-images</br></br>(Optional)</td>
|
||||
<td>Acceptable values: true/false</br>Default value: true</br>Switch whether to pull the images from the registry before deployment to find out Dockerfile's path in order to add it to the annotations</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>traffic-split-method </br></br>(Optional)</td>
|
||||
<td>Acceptable values: pod/smi.<br> Default value: pod <br>SMI: Percentage traffic split is done at request level using service mesh. Service mesh has to be setup by cluster admin. Orchestration of <a href="https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md" data-raw-source="TrafficSplit](https://github.com/deislabs/smi-spec/blob/master/traffic-split.md)">TrafficSplit</a> objects of SMI is handled by this action. <br>Pod: Percentage split not possible at request level in the absence of service mesh. Percentage input is used to calculate the replicas for baseline and canary as a percentage of replicas specified in the input manifests for the stable variant.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>traffic-split-annotations </br></br>(Optional)</td>
|
||||
<td>Annotations in the form of key/value pair to be added to TrafficSplit.</td>
|
||||
<tr>
|
||||
<td>percentage </br></br>(Optional but required if strategy is canary)</td>
|
||||
<td>Used to compute the number of replicas of '-baseline' and '-canary' variants of the workloads found in manifest files. For the specified percentage input, if (percentage * numberOfDesirerdReplicas)/100 is not a round number, the floor of this number is used while creating '-baseline' and '-canary'.<br/><br/>For example, if Deployment hello-world was found in the input manifest file with 'replicas: 4' and if 'strategy: canary' and 'percentage: 25' are given as inputs to the action, then the Deployments hello-world-baseline and hello-world-canary are created with 1 replica each. The '-baseline' variant is created with the same image and tag as the stable version (4 replica variant prior to deployment) while the '-canary' variant is created with the image and tag corresponding to the new changes being deployed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>baseline-and-canary-replicas </br></br> (Optional and relevant only if strategy is canary and traffic-split-method is smi)</td>
|
||||
<td>The number of baseline and canary replicas. Percentage traffic split is controlled in the service mesh plane, the actual number of replicas for canary and baseline variants could be controlled independently of the traffic split. For example, assume that the input Deployment manifest desired 30 replicas to be used for stable and that the following inputs were specified for the action </br></br><code> strategy: canary<br> trafficSplitMethod: smi<br> percentage: 20<br> baselineAndCanaryReplicas: 1</code></br></br> In this case, stable variant will receive 80% traffic while baseline and canary variants will receive 10% each (20% split equally between baseline and canary). However, instead of creating baseline and canary with 3 replicas each, the explicit count of baseline and canary replicas is honored. That is, only 1 replica each is created for baseline and canary variants.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>route-method </br></br>(Optional and relevant only if strategy is blue-green)</td>
|
||||
<td>Acceptable values: service/ingress/smi.</br>Default value: service.</br>Traffic is routed based on this input.
|
||||
<br>Service: Service selector labels are updated to target '-green' workloads.
|
||||
<br>Ingress: Ingress backends are updated to target the new '-green' services which in turn target '-green' deployments.
|
||||
<br>SMI: A <a href="https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md" data-raw-source="TrafficSplit](https://github.com/deislabs/smi-spec/blob/master/traffic-split.md)">TrafficSplit</a> object is created for each required service to route traffic to new workloads.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>version-switch-buffer </br></br>(Optional and relevant only if strategy is blue-green)</td>
|
||||
<td>Acceptable values: 1-300.</br>Default value: 0.</br>Waits for the given input in minutes before routing traffic to '-green' workloads.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>private-cluster </br></br>(Optional and relevant only using K8's deploy for a cluster with private cluster enabled)</td>
|
||||
<td>Acceptable values: true, false</br>Default value: false.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>force </br></br>(Optional)</td>
|
||||
<td>Deploy when a previous deployment already exists. If true then '--force' argument is added to the apply command. Using '--force' argument is not recommended in production.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>annotate-namespace</br></br>(Optional)</td>
|
||||
<td>Acceptable values: true/false</br>Default value: true</br>Switch whether to annotate the namespace resources object or not</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic deployment (without any deployment strategy)
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v3.1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
manifests: |
|
||||
dir/manifestsDirectory
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
```
|
||||
|
||||
### Private cluster deployment
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v4
|
||||
with:
|
||||
resource-group: yourResourceGroup
|
||||
name: yourClusterName
|
||||
action: deploy
|
||||
strategy: basic
|
||||
|
||||
private-cluster: true
|
||||
manifests: |
|
||||
manifests/azure-vote-backend-deployment.yaml
|
||||
manifests/azure-vote-backend-service.yaml
|
||||
manifests/azure-vote-frontend-deployment.yaml
|
||||
manifests/azure-vote-frontend-service.yaml
|
||||
images: |
|
||||
registry.azurecr.io/containername
|
||||
```
|
||||
|
||||
### Canary deployment without service mesh
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v3.1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
dir/manifestsDirectory
|
||||
strategy: canary
|
||||
action: deploy
|
||||
percentage: 20
|
||||
```
|
||||
|
||||
To promote/reject the canary created by the above snippet, the following YAML snippet could be used:
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v3.1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
dir/manifestsDirectory
|
||||
strategy: canary
|
||||
action: promote # substitute reject if you want to reject
|
||||
```
|
||||
|
||||
### Canary deployment based on Service Mesh Interface
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v3.1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
dir/manifestsDirectory
|
||||
strategy: canary
|
||||
action: deploy
|
||||
traffic-split-method: smi
|
||||
percentage: 20
|
||||
baseline-and-canary-replicas: 1
|
||||
```
|
||||
|
||||
To promote/reject the canary created by the above snippet, the following YAML snippet could be used:
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v3.1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }} '
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
dir/manifestsDirectory
|
||||
strategy: canary
|
||||
traffic-split-method: smi
|
||||
action: reject # substitute promote if you want to promote
|
||||
```
|
||||
|
||||
### Blue-Green deployment with different route methods
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v3.1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
ingress.yml
|
||||
strategy: blue-green
|
||||
action: deploy
|
||||
route-method: ingress # substitute with service/smi as per need
|
||||
version-switch-buffer: 15
|
||||
```
|
||||
|
||||
To promote/reject the green workload created by the above snippet, the following YAML snippet could be used:
|
||||
|
||||
```yaml
|
||||
- uses: Azure/k8s-deploy@v3.1
|
||||
with:
|
||||
namespace: 'myapp'
|
||||
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
|
||||
imagepullsecrets: |
|
||||
image-pull-secret1
|
||||
image-pull-secret2
|
||||
manifests: |
|
||||
deployment.yaml
|
||||
service.yaml
|
||||
ingress.yml
|
||||
strategy: blue-green
|
||||
route-method: ingress # should be the same as the value when action was deploy
|
||||
action: promote # substitute reject if you want to reject
|
||||
```
|
||||
|
||||
## End to end workflows
|
||||
|
||||
Following are a few examples of not just this action, but how this action could be used along with other container and k8s related actions for building images and deploying objects onto k8s clusters:
|
||||
|
||||
### Build container image and deploy to Azure Kubernetes Service cluster
|
||||
|
||||
```yaml
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: Azure/docker-login@v1
|
||||
with:
|
||||
login-server: contoso.azurecr.io
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- run: |
|
||||
docker build . -t contoso.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
docker push contoso.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
|
||||
- uses: azure/setup-kubectl@v2.0
|
||||
|
||||
# Set the target AKS cluster.
|
||||
- uses: Azure/aks-set-context@v1
|
||||
with:
|
||||
creds: '${{ secrets.AZURE_CREDENTIALS }}'
|
||||
cluster-name: contoso
|
||||
resource-group: contoso-rg
|
||||
|
||||
- uses: Azure/k8s-create-secret@v1.1
|
||||
with:
|
||||
container-registry-url: contoso.azurecr.io
|
||||
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
secret-name: demo-k8s-secret
|
||||
|
||||
- uses: Azure/k8s-deploy@v3.1
|
||||
with:
|
||||
action: deploy
|
||||
manifests: |
|
||||
manifests/deployment.yml
|
||||
manifests/service.yml
|
||||
images: |
|
||||
demo.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
imagepullsecrets: |
|
||||
demo-k8s-secret
|
||||
```
|
||||
|
||||
### Build container image and deploy to any Azure Kubernetes Service cluster
|
||||
|
||||
```yaml
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: Azure/docker-login@v1
|
||||
with:
|
||||
login-server: contoso.azurecr.io
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- run: |
|
||||
docker build . -t contoso.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
docker push contoso.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
|
||||
- uses: azure/setup-kubectl@v2.0
|
||||
|
||||
- uses: Azure/k8s-set-context@v2
|
||||
with:
|
||||
kubeconfig: ${{ secrets.KUBE_CONFIG }}
|
||||
|
||||
- uses: Azure/k8s-create-secret@v1.1
|
||||
with:
|
||||
container-registry-url: contoso.azurecr.io
|
||||
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
secret-name: demo-k8s-secret
|
||||
|
||||
- uses: Azure/k8s-deploy@v3.1
|
||||
with:
|
||||
action: deploy
|
||||
manifests: |
|
||||
manifests/deployment.yml
|
||||
manifests/service.yml
|
||||
images: |
|
||||
demo.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
imagepullsecrets: |
|
||||
demo-k8s-secret
|
||||
```
|
||||
|
||||
### Build image and add `dockerfile-path` label to it
|
||||
|
||||
We can use this image in other workflows once built.
|
||||
|
||||
```yaml
|
||||
on: [push]
|
||||
env:
|
||||
NAMESPACE: demo-ns2
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: Azure/docker-login@v1
|
||||
with:
|
||||
login-server: contoso.azurecr.io
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- run: |
|
||||
docker build . -t contoso.azurecr.io/k8sdemo:${{ github.sha }} --label dockerfile-path=https://github.com/${{github.repo}}/blob/${{github.sha}}/Dockerfile
|
||||
docker push contoso.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
```
|
||||
|
||||
### Use bake action to get manifests deploying to a Kubernetes cluster
|
||||
|
||||
```yaml
|
||||
on: [push]
|
||||
env:
|
||||
NAMESPACE: demo-ns2
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- uses: Azure/docker-login@v1
|
||||
with:
|
||||
login-server: contoso.azurecr.io
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- uses: azure/setup-kubectl@v2.0
|
||||
|
||||
# Set the target AKS cluster.
|
||||
- uses: Azure/aks-set-context@v1
|
||||
with:
|
||||
creds: '${{ secrets.AZURE_CREDENTIALS }}'
|
||||
cluster-name: contoso
|
||||
resource-group: contoso-rg
|
||||
|
||||
- uses: Azure/k8s-create-secret@v1.1
|
||||
with:
|
||||
namespace: ${{ env.NAMESPACE }}
|
||||
container-registry-url: contoso.azurecr.io
|
||||
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
secret-name: demo-k8s-secret
|
||||
|
||||
- uses: azure/k8s-bake@v2
|
||||
with:
|
||||
renderEngine: 'helm'
|
||||
helmChart: './aks-helloworld/'
|
||||
overrideFiles: './aks-helloworld/values-override.yaml'
|
||||
overrides: |
|
||||
replicas:2
|
||||
helm-version: 'latest'
|
||||
id: bake
|
||||
|
||||
- uses: Azure/k8s-deploy@v1.2
|
||||
with:
|
||||
action: deploy
|
||||
manifests: ${{ steps.bake.outputs.manifestsBundle }}
|
||||
images: |
|
||||
contoso.azurecr.io/k8sdemo:${{ github.sha }}
|
||||
imagepullsecrets: |
|
||||
demo-k8s-secret
|
||||
```
|
||||
|
||||
## Traceability Fields Support
|
||||
|
||||
- Environment variable `HELM_CHART_PATHS` is a list of helmchart files expected by k8s-deploy - it will be populated automatically if you are using k8s-bake to generate the manifests.
|
||||
- Use script to build image and add dockerfile-path label to it. The value expected is the link to the dockerfile: https://github.com/${{github.repo}}/blob/${{github.sha}}/Dockerfile. If your dockerfile is in the same repo and branch where the workflow is run, it can be a relative path and it will be converted to a link for traceability.
|
||||
- Run docker login action for each image registry - in case image build and image deploy are two distinct jobs in the same or separate workflows.
|
||||
|
||||
## Contributing
|
||||
|
||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
|
||||
|
||||
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
|
||||
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
|
||||
provided by the bot. You will only need to do this once across all repos using our CLA.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
## Support
|
||||
|
||||
k8s-deploy is an open source project that is [**not** covered by the Microsoft Azure support policy](https://support.microsoft.com/en-us/help/2941892/support-for-linux-and-open-source-technology-in-azure). [Please search open issues here](https://github.com/Azure/k8s-deploy/issues), and if your issue isn't already represented please [open a new one](https://github.com/Azure/k8s-deploy/issues/new/choose). The project maintainers will respond to the best of their abilities.
|
||||
|
||||
+35
-35
@@ -1,35 +1,35 @@
|
||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.1 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [many more](https://opensource.microsoft.com/).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [definition](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center at [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://technet.microsoft.com/en-us/security/dn606155).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.1 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [many more](https://opensource.microsoft.com/).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [definition](<https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)>) of a security vulnerability, please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center at [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://technet.microsoft.com/en-us/security/dn606155).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
- Full paths of source file(s) related to the manifestation of the issue
|
||||
- The location of the affected source code (tag/branch/commit or direct URL)
|
||||
- Any special configuration required to reproduce the issue
|
||||
- Step-by-step instructions to reproduce the issue
|
||||
- Proof-of-concept or exploit code (if possible)
|
||||
- Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
||||
|
||||
@@ -1,777 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as inputParam from '../src/input-parameters';
|
||||
import * as fileHelper from '../src/utilities/files-helper';
|
||||
import {
|
||||
Kubectl,
|
||||
} from '../src/kubectl-object-model';
|
||||
import {
|
||||
mocked
|
||||
} from 'ts-jest/utils';
|
||||
import * as kubectlUtils from '../src/utilities/kubectl-util';
|
||||
|
||||
var path = require('path');
|
||||
const inputParamMock = mocked(inputParam, true);
|
||||
var deploymentYaml = "";
|
||||
|
||||
import * as blueGreenHelper from '../src/utilities/strategy-helpers/blue-green-helper';
|
||||
import * as blueGreenHelperService from '../src/utilities/strategy-helpers/service-blue-green-helper';
|
||||
import * as blueGreenHelperIngress from '../src/utilities/strategy-helpers/ingress-blue-green-helper';
|
||||
import * as blueGreenHelperSMI from '../src/utilities/strategy-helpers/smi-blue-green-helper';
|
||||
|
||||
beforeAll(() => {
|
||||
deploymentYaml = fs.readFileSync(path.join(__dirname, 'manifests', 'bg.yml'), 'utf8');
|
||||
process.env["KUBECONFIG"] = 'kubeConfig';
|
||||
});
|
||||
|
||||
test("deployBlueGreen - checks if deployment can be done, then deploys", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: undefined
|
||||
};
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperService.deployBlueGreenService(kubeCtl, ['manifests/bg.yaml'])).toMatchObject({
|
||||
"newFilePaths": "hello",
|
||||
"result": ""
|
||||
});
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
expect(fileHelperMock.writeObjectsToFile).toBeCalled();
|
||||
expect(kubeCtl.apply).toBeCalled();
|
||||
});
|
||||
|
||||
test("blueGreenPromote - checks if in deployed state and then promotes", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: JSON.stringify({
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"name": "testservice"
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"app": "testapp",
|
||||
"k8s.deploy.color": "green"
|
||||
},
|
||||
"ports": [{
|
||||
"protocol": "TCP",
|
||||
"port": 80,
|
||||
"targetPort": 80
|
||||
}]
|
||||
}
|
||||
})
|
||||
};
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
//Invoke and assert
|
||||
const manifestObjects = blueGreenHelper.getManifestObjects(['manifests/bg.yaml']);
|
||||
expect(blueGreenHelperService.promoteBlueGreenService(kubeCtl, manifestObjects)).toMatchObject({});
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
expect(kubeCtl.apply).toBeCalledWith("hello");
|
||||
expect(fileHelperMock.writeObjectsToFile).toBeCalled();
|
||||
});
|
||||
|
||||
test("blueGreenReject - routes servcies to old deployment and deletes new deployment", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: JSON.stringify({
|
||||
"apiVersion": "apps/v1beta1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "testapp",
|
||||
"labels": {
|
||||
"k8s.deploy.color": "none"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "testapp",
|
||||
"k8s.deploy.color": "none"
|
||||
}
|
||||
},
|
||||
"replicas": 1,
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "testapp",
|
||||
"k8s.deploy.color": "none"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [{
|
||||
"name": "testapp",
|
||||
"image": "testcr.azurecr.io/testapp",
|
||||
"ports": [{
|
||||
"containerPort": 80
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
kubeCtl.delete = jest.fn().mockReturnValue('');
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperService.rejectBlueGreenService(kubeCtl, ['manifests/bg.yaml'])).toMatchObject({});
|
||||
expect(kubeCtl.delete).toBeCalledWith(["Deployment", "testapp-green"]);
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
expect(fileHelperMock.writeObjectsToFile).toBeCalled();
|
||||
});
|
||||
|
||||
test("blueGreenReject - deletes services if old deployment does not exist", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: undefined
|
||||
};
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
kubeCtl.delete = jest.fn().mockReturnValue('');
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperService.rejectBlueGreenService(kubeCtl, ['manifests/bg.yaml'])).toMatchObject({});
|
||||
expect(kubeCtl.delete).toBeCalledWith(["Deployment", "testapp-green"]);
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
expect(fileHelperMock.writeObjectsToFile).toBeCalled();
|
||||
});
|
||||
|
||||
test("isIngressRoute() - returns true if route-method is ingress", () => {
|
||||
// default is service
|
||||
expect(blueGreenHelper.isIngressRoute()).toBeFalsy();
|
||||
});
|
||||
|
||||
test("isIngressRoute() - returns true if route-method is ingress", () => {
|
||||
inputParamMock.routeMethod = 'ingress'
|
||||
expect(blueGreenHelper.isIngressRoute()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("deployBlueGreenIngress - creates deployments, services and other non ingress objects", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: undefined
|
||||
};
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperIngress.deployBlueGreenIngress(kubeCtl, ['manifests/bg.yaml'])).toMatchObject({
|
||||
"newFilePaths": "hello",
|
||||
"result": ""
|
||||
});
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
expect(kubeCtl.apply).toBeCalledWith("hello");
|
||||
});
|
||||
|
||||
test("blueGreenPromoteIngress - checks if in deployed state and then promotes ingress", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
let temp = {
|
||||
stdout: JSON.stringify({
|
||||
"apiVersion": "networking.k8s.io/v1beta1",
|
||||
"kind": "Ingress",
|
||||
"metadata": {
|
||||
"name": "testingress",
|
||||
"labels": {
|
||||
"k8s.deploy.color": "green"
|
||||
},
|
||||
"annotations": {
|
||||
"nginx.ingress.kubernetes.io/rewrite-target": "/"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [{
|
||||
"http": {
|
||||
"paths": [{
|
||||
"path": "/testpath",
|
||||
"pathType": "Prefix",
|
||||
"backend": {
|
||||
"serviceName": "testservice-green",
|
||||
"servicePort": 80
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
};
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
const manifestObjects = blueGreenHelper.getManifestObjects(['manifests/bg.yaml']);
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperIngress.promoteBlueGreenIngress(kubeCtl, manifestObjects)).toMatchObject({});
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
expect(kubeCtl.apply).toBeCalledWith("hello");
|
||||
});
|
||||
|
||||
test("blueGreenRejectIngress - routes ingress to stable services and deletes new deployments and services", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
kubeCtl.delete = jest.fn().mockReturnValue('');
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperIngress.rejectBlueGreenIngress(kubeCtl, ['manifests/bg.yaml'])).toMatchObject({});
|
||||
expect(kubeCtl.delete).toBeCalledWith(["Deployment", "testapp-green"]);
|
||||
expect(kubeCtl.delete).toBeCalledWith(["Service", "testservice-green"]);
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
expect(fileHelperMock.writeObjectsToFile).toBeCalled();
|
||||
});
|
||||
|
||||
test("isSMIRoute() - returns true if route-method is smi", () => {
|
||||
inputParamMock.routeMethod = 'smi'
|
||||
expect(blueGreenHelper.isSMIRoute()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("isSMIRoute() - returns true if route-method is smi", () => {
|
||||
inputParamMock.routeMethod = 'ingress'
|
||||
expect(blueGreenHelper.isSMIRoute()).toBeFalsy();
|
||||
});
|
||||
|
||||
test("deployBlueGreenSMI - checks if deployment can be done, then deploys along this auxiliary services and trafficsplit", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: undefined
|
||||
};
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
const kubectlUtilsMock = mocked(kubectlUtils, true);
|
||||
kubectlUtilsMock.getTrafficSplitAPIVersion = jest.fn().mockReturnValue('split.smi-spec.io/v1alpha2');
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperSMI.deployBlueGreenSMI(kubeCtl, ['manifests/bg.yaml'])).toMatchObject({
|
||||
"newFilePaths": "hello",
|
||||
"result": ""
|
||||
});
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
expect(fileHelperMock.writeObjectsToFile).toBeCalled();
|
||||
});
|
||||
|
||||
test("blueGreenPromoteSMI - checks weights of trafficsplit and then deploys", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
let temp = {
|
||||
stdout: JSON.stringify({
|
||||
"apiVersion": "split.smi-spec.io/v1alpha2",
|
||||
"kind": "TrafficSplit",
|
||||
"metadata": {
|
||||
"name": "testservice-rollout"
|
||||
},
|
||||
"spec": {
|
||||
"service": "testservice",
|
||||
"backends": [{
|
||||
"service": "testservice-stable",
|
||||
"weight": 0
|
||||
},
|
||||
{
|
||||
"service": "testservice-green",
|
||||
"weight": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
};
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
const manifestObjects = blueGreenHelper.getManifestObjects(['manifests/bg.yaml']);
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperSMI.promoteBlueGreenSMI(kubeCtl, manifestObjects)).toMatchObject({});
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
});
|
||||
|
||||
test("blueGreenRejectSMI - routes servcies to old deployment and deletes new deployment, auxiliary services and trafficsplit", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: JSON.stringify({
|
||||
"apiVersion": "apps/v1beta1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "testapp",
|
||||
"labels": {
|
||||
"k8s.deploy.color": "none"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "testapp",
|
||||
"k8s.deploy.color": "none"
|
||||
|
||||
}
|
||||
},
|
||||
"replicas": 1,
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "testapp",
|
||||
"k8s.deploy.color": "none"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [{
|
||||
"name": "testapp",
|
||||
"image": "testcr.azurecr.io/testapp",
|
||||
"ports": [{
|
||||
"containerPort": 80
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
kubeCtl.delete = jest.fn().mockReturnValue('');
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperSMI.rejectBlueGreenSMI(kubeCtl, ['manifests/bg.yaml'])).toMatchObject({});
|
||||
expect(kubeCtl.delete).toBeCalledWith(["Deployment", "testapp-green"]);
|
||||
expect(kubeCtl.delete).toBeCalledWith(["Service", "testservice-green"]);
|
||||
expect(kubeCtl.delete).toBeCalledWith(["Service", "testservice-stable"]);
|
||||
expect(kubeCtl.delete).toBeCalledWith(["TrafficSplit", "testservice-trafficsplit"]);
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
});
|
||||
|
||||
test("blueGreenRejectSMI - deletes service if stable deployment doesn't exist", () => {
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: undefined
|
||||
};
|
||||
kubeCtl.delete = jest.fn().mockReturnValue('');
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperSMI.rejectBlueGreenSMI(kubeCtl, ['manifests/bg.yaml'])).toMatchObject({});
|
||||
expect(kubeCtl.delete).toBeCalledWith(["Deployment", "testapp-green"]);
|
||||
expect(kubeCtl.delete).toBeCalledWith(["Service", "testservice-green"]);
|
||||
expect(kubeCtl.delete).toBeCalledWith(["Service", "testservice-stable"]);
|
||||
expect(kubeCtl.delete).toBeCalledWith(["TrafficSplit", "testservice-trafficsplit"]);
|
||||
expect(readFileSpy).toBeCalledWith("manifests/bg.yaml");
|
||||
});
|
||||
|
||||
// other functions and branches
|
||||
test("blueGreenRouteIngress - routes to green services in nextlabel is green", () => {
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const ingEntList = [{
|
||||
"apiVersion": "networking.k8s.io/v1beta1",
|
||||
"kind": "Ingress",
|
||||
"metadata": {
|
||||
"name": "test-ingress",
|
||||
"annotations": {
|
||||
"nginx.ingress.kubernetes.io/rewrite-target": "/"
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
"rules": [{
|
||||
"http": {
|
||||
"paths": [{
|
||||
"path": "/testpath",
|
||||
"pathType": "Prefix",
|
||||
"backend": {
|
||||
"serviceName": "testservice",
|
||||
"servicePort": 80
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/testpath",
|
||||
"pathType": "Prefix",
|
||||
"backend": {
|
||||
"serviceName": "random",
|
||||
"servicePort": 80
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "networking.k8s.io/v1beta1",
|
||||
"kind": "Ingress",
|
||||
"metadata": {
|
||||
"name": "test-ingress",
|
||||
"annotations": {
|
||||
"nginx.ingress.kubernetes.io/rewrite-target": "/"
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
"rules": [{
|
||||
"http": {
|
||||
"paths": [{
|
||||
"path": "/testpath",
|
||||
"pathType": "Prefix",
|
||||
"backend": {
|
||||
"serviceName": "random",
|
||||
"servicePort": 80
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const serEntList = [{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"name": "testservice"
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"app": "testapp",
|
||||
},
|
||||
"ports": [{
|
||||
"protocol": "TCP",
|
||||
"port": 80,
|
||||
"targetPort": 80
|
||||
}]
|
||||
}
|
||||
}];
|
||||
|
||||
let serviceEntityMap = new Map<string, string>();
|
||||
serviceEntityMap.set('testservice', 'testservice-green');
|
||||
fileHelperMock.writeObjectsToFile = jest.fn().mockReturnValue('hello');
|
||||
kubeCtl.apply = jest.fn().mockReturnValue('');
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperIngress.routeBlueGreenIngress(kubeCtl, 'green', serviceEntityMap, ingEntList));
|
||||
expect(kubeCtl.apply).toBeCalled();
|
||||
expect(fileHelperMock.writeObjectsToFile).toBeCalled();
|
||||
});
|
||||
|
||||
test("shouldWePromoteIngress - throws if routed ingress does not exist", () => {
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: undefined
|
||||
}
|
||||
|
||||
const ingEntList = [{
|
||||
"apiVersion": "networking.k8s.io/v1beta1",
|
||||
"kind": "Ingress",
|
||||
"metadata": {
|
||||
"name": "test-ingress",
|
||||
"annotations": {
|
||||
"nginx.ingress.kubernetes.io/rewrite-target": "/"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [{
|
||||
"http": {
|
||||
"paths": [{
|
||||
"path": "/testpath",
|
||||
"pathType": "Prefix",
|
||||
"backend": {
|
||||
"serviceName": "testservice",
|
||||
"servicePort": 80
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}];
|
||||
|
||||
let serviceEntityMap = new Map<string, string>();
|
||||
serviceEntityMap.set('testservice', 'testservice-green');
|
||||
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperIngress.validateIngressesState(kubeCtl, ingEntList, serviceEntityMap)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("validateTrafficSplitState - throws if trafficsplit in wrong state", () => {
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: JSON.stringify({
|
||||
"apiVersion": "split.smi-spec.io/v1alpha2",
|
||||
"kind": "TrafficSplit",
|
||||
"metadata": {
|
||||
"name": "testservice-trafficsplit"
|
||||
},
|
||||
"spec": {
|
||||
"service": "testservice",
|
||||
"backends": [{
|
||||
"service": "testservice-stable",
|
||||
"weight": 100
|
||||
},
|
||||
{
|
||||
"service": "testservice-green",
|
||||
"weight": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const depEntList = [{
|
||||
"apiVersion": "apps/v1beta1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "testapp",
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "testapp",
|
||||
}
|
||||
},
|
||||
"replicas": 1,
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "testapp",
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [{
|
||||
"name": "testapp",
|
||||
"image": "testcr.azurecr.io/testapp",
|
||||
"ports": [{
|
||||
"containerPort": 80
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
const serEntList = [{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"name": "testservice"
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"app": "testapp",
|
||||
},
|
||||
"ports": [{
|
||||
"protocol": "TCP",
|
||||
"port": 80,
|
||||
"targetPort": 80
|
||||
}]
|
||||
}
|
||||
}];
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperSMI.validateTrafficSplitsState(kubeCtl, serEntList)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("validateTrafficSplitState - throws if trafficsplit in wrong state", () => {
|
||||
const kubeCtl: jest.Mocked < Kubectl > = new Kubectl("") as any;
|
||||
let temp = {
|
||||
stdout: JSON.stringify({
|
||||
"apiVersion": "split.smi-spec.io/v1alpha2",
|
||||
"kind": "TrafficSplit",
|
||||
"metadata": {
|
||||
"name": "testservice-trafficsplit"
|
||||
},
|
||||
"spec": {
|
||||
"service": "testservice",
|
||||
"backends": [{
|
||||
"service": "testservice-stable",
|
||||
"weight": 0
|
||||
},
|
||||
{
|
||||
"service": "testservice-green",
|
||||
"weight": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const depEntList = [{
|
||||
"apiVersion": "apps/v1beta1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "testapp",
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "testapp",
|
||||
}
|
||||
},
|
||||
"replicas": 1,
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "testapp",
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [{
|
||||
"name": "testapp",
|
||||
"image": "testcr.azurecr.io/testapp",
|
||||
"ports": [{
|
||||
"containerPort": 80
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
const serEntList = [{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"name": "testservice"
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"app": "testapp",
|
||||
},
|
||||
"ports": [{
|
||||
"protocol": "TCP",
|
||||
"port": 80,
|
||||
"targetPort": 80
|
||||
}]
|
||||
}
|
||||
}];
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(JSON.parse(JSON.stringify(temp)));
|
||||
|
||||
//Invoke and assert
|
||||
expect(blueGreenHelperSMI.validateTrafficSplitsState(kubeCtl, serEntList)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("getSuffix() - returns BLUE_GREEN_SUFFIX if BLUE_GREEN_NEW_LABEL_VALUE is given, else emrty string", () => {
|
||||
expect(blueGreenHelper.getSuffix('green')).toBe('-green');
|
||||
});
|
||||
|
||||
test("getSuffix() - returns BLUE_GREEN_SUFFIX if BLUE_GREEN_NEW_LABEL_VALUE is given, else emrty string", () => {
|
||||
expect(blueGreenHelper.getSuffix('random')).toBe('');
|
||||
});
|
||||
|
||||
test("getServiceSpacLabel() - returns empty string if BLUE_GREEN_VERSION_LABEL in spec selector doesn't exist", () => {
|
||||
let input = {
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "sample-deployment"
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "sample",
|
||||
"k8s.deploy.color": "green"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "sample"
|
||||
},
|
||||
"annotations": {
|
||||
"prometheus.io/scrape": "true",
|
||||
"prometheus.io/port": "8888"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [{
|
||||
"name": "sample",
|
||||
"image": "tsugunt/sample:v34",
|
||||
"ports": [{
|
||||
"containerPort": 8888
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(blueGreenHelperService.getServiceSpecLabel(input)).toBe('');
|
||||
});
|
||||
|
||||
test("getDeploymentMatchLabels() - return false is input doesnt have matchLabels", () => {
|
||||
let input = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"name": "sample-service"
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"app": "sample",
|
||||
"k8s.deploy.color": "green"
|
||||
},
|
||||
"ports": [{
|
||||
"protocol": "TCP",
|
||||
"port": 80,
|
||||
"targetPort": 8888,
|
||||
"nodePort": 31002
|
||||
}],
|
||||
"type": "NodePort"
|
||||
}
|
||||
}
|
||||
|
||||
expect(blueGreenHelper.getDeploymentMatchLabels(input)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("getServiceSelector() - return false if spec selector does not exist", () => {
|
||||
let input = {
|
||||
"apiVersion": "networking.k8s.io/v1beta1",
|
||||
"kind": "Ingress",
|
||||
"metadata": {
|
||||
"name": "test-ingress",
|
||||
"annotations": {
|
||||
"nginx.ingress.kubernetes.io/rewrite-target": "/"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"rules": [{
|
||||
"http": {
|
||||
"paths": [{
|
||||
"path": "/testpath",
|
||||
"pathType": "Prefix",
|
||||
"backend": {
|
||||
"serviceName": "test",
|
||||
"servicePort": 80
|
||||
}
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
expect(blueGreenHelper.getServiceSelector(input)).toBeFalsy();
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
apiVersion : apps/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: testapp
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: testapp
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: testapp
|
||||
spec:
|
||||
containers:
|
||||
- name: testapp
|
||||
image: testcr.azurecr.io/testapp
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: testservice
|
||||
spec:
|
||||
selector:
|
||||
app: testapp
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
---
|
||||
@@ -1,85 +0,0 @@
|
||||
apiVersion : apps/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: testapp
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: testapp
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: testapp
|
||||
spec:
|
||||
containers:
|
||||
- name: testapp
|
||||
image: testcr.azurecr.io/testapp
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: testservice
|
||||
spec:
|
||||
selector:
|
||||
app: testapp
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: testingress
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /testpath
|
||||
pathType: Prefix
|
||||
backend:
|
||||
serviceName: testservice
|
||||
servicePort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: testconfigmap
|
||||
data:
|
||||
# property-like keys; each key maps to a simple value
|
||||
whats_this: "testing"
|
||||
why_this: "testing"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: testservice-2
|
||||
spec:
|
||||
selector:
|
||||
app: testapp-2
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: testingress-1
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /testpath
|
||||
pathType: Prefix
|
||||
backend:
|
||||
serviceName: testnotservice
|
||||
servicePort: 80
|
||||
---
|
||||
@@ -1,16 +0,0 @@
|
||||
apiVersion : apps/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: testapp
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: testapp
|
||||
spec:
|
||||
containers:
|
||||
- name: testapp
|
||||
image: testcr.azurecr.io/testapp
|
||||
ports:
|
||||
- containerPort: 80
|
||||
@@ -1,397 +0,0 @@
|
||||
import * as KubernetesManifestUtility from '../src/utilities/manifest-stability-utility';
|
||||
import * as KubernetesObjectUtility from '../src/utilities/resource-object-utility';
|
||||
import * as action from '../src/run';
|
||||
import * as core from '@actions/core';
|
||||
import * as deployment from '../src/utilities/strategy-helpers/deployment-helper';
|
||||
import * as fs from 'fs';
|
||||
import * as io from '@actions/io';
|
||||
import * as toolCache from '@actions/tool-cache';
|
||||
import * as fileHelper from '../src/utilities/files-helper';
|
||||
import { getWorkflowAnnotationKeyLabel, getWorkflowAnnotationsJson } from '../src/constants';
|
||||
import * as inputParam from '../src/input-parameters';
|
||||
|
||||
import { Kubectl, Resource } from '../src/kubectl-object-model';
|
||||
import * as httpClient from '../src/utilities/httpClient';
|
||||
import * as utility from '../src/utilities/utility';
|
||||
|
||||
import { getkubectlDownloadURL } from "../src/utilities/kubectl-util";
|
||||
import { mocked } from 'ts-jest/utils';
|
||||
|
||||
var path = require('path');
|
||||
const os = require("os");
|
||||
|
||||
const coreMock = mocked(core, true);
|
||||
const ioMock = mocked(io, true);
|
||||
const inputParamMock = mocked(inputParam, true);
|
||||
|
||||
const toolCacheMock = mocked(toolCache, true);
|
||||
const fileUtility = mocked(fs, true);
|
||||
|
||||
const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/release/stable.txt';
|
||||
|
||||
var deploymentYaml = "";
|
||||
|
||||
const getAllPodsMock = {
|
||||
'code': 0,
|
||||
'stdout': '{"apiVersion": "v1","items": [{"apiVersion": "v1","kind": "Pod","metadata": {"labels": {"app": "testapp","pod-template-hash": "776cbc86f9"},"name": "testpod-776cbc86f9-pjrb6","namespace": "testnamespace","ownerReferences": [{"apiVersion": "apps/v1","blockOwnerDeletion": true,"controller": true,"kind": "ReplicaSet","name": "testpod-776cbc86f9","uid": "de544628-6589-4354-81fe-05faf00d336a"}],"resourceVersion": "12362496","selfLink": "/api/v1/namespaces/akskodey8187/pods/akskodey-776cbc86f9-pjrb6","uid": "c7d5f4c1-11a1-4884-8a66-09b015c72f69"},"spec": {"containers": [{"image": "imageId","imagePullPolicy": "IfNotPresent","name": "containerName","ports": [{"containerPort": 80,"protocol": "TCP"}]}]},"status": {"hostIP": "10.240.0.4","phase": "Running","podIP": "10.244.0.25","qosClass": "BestEffort","startTime": "2020-06-04T07:59:42Z"}}]}'
|
||||
};
|
||||
|
||||
const getNamespaceMock = {
|
||||
'code': 0,
|
||||
'stdout': '{"apiVersion": "v1","kind": "Namespace","metadata": {"annotations": {"githubWorkflow_c11401b9d232942bac19cbc5bc32b42d": "{\'run\': \'202489005\',\'repository\': \'testUser/hello-kubernetes\',\'workflow\': \'workflow1\',\'jobName\': \'build-and-deploy\',\'createdBy\': \'testUser\',\'runUri\': \'https://github.com/testUser/hello-kubernetes/actions/runs/202489005\',\'commit\': \'currentCommit\',\'lastSuccessRunCommit\': \'lastCommit\',\'branch\': \'refs/heads/branch-rename\',\'deployTimestamp\': \'1597062957973\',\'dockerfilePaths\': \'{}\',\'manifestsPaths\': \'[]\',\'helmChartPaths\': \'[]\',\'provider\': \'GitHub\'}","githubWorkflow_21fd7a597282ca5adc05ba99018b3706": "{\'run\': \'202504411\',\'repository\': \'testUser/hello-kubernetes\',\'workflow\': \'workflowMaster\',\'jobName\': \'build-and-deploy\',\'createdBy\': \'testUser\',\'runUri\': \'https://github.com/testUser/hello-kubernetes/actions/runs/202504411\',\'commit\': \'currentCommit1\',\'lastSuccessRunCommit\': \'NA\',\'branch\': \'refs/heads/master\',\'deployTimestamp\': \'1597063919873\',\'dockerfilePaths\': \'{}\',\'manifestsPaths\': \'[]\',\'helmChartPaths\': \'[]\',\'provider\': \'GitHub\'}"}},"spec": {"finalizers": ["kubernetes"]},"status": {"phase": "Active"}}'
|
||||
};
|
||||
|
||||
const getWorkflowsUrlResponse = {
|
||||
'statusCode': httpClient.StatusCodes.OK,
|
||||
'body': {
|
||||
"total_count": 2,
|
||||
"workflows": [
|
||||
{
|
||||
"id": 1477727,
|
||||
"node_id": "MDg6V29ya2Zsb3cxNDYwNzI3",
|
||||
"name": ".github/workflows/workflow.yml",
|
||||
"path": ".github/workflows/workflow.yml",
|
||||
"state": "active",
|
||||
"created_at": "2020-06-03T23:41:06.000+05:30",
|
||||
"updated_at": "2020-08-07T15:46:42.000+05:30",
|
||||
"url": "https://api.github.com/repos/testUser/hello-kubernetes/actions/workflows/1460727",
|
||||
"html_url": "https://github.com/testUser/hello-kubernetes/blob/master/.github/workflows/workflow.yml",
|
||||
"badge_url": "https://github.com/testUser/hello-kubernetes/workflows/.github/workflows/workflow.yml/badge.svg"
|
||||
},
|
||||
{
|
||||
"id": 1532230,
|
||||
"node_id": "MDg6V29ya2Zsb3cxNTMyMzMw",
|
||||
"name": "NewWorkflow",
|
||||
"path": ".github/workflows/workflow1.yml",
|
||||
"state": "active",
|
||||
"created_at": "2020-06-11T16:05:23.000+05:30",
|
||||
"updated_at": "2020-08-07T15:46:42.000+05:30",
|
||||
"url": "https://api.github.com/repos/testUser/hello-kubernetes/actions/workflows/1532330",
|
||||
"html_url": "https://github.com/testUser/hello-kubernetes/blob/master/.github/workflows/workflowNew.yml",
|
||||
"badge_url": "https://github.com/testUser/hello-kubernetes/workflows/KoDeyi/badge.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
} as httpClient.WebResponse;
|
||||
|
||||
const resources: Resource[] = [{ type: "Deployment", name: "AppName" }];
|
||||
|
||||
beforeEach(() => {
|
||||
deploymentYaml = fs.readFileSync(path.join(__dirname, 'manifests', 'deployment.yml'), 'utf8');
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => 1234561234567);
|
||||
|
||||
process.env["KUBECONFIG"] = 'kubeConfig';
|
||||
process.env['GITHUB_RUN_ID'] = '12345';
|
||||
process.env['GITHUB_WORKFLOW'] = '.github/workflows/workflow.yml';
|
||||
process.env['GITHUB_JOB'] = 'build-and-deploy';
|
||||
process.env['GITHUB_ACTOR'] = 'testUser';
|
||||
process.env['GITHUB_REPOSITORY'] = 'testRepo';
|
||||
process.env['GITHUB_SHA'] = 'testCommit';
|
||||
process.env['GITHUB_REF'] = 'testBranch';
|
||||
process.env['GITHUB_TOKEN'] = 'testToken';
|
||||
})
|
||||
|
||||
test("setKubectlPath() - install a particular version", async () => {
|
||||
const kubectlVersion = 'v1.18.0'
|
||||
//Mocks
|
||||
coreMock.getInput = jest.fn().mockReturnValue(kubectlVersion);
|
||||
toolCacheMock.find = jest.fn().mockReturnValue(undefined);
|
||||
toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath');
|
||||
toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath');
|
||||
fileUtility.chmodSync = jest.fn();
|
||||
|
||||
//Invoke and assert
|
||||
await expect(action.run()).resolves.not.toThrow();
|
||||
expect(toolCacheMock.find).toBeCalledWith('kubectl', kubectlVersion);
|
||||
expect(toolCacheMock.downloadTool).toBeCalledWith(getkubectlDownloadURL(kubectlVersion));
|
||||
});
|
||||
|
||||
test("setKubectlPath() - install a latest version", async () => {
|
||||
const kubectlVersion = 'latest'
|
||||
//Mocks
|
||||
coreMock.getInput = jest.fn().mockReturnValue(kubectlVersion);
|
||||
jest.spyOn(fs, 'readFileSync').mockImplementation(() => "");
|
||||
toolCacheMock.find = jest.fn().mockReturnValue(undefined);
|
||||
toolCacheMock.downloadTool = jest.fn().mockResolvedValue('');
|
||||
toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath');
|
||||
fileUtility.chmodSync = jest.fn();
|
||||
|
||||
//Invoke and assert
|
||||
await expect(action.run()).resolves.not.toThrow();
|
||||
expect(toolCacheMock.find).toBeCalledWith('kubectl', kubectlVersion);
|
||||
expect(toolCacheMock.downloadTool).toBeCalledWith(stableVersionUrl);
|
||||
|
||||
});
|
||||
|
||||
test("setKubectlPath() - kubectl version already avilable", async () => {
|
||||
const kubectlVersion = 'v1.18.0'
|
||||
//Mock
|
||||
coreMock.getInput = jest.fn().mockReturnValue(kubectlVersion);
|
||||
toolCacheMock.find = jest.fn().mockReturnValue('validPath');
|
||||
toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath');
|
||||
toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath');
|
||||
fileUtility.chmodSync = jest.fn();
|
||||
|
||||
//Invoke and assert
|
||||
await expect(action.run()).resolves.not.toThrow();
|
||||
expect(toolCacheMock.find).toBeCalledWith('kubectl', kubectlVersion);
|
||||
expect(toolCacheMock.downloadTool).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("setKubectlPath() - kubectl version not provided and kubectl avilable on machine", async () => {
|
||||
//Mock
|
||||
coreMock.getInput = jest.fn().mockReturnValue(undefined);
|
||||
ioMock.which = jest.fn().mockReturnValue('validPath');
|
||||
|
||||
//Invoke and assert
|
||||
await expect(action.run()).resolves.not.toThrow();
|
||||
expect(ioMock.which).toBeCalledWith('kubectl', false);
|
||||
expect(toolCacheMock.downloadTool).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
test("setKubectlPath() - kubectl version not provided and kubectl not avilable on machine", async () => {
|
||||
//Mock
|
||||
coreMock.getInput = jest.fn().mockReturnValue(undefined);
|
||||
ioMock.which = jest.fn().mockReturnValue(undefined);
|
||||
toolCacheMock.findAllVersions = jest.fn().mockReturnValue(undefined);
|
||||
|
||||
//Invoke and assert
|
||||
await expect(action.run()).rejects.toThrowError();
|
||||
expect(ioMock.which).toBeCalledWith('kubectl', false);
|
||||
});
|
||||
|
||||
test("run() - action not provided", async () => {
|
||||
const kubectlVersion = 'v1.18.0'
|
||||
coreMock.getInput = jest.fn().mockImplementation((name) => {
|
||||
if (name == 'action') {
|
||||
return undefined;
|
||||
}
|
||||
return kubectlVersion;
|
||||
});
|
||||
coreMock.setFailed = jest.fn();
|
||||
//Mocks
|
||||
toolCacheMock.find = jest.fn().mockReturnValue(undefined);
|
||||
toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath');
|
||||
toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath');
|
||||
fileUtility.chmodSync = jest.fn();
|
||||
|
||||
//Invoke and assert
|
||||
await expect(action.run()).resolves.not.toThrow();
|
||||
expect(coreMock.setFailed).toBeCalledWith('Not a valid action. The allowed actions are deploy, promote, reject');
|
||||
});
|
||||
|
||||
test("run() - deploy - Manifiest not provided", async () => {
|
||||
//Mocks
|
||||
const kubectlVersion = 'v1.18.0'
|
||||
coreMock.getInput = jest.fn().mockImplementation((name) => {
|
||||
if (name == 'manifests') {
|
||||
return undefined;
|
||||
}
|
||||
if (name == 'action') {
|
||||
return 'deploy';
|
||||
}
|
||||
return kubectlVersion;
|
||||
});
|
||||
coreMock.setFailed = jest.fn();
|
||||
toolCacheMock.find = jest.fn().mockReturnValue(undefined);
|
||||
toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath');
|
||||
toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath');
|
||||
fileUtility.chmodSync = jest.fn();
|
||||
|
||||
//Invoke and assert
|
||||
await expect(action.run()).resolves.not.toThrow();
|
||||
expect(coreMock.setFailed).toBeCalledWith('No manifests supplied to deploy');
|
||||
});
|
||||
|
||||
test("deployment - deploy() - Invokes with no manifestfiles", async () => {
|
||||
const kubeCtl: jest.Mocked<Kubectl> = new Kubectl("") as any;
|
||||
|
||||
//Invoke and assert
|
||||
await expect(deployment.deploy(kubeCtl, [], undefined)).rejects.toThrowError('ManifestFileNotFound');
|
||||
});
|
||||
|
||||
test("run() - deploy", async () => {
|
||||
const kubectlVersion = 'v1.18.0'
|
||||
//Mocks
|
||||
coreMock.getInput = jest.fn().mockImplementation((name) => {
|
||||
if (name == 'manifests') {
|
||||
return 'manifests/deployment.yaml';
|
||||
}
|
||||
if (name == 'action') {
|
||||
return 'deploy';
|
||||
}
|
||||
if (name == 'strategy') {
|
||||
return undefined;
|
||||
}
|
||||
return kubectlVersion;
|
||||
});
|
||||
|
||||
coreMock.setFailed = jest.fn();
|
||||
toolCacheMock.find = jest.fn().mockReturnValue('validPath');
|
||||
toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath');
|
||||
toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath');
|
||||
fileUtility.chmodSync = jest.fn();
|
||||
const deploySpy = jest.spyOn(deployment, 'deploy').mockImplementation();
|
||||
|
||||
//Invoke and assert
|
||||
await expect(action.run()).resolves.not.toThrow();
|
||||
expect(deploySpy).toBeCalledWith({ "ignoreSSLErrors": false, "kubectlPath": 'validPath', "namespace": "v1.18.0" }, ['manifests/deployment.yaml'], undefined);
|
||||
deploySpy.mockRestore();
|
||||
});
|
||||
|
||||
test("deployment - deploy() - Invokes with manifestfiles", async () => {
|
||||
const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true);
|
||||
const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true);
|
||||
const kubeCtl: jest.Mocked<Kubectl> = new Kubectl("") as any;
|
||||
kubeCtl.apply = jest.fn().mockReturnValue("");
|
||||
KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources);
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock);
|
||||
kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock);
|
||||
kubeCtl.describe = jest.fn().mockReturnValue("");
|
||||
kubeCtl.annotateFiles = jest.fn().mockReturnValue("");
|
||||
kubeCtl.annotate = jest.fn().mockReturnValue("");
|
||||
kubeCtl.labelFiles = jest.fn().mockReturnValue("");
|
||||
KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue("");
|
||||
|
||||
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
|
||||
jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(getWorkflowsUrlResponse));
|
||||
|
||||
//Invoke and assert
|
||||
await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError();
|
||||
expect(readFileSpy).toBeCalledWith("manifests/deployment.yaml");
|
||||
expect(kubeCtl.getResource).toBeCalledWith("ingress", "AppName");
|
||||
});
|
||||
|
||||
test("deployment - deploy() - deploy force flag on", async () => {
|
||||
//Mocks
|
||||
inputParamMock.forceDeployment = true;
|
||||
const applyResMock = {
|
||||
'code': 0,
|
||||
'stderr': '',
|
||||
'error': Error(""),
|
||||
'stdout': 'changes configured'
|
||||
};
|
||||
const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true);
|
||||
const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true);
|
||||
const kubeCtl: jest.Mocked<Kubectl> = new Kubectl("") as any;
|
||||
KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources);
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock);
|
||||
kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock);
|
||||
kubeCtl.describe = jest.fn().mockReturnValue("");
|
||||
kubeCtl.annotateFiles = jest.fn().mockReturnValue("");
|
||||
kubeCtl.annotate = jest.fn().mockReturnValue("");
|
||||
kubeCtl.labelFiles = jest.fn().mockReturnValue("");
|
||||
KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue("");
|
||||
|
||||
const deploySpy = jest.spyOn(kubeCtl, 'apply').mockImplementation(() => applyResMock);
|
||||
jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(getWorkflowsUrlResponse));
|
||||
|
||||
//Invoke and assert
|
||||
await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError();
|
||||
expect(deploySpy).toBeCalledWith(expect.anything(), true);
|
||||
deploySpy.mockRestore();
|
||||
});
|
||||
|
||||
test("deployment - deploy() - Annotate & label resources", async () => {
|
||||
var deploymentConfig: utility.DeploymentConfig = { manifestFilePaths :['manifests/deployment.yaml'], helmChartFilePaths : [], dockerfilePaths : {} } ;
|
||||
let annotationKeyValStr = getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW) + '=' + getWorkflowAnnotationsJson('currentCommit', '.github/workflows/workflow.yml', deploymentConfig);
|
||||
const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true);
|
||||
KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue("");
|
||||
const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true);
|
||||
KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources);
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const fsMock = (mocked(fs, true));
|
||||
fileHelperMock.getTempDirectory = jest.fn().mockReturnValue("~/Deployment_testapp_currentTimestamp");
|
||||
fsMock.writeFileSync =jest.fn().mockReturnValue("");
|
||||
jest.spyOn(utility, 'getWorkflowFilePath').mockImplementation(() => Promise.resolve(process.env.GITHUB_WORKFLOW));
|
||||
jest.spyOn(utility, 'getDeploymentConfig').mockImplementation(()=> Promise.resolve(deploymentConfig));
|
||||
|
||||
const kubeCtl: jest.Mocked<Kubectl> = new Kubectl("") as any;
|
||||
kubeCtl.apply = jest.fn().mockReturnValue("");
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock);
|
||||
kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock);
|
||||
kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9");
|
||||
kubeCtl.annotateFiles = jest.fn().mockReturnValue("");
|
||||
kubeCtl.annotate = jest.fn().mockReturnValue("");
|
||||
kubeCtl.labelFiles = jest.fn();
|
||||
//Invoke and assert
|
||||
await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError();
|
||||
expect(kubeCtl.annotate).toHaveBeenNthCalledWith(1, 'namespace', 'default', annotationKeyValStr);
|
||||
expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp/deployment.yaml"], annotationKeyValStr);
|
||||
expect(kubeCtl.annotate).toBeCalledTimes(2);
|
||||
expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp/deployment.yaml"],
|
||||
[`workflowFriendlyName=workflow.yml`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`]);
|
||||
});
|
||||
|
||||
test("deployment - deploy() - Annotate & label resources for a new workflow", async () => {
|
||||
process.env.GITHUB_WORKFLOW = '.github/workflows/NewWorkflow.yml';
|
||||
var deploymentConfig: utility.DeploymentConfig = { manifestFilePaths :['manifests/deployment.yaml'], helmChartFilePaths : [], dockerfilePaths : {} } ;
|
||||
let annotationKeyValStr = getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW) + '=' + getWorkflowAnnotationsJson('NA', '.github/workflows/NewWorkflow.yml', deploymentConfig);
|
||||
const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true);
|
||||
KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue("");
|
||||
const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true);
|
||||
KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources);
|
||||
const fileHelperMock = mocked(fileHelper, true);
|
||||
const fsMock = (mocked(fs, true));
|
||||
fileHelperMock.getTempDirectory = jest.fn().mockReturnValue("~/Deployment_testapp_currentTimestamp");
|
||||
fsMock.writeFileSync =jest.fn().mockReturnValue("");
|
||||
jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(getWorkflowsUrlResponse));
|
||||
jest.spyOn(utility, 'getDeploymentConfig').mockImplementation(()=> Promise.resolve(deploymentConfig));
|
||||
|
||||
const kubeCtl: jest.Mocked<Kubectl> = new Kubectl("") as any;
|
||||
kubeCtl.apply = jest.fn().mockReturnValue("");
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock);
|
||||
kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock);
|
||||
kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9");
|
||||
kubeCtl.annotateFiles = jest.fn().mockReturnValue("");
|
||||
kubeCtl.annotate = jest.fn().mockReturnValue("");
|
||||
kubeCtl.labelFiles = jest.fn();
|
||||
//Invoke and assert
|
||||
await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError();
|
||||
expect(kubeCtl.annotate).toHaveBeenNthCalledWith(1, 'namespace', 'default', annotationKeyValStr);
|
||||
expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp/deployment.yaml"], annotationKeyValStr);
|
||||
expect(kubeCtl.annotate).toBeCalledTimes(2);
|
||||
expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp/deployment.yaml"],
|
||||
[`workflowFriendlyName=NewWorkflow.yml`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`]);
|
||||
});
|
||||
|
||||
test("deployment - deploy() - Annotate resources failed", async () => {
|
||||
//Mocks
|
||||
inputParamMock.forceDeployment = true;
|
||||
const annotateMock = {
|
||||
'code': 1,
|
||||
'stderr': 'kubectl annotate failed',
|
||||
'error': Error(""),
|
||||
'stdout': ''
|
||||
};
|
||||
const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true);
|
||||
const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true);
|
||||
const kubeCtl: jest.Mocked<Kubectl> = new Kubectl("") as any;
|
||||
KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources);
|
||||
kubeCtl.apply = jest.fn().mockReturnValue("");
|
||||
kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock);
|
||||
kubeCtl.getAllPods = jest.fn().mockReturnValue(getAllPodsMock);
|
||||
kubeCtl.describe = jest.fn().mockReturnValue("");
|
||||
kubeCtl.annotateFiles = jest.fn().mockReturnValue("");
|
||||
kubeCtl.annotate = jest.fn().mockReturnValue(annotateMock);
|
||||
kubeCtl.labelFiles = jest.fn().mockReturnValue("");
|
||||
KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue("");
|
||||
|
||||
const consoleOutputSpy = jest.spyOn(process.stdout, "write").mockImplementation();
|
||||
//Invoke and assert
|
||||
await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError();
|
||||
expect(consoleOutputSpy).toHaveBeenNthCalledWith(1, '::warning::kubectl annotate failed' + os.EOL)
|
||||
});
|
||||
|
||||
test("utility - getWorkflowFilePath() - Get workflow file path under API failure", async () => {
|
||||
//Mocks
|
||||
const errorWebResponse = {
|
||||
'statusCode': httpClient.StatusCodes.UNAUTHORIZED,
|
||||
'body': {}
|
||||
} as httpClient.WebResponse
|
||||
jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(errorWebResponse));
|
||||
|
||||
//Invoke and assert
|
||||
await expect(utility.getWorkflowFilePath(process.env.GITHUB_TOKEN)).resolves.not.toThrowError;
|
||||
await expect(utility.getWorkflowFilePath(process.env.GITHUB_TOKEN)).resolves.toBe(process.env.GITHUB_WORKFLOW);
|
||||
});
|
||||
+80
-62
@@ -1,62 +1,80 @@
|
||||
name: 'Deploy to Kubernetes cluster'
|
||||
description: 'Deploy to a Kubernetes cluster including, but not limited to Azure Kubernetes Service (AKS) clusters'
|
||||
inputs:
|
||||
# Please ensure you have used either azure/k8s-actions/aks-set-context or azure/k8s-actions/k8s-set-context in the workflow before this action
|
||||
namespace:
|
||||
description: 'Choose the target Kubernetes namespace. If the namespace is not provided, the commands will run in the default namespace.'
|
||||
required: false
|
||||
manifests:
|
||||
description: 'Path to the manifest files which will be used for deployment.'
|
||||
required: true
|
||||
default: ''
|
||||
images:
|
||||
description: 'Fully qualified resource URL of the image(s) to be used for substitutions on the manifest files Example: contosodemo.azurecr.io/helloworld:test'
|
||||
required: false
|
||||
imagepullsecrets:
|
||||
description: 'Name of a docker-registry secret that has already been set up within the cluster. Each of these secret names are added under imagePullSecrets field for the workloads found in the input manifest files'
|
||||
required: false
|
||||
kubectl-version:
|
||||
description: 'Version of kubectl. Installs a specific version of kubectl binary'
|
||||
required: false
|
||||
strategy:
|
||||
description: 'Deployment strategy to be used. Allowed values are none, canary and blue-green'
|
||||
required: false
|
||||
default: 'none'
|
||||
route-method:
|
||||
description: 'Route based on service, ingress or SMI for blue-green strategy'
|
||||
required: false
|
||||
default: 'service'
|
||||
version-switch-buffer:
|
||||
description: 'Indicates the buffer time in minutes before the switch is made to the green version (max is 300 min ie. 5hrs)'
|
||||
required: false
|
||||
default: 0
|
||||
traffic-split-method:
|
||||
description: "Traffic split method to be used. Allowed values are pod, smi"
|
||||
required: false
|
||||
default: 'pod'
|
||||
baseline-and-canary-replicas:
|
||||
description: 'Baseline and canary replicas count; valid value i.e between 0 to 100.'
|
||||
required: false
|
||||
default: 0
|
||||
percentage:
|
||||
description: 'Percentage of traffic redirect to canary deployment'
|
||||
required: false
|
||||
default: 0
|
||||
action:
|
||||
description: 'deploy/promote/reject'
|
||||
required: true
|
||||
default: 'deploy'
|
||||
force:
|
||||
description: 'Deploy when a previous deployment already exists. If true then --force argument is added to the apply command'
|
||||
required: false
|
||||
default: false
|
||||
token:
|
||||
description: 'Github token'
|
||||
default: ${{ github.token }}
|
||||
required: true
|
||||
|
||||
branding:
|
||||
color: 'green' # optional, decorates the entry in the GitHub Marketplace
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'lib/run.js'
|
||||
name: 'Deploy to Kubernetes cluster'
|
||||
description: 'Deploy to a Kubernetes cluster including, but not limited to Azure Kubernetes Service (AKS) clusters'
|
||||
inputs:
|
||||
# Please ensure you have used either azure/k8s-actions/aks-set-context or azure/k8s-actions/k8s-set-context in the workflow before this action
|
||||
# You also need to have kubectl installed (azure/setup-kubectl)
|
||||
namespace:
|
||||
description: 'Choose the target Kubernetes namespace. If the namespace is not provided, the commands will run in the default namespace.'
|
||||
required: false
|
||||
manifests:
|
||||
description: 'Path to the manifest files which will be used for deployment.'
|
||||
required: true
|
||||
images:
|
||||
description: 'Fully qualified resource URL of the image(s) to be used for substitutions on the manifest files Example: contosodemo.azurecr.io/helloworld:test'
|
||||
required: false
|
||||
imagepullsecrets:
|
||||
description: 'Name of a docker-registry secret that has already been set up within the cluster. Each of these secret names are added under imagePullSecrets field for the workloads found in the input manifest files'
|
||||
required: false
|
||||
pull-images:
|
||||
description: "Switch whether to pull the images from the registry before deployment to find out Dockerfile's path in order to add it to the annotations"
|
||||
required: false
|
||||
default: true
|
||||
strategy:
|
||||
description: 'Deployment strategy to be used. Allowed values are basic, canary and blue-green'
|
||||
required: true
|
||||
default: 'basic'
|
||||
route-method:
|
||||
description: 'Route based on service, ingress or SMI for blue-green strategy'
|
||||
required: false
|
||||
default: 'service'
|
||||
version-switch-buffer:
|
||||
description: 'Indicates the buffer time in minutes before the switch is made to the green version (max is 300 min ie. 5hrs)'
|
||||
required: false
|
||||
default: 0
|
||||
traffic-split-method:
|
||||
description: 'Traffic split method to be used. Allowed values are pod and smi'
|
||||
required: false
|
||||
default: 'pod'
|
||||
traffic-split-annotations:
|
||||
description: 'Annotations in the form of key/value pair to be added to TrafficSplit. Relevant only if deployement strategy is blue-green or canary'
|
||||
required: false
|
||||
baseline-and-canary-replicas:
|
||||
description: 'Baseline and canary replicas count. Valid value between 0 to 100 (inclusive)'
|
||||
required: false
|
||||
default: ''
|
||||
percentage:
|
||||
description: 'Percentage of traffic redirect to canary deployment'
|
||||
required: false
|
||||
default: 0
|
||||
action:
|
||||
description: 'deploy, promote, or reject'
|
||||
required: true
|
||||
default: 'deploy'
|
||||
force:
|
||||
description: 'Deploy when a previous deployment already exists. If true then --force argument is added to the apply command'
|
||||
required: false
|
||||
default: false
|
||||
token:
|
||||
description: 'Github token'
|
||||
default: ${{ github.token }}
|
||||
required: true
|
||||
annotate-namespace:
|
||||
description: 'Annotate the target namespace'
|
||||
required: false
|
||||
default: true
|
||||
private-cluster:
|
||||
description: 'True if cluster is AKS private cluster'
|
||||
required: false
|
||||
default: false
|
||||
resource-group:
|
||||
description: 'Name of resource group - Only required if using private cluster'
|
||||
required: false
|
||||
name:
|
||||
description: 'Resource group name - Only required if using private cluster'
|
||||
required: false
|
||||
|
||||
branding:
|
||||
color: 'green'
|
||||
runs:
|
||||
using: 'node16'
|
||||
main: 'lib/index.js'
|
||||
|
||||
+9
-8
@@ -1,10 +1,11 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
transform: {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
},
|
||||
verbose: true,
|
||||
testTimeout: 9000
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.promote = void 0;
|
||||
const core = require("@actions/core");
|
||||
const deploymentHelper = require("../utilities/strategy-helpers/deployment-helper");
|
||||
const canaryDeploymentHelper = require("../utilities/strategy-helpers/canary-deployment-helper");
|
||||
const SMICanaryDeploymentHelper = require("../utilities/strategy-helpers/smi-canary-deployment-helper");
|
||||
const utils = require("../utilities/manifest-utilities");
|
||||
const TaskInputParameters = require("../input-parameters");
|
||||
const manifest_utilities_1 = require("../utilities/manifest-utilities");
|
||||
const KubernetesObjectUtility = require("../utilities/resource-object-utility");
|
||||
const models = require("../constants");
|
||||
const KubernetesManifestUtility = require("../utilities/manifest-stability-utility");
|
||||
const blue_green_helper_1 = require("../utilities/strategy-helpers/blue-green-helper");
|
||||
const blue_green_helper_2 = require("../utilities/strategy-helpers/blue-green-helper");
|
||||
const service_blue_green_helper_1 = require("../utilities/strategy-helpers/service-blue-green-helper");
|
||||
const ingress_blue_green_helper_1 = require("../utilities/strategy-helpers/ingress-blue-green-helper");
|
||||
const smi_blue_green_helper_1 = require("../utilities/strategy-helpers/smi-blue-green-helper");
|
||||
const kubectl_object_model_1 = require("../kubectl-object-model");
|
||||
function promote() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const kubectl = new kubectl_object_model_1.Kubectl(yield utils.getKubectl(), TaskInputParameters.namespace, true);
|
||||
if (canaryDeploymentHelper.isCanaryDeploymentStrategy()) {
|
||||
yield promoteCanary(kubectl);
|
||||
}
|
||||
else if (blue_green_helper_2.isBlueGreenDeploymentStrategy()) {
|
||||
yield promoteBlueGreen(kubectl);
|
||||
}
|
||||
else {
|
||||
core.debug('Strategy is not canary or blue-green deployment. Invalid request.');
|
||||
throw ('InvalidPromotetActionDeploymentStrategy');
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.promote = promote;
|
||||
function promoteCanary(kubectl) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let includeServices = false;
|
||||
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||
includeServices = true;
|
||||
// In case of SMI traffic split strategy when deployment is promoted, first we will redirect traffic to
|
||||
// Canary deployment, then update stable deployment and then redirect traffic to stable deployment
|
||||
core.debug('Redirecting traffic to canary deployment');
|
||||
SMICanaryDeploymentHelper.redirectTrafficToCanaryDeployment(kubectl, TaskInputParameters.manifests);
|
||||
core.debug('Deploying input manifests with SMI canary strategy');
|
||||
yield deploymentHelper.deploy(kubectl, TaskInputParameters.manifests, 'None');
|
||||
core.debug('Redirecting traffic to stable deployment');
|
||||
SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(kubectl, TaskInputParameters.manifests);
|
||||
}
|
||||
else {
|
||||
core.debug('Deploying input manifests');
|
||||
yield deploymentHelper.deploy(kubectl, TaskInputParameters.manifests, 'None');
|
||||
}
|
||||
core.debug('Deployment strategy selected is Canary. Deleting canary and baseline workloads.');
|
||||
try {
|
||||
canaryDeploymentHelper.deleteCanaryDeployment(kubectl, TaskInputParameters.manifests, includeServices);
|
||||
}
|
||||
catch (ex) {
|
||||
core.warning('Exception occurred while deleting canary and baseline workloads. Exception: ' + ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
function promoteBlueGreen(kubectl) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// updated container images and pull secrets
|
||||
let inputManifestFiles = manifest_utilities_1.getUpdatedManifestFiles(TaskInputParameters.manifests);
|
||||
const manifestObjects = blue_green_helper_1.getManifestObjects(inputManifestFiles);
|
||||
core.debug('deleting old deployment and making new ones');
|
||||
let result;
|
||||
if (blue_green_helper_2.isIngressRoute()) {
|
||||
result = yield ingress_blue_green_helper_1.promoteBlueGreenIngress(kubectl, manifestObjects);
|
||||
}
|
||||
else if (blue_green_helper_2.isSMIRoute()) {
|
||||
result = yield smi_blue_green_helper_1.promoteBlueGreenSMI(kubectl, manifestObjects);
|
||||
}
|
||||
else {
|
||||
result = yield service_blue_green_helper_1.promoteBlueGreenService(kubectl, manifestObjects);
|
||||
}
|
||||
// checking stability of newly created deployments
|
||||
const deployedManifestFiles = result.newFilePaths;
|
||||
const resources = KubernetesObjectUtility.getResources(deployedManifestFiles, models.deploymentTypes.concat([models.DiscoveryAndLoadBalancerResource.service]));
|
||||
yield KubernetesManifestUtility.checkManifestStability(kubectl, resources);
|
||||
core.debug('routing to new deployments');
|
||||
if (blue_green_helper_2.isIngressRoute()) {
|
||||
ingress_blue_green_helper_1.routeBlueGreenIngress(kubectl, null, manifestObjects.serviceNameMap, manifestObjects.ingressEntityList);
|
||||
blue_green_helper_1.deleteWorkloadsAndServicesWithLabel(kubectl, blue_green_helper_2.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList, manifestObjects.serviceEntityList);
|
||||
}
|
||||
else if (blue_green_helper_2.isSMIRoute()) {
|
||||
smi_blue_green_helper_1.routeBlueGreenSMI(kubectl, blue_green_helper_2.NONE_LABEL_VALUE, manifestObjects.serviceEntityList);
|
||||
blue_green_helper_1.deleteWorkloadsWithLabel(kubectl, blue_green_helper_2.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList);
|
||||
smi_blue_green_helper_1.cleanupSMI(kubectl, manifestObjects.serviceEntityList);
|
||||
}
|
||||
else {
|
||||
service_blue_green_helper_1.routeBlueGreenService(kubectl, blue_green_helper_2.NONE_LABEL_VALUE, manifestObjects.serviceEntityList);
|
||||
blue_green_helper_1.deleteWorkloadsWithLabel(kubectl, blue_green_helper_2.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.reject = void 0;
|
||||
const core = require("@actions/core");
|
||||
const canaryDeploymentHelper = require("../utilities/strategy-helpers/canary-deployment-helper");
|
||||
const SMICanaryDeploymentHelper = require("../utilities/strategy-helpers/smi-canary-deployment-helper");
|
||||
const kubectl_object_model_1 = require("../kubectl-object-model");
|
||||
const utils = require("../utilities/manifest-utilities");
|
||||
const TaskInputParameters = require("../input-parameters");
|
||||
const service_blue_green_helper_1 = require("../utilities/strategy-helpers/service-blue-green-helper");
|
||||
const ingress_blue_green_helper_1 = require("../utilities/strategy-helpers/ingress-blue-green-helper");
|
||||
const smi_blue_green_helper_1 = require("../utilities/strategy-helpers/smi-blue-green-helper");
|
||||
const blue_green_helper_1 = require("../utilities/strategy-helpers/blue-green-helper");
|
||||
const deployment_helper_1 = require("../utilities/strategy-helpers/deployment-helper");
|
||||
function reject() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const kubectl = new kubectl_object_model_1.Kubectl(yield utils.getKubectl(), TaskInputParameters.namespace, true);
|
||||
if (canaryDeploymentHelper.isCanaryDeploymentStrategy()) {
|
||||
yield rejectCanary(kubectl);
|
||||
}
|
||||
else if (blue_green_helper_1.isBlueGreenDeploymentStrategy()) {
|
||||
yield rejectBlueGreen(kubectl);
|
||||
}
|
||||
else {
|
||||
core.debug('Strategy is not canary or blue-green deployment. Invalid request.');
|
||||
throw ('InvalidDeletetActionDeploymentStrategy');
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.reject = reject;
|
||||
function rejectCanary(kubectl) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let includeServices = false;
|
||||
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||
core.debug('Reject deployment with SMI canary strategy');
|
||||
includeServices = true;
|
||||
SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(kubectl, TaskInputParameters.manifests);
|
||||
}
|
||||
core.debug('Deployment strategy selected is Canary. Deleting baseline and canary workloads.');
|
||||
canaryDeploymentHelper.deleteCanaryDeployment(kubectl, TaskInputParameters.manifests, includeServices);
|
||||
});
|
||||
}
|
||||
function rejectBlueGreen(kubectl) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let inputManifestFiles = deployment_helper_1.getManifestFiles(TaskInputParameters.manifests);
|
||||
if (blue_green_helper_1.isIngressRoute()) {
|
||||
yield ingress_blue_green_helper_1.rejectBlueGreenIngress(kubectl, inputManifestFiles);
|
||||
}
|
||||
else if (blue_green_helper_1.isSMIRoute()) {
|
||||
yield smi_blue_green_helper_1.rejectBlueGreenSMI(kubectl, inputManifestFiles);
|
||||
}
|
||||
else {
|
||||
yield service_blue_green_helper_1.rejectBlueGreenService(kubectl, inputManifestFiles);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getWorkflowAnnotationKeyLabel = exports.getWorkflowAnnotationsJson = exports.workloadTypesWithRolloutStatus = exports.workloadTypes = exports.deploymentTypes = exports.ServiceTypes = exports.DiscoveryAndLoadBalancerResource = exports.KubernetesWorkload = void 0;
|
||||
class KubernetesWorkload {
|
||||
}
|
||||
exports.KubernetesWorkload = KubernetesWorkload;
|
||||
KubernetesWorkload.pod = 'Pod';
|
||||
KubernetesWorkload.replicaset = 'Replicaset';
|
||||
KubernetesWorkload.deployment = 'Deployment';
|
||||
KubernetesWorkload.statefulSet = 'StatefulSet';
|
||||
KubernetesWorkload.daemonSet = 'DaemonSet';
|
||||
KubernetesWorkload.job = 'job';
|
||||
KubernetesWorkload.cronjob = 'cronjob';
|
||||
class DiscoveryAndLoadBalancerResource {
|
||||
}
|
||||
exports.DiscoveryAndLoadBalancerResource = DiscoveryAndLoadBalancerResource;
|
||||
DiscoveryAndLoadBalancerResource.service = 'service';
|
||||
DiscoveryAndLoadBalancerResource.ingress = 'ingress';
|
||||
class ServiceTypes {
|
||||
}
|
||||
exports.ServiceTypes = ServiceTypes;
|
||||
ServiceTypes.loadBalancer = 'LoadBalancer';
|
||||
ServiceTypes.nodePort = 'NodePort';
|
||||
ServiceTypes.clusterIP = 'ClusterIP';
|
||||
exports.deploymentTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset'];
|
||||
exports.workloadTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob'];
|
||||
exports.workloadTypesWithRolloutStatus = ['deployment', 'daemonset', 'statefulset'];
|
||||
function getWorkflowAnnotationsJson(lastSuccessRunSha, workflowFilePath, deploymentConfig) {
|
||||
let annotationObject = {};
|
||||
annotationObject["run"] = process.env.GITHUB_RUN_ID;
|
||||
annotationObject["repository"] = process.env.GITHUB_REPOSITORY;
|
||||
annotationObject["workflow"] = process.env.GITHUB_WORKFLOW;
|
||||
annotationObject["workflowFileName"] = workflowFilePath.replace(".github/workflows/", "");
|
||||
annotationObject["jobName"] = process.env.GITHUB_JOB;
|
||||
annotationObject["createdBy"] = process.env.GITHUB_ACTOR;
|
||||
annotationObject["runUri"] = `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||
annotationObject["commit"] = process.env.GITHUB_SHA;
|
||||
annotationObject["lastSuccessRunCommit"] = lastSuccessRunSha;
|
||||
annotationObject["branch"] = process.env.GITHUB_REF;
|
||||
annotationObject["deployTimestamp"] = Date.now();
|
||||
annotationObject["dockerfilePaths"] = deploymentConfig.dockerfilePaths;
|
||||
annotationObject["manifestsPaths"] = deploymentConfig.manifestFilePaths;
|
||||
annotationObject["helmChartPaths"] = deploymentConfig.helmChartFilePaths;
|
||||
annotationObject["provider"] = "GitHub";
|
||||
return JSON.stringify(annotationObject);
|
||||
}
|
||||
exports.getWorkflowAnnotationsJson = getWorkflowAnnotationsJson;
|
||||
function getWorkflowAnnotationKeyLabel(workflowFilePath) {
|
||||
const hashKey = require("crypto").createHash("MD5")
|
||||
.update(`${process.env.GITHUB_REPOSITORY}/${workflowFilePath}`)
|
||||
.digest("hex");
|
||||
return `githubWorkflow_${hashKey}`;
|
||||
}
|
||||
exports.getWorkflowAnnotationKeyLabel = getWorkflowAnnotationKeyLabel;
|
||||
@@ -1,31 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DockerExec = void 0;
|
||||
const tool_runner_1 = require("./utilities/tool-runner");
|
||||
class DockerExec {
|
||||
constructor(dockerPath) {
|
||||
this.dockerPath = dockerPath;
|
||||
}
|
||||
;
|
||||
pull(image, args, silent) {
|
||||
args = ['pull', image, ...args];
|
||||
let result = this.execute(args, silent);
|
||||
if (result.stderr != '' && result.code != 0) {
|
||||
throw new Error(`docker images pull failed with: ${result.error}`);
|
||||
}
|
||||
}
|
||||
inspect(image, args, silent) {
|
||||
args = ['inspect', image, ...args];
|
||||
let result = this.execute(args, silent);
|
||||
if (result.stderr != '' && result.code != 0) {
|
||||
throw new Error(`docker inspect call failed with: ${result.error}`);
|
||||
}
|
||||
return result.stdout;
|
||||
}
|
||||
execute(args, silent) {
|
||||
const command = new tool_runner_1.ToolRunner(this.dockerPath);
|
||||
command.arg(args);
|
||||
return command.execSync({ silent: !!silent });
|
||||
}
|
||||
}
|
||||
exports.DockerExec = DockerExec;
|
||||
@@ -1,35 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GitHubClient = void 0;
|
||||
const core = require("@actions/core");
|
||||
const httpClient_1 = require("./utilities/httpClient");
|
||||
class GitHubClient {
|
||||
constructor(repository, token) {
|
||||
this._repository = repository;
|
||||
this._token = token;
|
||||
}
|
||||
getWorkflows() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const getWorkflowFileNameUrl = `https://api.github.com/repos/${this._repository}/actions/workflows`;
|
||||
const webRequest = new httpClient_1.WebRequest();
|
||||
webRequest.method = "GET";
|
||||
webRequest.uri = getWorkflowFileNameUrl;
|
||||
webRequest.headers = {
|
||||
Authorization: `Bearer ${this._token}`
|
||||
};
|
||||
core.debug(`Getting workflows for repo: ${this._repository}`);
|
||||
const response = yield httpClient_1.sendRequest(webRequest);
|
||||
return Promise.resolve(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.GitHubClient = GitHubClient;
|
||||
+23694
File diff suppressed because one or more lines are too long
@@ -1,56 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.githubToken = exports.forceDeployment = exports.baselineAndCanaryReplicas = exports.versionSwitchBuffer = exports.routeMethod = exports.trafficSplitMethod = exports.deploymentStrategy = exports.canaryPercentage = exports.manifests = exports.imagePullSecrets = exports.containers = exports.namespace = void 0;
|
||||
const core = require("@actions/core");
|
||||
exports.namespace = core.getInput('namespace');
|
||||
exports.containers = core.getInput('images').split('\n');
|
||||
exports.imagePullSecrets = core.getInput('imagepullsecrets').split('\n').filter(secret => secret.trim().length > 0);
|
||||
exports.manifests = core.getInput('manifests').split('\n');
|
||||
exports.canaryPercentage = core.getInput('percentage');
|
||||
exports.deploymentStrategy = core.getInput('strategy');
|
||||
exports.trafficSplitMethod = core.getInput('traffic-split-method');
|
||||
exports.routeMethod = core.getInput('route-method');
|
||||
exports.versionSwitchBuffer = core.getInput('version-switch-buffer');
|
||||
exports.baselineAndCanaryReplicas = core.getInput('baseline-and-canary-replicas');
|
||||
exports.forceDeployment = core.getInput('force').toLowerCase() == 'true';
|
||||
exports.githubToken = core.getInput("token");
|
||||
if (!exports.namespace) {
|
||||
core.debug('Namespace was not supplied; using "default" namespace instead.');
|
||||
exports.namespace = 'default';
|
||||
}
|
||||
if (!exports.githubToken) {
|
||||
core.error("'token' input is not supplied. Set it to a PAT/GITHUB_TOKEN");
|
||||
}
|
||||
try {
|
||||
const pe = parseInt(exports.canaryPercentage);
|
||||
if (pe < 0 || pe > 100) {
|
||||
core.setFailed('A valid percentage value is between 0 and 100');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
core.setFailed("Enter a valid 'percentage' integer value ");
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
const pe = parseInt(exports.baselineAndCanaryReplicas);
|
||||
if (pe < 0 || pe > 100) {
|
||||
core.setFailed('A valid baseline-and-canary-replicas value is between 0 and 100');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
core.setFailed("Enter a valid 'baseline-and-canary-replicas' integer value");
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
const pe = parseInt(exports.versionSwitchBuffer);
|
||||
if (pe < 0 || pe > 300) {
|
||||
core.setFailed('Invalid buffer time, valid version-switch-buffer is a value more than or equal to 0 and lesser than or equal 300');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
core.setFailed("Enter a valid 'version-switch-buffer' integer value");
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Kubectl = void 0;
|
||||
const tool_runner_1 = require("./utilities/tool-runner");
|
||||
class Kubectl {
|
||||
constructor(kubectlPath, namespace, ignoreSSLErrors) {
|
||||
this.kubectlPath = kubectlPath;
|
||||
this.ignoreSSLErrors = !!ignoreSSLErrors;
|
||||
if (!!namespace) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
else {
|
||||
this.namespace = 'default';
|
||||
}
|
||||
}
|
||||
apply(configurationPaths, force) {
|
||||
let applyArgs = ['apply', '-f', this.createInlineArray(configurationPaths)];
|
||||
if (!!force) {
|
||||
console.log("force flag is on, deployment will continue even if previous deployment already exists");
|
||||
applyArgs.push('--force');
|
||||
}
|
||||
return this.execute(applyArgs);
|
||||
}
|
||||
describe(resourceType, resourceName, silent) {
|
||||
return this.execute(['describe', resourceType, resourceName], silent);
|
||||
}
|
||||
getNewReplicaSet(deployment) {
|
||||
let newReplicaSet = '';
|
||||
const result = this.describe('deployment', deployment, true);
|
||||
if (result && result.stdout) {
|
||||
const stdout = result.stdout.split('\n');
|
||||
stdout.forEach((line) => {
|
||||
if (!!line && line.toLowerCase().indexOf('newreplicaset') > -1) {
|
||||
newReplicaSet = line.substr(14).trim().split(' ')[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
return newReplicaSet;
|
||||
}
|
||||
annotate(resourceType, resourceName, annotation) {
|
||||
let args = ['annotate', resourceType, resourceName];
|
||||
args.push(annotation);
|
||||
args.push(`--overwrite`);
|
||||
return this.execute(args);
|
||||
}
|
||||
annotateFiles(files, annotation) {
|
||||
let args = ['annotate'];
|
||||
args = args.concat(['-f', this.createInlineArray(files)]);
|
||||
args.push(annotation);
|
||||
args.push(`--overwrite`);
|
||||
return this.execute(args);
|
||||
}
|
||||
labelFiles(files, labels) {
|
||||
let args = ['label'];
|
||||
args = args.concat(['-f', this.createInlineArray(files)]);
|
||||
args = args.concat(labels);
|
||||
args.push(`--overwrite`);
|
||||
return this.execute(args);
|
||||
}
|
||||
getAllPods() {
|
||||
return this.execute(['get', 'pods', '-o', 'json'], true);
|
||||
}
|
||||
getClusterInfo() {
|
||||
return this.execute(['cluster-info'], true);
|
||||
}
|
||||
checkRolloutStatus(resourceType, name) {
|
||||
return this.execute(['rollout', 'status', resourceType + '/' + name]);
|
||||
}
|
||||
getResource(resourceType, name) {
|
||||
return this.execute(['get', resourceType + '/' + name, '-o', 'json']);
|
||||
}
|
||||
getResources(applyOutput, filterResourceTypes) {
|
||||
const outputLines = applyOutput.split('\n');
|
||||
const results = [];
|
||||
outputLines.forEach(line => {
|
||||
const words = line.split(' ');
|
||||
if (words.length > 2) {
|
||||
const resourceType = words[0].trim();
|
||||
const resourceName = JSON.parse(words[1].trim());
|
||||
if (filterResourceTypes.filter(type => !!type && resourceType.toLowerCase().startsWith(type.toLowerCase())).length > 0) {
|
||||
results.push({
|
||||
type: resourceType,
|
||||
name: resourceName
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
executeCommand(customCommand, args) {
|
||||
if (!customCommand)
|
||||
throw new Error('NullCommandForKubectl');
|
||||
return args ? this.execute([customCommand, args]) : this.execute([customCommand]);
|
||||
}
|
||||
delete(args) {
|
||||
if (typeof args === 'string')
|
||||
return this.execute(['delete', args]);
|
||||
else
|
||||
return this.execute(['delete'].concat(args));
|
||||
}
|
||||
execute(args, silent) {
|
||||
if (this.ignoreSSLErrors) {
|
||||
args.push('--insecure-skip-tls-verify');
|
||||
}
|
||||
args = args.concat(['--namespace', this.namespace]);
|
||||
const command = new tool_runner_1.ToolRunner(this.kubectlPath);
|
||||
command.arg(args);
|
||||
return command.execSync({ silent: !!silent });
|
||||
}
|
||||
createInlineArray(str) {
|
||||
if (typeof str === 'string') {
|
||||
return str;
|
||||
}
|
||||
return str.join(',');
|
||||
}
|
||||
}
|
||||
exports.Kubectl = Kubectl;
|
||||
@@ -1,80 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const util = require("util");
|
||||
const fs = require("fs");
|
||||
const toolCache = require("@actions/tool-cache");
|
||||
const core = require("@actions/core");
|
||||
const kubectlToolName = 'kubectl';
|
||||
const stableKubectlVersion = 'v1.15.0';
|
||||
const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/release/stable.txt';
|
||||
function getExecutableExtension() {
|
||||
if (os.type().match(/^Win/)) {
|
||||
return '.exe';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function getkubectlDownloadURL(version) {
|
||||
switch (os.type()) {
|
||||
case 'Linux':
|
||||
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/linux/amd64/kubectl', version);
|
||||
case 'Darwin':
|
||||
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/darwin/amd64/kubectl', version);
|
||||
case 'Windows_NT':
|
||||
default:
|
||||
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/windows/amd64/kubectl.exe', version);
|
||||
}
|
||||
}
|
||||
function getStableKubectlVersion() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
return toolCache.downloadTool(stableVersionUrl).then((downloadPath) => {
|
||||
let version = fs.readFileSync(downloadPath, 'utf8').toString().trim();
|
||||
if (!version) {
|
||||
version = stableKubectlVersion;
|
||||
}
|
||||
return version;
|
||||
}, (error) => {
|
||||
core.debug(error);
|
||||
core.warning('GetStableVersionFailed');
|
||||
return stableKubectlVersion;
|
||||
});
|
||||
});
|
||||
}
|
||||
exports.getStableKubectlVersion = getStableKubectlVersion;
|
||||
function downloadKubectl(version) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let cachedToolpath = toolCache.find(kubectlToolName, version);
|
||||
let kubectlDownloadPath = '';
|
||||
if (!cachedToolpath) {
|
||||
try {
|
||||
kubectlDownloadPath = yield toolCache.downloadTool(getkubectlDownloadURL(version));
|
||||
}
|
||||
catch (exception) {
|
||||
throw new Error('DownloadKubectlFailed');
|
||||
}
|
||||
cachedToolpath = yield toolCache.cacheFile(kubectlDownloadPath, kubectlToolName + getExecutableExtension(), kubectlToolName, version);
|
||||
}
|
||||
const kubectlPath = path.join(cachedToolpath, kubectlToolName + getExecutableExtension());
|
||||
fs.chmodSync(kubectlPath, '777');
|
||||
return kubectlPath;
|
||||
});
|
||||
}
|
||||
exports.downloadKubectl = downloadKubectl;
|
||||
function getTrafficSplitAPIVersion(kubectl) {
|
||||
const result = kubectl.executeCommand('api-versions');
|
||||
const trafficSplitAPIVersion = result.stdout.split('\n').find(version => version.startsWith('split.smi-spec.io'));
|
||||
if (trafficSplitAPIVersion == null || typeof trafficSplitAPIVersion == 'undefined') {
|
||||
throw new Error('UnableToCreateTrafficSplitManifestFile');
|
||||
}
|
||||
return trafficSplitAPIVersion;
|
||||
}
|
||||
exports.getTrafficSplitAPIVersion = getTrafficSplitAPIVersion;
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.run = void 0;
|
||||
const core = require("@actions/core");
|
||||
const io = require("@actions/io");
|
||||
const path = require("path");
|
||||
const toolCache = require("@actions/tool-cache");
|
||||
const kubectl_util_1 = require("./utilities/kubectl-util");
|
||||
const utility_1 = require("./utilities/utility");
|
||||
const kubectl_object_model_1 = require("./kubectl-object-model");
|
||||
const deployment_helper_1 = require("./utilities/strategy-helpers/deployment-helper");
|
||||
const promote_1 = require("./actions/promote");
|
||||
const reject_1 = require("./actions/reject");
|
||||
let kubectlPath = "";
|
||||
function setKubectlPath() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (core.getInput('kubectl-version')) {
|
||||
const version = core.getInput('kubectl-version');
|
||||
kubectlPath = toolCache.find('kubectl', version);
|
||||
if (!kubectlPath) {
|
||||
kubectlPath = yield installKubectl(version);
|
||||
}
|
||||
}
|
||||
else {
|
||||
kubectlPath = yield io.which('kubectl', false);
|
||||
if (!kubectlPath) {
|
||||
const allVersions = toolCache.findAllVersions('kubectl');
|
||||
kubectlPath = allVersions.length > 0 ? toolCache.find('kubectl', allVersions[0]) : '';
|
||||
if (!kubectlPath) {
|
||||
throw new Error('Kubectl is not installed, either add install-kubectl action or provide "kubectl-version" input to download kubectl');
|
||||
}
|
||||
kubectlPath = path.join(kubectlPath, `kubectl${utility_1.getExecutableExtension()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function installKubectl(version) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (utility_1.isEqual(version, 'latest')) {
|
||||
version = yield kubectl_util_1.getStableKubectlVersion();
|
||||
}
|
||||
return yield kubectl_util_1.downloadKubectl(version);
|
||||
});
|
||||
}
|
||||
function checkClusterContext() {
|
||||
if (!process.env["KUBECONFIG"]) {
|
||||
core.warning('KUBECONFIG env is not explicitly set. Ensure cluster context is set by using k8s-set-context / aks-set-context action.');
|
||||
}
|
||||
}
|
||||
function run() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
checkClusterContext();
|
||||
yield setKubectlPath();
|
||||
let manifestsInput = core.getInput('manifests');
|
||||
if (!manifestsInput) {
|
||||
core.setFailed('No manifests supplied to deploy');
|
||||
return;
|
||||
}
|
||||
let namespace = core.getInput('namespace');
|
||||
if (!namespace) {
|
||||
namespace = 'default';
|
||||
}
|
||||
let action = core.getInput('action');
|
||||
let manifests = manifestsInput.split('\n');
|
||||
if (action === 'deploy') {
|
||||
let strategy = core.getInput('strategy');
|
||||
console.log("strategy: ", strategy);
|
||||
yield deployment_helper_1.deploy(new kubectl_object_model_1.Kubectl(kubectlPath, namespace), manifests, strategy);
|
||||
}
|
||||
else if (action === 'promote') {
|
||||
yield promote_1.promote();
|
||||
}
|
||||
else if (action === 'reject') {
|
||||
yield reject_1.reject();
|
||||
}
|
||||
else {
|
||||
core.setFailed('Not a valid action. The allowed actions are deploy, promote, reject');
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.run = run;
|
||||
run().catch(core.setFailed);
|
||||
@@ -1,78 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.writeManifestToFile = exports.writeObjectsToFile = exports.assertFileExists = exports.ensureDirExists = exports.getNewUserDirPath = exports.getTempDirectory = void 0;
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const core = require("@actions/core");
|
||||
const os = require("os");
|
||||
function getTempDirectory() {
|
||||
return process.env['runner.tempDirectory'] || os.tmpdir();
|
||||
}
|
||||
exports.getTempDirectory = getTempDirectory;
|
||||
function getNewUserDirPath() {
|
||||
let userDir = path.join(getTempDirectory(), 'kubectlTask');
|
||||
ensureDirExists(userDir);
|
||||
userDir = path.join(userDir, getCurrentTime().toString());
|
||||
ensureDirExists(userDir);
|
||||
return userDir;
|
||||
}
|
||||
exports.getNewUserDirPath = getNewUserDirPath;
|
||||
function ensureDirExists(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath);
|
||||
}
|
||||
}
|
||||
exports.ensureDirExists = ensureDirExists;
|
||||
function assertFileExists(path) {
|
||||
if (!fs.existsSync(path)) {
|
||||
core.error(`FileNotFoundException : ${path}`);
|
||||
throw new Error(`FileNotFoundException: ${path}`);
|
||||
}
|
||||
}
|
||||
exports.assertFileExists = assertFileExists;
|
||||
function writeObjectsToFile(inputObjects) {
|
||||
const newFilePaths = [];
|
||||
if (!!inputObjects) {
|
||||
inputObjects.forEach((inputObject) => {
|
||||
try {
|
||||
const inputObjectString = JSON.stringify(inputObject);
|
||||
if (!!inputObject.kind && !!inputObject.metadata && !!inputObject.metadata.name) {
|
||||
const fileName = getManifestFileName(inputObject.kind, inputObject.metadata.name);
|
||||
fs.writeFileSync(path.join(fileName), inputObjectString);
|
||||
newFilePaths.push(fileName);
|
||||
}
|
||||
else {
|
||||
core.debug('Input object is not proper K8s resource object. Object: ' + inputObjectString);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
core.debug('Exception occurred while writing object to file : ' + inputObject + ' . Exception: ' + ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
return newFilePaths;
|
||||
}
|
||||
exports.writeObjectsToFile = writeObjectsToFile;
|
||||
function writeManifestToFile(inputObjectString, kind, name) {
|
||||
if (inputObjectString) {
|
||||
try {
|
||||
const fileName = getManifestFileName(kind, name);
|
||||
fs.writeFileSync(path.join(fileName), inputObjectString);
|
||||
return fileName;
|
||||
}
|
||||
catch (ex) {
|
||||
core.debug('Exception occurred while writing object to file : ' + inputObjectString + ' . Exception: ' + ex);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
exports.writeManifestToFile = writeManifestToFile;
|
||||
function getManifestFileName(kind, name) {
|
||||
const filePath = kind + '_' + name + '_' + getCurrentTime().toString();
|
||||
const tempDirectory = getTempDirectory();
|
||||
const fileName = path.join(tempDirectory, path.basename(filePath));
|
||||
return fileName;
|
||||
}
|
||||
function getCurrentTime() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.sleepFor = exports.sendRequest = exports.WebRequestOptions = exports.WebResponse = exports.WebRequest = exports.StatusCodes = void 0;
|
||||
// Taken from https://github.com/Azure/aks-set-context/blob/master/src/client.ts
|
||||
const util = require("util");
|
||||
const fs = require("fs");
|
||||
const httpClient = require("typed-rest-client/HttpClient");
|
||||
const core = require("@actions/core");
|
||||
var httpCallbackClient = new httpClient.HttpClient('GITHUB_RUNNER', null, {});
|
||||
var StatusCodes;
|
||||
(function (StatusCodes) {
|
||||
StatusCodes[StatusCodes["OK"] = 200] = "OK";
|
||||
StatusCodes[StatusCodes["CREATED"] = 201] = "CREATED";
|
||||
StatusCodes[StatusCodes["ACCEPTED"] = 202] = "ACCEPTED";
|
||||
StatusCodes[StatusCodes["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
|
||||
StatusCodes[StatusCodes["NOT_FOUND"] = 404] = "NOT_FOUND";
|
||||
StatusCodes[StatusCodes["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
|
||||
StatusCodes[StatusCodes["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
|
||||
})(StatusCodes = exports.StatusCodes || (exports.StatusCodes = {}));
|
||||
class WebRequest {
|
||||
}
|
||||
exports.WebRequest = WebRequest;
|
||||
class WebResponse {
|
||||
}
|
||||
exports.WebResponse = WebResponse;
|
||||
class WebRequestOptions {
|
||||
}
|
||||
exports.WebRequestOptions = WebRequestOptions;
|
||||
function sendRequest(request, options) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let i = 0;
|
||||
let retryCount = options && options.retryCount ? options.retryCount : 5;
|
||||
let retryIntervalInSeconds = options && options.retryIntervalInSeconds ? options.retryIntervalInSeconds : 2;
|
||||
let retriableErrorCodes = options && options.retriableErrorCodes ? options.retriableErrorCodes : ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ESOCKETTIMEDOUT", "ECONNREFUSED", "EHOSTUNREACH", "EPIPE", "EA_AGAIN"];
|
||||
let retriableStatusCodes = options && options.retriableStatusCodes ? options.retriableStatusCodes : [408, 409, 500, 502, 503, 504];
|
||||
let timeToWait = retryIntervalInSeconds;
|
||||
while (true) {
|
||||
try {
|
||||
if (request.body && typeof (request.body) !== 'string' && !request.body["readable"]) {
|
||||
request.body = fs.createReadStream(request.body["path"]);
|
||||
}
|
||||
let response = yield sendRequestInternal(request);
|
||||
if (retriableStatusCodes.indexOf(response.statusCode) != -1 && ++i < retryCount) {
|
||||
core.debug(util.format("Encountered a retriable status code: %s. Message: '%s'.", response.statusCode, response.statusMessage));
|
||||
yield sleepFor(timeToWait);
|
||||
timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds;
|
||||
continue;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
catch (error) {
|
||||
if (retriableErrorCodes.indexOf(error.code) != -1 && ++i < retryCount) {
|
||||
core.debug(util.format("Encountered a retriable error:%s. Message: %s.", error.code, error.message));
|
||||
yield sleepFor(timeToWait);
|
||||
timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds;
|
||||
}
|
||||
else {
|
||||
if (error.code) {
|
||||
core.debug("error code =" + error.code);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.sendRequest = sendRequest;
|
||||
function sleepFor(sleepDurationInSeconds) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, sleepDurationInSeconds * 1000);
|
||||
});
|
||||
}
|
||||
exports.sleepFor = sleepFor;
|
||||
function sendRequestInternal(request) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
core.debug(util.format("[%s]%s", request.method, request.uri));
|
||||
var response = yield httpCallbackClient.request(request.method, request.uri, request.body, request.headers);
|
||||
return yield toWebResponse(response);
|
||||
});
|
||||
}
|
||||
function toWebResponse(response) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
var res = new WebResponse();
|
||||
if (response) {
|
||||
res.statusCode = response.message.statusCode;
|
||||
res.statusMessage = response.message.statusMessage;
|
||||
res.headers = response.message.headers;
|
||||
var body = yield response.readBody();
|
||||
if (body) {
|
||||
try {
|
||||
res.body = JSON.parse(body);
|
||||
}
|
||||
catch (error) {
|
||||
core.debug("Could not parse response: " + JSON.stringify(error));
|
||||
core.debug("Response: " + JSON.stringify(res.body));
|
||||
res.body = body;
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getTrafficSplitAPIVersion = exports.downloadKubectl = exports.getStableKubectlVersion = exports.getkubectlDownloadURL = void 0;
|
||||
const core = require("@actions/core");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const toolCache = require("@actions/tool-cache");
|
||||
const util = require("util");
|
||||
const kubectlToolName = 'kubectl';
|
||||
const stableKubectlVersion = 'v1.15.0';
|
||||
const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/release/stable.txt';
|
||||
const trafficSplitAPIVersionPrefix = 'split.smi-spec.io';
|
||||
function getExecutableExtension() {
|
||||
if (os.type().match(/^Win/)) {
|
||||
return '.exe';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function getkubectlDownloadURL(version) {
|
||||
switch (os.type()) {
|
||||
case 'Linux':
|
||||
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/linux/amd64/kubectl', version);
|
||||
case 'Darwin':
|
||||
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/darwin/amd64/kubectl', version);
|
||||
case 'Windows_NT':
|
||||
default:
|
||||
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/windows/amd64/kubectl.exe', version);
|
||||
}
|
||||
}
|
||||
exports.getkubectlDownloadURL = getkubectlDownloadURL;
|
||||
function getStableKubectlVersion() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
return toolCache.downloadTool(stableVersionUrl).then((downloadPath) => {
|
||||
let version = fs.readFileSync(downloadPath, 'utf8').toString().trim();
|
||||
if (!version) {
|
||||
version = stableKubectlVersion;
|
||||
}
|
||||
return version;
|
||||
}, (error) => {
|
||||
core.debug(error);
|
||||
core.warning('GetStableVersionFailed');
|
||||
return stableKubectlVersion;
|
||||
});
|
||||
});
|
||||
}
|
||||
exports.getStableKubectlVersion = getStableKubectlVersion;
|
||||
function downloadKubectl(version) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let cachedToolpath = toolCache.find(kubectlToolName, version);
|
||||
let kubectlDownloadPath = '';
|
||||
if (!cachedToolpath) {
|
||||
try {
|
||||
kubectlDownloadPath = yield toolCache.downloadTool(getkubectlDownloadURL(version));
|
||||
}
|
||||
catch (exception) {
|
||||
throw new Error('DownloadKubectlFailed');
|
||||
}
|
||||
cachedToolpath = yield toolCache.cacheFile(kubectlDownloadPath, kubectlToolName + getExecutableExtension(), kubectlToolName, version);
|
||||
}
|
||||
const kubectlPath = path.join(cachedToolpath, kubectlToolName + getExecutableExtension());
|
||||
fs.chmodSync(kubectlPath, '777');
|
||||
return kubectlPath;
|
||||
});
|
||||
}
|
||||
exports.downloadKubectl = downloadKubectl;
|
||||
function getTrafficSplitAPIVersion(kubectl) {
|
||||
const result = kubectl.executeCommand('api-versions');
|
||||
const trafficSplitAPIVersion = result.stdout.split('\n').find(version => version.startsWith(trafficSplitAPIVersionPrefix));
|
||||
if (!trafficSplitAPIVersion) {
|
||||
throw new Error('UnableToCreateTrafficSplitManifestFile');
|
||||
}
|
||||
return trafficSplitAPIVersion;
|
||||
}
|
||||
exports.getTrafficSplitAPIVersion = getTrafficSplitAPIVersion;
|
||||
@@ -1,158 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.checkPodStatus = exports.checkManifestStability = void 0;
|
||||
const core = require("@actions/core");
|
||||
const utils = require("./utility");
|
||||
const KubernetesConstants = require("../constants");
|
||||
function checkManifestStability(kubectl, resources) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let rolloutStatusHasErrors = false;
|
||||
const numberOfResources = resources.length;
|
||||
for (let i = 0; i < numberOfResources; i++) {
|
||||
const resource = resources[i];
|
||||
if (KubernetesConstants.workloadTypesWithRolloutStatus.indexOf(resource.type.toLowerCase()) >= 0) {
|
||||
try {
|
||||
var result = kubectl.checkRolloutStatus(resource.type, resource.name);
|
||||
utils.checkForErrors([result]);
|
||||
}
|
||||
catch (ex) {
|
||||
core.error(ex);
|
||||
kubectl.describe(resource.type, resource.name);
|
||||
rolloutStatusHasErrors = true;
|
||||
}
|
||||
}
|
||||
if (utils.isEqual(resource.type, KubernetesConstants.KubernetesWorkload.pod, true)) {
|
||||
try {
|
||||
yield checkPodStatus(kubectl, resource.name);
|
||||
}
|
||||
catch (ex) {
|
||||
core.warning(`CouldNotDeterminePodStatus ${JSON.stringify(ex)}`);
|
||||
kubectl.describe(resource.type, resource.name);
|
||||
}
|
||||
}
|
||||
if (utils.isEqual(resource.type, KubernetesConstants.DiscoveryAndLoadBalancerResource.service, true)) {
|
||||
try {
|
||||
const service = getService(kubectl, resource.name);
|
||||
const spec = service.spec;
|
||||
const status = service.status;
|
||||
if (utils.isEqual(spec.type, KubernetesConstants.ServiceTypes.loadBalancer, true)) {
|
||||
if (!isLoadBalancerIPAssigned(status)) {
|
||||
yield waitForServiceExternalIPAssignment(kubectl, resource.name);
|
||||
}
|
||||
else {
|
||||
console.log('ServiceExternalIP', resource.name, status.loadBalancer.ingress[0].ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
core.warning(`CouldNotDetermineServiceStatus of: ${resource.name} Error: ${JSON.stringify(ex)}`);
|
||||
kubectl.describe(resource.type, resource.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rolloutStatusHasErrors) {
|
||||
throw new Error('RolloutStatusTimedout');
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.checkManifestStability = checkManifestStability;
|
||||
function checkPodStatus(kubectl, podName) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const sleepTimeout = 10 * 1000; // 10 seconds
|
||||
const iterations = 60; // 60 * 10 seconds timeout = 10 minutes max timeout
|
||||
let podStatus;
|
||||
let kubectlDescribeNeeded = false;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
yield utils.sleep(sleepTimeout);
|
||||
core.debug(`Polling for pod status: ${podName}`);
|
||||
podStatus = getPodStatus(kubectl, podName);
|
||||
if (podStatus.phase && podStatus.phase !== 'Pending' && podStatus.phase !== 'Unknown') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
podStatus = getPodStatus(kubectl, podName);
|
||||
switch (podStatus.phase) {
|
||||
case 'Succeeded':
|
||||
case 'Running':
|
||||
if (isPodReady(podStatus)) {
|
||||
console.log(`pod/${podName} is successfully rolled out`);
|
||||
}
|
||||
else {
|
||||
kubectlDescribeNeeded = true;
|
||||
}
|
||||
break;
|
||||
case 'Pending':
|
||||
if (!isPodReady(podStatus)) {
|
||||
core.warning(`pod/${podName} rollout status check timedout`);
|
||||
kubectlDescribeNeeded = true;
|
||||
}
|
||||
break;
|
||||
case 'Failed':
|
||||
core.error(`pod/${podName} rollout failed`);
|
||||
kubectlDescribeNeeded = true;
|
||||
break;
|
||||
default:
|
||||
core.warning(`pod/${podName} rollout status: ${podStatus.phase}`);
|
||||
}
|
||||
if (kubectlDescribeNeeded) {
|
||||
kubectl.describe('pod', podName);
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.checkPodStatus = checkPodStatus;
|
||||
function getPodStatus(kubectl, podName) {
|
||||
const podResult = kubectl.getResource('pod', podName);
|
||||
utils.checkForErrors([podResult]);
|
||||
const podStatus = JSON.parse(podResult.stdout).status;
|
||||
core.debug(`Pod Status: ${JSON.stringify(podStatus)}`);
|
||||
return podStatus;
|
||||
}
|
||||
function isPodReady(podStatus) {
|
||||
let allContainersAreReady = true;
|
||||
podStatus.containerStatuses.forEach(container => {
|
||||
if (container.ready === false) {
|
||||
console.log(`'${container.name}' status: ${JSON.stringify(container.state)}`);
|
||||
allContainersAreReady = false;
|
||||
}
|
||||
});
|
||||
if (!allContainersAreReady) {
|
||||
core.warning('AllContainersNotInReadyState');
|
||||
}
|
||||
return allContainersAreReady;
|
||||
}
|
||||
function getService(kubectl, serviceName) {
|
||||
const serviceResult = kubectl.getResource(KubernetesConstants.DiscoveryAndLoadBalancerResource.service, serviceName);
|
||||
utils.checkForErrors([serviceResult]);
|
||||
return JSON.parse(serviceResult.stdout);
|
||||
}
|
||||
function waitForServiceExternalIPAssignment(kubectl, serviceName) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const sleepTimeout = 10 * 1000; // 10 seconds
|
||||
const iterations = 18; // 18 * 10 seconds timeout = 3 minutes max timeout
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
console.log(`waitForServiceIpAssignment : ${serviceName}`);
|
||||
yield utils.sleep(sleepTimeout);
|
||||
let status = (getService(kubectl, serviceName)).status;
|
||||
if (isLoadBalancerIPAssigned(status)) {
|
||||
console.log('ServiceExternalIP', serviceName, status.loadBalancer.ingress[0].ip);
|
||||
return;
|
||||
}
|
||||
}
|
||||
core.warning(`waitForServiceIpAssignmentTimedOut ${serviceName}`);
|
||||
});
|
||||
}
|
||||
function isLoadBalancerIPAssigned(status) {
|
||||
if (status && status.loadBalancer && status.loadBalancer.ingress && status.loadBalancer.ingress.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isWorkloadEntity = exports.getUpdatedManifestFiles = exports.updateImagePullSecrets = exports.substituteImageNameInSpecFile = exports.getDeleteCmdArgs = exports.createKubectlArgs = exports.getKubectl = exports.getManifestFiles = void 0;
|
||||
const core = require("@actions/core");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const path = require("path");
|
||||
const kubectlutility = require("./kubectl-util");
|
||||
const io = require("@actions/io");
|
||||
const utility_1 = require("./utility");
|
||||
const fileHelper = require("./files-helper");
|
||||
const KubernetesObjectUtility = require("./resource-object-utility");
|
||||
const TaskInputParameters = require("../input-parameters");
|
||||
function getManifestFiles(manifestFilePaths) {
|
||||
if (!manifestFilePaths) {
|
||||
core.debug('file input is not present');
|
||||
return null;
|
||||
}
|
||||
return manifestFilePaths;
|
||||
}
|
||||
exports.getManifestFiles = getManifestFiles;
|
||||
function getKubectl() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
try {
|
||||
return Promise.resolve(io.which('kubectl', true));
|
||||
}
|
||||
catch (ex) {
|
||||
return kubectlutility.downloadKubectl(yield kubectlutility.getStableKubectlVersion());
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.getKubectl = getKubectl;
|
||||
function createKubectlArgs(kinds, names) {
|
||||
let args = '';
|
||||
if (!!kinds && kinds.size > 0) {
|
||||
args = args + createInlineArray(Array.from(kinds.values()));
|
||||
}
|
||||
if (!!names && names.size > 0) {
|
||||
args = args + ' ' + Array.from(names.values()).join(' ');
|
||||
}
|
||||
return args;
|
||||
}
|
||||
exports.createKubectlArgs = createKubectlArgs;
|
||||
function getDeleteCmdArgs(argsPrefix, inputArgs) {
|
||||
let args = '';
|
||||
if (!!argsPrefix && argsPrefix.length > 0) {
|
||||
args = argsPrefix;
|
||||
}
|
||||
if (!!inputArgs && inputArgs.length > 0) {
|
||||
if (args.length > 0) {
|
||||
args = args + ' ';
|
||||
}
|
||||
args = args + inputArgs;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
exports.getDeleteCmdArgs = getDeleteCmdArgs;
|
||||
/*
|
||||
For example,
|
||||
currentString: `image: "example/example-image"`
|
||||
imageName: `example/example-image`
|
||||
imageNameWithNewTag: `example/example-image:identifiertag`
|
||||
|
||||
This substituteImageNameInSpecFile function would return
|
||||
return Value: `image: "example/example-image:identifiertag"`
|
||||
*/
|
||||
function substituteImageNameInSpecFile(currentString, imageName, imageNameWithNewTag) {
|
||||
if (currentString.indexOf(imageName) < 0) {
|
||||
core.debug(`No occurence of replacement token: ${imageName} found`);
|
||||
return currentString;
|
||||
}
|
||||
return currentString.split('\n').reduce((acc, line) => {
|
||||
const imageKeyword = line.match(/^ *image:/);
|
||||
if (imageKeyword) {
|
||||
let [currentImageName, currentImageTag] = line
|
||||
.substring(imageKeyword[0].length) // consume the line from keyword onwards
|
||||
.trim()
|
||||
.replace(/[',"]/g, '') // replace allowed quotes with nothing
|
||||
.split(':');
|
||||
if (!currentImageTag && currentImageName.indexOf(' ') > 0) {
|
||||
currentImageName = currentImageName.split(' ')[0]; // Stripping off comments
|
||||
}
|
||||
if (currentImageName === imageName) {
|
||||
return acc + `${imageKeyword[0]} ${imageNameWithNewTag}\n`;
|
||||
}
|
||||
}
|
||||
return acc + line + '\n';
|
||||
}, '');
|
||||
}
|
||||
exports.substituteImageNameInSpecFile = substituteImageNameInSpecFile;
|
||||
function createInlineArray(str) {
|
||||
if (typeof str === 'string') {
|
||||
return str;
|
||||
}
|
||||
return str.join(',');
|
||||
}
|
||||
function getImagePullSecrets(inputObject) {
|
||||
if (!inputObject || !inputObject.spec) {
|
||||
return;
|
||||
}
|
||||
if (utility_1.isEqual(inputObject.kind, 'pod')
|
||||
&& inputObject
|
||||
&& inputObject.spec
|
||||
&& inputObject.spec.imagePullSecrets) {
|
||||
return inputObject.spec.imagePullSecrets;
|
||||
}
|
||||
else if (utility_1.isEqual(inputObject.kind, 'cronjob')
|
||||
&& inputObject
|
||||
&& inputObject.spec
|
||||
&& inputObject.spec.jobTemplate
|
||||
&& inputObject.spec.jobTemplate.spec
|
||||
&& inputObject.spec.jobTemplate.spec.template
|
||||
&& inputObject.spec.jobTemplate.spec.template.spec
|
||||
&& inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets) {
|
||||
return inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets;
|
||||
}
|
||||
else if (inputObject
|
||||
&& inputObject.spec
|
||||
&& inputObject.spec.template
|
||||
&& inputObject.spec.template.spec
|
||||
&& inputObject.spec.template.spec.imagePullSecrets) {
|
||||
return inputObject.spec.template.spec.imagePullSecrets;
|
||||
}
|
||||
}
|
||||
function setImagePullSecrets(inputObject, newImagePullSecrets) {
|
||||
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
|
||||
return;
|
||||
}
|
||||
if (utility_1.isEqual(inputObject.kind, 'pod')) {
|
||||
if (inputObject
|
||||
&& inputObject.spec) {
|
||||
if (newImagePullSecrets.length > 0) {
|
||||
inputObject.spec.imagePullSecrets = newImagePullSecrets;
|
||||
}
|
||||
else {
|
||||
delete inputObject.spec.imagePullSecrets;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (utility_1.isEqual(inputObject.kind, 'cronjob')) {
|
||||
if (inputObject
|
||||
&& inputObject.spec
|
||||
&& inputObject.spec.jobTemplate
|
||||
&& inputObject.spec.jobTemplate.spec
|
||||
&& inputObject.spec.jobTemplate.spec.template
|
||||
&& inputObject.spec.jobTemplate.spec.template.spec) {
|
||||
if (newImagePullSecrets.length > 0) {
|
||||
inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets = newImagePullSecrets;
|
||||
}
|
||||
else {
|
||||
delete inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!!inputObject.spec.template && !!inputObject.spec.template.spec) {
|
||||
if (inputObject
|
||||
&& inputObject.spec
|
||||
&& inputObject.spec.template
|
||||
&& inputObject.spec.template.spec) {
|
||||
if (newImagePullSecrets.length > 0) {
|
||||
inputObject.spec.template.spec.imagePullSecrets = newImagePullSecrets;
|
||||
}
|
||||
else {
|
||||
delete inputObject.spec.template.spec.imagePullSecrets;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function substituteImageNameInSpecContent(currentString, imageName, imageNameWithNewTag) {
|
||||
if (currentString.indexOf(imageName) < 0) {
|
||||
core.debug(`No occurence of replacement token: ${imageName} found`);
|
||||
return currentString;
|
||||
}
|
||||
return currentString.split('\n').reduce((acc, line) => {
|
||||
const imageKeyword = line.match(/^ *image:/);
|
||||
if (imageKeyword) {
|
||||
const [currentImageName, currentImageTag] = line
|
||||
.substring(imageKeyword[0].length) // consume the line from keyword onwards
|
||||
.trim()
|
||||
.replace(/[',"]/g, '') // replace allowed quotes with nothing
|
||||
.split(':');
|
||||
if (currentImageName === imageName) {
|
||||
return acc + `${imageKeyword[0]} ${imageNameWithNewTag}\n`;
|
||||
}
|
||||
}
|
||||
return acc + line + '\n';
|
||||
}, '');
|
||||
}
|
||||
function updateContainerImagesInManifestFiles(filePaths, containers) {
|
||||
if (!!containers && containers.length > 0) {
|
||||
const newFilePaths = [];
|
||||
const tempDirectory = fileHelper.getTempDirectory();
|
||||
filePaths.forEach((filePath) => {
|
||||
let contents = fs.readFileSync(filePath).toString();
|
||||
containers.forEach((container) => {
|
||||
let imageName = container.split(':')[0];
|
||||
if (imageName.indexOf('@') > 0) {
|
||||
imageName = imageName.split('@')[0];
|
||||
}
|
||||
if (contents.indexOf(imageName) > 0) {
|
||||
contents = substituteImageNameInSpecFile(contents, imageName, container);
|
||||
}
|
||||
});
|
||||
const fileName = path.join(tempDirectory, path.basename(filePath));
|
||||
fs.writeFileSync(path.join(fileName), contents);
|
||||
newFilePaths.push(fileName);
|
||||
});
|
||||
return newFilePaths;
|
||||
}
|
||||
return filePaths;
|
||||
}
|
||||
function updateImagePullSecrets(inputObject, newImagePullSecrets) {
|
||||
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
|
||||
return;
|
||||
}
|
||||
let newImagePullSecretsObjects;
|
||||
if (newImagePullSecrets.length > 0) {
|
||||
newImagePullSecretsObjects = Array.from(newImagePullSecrets, x => { return !!x ? { 'name': x } : null; });
|
||||
}
|
||||
else {
|
||||
newImagePullSecretsObjects = [];
|
||||
}
|
||||
let existingImagePullSecretObjects = getImagePullSecrets(inputObject);
|
||||
if (!existingImagePullSecretObjects) {
|
||||
existingImagePullSecretObjects = new Array();
|
||||
}
|
||||
existingImagePullSecretObjects = existingImagePullSecretObjects.concat(newImagePullSecretsObjects);
|
||||
setImagePullSecrets(inputObject, existingImagePullSecretObjects);
|
||||
}
|
||||
exports.updateImagePullSecrets = updateImagePullSecrets;
|
||||
function updateImagePullSecretsInManifestFiles(filePaths, imagePullSecrets) {
|
||||
if (!!imagePullSecrets && imagePullSecrets.length > 0) {
|
||||
const newObjectsList = [];
|
||||
filePaths.forEach((filePath) => {
|
||||
const fileContents = fs.readFileSync(filePath).toString();
|
||||
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||
if (!!inputObject && !!inputObject.kind) {
|
||||
const kind = inputObject.kind;
|
||||
if (KubernetesObjectUtility.isWorkloadEntity(kind)) {
|
||||
KubernetesObjectUtility.updateImagePullSecrets(inputObject, imagePullSecrets, false);
|
||||
}
|
||||
newObjectsList.push(inputObject);
|
||||
}
|
||||
});
|
||||
});
|
||||
core.debug('New K8s objects after adding imagePullSecrets are :' + JSON.stringify(newObjectsList));
|
||||
const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
return newFilePaths;
|
||||
}
|
||||
return filePaths;
|
||||
}
|
||||
function getUpdatedManifestFiles(manifestFilePaths) {
|
||||
let inputManifestFiles = getManifestFiles(manifestFilePaths);
|
||||
if (!inputManifestFiles || inputManifestFiles.length === 0) {
|
||||
throw new Error(`ManifestFileNotFound : ${manifestFilePaths}`);
|
||||
}
|
||||
// artifact substitution
|
||||
inputManifestFiles = updateContainerImagesInManifestFiles(inputManifestFiles, TaskInputParameters.containers);
|
||||
// imagePullSecrets addition
|
||||
inputManifestFiles = updateImagePullSecretsInManifestFiles(inputManifestFiles, TaskInputParameters.imagePullSecrets);
|
||||
return inputManifestFiles;
|
||||
}
|
||||
exports.getUpdatedManifestFiles = getUpdatedManifestFiles;
|
||||
const workloadTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob'];
|
||||
function isWorkloadEntity(kind) {
|
||||
if (!kind) {
|
||||
core.debug('ResourceKindNotDefined');
|
||||
return false;
|
||||
}
|
||||
return workloadTypes.some((type) => {
|
||||
return utility_1.isEqual(type, kind);
|
||||
});
|
||||
}
|
||||
exports.isWorkloadEntity = isWorkloadEntity;
|
||||
@@ -1,277 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getResources = exports.updateSelectorLabels = exports.updateSpecLabels = exports.updateImagePullSecrets = exports.updateObjectAnnotations = exports.updateObjectLabels = exports.getReplicaCount = exports.isIngressEntity = exports.isServiceEntity = exports.isWorkloadEntity = exports.isDeploymentEntity = void 0;
|
||||
const fs = require("fs");
|
||||
const core = require("@actions/core");
|
||||
const yaml = require("js-yaml");
|
||||
const constants_1 = require("../constants");
|
||||
const string_comparison_1 = require("./string-comparison");
|
||||
const INGRESS = "Ingress";
|
||||
function isDeploymentEntity(kind) {
|
||||
if (!kind) {
|
||||
throw ('ResourceKindNotDefined');
|
||||
}
|
||||
return constants_1.deploymentTypes.some((type) => {
|
||||
return string_comparison_1.isEqual(type, kind, string_comparison_1.StringComparer.OrdinalIgnoreCase);
|
||||
});
|
||||
}
|
||||
exports.isDeploymentEntity = isDeploymentEntity;
|
||||
function isWorkloadEntity(kind) {
|
||||
if (!kind) {
|
||||
throw ('ResourceKindNotDefined');
|
||||
}
|
||||
return constants_1.workloadTypes.some((type) => {
|
||||
return string_comparison_1.isEqual(type, kind, string_comparison_1.StringComparer.OrdinalIgnoreCase);
|
||||
});
|
||||
}
|
||||
exports.isWorkloadEntity = isWorkloadEntity;
|
||||
function isServiceEntity(kind) {
|
||||
if (!kind) {
|
||||
throw ('ResourceKindNotDefined');
|
||||
}
|
||||
return string_comparison_1.isEqual("Service", kind, string_comparison_1.StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
exports.isServiceEntity = isServiceEntity;
|
||||
function isIngressEntity(kind) {
|
||||
if (!kind) {
|
||||
throw ('ResourceKindNotDefined');
|
||||
}
|
||||
return string_comparison_1.isEqual(INGRESS, kind, string_comparison_1.StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
exports.isIngressEntity = isIngressEntity;
|
||||
function getReplicaCount(inputObject) {
|
||||
if (!inputObject) {
|
||||
throw ('NullInputObject');
|
||||
}
|
||||
if (!inputObject.kind) {
|
||||
throw ('ResourceKindNotDefined');
|
||||
}
|
||||
const kind = inputObject.kind;
|
||||
if (!string_comparison_1.isEqual(kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase) && !string_comparison_1.isEqual(kind, constants_1.KubernetesWorkload.daemonSet, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||
return inputObject.spec.replicas;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
exports.getReplicaCount = getReplicaCount;
|
||||
function updateObjectLabels(inputObject, newLabels, override) {
|
||||
if (!inputObject) {
|
||||
throw ('NullInputObject');
|
||||
}
|
||||
if (!inputObject.metadata) {
|
||||
throw ('NullInputObjectMetadata');
|
||||
}
|
||||
if (!newLabels) {
|
||||
return;
|
||||
}
|
||||
if (override) {
|
||||
inputObject.metadata.labels = newLabels;
|
||||
}
|
||||
else {
|
||||
let existingLabels = inputObject.metadata.labels;
|
||||
if (!existingLabels) {
|
||||
existingLabels = new Map();
|
||||
}
|
||||
Object.keys(newLabels).forEach(function (key) {
|
||||
existingLabels[key] = newLabels[key];
|
||||
});
|
||||
inputObject.metadata.labels = existingLabels;
|
||||
}
|
||||
}
|
||||
exports.updateObjectLabels = updateObjectLabels;
|
||||
function updateObjectAnnotations(inputObject, newAnnotations, override) {
|
||||
if (!inputObject) {
|
||||
throw ('NullInputObject');
|
||||
}
|
||||
if (!inputObject.metadata) {
|
||||
throw ('NullInputObjectMetadata');
|
||||
}
|
||||
if (!newAnnotations) {
|
||||
return;
|
||||
}
|
||||
if (override) {
|
||||
inputObject.metadata.annotations = newAnnotations;
|
||||
}
|
||||
else {
|
||||
let existingAnnotations = inputObject.metadata.annotations;
|
||||
if (!existingAnnotations) {
|
||||
existingAnnotations = new Map();
|
||||
}
|
||||
Object.keys(newAnnotations).forEach(function (key) {
|
||||
existingAnnotations[key] = newAnnotations[key];
|
||||
});
|
||||
inputObject.metadata.annotations = existingAnnotations;
|
||||
}
|
||||
}
|
||||
exports.updateObjectAnnotations = updateObjectAnnotations;
|
||||
function updateImagePullSecrets(inputObject, newImagePullSecrets, override) {
|
||||
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
|
||||
return;
|
||||
}
|
||||
const newImagePullSecretsObjects = Array.from(newImagePullSecrets, x => { return { 'name': x }; });
|
||||
let existingImagePullSecretObjects = getImagePullSecrets(inputObject);
|
||||
if (override) {
|
||||
existingImagePullSecretObjects = newImagePullSecretsObjects;
|
||||
}
|
||||
else {
|
||||
if (!existingImagePullSecretObjects) {
|
||||
existingImagePullSecretObjects = new Array();
|
||||
}
|
||||
existingImagePullSecretObjects = existingImagePullSecretObjects.concat(newImagePullSecretsObjects);
|
||||
}
|
||||
setImagePullSecrets(inputObject, existingImagePullSecretObjects);
|
||||
}
|
||||
exports.updateImagePullSecrets = updateImagePullSecrets;
|
||||
function updateSpecLabels(inputObject, newLabels, override) {
|
||||
if (!inputObject) {
|
||||
throw ('NullInputObject');
|
||||
}
|
||||
if (!inputObject.kind) {
|
||||
throw ('ResourceKindNotDefined');
|
||||
}
|
||||
if (!newLabels) {
|
||||
return;
|
||||
}
|
||||
let existingLabels = getSpecLabels(inputObject);
|
||||
if (override) {
|
||||
existingLabels = newLabels;
|
||||
}
|
||||
else {
|
||||
if (!existingLabels) {
|
||||
existingLabels = new Map();
|
||||
}
|
||||
Object.keys(newLabels).forEach(function (key) {
|
||||
existingLabels[key] = newLabels[key];
|
||||
});
|
||||
}
|
||||
setSpecLabels(inputObject, existingLabels);
|
||||
}
|
||||
exports.updateSpecLabels = updateSpecLabels;
|
||||
function updateSelectorLabels(inputObject, newLabels, override) {
|
||||
if (!inputObject) {
|
||||
throw ('NullInputObject');
|
||||
}
|
||||
if (!inputObject.kind) {
|
||||
throw ('ResourceKindNotDefined');
|
||||
}
|
||||
if (!newLabels) {
|
||||
return;
|
||||
}
|
||||
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||
return;
|
||||
}
|
||||
let existingLabels = getSpecSelectorLabels(inputObject);
|
||||
if (override) {
|
||||
existingLabels = newLabels;
|
||||
}
|
||||
else {
|
||||
if (!existingLabels) {
|
||||
existingLabels = new Map();
|
||||
}
|
||||
Object.keys(newLabels).forEach(function (key) {
|
||||
existingLabels[key] = newLabels[key];
|
||||
});
|
||||
}
|
||||
setSpecSelectorLabels(inputObject, existingLabels);
|
||||
}
|
||||
exports.updateSelectorLabels = updateSelectorLabels;
|
||||
function getResources(filePaths, filterResourceTypes) {
|
||||
if (!filePaths) {
|
||||
return [];
|
||||
}
|
||||
const resources = [];
|
||||
filePaths.forEach((filePath) => {
|
||||
const fileContents = fs.readFileSync(filePath);
|
||||
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||
const inputObjectKind = inputObject ? inputObject.kind : '';
|
||||
if (filterResourceTypes.filter(type => string_comparison_1.isEqual(inputObjectKind, type, string_comparison_1.StringComparer.OrdinalIgnoreCase)).length > 0) {
|
||||
const resource = {
|
||||
type: inputObject.kind,
|
||||
name: inputObject.metadata.name
|
||||
};
|
||||
resources.push(resource);
|
||||
}
|
||||
});
|
||||
});
|
||||
return resources;
|
||||
}
|
||||
exports.getResources = getResources;
|
||||
function getSpecLabels(inputObject) {
|
||||
if (!inputObject) {
|
||||
return null;
|
||||
}
|
||||
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||
return inputObject.metadata.labels;
|
||||
}
|
||||
if (!!inputObject.spec && !!inputObject.spec.template && !!inputObject.spec.template.metadata) {
|
||||
return inputObject.spec.template.metadata.labels;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function getImagePullSecrets(inputObject) {
|
||||
if (!inputObject || !inputObject.spec) {
|
||||
return null;
|
||||
}
|
||||
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.cronjob, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||
try {
|
||||
return inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets;
|
||||
}
|
||||
catch (ex) {
|
||||
core.debug(`Fetching imagePullSecrets failed due to this error: ${JSON.stringify(ex)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||
return inputObject.spec.imagePullSecrets;
|
||||
}
|
||||
if (!!inputObject.spec.template && !!inputObject.spec.template.spec) {
|
||||
return inputObject.spec.template.spec.imagePullSecrets;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function setImagePullSecrets(inputObject, newImagePullSecrets) {
|
||||
if (!inputObject || !inputObject.spec || !newImagePullSecrets) {
|
||||
return;
|
||||
}
|
||||
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||
inputObject.spec.imagePullSecrets = newImagePullSecrets;
|
||||
return;
|
||||
}
|
||||
if (string_comparison_1.isEqual(inputObject.kind, constants_1.KubernetesWorkload.cronjob, string_comparison_1.StringComparer.OrdinalIgnoreCase)) {
|
||||
try {
|
||||
inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets = newImagePullSecrets;
|
||||
}
|
||||
catch (ex) {
|
||||
core.debug(`Overriding imagePullSecrets failed due to this error: ${JSON.stringify(ex)}`);
|
||||
//Do nothing
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!!inputObject.spec.template && !!inputObject.spec.template.spec) {
|
||||
inputObject.spec.template.spec.imagePullSecrets = newImagePullSecrets;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
function setSpecLabels(inputObject, newLabels) {
|
||||
let specLabels = getSpecLabels(inputObject);
|
||||
if (!!newLabels) {
|
||||
specLabels = newLabels;
|
||||
}
|
||||
}
|
||||
function getSpecSelectorLabels(inputObject) {
|
||||
if (!!inputObject && !!inputObject.spec && !!inputObject.spec.selector) {
|
||||
if (isServiceEntity(inputObject.kind)) {
|
||||
return inputObject.spec.selector;
|
||||
}
|
||||
else {
|
||||
return inputObject.spec.selector.matchLabels;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function setSpecSelectorLabels(inputObject, newLabels) {
|
||||
let selectorLabels = getSpecSelectorLabels(inputObject);
|
||||
if (!!selectorLabels) {
|
||||
selectorLabels = newLabels;
|
||||
}
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.fetchResource = exports.isServiceSelectorSubsetOfMatchLabel = exports.getServiceSelector = exports.getDeploymentMatchLabels = exports.getBlueGreenResourceName = exports.addBlueGreenLabelsAndAnnotations = exports.getNewBlueGreenObject = exports.createWorkloadsWithLabel = exports.isServiceRouted = exports.getManifestObjects = exports.getSuffix = exports.deleteObjects = exports.deleteWorkloadsAndServicesWithLabel = exports.deleteWorkloadsWithLabel = exports.routeBlueGreen = exports.isSMIRoute = exports.isIngressRoute = exports.isBlueGreenDeploymentStrategy = exports.STABLE_SUFFIX = exports.GREEN_SUFFIX = exports.BLUE_GREEN_VERSION_LABEL = exports.NONE_LABEL_VALUE = exports.GREEN_LABEL_VALUE = exports.BLUE_GREEN_DEPLOYMENT_STRATEGY = void 0;
|
||||
const core = require("@actions/core");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const utility_1 = require("../utility");
|
||||
const constants_1 = require("../../constants");
|
||||
const fileHelper = require("../files-helper");
|
||||
const helper = require("../resource-object-utility");
|
||||
const TaskInputParameters = require("../../input-parameters");
|
||||
const service_blue_green_helper_1 = require("./service-blue-green-helper");
|
||||
const ingress_blue_green_helper_1 = require("./ingress-blue-green-helper");
|
||||
const smi_blue_green_helper_1 = require("./smi-blue-green-helper");
|
||||
exports.BLUE_GREEN_DEPLOYMENT_STRATEGY = 'BLUE-GREEN';
|
||||
exports.GREEN_LABEL_VALUE = 'green';
|
||||
exports.NONE_LABEL_VALUE = 'None';
|
||||
exports.BLUE_GREEN_VERSION_LABEL = 'k8s.deploy.color';
|
||||
exports.GREEN_SUFFIX = '-green';
|
||||
exports.STABLE_SUFFIX = '-stable';
|
||||
const INGRESS_ROUTE = 'INGRESS';
|
||||
const SMI_ROUTE = 'SMI';
|
||||
function isBlueGreenDeploymentStrategy() {
|
||||
const deploymentStrategy = TaskInputParameters.deploymentStrategy;
|
||||
return deploymentStrategy && deploymentStrategy.toUpperCase() === exports.BLUE_GREEN_DEPLOYMENT_STRATEGY;
|
||||
}
|
||||
exports.isBlueGreenDeploymentStrategy = isBlueGreenDeploymentStrategy;
|
||||
function isIngressRoute() {
|
||||
const routeMethod = TaskInputParameters.routeMethod;
|
||||
return routeMethod && routeMethod.toUpperCase() === INGRESS_ROUTE;
|
||||
}
|
||||
exports.isIngressRoute = isIngressRoute;
|
||||
function isSMIRoute() {
|
||||
const routeMethod = TaskInputParameters.routeMethod;
|
||||
return routeMethod && routeMethod.toUpperCase() === SMI_ROUTE;
|
||||
}
|
||||
exports.isSMIRoute = isSMIRoute;
|
||||
function routeBlueGreen(kubectl, inputManifestFiles) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// get buffer time
|
||||
let bufferTime = parseInt(TaskInputParameters.versionSwitchBuffer);
|
||||
//logging start of buffer time
|
||||
let dateNow = new Date();
|
||||
console.log(`Starting buffer time of ${bufferTime} minute(s) at ${dateNow.toISOString()}`);
|
||||
// waiting
|
||||
yield utility_1.sleep(bufferTime * 1000 * 60);
|
||||
// logging end of buffer time
|
||||
dateNow = new Date();
|
||||
console.log(`Stopping buffer time of ${bufferTime} minute(s) at ${dateNow.toISOString()}`);
|
||||
const manifestObjects = getManifestObjects(inputManifestFiles);
|
||||
// routing to new deployments
|
||||
if (isIngressRoute()) {
|
||||
ingress_blue_green_helper_1.routeBlueGreenIngress(kubectl, exports.GREEN_LABEL_VALUE, manifestObjects.serviceNameMap, manifestObjects.ingressEntityList);
|
||||
}
|
||||
else if (isSMIRoute()) {
|
||||
smi_blue_green_helper_1.routeBlueGreenSMI(kubectl, exports.GREEN_LABEL_VALUE, manifestObjects.serviceEntityList);
|
||||
}
|
||||
else {
|
||||
service_blue_green_helper_1.routeBlueGreenService(kubectl, exports.GREEN_LABEL_VALUE, manifestObjects.serviceEntityList);
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.routeBlueGreen = routeBlueGreen;
|
||||
function deleteWorkloadsWithLabel(kubectl, deleteLabel, deploymentEntityList) {
|
||||
let resourcesToDelete = [];
|
||||
deploymentEntityList.forEach((inputObject) => {
|
||||
const name = inputObject.metadata.name;
|
||||
const kind = inputObject.kind;
|
||||
if (deleteLabel === exports.NONE_LABEL_VALUE) {
|
||||
// if dellabel is none, deletes stable deployments
|
||||
const resourceToDelete = { name: name, kind: kind };
|
||||
resourcesToDelete.push(resourceToDelete);
|
||||
}
|
||||
else {
|
||||
// if dellabel is not none, then deletes new green deployments
|
||||
const resourceToDelete = { name: getBlueGreenResourceName(name, exports.GREEN_SUFFIX), kind: kind };
|
||||
resourcesToDelete.push(resourceToDelete);
|
||||
}
|
||||
});
|
||||
// deletes the deployments
|
||||
deleteObjects(kubectl, resourcesToDelete);
|
||||
}
|
||||
exports.deleteWorkloadsWithLabel = deleteWorkloadsWithLabel;
|
||||
function deleteWorkloadsAndServicesWithLabel(kubectl, deleteLabel, deploymentEntityList, serviceEntityList) {
|
||||
// need to delete services and deployments
|
||||
const deletionEntitiesList = deploymentEntityList.concat(serviceEntityList);
|
||||
let resourcesToDelete = [];
|
||||
deletionEntitiesList.forEach((inputObject) => {
|
||||
const name = inputObject.metadata.name;
|
||||
const kind = inputObject.kind;
|
||||
if (deleteLabel === exports.NONE_LABEL_VALUE) {
|
||||
// if not dellabel, delete stable objects
|
||||
const resourceToDelete = { name: name, kind: kind };
|
||||
resourcesToDelete.push(resourceToDelete);
|
||||
}
|
||||
else {
|
||||
// else delete green labels
|
||||
const resourceToDelete = { name: getBlueGreenResourceName(name, exports.GREEN_SUFFIX), kind: kind };
|
||||
resourcesToDelete.push(resourceToDelete);
|
||||
}
|
||||
});
|
||||
deleteObjects(kubectl, resourcesToDelete);
|
||||
}
|
||||
exports.deleteWorkloadsAndServicesWithLabel = deleteWorkloadsAndServicesWithLabel;
|
||||
function deleteObjects(kubectl, deleteList) {
|
||||
// delete services and deployments
|
||||
deleteList.forEach((delObject) => {
|
||||
try {
|
||||
const result = kubectl.delete([delObject.kind, delObject.name]);
|
||||
utility_1.checkForErrors([result]);
|
||||
}
|
||||
catch (ex) {
|
||||
// Ignore failures of delete if doesn't exist
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.deleteObjects = deleteObjects;
|
||||
function getSuffix(label) {
|
||||
if (label === exports.GREEN_LABEL_VALUE) {
|
||||
return exports.GREEN_SUFFIX;
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
exports.getSuffix = getSuffix;
|
||||
// other common functions
|
||||
function getManifestObjects(filePaths) {
|
||||
const deploymentEntityList = [];
|
||||
const routedServiceEntityList = [];
|
||||
const unroutedServiceEntityList = [];
|
||||
const ingressEntityList = [];
|
||||
const otherEntitiesList = [];
|
||||
let serviceNameMap = new Map();
|
||||
filePaths.forEach((filePath) => {
|
||||
const fileContents = fs.readFileSync(filePath);
|
||||
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||
if (!!inputObject) {
|
||||
const kind = inputObject.kind;
|
||||
const name = inputObject.metadata.name;
|
||||
if (helper.isDeploymentEntity(kind)) {
|
||||
deploymentEntityList.push(inputObject);
|
||||
}
|
||||
else if (helper.isServiceEntity(kind)) {
|
||||
if (isServiceRouted(inputObject, deploymentEntityList)) {
|
||||
routedServiceEntityList.push(inputObject);
|
||||
serviceNameMap.set(name, getBlueGreenResourceName(name, exports.GREEN_SUFFIX));
|
||||
}
|
||||
else {
|
||||
unroutedServiceEntityList.push(inputObject);
|
||||
}
|
||||
}
|
||||
else if (helper.isIngressEntity(kind)) {
|
||||
ingressEntityList.push(inputObject);
|
||||
}
|
||||
else {
|
||||
otherEntitiesList.push(inputObject);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return { serviceEntityList: routedServiceEntityList, serviceNameMap: serviceNameMap, unroutedServiceEntityList: unroutedServiceEntityList, deploymentEntityList: deploymentEntityList, ingressEntityList: ingressEntityList, otherObjects: otherEntitiesList };
|
||||
}
|
||||
exports.getManifestObjects = getManifestObjects;
|
||||
function isServiceRouted(serviceObject, deploymentEntityList) {
|
||||
let shouldBeRouted = false;
|
||||
const serviceSelector = getServiceSelector(serviceObject);
|
||||
if (!!serviceSelector) {
|
||||
if (deploymentEntityList.some((depObject) => {
|
||||
// finding if there is a deployment in the given manifests the service targets
|
||||
const matchLabels = getDeploymentMatchLabels(depObject);
|
||||
return (!!matchLabels && isServiceSelectorSubsetOfMatchLabel(serviceSelector, matchLabels));
|
||||
})) {
|
||||
shouldBeRouted = true;
|
||||
}
|
||||
}
|
||||
return shouldBeRouted;
|
||||
}
|
||||
exports.isServiceRouted = isServiceRouted;
|
||||
function createWorkloadsWithLabel(kubectl, deploymentObjectList, nextLabel) {
|
||||
const newObjectsList = [];
|
||||
deploymentObjectList.forEach((inputObject) => {
|
||||
// creating deployment with label
|
||||
const newBlueGreenObject = getNewBlueGreenObject(inputObject, nextLabel);
|
||||
core.debug('New blue-green object is: ' + JSON.stringify(newBlueGreenObject));
|
||||
newObjectsList.push(newBlueGreenObject);
|
||||
});
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
const result = kubectl.apply(manifestFiles);
|
||||
return { 'result': result, 'newFilePaths': manifestFiles };
|
||||
}
|
||||
exports.createWorkloadsWithLabel = createWorkloadsWithLabel;
|
||||
function getNewBlueGreenObject(inputObject, labelValue) {
|
||||
const newObject = JSON.parse(JSON.stringify(inputObject));
|
||||
// Updating name only if label is green label is given
|
||||
if (labelValue === exports.GREEN_LABEL_VALUE) {
|
||||
newObject.metadata.name = getBlueGreenResourceName(inputObject.metadata.name, exports.GREEN_SUFFIX);
|
||||
}
|
||||
// Adding labels and annotations
|
||||
addBlueGreenLabelsAndAnnotations(newObject, labelValue);
|
||||
return newObject;
|
||||
}
|
||||
exports.getNewBlueGreenObject = getNewBlueGreenObject;
|
||||
function addBlueGreenLabelsAndAnnotations(inputObject, labelValue) {
|
||||
//creating the k8s.deploy.color label
|
||||
const newLabels = new Map();
|
||||
newLabels[exports.BLUE_GREEN_VERSION_LABEL] = labelValue;
|
||||
// updating object labels and selector labels
|
||||
helper.updateObjectLabels(inputObject, newLabels, false);
|
||||
helper.updateSelectorLabels(inputObject, newLabels, false);
|
||||
// updating spec labels if it is a service
|
||||
if (!helper.isServiceEntity(inputObject.kind)) {
|
||||
helper.updateSpecLabels(inputObject, newLabels, false);
|
||||
}
|
||||
}
|
||||
exports.addBlueGreenLabelsAndAnnotations = addBlueGreenLabelsAndAnnotations;
|
||||
function getBlueGreenResourceName(name, suffix) {
|
||||
return `${name}${suffix}`;
|
||||
}
|
||||
exports.getBlueGreenResourceName = getBlueGreenResourceName;
|
||||
function getDeploymentMatchLabels(deploymentObject) {
|
||||
if (!!deploymentObject && deploymentObject.kind.toUpperCase() == constants_1.KubernetesWorkload.pod.toUpperCase() && !!deploymentObject.metadata && !!deploymentObject.metadata.labels) {
|
||||
return deploymentObject.metadata.labels;
|
||||
}
|
||||
else if (!!deploymentObject && deploymentObject.spec && deploymentObject.spec.selector && deploymentObject.spec.selector.matchLabels) {
|
||||
return deploymentObject.spec.selector.matchLabels;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
exports.getDeploymentMatchLabels = getDeploymentMatchLabels;
|
||||
function getServiceSelector(serviceObject) {
|
||||
if (!!serviceObject && serviceObject.spec && serviceObject.spec.selector) {
|
||||
return serviceObject.spec.selector;
|
||||
}
|
||||
else
|
||||
return null;
|
||||
}
|
||||
exports.getServiceSelector = getServiceSelector;
|
||||
function isServiceSelectorSubsetOfMatchLabel(serviceSelector, matchLabels) {
|
||||
let serviceSelectorMap = new Map();
|
||||
let matchLabelsMap = new Map();
|
||||
JSON.parse(JSON.stringify(serviceSelector), (key, value) => {
|
||||
serviceSelectorMap.set(key, value);
|
||||
});
|
||||
JSON.parse(JSON.stringify(matchLabels), (key, value) => {
|
||||
matchLabelsMap.set(key, value);
|
||||
});
|
||||
let isMatch = true;
|
||||
serviceSelectorMap.forEach((value, key) => {
|
||||
if (!!key && (!matchLabelsMap.has(key) || matchLabelsMap.get(key)) != value) {
|
||||
isMatch = false;
|
||||
}
|
||||
});
|
||||
return isMatch;
|
||||
}
|
||||
exports.isServiceSelectorSubsetOfMatchLabel = isServiceSelectorSubsetOfMatchLabel;
|
||||
function fetchResource(kubectl, kind, name) {
|
||||
const result = kubectl.getResource(kind, name);
|
||||
if (result == null || !!result.stderr) {
|
||||
return null;
|
||||
}
|
||||
if (!!result.stdout) {
|
||||
const resource = JSON.parse(result.stdout);
|
||||
try {
|
||||
UnsetsClusterSpecficDetails(resource);
|
||||
return resource;
|
||||
}
|
||||
catch (ex) {
|
||||
core.debug('Exception occurred while Parsing ' + resource + ' in Json object');
|
||||
core.debug(`Exception:${ex}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
exports.fetchResource = fetchResource;
|
||||
function UnsetsClusterSpecficDetails(resource) {
|
||||
if (resource == null) {
|
||||
return;
|
||||
}
|
||||
// Unsets the cluster specific details in the object
|
||||
if (!!resource) {
|
||||
const metadata = resource.metadata;
|
||||
const status = resource.status;
|
||||
if (!!metadata) {
|
||||
const newMetadata = {
|
||||
'annotations': metadata.annotations,
|
||||
'labels': metadata.labels,
|
||||
'name': metadata.name
|
||||
};
|
||||
resource.metadata = newMetadata;
|
||||
}
|
||||
if (!!status) {
|
||||
resource.status = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getStableResourceName = exports.getBaselineResourceName = exports.getCanaryResourceName = exports.isSMICanaryStrategy = exports.isCanaryDeploymentStrategy = exports.fetchResource = exports.fetchCanaryResource = exports.getNewCanaryResource = exports.getNewBaselineResource = exports.getStableResource = exports.isResourceMarkedAsStable = exports.markResourceAsStable = exports.deleteCanaryDeployment = exports.STABLE_LABEL_VALUE = exports.STABLE_SUFFIX = exports.CANARY_LABEL_VALUE = exports.BASELINE_LABEL_VALUE = exports.CANARY_VERSION_LABEL = exports.TRAFFIC_SPLIT_STRATEGY = exports.CANARY_DEPLOYMENT_STRATEGY = void 0;
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const core = require("@actions/core");
|
||||
const TaskInputParameters = require("../../input-parameters");
|
||||
const helper = require("../resource-object-utility");
|
||||
const constants_1 = require("../../constants");
|
||||
const string_comparison_1 = require("../string-comparison");
|
||||
const utility_1 = require("../utility");
|
||||
const utils = require("../manifest-utilities");
|
||||
exports.CANARY_DEPLOYMENT_STRATEGY = 'CANARY';
|
||||
exports.TRAFFIC_SPLIT_STRATEGY = 'SMI';
|
||||
exports.CANARY_VERSION_LABEL = 'workflow/version';
|
||||
const BASELINE_SUFFIX = '-baseline';
|
||||
exports.BASELINE_LABEL_VALUE = 'baseline';
|
||||
const CANARY_SUFFIX = '-canary';
|
||||
exports.CANARY_LABEL_VALUE = 'canary';
|
||||
exports.STABLE_SUFFIX = '-stable';
|
||||
exports.STABLE_LABEL_VALUE = 'stable';
|
||||
function deleteCanaryDeployment(kubectl, manifestFilePaths, includeServices) {
|
||||
// get manifest files
|
||||
const inputManifestFiles = utils.getManifestFiles(manifestFilePaths);
|
||||
if (inputManifestFiles == null || inputManifestFiles.length == 0) {
|
||||
throw new Error('ManifestFileNotFound');
|
||||
}
|
||||
// create delete cmd prefix
|
||||
cleanUpCanary(kubectl, inputManifestFiles, includeServices);
|
||||
}
|
||||
exports.deleteCanaryDeployment = deleteCanaryDeployment;
|
||||
function markResourceAsStable(inputObject) {
|
||||
if (isResourceMarkedAsStable(inputObject)) {
|
||||
return inputObject;
|
||||
}
|
||||
const newObject = JSON.parse(JSON.stringify(inputObject));
|
||||
// Adding labels and annotations.
|
||||
addCanaryLabelsAndAnnotations(newObject, exports.STABLE_LABEL_VALUE);
|
||||
core.debug("Added stable label: " + JSON.stringify(newObject));
|
||||
return newObject;
|
||||
}
|
||||
exports.markResourceAsStable = markResourceAsStable;
|
||||
function isResourceMarkedAsStable(inputObject) {
|
||||
return inputObject &&
|
||||
inputObject.metadata &&
|
||||
inputObject.metadata.labels &&
|
||||
inputObject.metadata.labels[exports.CANARY_VERSION_LABEL] == exports.STABLE_LABEL_VALUE;
|
||||
}
|
||||
exports.isResourceMarkedAsStable = isResourceMarkedAsStable;
|
||||
function getStableResource(inputObject) {
|
||||
var replicaCount = isSpecContainsReplicas(inputObject.kind) ? inputObject.metadata.replicas : 0;
|
||||
return getNewCanaryObject(inputObject, replicaCount, exports.STABLE_LABEL_VALUE);
|
||||
}
|
||||
exports.getStableResource = getStableResource;
|
||||
function getNewBaselineResource(stableObject, replicas) {
|
||||
return getNewCanaryObject(stableObject, replicas, exports.BASELINE_LABEL_VALUE);
|
||||
}
|
||||
exports.getNewBaselineResource = getNewBaselineResource;
|
||||
function getNewCanaryResource(inputObject, replicas) {
|
||||
return getNewCanaryObject(inputObject, replicas, exports.CANARY_LABEL_VALUE);
|
||||
}
|
||||
exports.getNewCanaryResource = getNewCanaryResource;
|
||||
function fetchCanaryResource(kubectl, kind, name) {
|
||||
return fetchResource(kubectl, kind, getCanaryResourceName(name));
|
||||
}
|
||||
exports.fetchCanaryResource = fetchCanaryResource;
|
||||
function fetchResource(kubectl, kind, name) {
|
||||
const result = kubectl.getResource(kind, name);
|
||||
if (result == null || !!result.stderr) {
|
||||
return null;
|
||||
}
|
||||
if (!!result.stdout) {
|
||||
const resource = JSON.parse(result.stdout);
|
||||
try {
|
||||
UnsetsClusterSpecficDetails(resource);
|
||||
return resource;
|
||||
}
|
||||
catch (ex) {
|
||||
core.debug('Exception occurred while Parsing ' + resource + ' in Json object');
|
||||
core.debug(`Exception:${ex}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
exports.fetchResource = fetchResource;
|
||||
function isCanaryDeploymentStrategy() {
|
||||
const deploymentStrategy = TaskInputParameters.deploymentStrategy;
|
||||
return deploymentStrategy && deploymentStrategy.toUpperCase() === exports.CANARY_DEPLOYMENT_STRATEGY;
|
||||
}
|
||||
exports.isCanaryDeploymentStrategy = isCanaryDeploymentStrategy;
|
||||
function isSMICanaryStrategy() {
|
||||
const deploymentStrategy = TaskInputParameters.trafficSplitMethod;
|
||||
return isCanaryDeploymentStrategy() && deploymentStrategy && deploymentStrategy.toUpperCase() === exports.TRAFFIC_SPLIT_STRATEGY;
|
||||
}
|
||||
exports.isSMICanaryStrategy = isSMICanaryStrategy;
|
||||
function getCanaryResourceName(name) {
|
||||
return name + CANARY_SUFFIX;
|
||||
}
|
||||
exports.getCanaryResourceName = getCanaryResourceName;
|
||||
function getBaselineResourceName(name) {
|
||||
return name + BASELINE_SUFFIX;
|
||||
}
|
||||
exports.getBaselineResourceName = getBaselineResourceName;
|
||||
function getStableResourceName(name) {
|
||||
return name + exports.STABLE_SUFFIX;
|
||||
}
|
||||
exports.getStableResourceName = getStableResourceName;
|
||||
function UnsetsClusterSpecficDetails(resource) {
|
||||
if (resource == null) {
|
||||
return;
|
||||
}
|
||||
// Unsets the cluster specific details in the object
|
||||
if (!!resource) {
|
||||
const metadata = resource.metadata;
|
||||
const status = resource.status;
|
||||
if (!!metadata) {
|
||||
const newMetadata = {
|
||||
'annotations': metadata.annotations,
|
||||
'labels': metadata.labels,
|
||||
'name': metadata.name
|
||||
};
|
||||
resource.metadata = newMetadata;
|
||||
}
|
||||
if (!!status) {
|
||||
resource.status = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
function getNewCanaryObject(inputObject, replicas, type) {
|
||||
const newObject = JSON.parse(JSON.stringify(inputObject));
|
||||
// Updating name
|
||||
if (type === exports.CANARY_LABEL_VALUE) {
|
||||
newObject.metadata.name = getCanaryResourceName(inputObject.metadata.name);
|
||||
}
|
||||
else if (type === exports.STABLE_LABEL_VALUE) {
|
||||
newObject.metadata.name = getStableResourceName(inputObject.metadata.name);
|
||||
}
|
||||
else {
|
||||
newObject.metadata.name = getBaselineResourceName(inputObject.metadata.name);
|
||||
}
|
||||
// Adding labels and annotations.
|
||||
addCanaryLabelsAndAnnotations(newObject, type);
|
||||
// Updating no. of replicas
|
||||
if (isSpecContainsReplicas(newObject.kind)) {
|
||||
newObject.spec.replicas = replicas;
|
||||
}
|
||||
return newObject;
|
||||
}
|
||||
function isSpecContainsReplicas(kind) {
|
||||
return !string_comparison_1.isEqual(kind, constants_1.KubernetesWorkload.pod, string_comparison_1.StringComparer.OrdinalIgnoreCase) &&
|
||||
!string_comparison_1.isEqual(kind, constants_1.KubernetesWorkload.daemonSet, string_comparison_1.StringComparer.OrdinalIgnoreCase) &&
|
||||
!helper.isServiceEntity(kind);
|
||||
}
|
||||
function addCanaryLabelsAndAnnotations(inputObject, type) {
|
||||
const newLabels = new Map();
|
||||
newLabels[exports.CANARY_VERSION_LABEL] = type;
|
||||
helper.updateObjectLabels(inputObject, newLabels, false);
|
||||
helper.updateObjectAnnotations(inputObject, newLabels, false);
|
||||
helper.updateSelectorLabels(inputObject, newLabels, false);
|
||||
if (!helper.isServiceEntity(inputObject.kind)) {
|
||||
helper.updateSpecLabels(inputObject, newLabels, false);
|
||||
}
|
||||
}
|
||||
function cleanUpCanary(kubectl, files, includeServices) {
|
||||
var deleteObject = function (kind, name) {
|
||||
try {
|
||||
const result = kubectl.delete([kind, name]);
|
||||
utility_1.checkForErrors([result]);
|
||||
}
|
||||
catch (ex) {
|
||||
// Ignore failures of delete if doesn't exist
|
||||
}
|
||||
};
|
||||
files.forEach((filePath) => {
|
||||
const fileContents = fs.readFileSync(filePath);
|
||||
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||
const name = inputObject.metadata.name;
|
||||
const kind = inputObject.kind;
|
||||
if (helper.isDeploymentEntity(kind) || (includeServices && helper.isServiceEntity(kind))) {
|
||||
const canaryObjectName = getCanaryResourceName(name);
|
||||
const baselineObjectName = getBaselineResourceName(name);
|
||||
deleteObject(kind, canaryObjectName);
|
||||
deleteObject(kind, baselineObjectName);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getManifestFiles = exports.deploy = void 0;
|
||||
const fs = require("fs");
|
||||
const core = require("@actions/core");
|
||||
const yaml = require("js-yaml");
|
||||
const canaryDeploymentHelper = require("./canary-deployment-helper");
|
||||
const KubernetesObjectUtility = require("../resource-object-utility");
|
||||
const TaskInputParameters = require("../../input-parameters");
|
||||
const models = require("../../constants");
|
||||
const fileHelper = require("../files-helper");
|
||||
const utils = require("../manifest-utilities");
|
||||
const KubernetesManifestUtility = require("../manifest-stability-utility");
|
||||
const KubernetesConstants = require("../../constants");
|
||||
const manifest_utilities_1 = require("../manifest-utilities");
|
||||
const pod_canary_deployment_helper_1 = require("./pod-canary-deployment-helper");
|
||||
const smi_canary_deployment_helper_1 = require("./smi-canary-deployment-helper");
|
||||
const utility_1 = require("../utility");
|
||||
const blue_green_helper_1 = require("./blue-green-helper");
|
||||
const service_blue_green_helper_1 = require("./service-blue-green-helper");
|
||||
const ingress_blue_green_helper_1 = require("./ingress-blue-green-helper");
|
||||
const smi_blue_green_helper_1 = require("./smi-blue-green-helper");
|
||||
function deploy(kubectl, manifestFilePaths, deploymentStrategy) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// get manifest files
|
||||
let inputManifestFiles = manifest_utilities_1.getUpdatedManifestFiles(manifestFilePaths);
|
||||
// deployment
|
||||
const deployedManifestFiles = deployManifests(inputManifestFiles, kubectl, isCanaryDeploymentStrategy(deploymentStrategy), blue_green_helper_1.isBlueGreenDeploymentStrategy());
|
||||
// check manifest stability
|
||||
const resourceTypes = KubernetesObjectUtility.getResources(deployedManifestFiles, models.deploymentTypes.concat([KubernetesConstants.DiscoveryAndLoadBalancerResource.service]));
|
||||
yield checkManifestStability(kubectl, resourceTypes);
|
||||
// route blue-green deployments
|
||||
if (blue_green_helper_1.isBlueGreenDeploymentStrategy()) {
|
||||
yield blue_green_helper_1.routeBlueGreen(kubectl, inputManifestFiles);
|
||||
}
|
||||
// print ingress resources
|
||||
const ingressResources = KubernetesObjectUtility.getResources(deployedManifestFiles, [KubernetesConstants.DiscoveryAndLoadBalancerResource.ingress]);
|
||||
ingressResources.forEach(ingressResource => {
|
||||
kubectl.getResource(KubernetesConstants.DiscoveryAndLoadBalancerResource.ingress, ingressResource.name);
|
||||
});
|
||||
// annotate resources
|
||||
let allPods;
|
||||
try {
|
||||
allPods = JSON.parse((kubectl.getAllPods()).stdout);
|
||||
}
|
||||
catch (e) {
|
||||
core.debug("Unable to parse pods; Error: " + e);
|
||||
}
|
||||
annotateAndLabelResources(deployedManifestFiles, kubectl, resourceTypes, allPods);
|
||||
});
|
||||
}
|
||||
exports.deploy = deploy;
|
||||
function getManifestFiles(manifestFilePaths) {
|
||||
const files = utils.getManifestFiles(manifestFilePaths);
|
||||
if (files == null || files.length === 0) {
|
||||
throw new Error(`ManifestFileNotFound : ${manifestFilePaths}`);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
exports.getManifestFiles = getManifestFiles;
|
||||
function deployManifests(files, kubectl, isCanaryDeploymentStrategy, isBlueGreenDeploymentStrategy) {
|
||||
let result;
|
||||
if (isCanaryDeploymentStrategy) {
|
||||
let canaryDeploymentOutput;
|
||||
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||
canaryDeploymentOutput = smi_canary_deployment_helper_1.deploySMICanary(kubectl, files);
|
||||
}
|
||||
else {
|
||||
canaryDeploymentOutput = pod_canary_deployment_helper_1.deployPodCanary(kubectl, files);
|
||||
}
|
||||
result = canaryDeploymentOutput.result;
|
||||
files = canaryDeploymentOutput.newFilePaths;
|
||||
}
|
||||
else if (isBlueGreenDeploymentStrategy) {
|
||||
let blueGreenDeploymentOutput;
|
||||
if (blue_green_helper_1.isIngressRoute()) {
|
||||
blueGreenDeploymentOutput = ingress_blue_green_helper_1.deployBlueGreenIngress(kubectl, files);
|
||||
}
|
||||
else if (blue_green_helper_1.isSMIRoute()) {
|
||||
blueGreenDeploymentOutput = smi_blue_green_helper_1.deployBlueGreenSMI(kubectl, files);
|
||||
}
|
||||
else {
|
||||
blueGreenDeploymentOutput = service_blue_green_helper_1.deployBlueGreenService(kubectl, files);
|
||||
}
|
||||
result = blueGreenDeploymentOutput.result;
|
||||
files = blueGreenDeploymentOutput.newFilePaths;
|
||||
}
|
||||
else {
|
||||
if (canaryDeploymentHelper.isSMICanaryStrategy()) {
|
||||
const updatedManifests = appendStableVersionLabelToResource(files, kubectl);
|
||||
result = kubectl.apply(updatedManifests, TaskInputParameters.forceDeployment);
|
||||
}
|
||||
else {
|
||||
result = kubectl.apply(files, TaskInputParameters.forceDeployment);
|
||||
}
|
||||
}
|
||||
utility_1.checkForErrors([result]);
|
||||
return files;
|
||||
}
|
||||
function appendStableVersionLabelToResource(files, kubectl) {
|
||||
const manifestFiles = [];
|
||||
const newObjectsList = [];
|
||||
files.forEach((filePath) => {
|
||||
const fileContents = fs.readFileSync(filePath);
|
||||
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||
const kind = inputObject.kind;
|
||||
if (KubernetesObjectUtility.isDeploymentEntity(kind)) {
|
||||
const updatedObject = canaryDeploymentHelper.markResourceAsStable(inputObject);
|
||||
newObjectsList.push(updatedObject);
|
||||
}
|
||||
else {
|
||||
manifestFiles.push(filePath);
|
||||
}
|
||||
});
|
||||
});
|
||||
const updatedManifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
manifestFiles.push(...updatedManifestFiles);
|
||||
return manifestFiles;
|
||||
}
|
||||
function checkManifestStability(kubectl, resources) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
yield KubernetesManifestUtility.checkManifestStability(kubectl, resources);
|
||||
});
|
||||
}
|
||||
function annotateAndLabelResources(files, kubectl, resourceTypes, allPods) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const workflowFilePath = yield utility_1.getWorkflowFilePath(TaskInputParameters.githubToken);
|
||||
const deploymentConfig = yield utility_1.getDeploymentConfig();
|
||||
const annotationKeyLabel = models.getWorkflowAnnotationKeyLabel(workflowFilePath);
|
||||
annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel, workflowFilePath, deploymentConfig);
|
||||
labelResources(files, kubectl, annotationKeyLabel);
|
||||
});
|
||||
}
|
||||
function annotateResources(files, kubectl, resourceTypes, allPods, annotationKey, workflowFilePath, deploymentConfig) {
|
||||
const annotateResults = [];
|
||||
const lastSuccessSha = utility_1.getLastSuccessfulRunSha(kubectl, TaskInputParameters.namespace, annotationKey);
|
||||
let annotationKeyValStr = annotationKey + '=' + models.getWorkflowAnnotationsJson(lastSuccessSha, workflowFilePath, deploymentConfig);
|
||||
annotateResults.push(kubectl.annotate('namespace', TaskInputParameters.namespace, annotationKeyValStr));
|
||||
annotateResults.push(kubectl.annotateFiles(files, annotationKeyValStr));
|
||||
resourceTypes.forEach(resource => {
|
||||
if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) {
|
||||
utility_1.annotateChildPods(kubectl, resource.type, resource.name, annotationKeyValStr, allPods)
|
||||
.forEach(execResult => annotateResults.push(execResult));
|
||||
}
|
||||
});
|
||||
utility_1.checkForErrors(annotateResults, true);
|
||||
}
|
||||
function labelResources(files, kubectl, label) {
|
||||
let workflowName = process.env.GITHUB_WORKFLOW;
|
||||
workflowName = workflowName.startsWith('.github/workflows/') ?
|
||||
workflowName.replace(".github/workflows/", "") : workflowName;
|
||||
const labels = [`workflowFriendlyName=${workflowName}`, `workflow=${label}`];
|
||||
utility_1.checkForErrors([kubectl.labelFiles(files, labels)], true);
|
||||
}
|
||||
function isCanaryDeploymentStrategy(deploymentStrategy) {
|
||||
return deploymentStrategy != null && deploymentStrategy.toUpperCase() === canaryDeploymentHelper.CANARY_DEPLOYMENT_STRATEGY.toUpperCase();
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.updateIngressBackend = exports.getUpdatedBlueGreenIngress = exports.validateIngressesState = exports.routeBlueGreenIngress = exports.rejectBlueGreenIngress = exports.promoteBlueGreenIngress = exports.deployBlueGreenIngress = void 0;
|
||||
const core = require("@actions/core");
|
||||
const fileHelper = require("../files-helper");
|
||||
const blue_green_helper_1 = require("./blue-green-helper");
|
||||
const blue_green_helper_2 = require("./blue-green-helper");
|
||||
const BACKEND = 'BACKEND';
|
||||
function deployBlueGreenIngress(kubectl, filePaths) {
|
||||
// get all kubernetes objects defined in manifest files
|
||||
const manifestObjects = blue_green_helper_1.getManifestObjects(filePaths);
|
||||
// create deployments with green label value
|
||||
const result = blue_green_helper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blue_green_helper_2.GREEN_LABEL_VALUE);
|
||||
// create new services and other objects
|
||||
let newObjectsList = [];
|
||||
manifestObjects.serviceEntityList.forEach(inputObject => {
|
||||
const newBlueGreenObject = blue_green_helper_1.getNewBlueGreenObject(inputObject, blue_green_helper_2.GREEN_LABEL_VALUE);
|
||||
;
|
||||
core.debug('New blue-green object is: ' + JSON.stringify(newBlueGreenObject));
|
||||
newObjectsList.push(newBlueGreenObject);
|
||||
});
|
||||
newObjectsList = newObjectsList.concat(manifestObjects.otherObjects).concat(manifestObjects.unroutedServiceEntityList);
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
kubectl.apply(manifestFiles);
|
||||
// return results to check for manifest stability
|
||||
return result;
|
||||
}
|
||||
exports.deployBlueGreenIngress = deployBlueGreenIngress;
|
||||
function promoteBlueGreenIngress(kubectl, manifestObjects) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
//checking if anything to promote
|
||||
if (!validateIngressesState(kubectl, manifestObjects.ingressEntityList, manifestObjects.serviceNameMap)) {
|
||||
throw ('NotInPromoteStateIngress');
|
||||
}
|
||||
// create stable deployments with new configuration
|
||||
const result = blue_green_helper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blue_green_helper_2.NONE_LABEL_VALUE);
|
||||
// create stable services with new configuration
|
||||
const newObjectsList = [];
|
||||
manifestObjects.serviceEntityList.forEach((inputObject) => {
|
||||
const newBlueGreenObject = blue_green_helper_1.getNewBlueGreenObject(inputObject, blue_green_helper_2.NONE_LABEL_VALUE);
|
||||
core.debug('New blue-green object is: ' + JSON.stringify(newBlueGreenObject));
|
||||
newObjectsList.push(newBlueGreenObject);
|
||||
});
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
kubectl.apply(manifestFiles);
|
||||
// returning deployments to check for rollout stability
|
||||
return result;
|
||||
});
|
||||
}
|
||||
exports.promoteBlueGreenIngress = promoteBlueGreenIngress;
|
||||
function rejectBlueGreenIngress(kubectl, filePaths) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// get all kubernetes objects defined in manifest files
|
||||
const manifestObjects = blue_green_helper_1.getManifestObjects(filePaths);
|
||||
// routing ingress to stables services
|
||||
routeBlueGreenIngress(kubectl, null, manifestObjects.serviceNameMap, manifestObjects.ingressEntityList);
|
||||
// deleting green services and deployments
|
||||
blue_green_helper_1.deleteWorkloadsAndServicesWithLabel(kubectl, blue_green_helper_2.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList, manifestObjects.serviceEntityList);
|
||||
});
|
||||
}
|
||||
exports.rejectBlueGreenIngress = rejectBlueGreenIngress;
|
||||
function routeBlueGreenIngress(kubectl, nextLabel, serviceNameMap, ingressEntityList) {
|
||||
let newObjectsList = [];
|
||||
if (!nextLabel) {
|
||||
newObjectsList = ingressEntityList.filter(ingress => isIngressRouted(ingress, serviceNameMap));
|
||||
}
|
||||
else {
|
||||
ingressEntityList.forEach((inputObject) => {
|
||||
if (isIngressRouted(inputObject, serviceNameMap)) {
|
||||
const newBlueGreenIngressObject = getUpdatedBlueGreenIngress(inputObject, serviceNameMap, blue_green_helper_2.GREEN_LABEL_VALUE);
|
||||
newObjectsList.push(newBlueGreenIngressObject);
|
||||
}
|
||||
else {
|
||||
newObjectsList.push(inputObject);
|
||||
}
|
||||
});
|
||||
}
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
kubectl.apply(manifestFiles);
|
||||
}
|
||||
exports.routeBlueGreenIngress = routeBlueGreenIngress;
|
||||
function validateIngressesState(kubectl, ingressEntityList, serviceNameMap) {
|
||||
let areIngressesTargetingNewServices = true;
|
||||
ingressEntityList.forEach((inputObject) => {
|
||||
if (isIngressRouted(inputObject, serviceNameMap)) {
|
||||
//querying existing ingress
|
||||
let existingIngress = blue_green_helper_1.fetchResource(kubectl, inputObject.kind, inputObject.metadata.name);
|
||||
if (!!existingIngress) {
|
||||
let currentLabel;
|
||||
// checking its label
|
||||
try {
|
||||
currentLabel = existingIngress.metadata.labels[blue_green_helper_2.BLUE_GREEN_VERSION_LABEL];
|
||||
}
|
||||
catch (_a) {
|
||||
// if no label exists, then not an ingress targeting green deployments
|
||||
areIngressesTargetingNewServices = false;
|
||||
}
|
||||
if (currentLabel != blue_green_helper_2.GREEN_LABEL_VALUE) {
|
||||
// if not green label, then wrong configuration
|
||||
areIngressesTargetingNewServices = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// no ingress at all, so nothing to promote
|
||||
areIngressesTargetingNewServices = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return areIngressesTargetingNewServices;
|
||||
}
|
||||
exports.validateIngressesState = validateIngressesState;
|
||||
function isIngressRouted(ingressObject, serviceNameMap) {
|
||||
let isIngressRouted = false;
|
||||
// sees if ingress targets a service in the given manifests
|
||||
JSON.parse(JSON.stringify(ingressObject), (key, value) => {
|
||||
if (key === 'serviceName' && serviceNameMap.has(value)) {
|
||||
isIngressRouted = true;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
return isIngressRouted;
|
||||
}
|
||||
function getUpdatedBlueGreenIngress(inputObject, serviceNameMap, type) {
|
||||
if (!type) {
|
||||
// returning original with no modifications
|
||||
return inputObject;
|
||||
}
|
||||
const newObject = JSON.parse(JSON.stringify(inputObject));
|
||||
// adding green labels and values
|
||||
blue_green_helper_1.addBlueGreenLabelsAndAnnotations(newObject, type);
|
||||
// Updating ingress labels
|
||||
let finalObject = updateIngressBackend(newObject, serviceNameMap);
|
||||
return finalObject;
|
||||
}
|
||||
exports.getUpdatedBlueGreenIngress = getUpdatedBlueGreenIngress;
|
||||
function updateIngressBackend(inputObject, serviceNameMap) {
|
||||
inputObject = JSON.parse(JSON.stringify(inputObject), (key, value) => {
|
||||
if (key.toUpperCase() === BACKEND) {
|
||||
let serviceName = value.serviceName;
|
||||
if (serviceNameMap.has(serviceName)) {
|
||||
// updating service name with corresponding bluegreen name only if service is provied in given manifests
|
||||
value.serviceName = serviceNameMap.get(serviceName);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
return inputObject;
|
||||
}
|
||||
exports.updateIngressBackend = updateIngressBackend;
|
||||
@@ -1,58 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.deployPodCanary = void 0;
|
||||
const core = require("@actions/core");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const TaskInputParameters = require("../../input-parameters");
|
||||
const fileHelper = require("../files-helper");
|
||||
const helper = require("../resource-object-utility");
|
||||
const canaryDeploymentHelper = require("./canary-deployment-helper");
|
||||
function deployPodCanary(kubectl, filePaths) {
|
||||
const newObjectsList = [];
|
||||
const percentage = parseInt(TaskInputParameters.canaryPercentage);
|
||||
filePaths.forEach((filePath) => {
|
||||
const fileContents = fs.readFileSync(filePath);
|
||||
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||
const name = inputObject.metadata.name;
|
||||
const kind = inputObject.kind;
|
||||
if (helper.isDeploymentEntity(kind)) {
|
||||
core.debug('Calculating replica count for canary');
|
||||
const canaryReplicaCount = calculateReplicaCountForCanary(inputObject, percentage);
|
||||
core.debug('Replica count is ' + canaryReplicaCount);
|
||||
// Get stable object
|
||||
core.debug('Querying stable object');
|
||||
const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, name);
|
||||
if (!stableObject) {
|
||||
core.debug('Stable object not found. Creating only canary object');
|
||||
// If stable object not found, create canary deployment.
|
||||
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||
newObjectsList.push(newCanaryObject);
|
||||
}
|
||||
else {
|
||||
core.debug('Stable object found. Creating canary and baseline objects');
|
||||
// If canary object not found, create canary and baseline object.
|
||||
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||
const newBaselineObject = canaryDeploymentHelper.getNewBaselineResource(stableObject, canaryReplicaCount);
|
||||
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||
core.debug('New baseline object is: ' + JSON.stringify(newBaselineObject));
|
||||
newObjectsList.push(newCanaryObject);
|
||||
newObjectsList.push(newBaselineObject);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Updating non deployment entity as it is.
|
||||
newObjectsList.push(inputObject);
|
||||
}
|
||||
});
|
||||
});
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
const result = kubectl.apply(manifestFiles, TaskInputParameters.forceDeployment);
|
||||
return { 'result': result, 'newFilePaths': manifestFiles };
|
||||
}
|
||||
exports.deployPodCanary = deployPodCanary;
|
||||
function calculateReplicaCountForCanary(inputObject, percentage) {
|
||||
const inputReplicaCount = helper.getReplicaCount(inputObject);
|
||||
return Math.round((inputReplicaCount * percentage) / 100);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getServiceSpecLabel = exports.validateServicesState = exports.routeBlueGreenService = exports.rejectBlueGreenService = exports.promoteBlueGreenService = exports.deployBlueGreenService = void 0;
|
||||
const fileHelper = require("../files-helper");
|
||||
const blue_green_helper_1 = require("./blue-green-helper");
|
||||
const blue_green_helper_2 = require("./blue-green-helper");
|
||||
function deployBlueGreenService(kubectl, filePaths) {
|
||||
// get all kubernetes objects defined in manifest files
|
||||
const manifestObjects = blue_green_helper_1.getManifestObjects(filePaths);
|
||||
// create deployments with green label value
|
||||
const result = blue_green_helper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blue_green_helper_2.GREEN_LABEL_VALUE);
|
||||
// create other non deployment and non service entities
|
||||
const newObjectsList = manifestObjects.otherObjects.concat(manifestObjects.ingressEntityList).concat(manifestObjects.unroutedServiceEntityList);
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
kubectl.apply(manifestFiles);
|
||||
// returning deployment details to check for rollout stability
|
||||
return result;
|
||||
}
|
||||
exports.deployBlueGreenService = deployBlueGreenService;
|
||||
function promoteBlueGreenService(kubectl, manifestObjects) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// checking if services are in the right state ie. targeting green deployments
|
||||
if (!validateServicesState(kubectl, manifestObjects.serviceEntityList)) {
|
||||
throw ('NotInPromoteState');
|
||||
}
|
||||
// creating stable deployments with new configurations
|
||||
const result = blue_green_helper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blue_green_helper_2.NONE_LABEL_VALUE);
|
||||
// returning deployment details to check for rollout stability
|
||||
return result;
|
||||
});
|
||||
}
|
||||
exports.promoteBlueGreenService = promoteBlueGreenService;
|
||||
function rejectBlueGreenService(kubectl, filePaths) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// get all kubernetes objects defined in manifest files
|
||||
const manifestObjects = blue_green_helper_1.getManifestObjects(filePaths);
|
||||
// routing to stable objects
|
||||
routeBlueGreenService(kubectl, blue_green_helper_2.NONE_LABEL_VALUE, manifestObjects.serviceEntityList);
|
||||
// deleting the new deployments with green suffix
|
||||
blue_green_helper_1.deleteWorkloadsWithLabel(kubectl, blue_green_helper_2.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList);
|
||||
});
|
||||
}
|
||||
exports.rejectBlueGreenService = rejectBlueGreenService;
|
||||
function routeBlueGreenService(kubectl, nextLabel, serviceEntityList) {
|
||||
const newObjectsList = [];
|
||||
serviceEntityList.forEach((serviceObject) => {
|
||||
const newBlueGreenServiceObject = getUpdatedBlueGreenService(serviceObject, nextLabel);
|
||||
newObjectsList.push(newBlueGreenServiceObject);
|
||||
});
|
||||
// configures the services
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
kubectl.apply(manifestFiles);
|
||||
}
|
||||
exports.routeBlueGreenService = routeBlueGreenService;
|
||||
// adding green labels to configure existing service
|
||||
function getUpdatedBlueGreenService(inputObject, labelValue) {
|
||||
const newObject = JSON.parse(JSON.stringify(inputObject));
|
||||
// Adding labels and annotations.
|
||||
blue_green_helper_1.addBlueGreenLabelsAndAnnotations(newObject, labelValue);
|
||||
return newObject;
|
||||
}
|
||||
function validateServicesState(kubectl, serviceEntityList) {
|
||||
let areServicesGreen = true;
|
||||
serviceEntityList.forEach((serviceObject) => {
|
||||
// finding the existing routed service
|
||||
const existingService = blue_green_helper_1.fetchResource(kubectl, serviceObject.kind, serviceObject.metadata.name);
|
||||
if (!!existingService) {
|
||||
let currentLabel = getServiceSpecLabel(existingService);
|
||||
if (currentLabel != blue_green_helper_2.GREEN_LABEL_VALUE) {
|
||||
// service should be targeting deployments with green label
|
||||
areServicesGreen = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// service targeting deployment doesn't exist
|
||||
areServicesGreen = false;
|
||||
}
|
||||
});
|
||||
return areServicesGreen;
|
||||
}
|
||||
exports.validateServicesState = validateServicesState;
|
||||
function getServiceSpecLabel(inputObject) {
|
||||
if (!!inputObject && inputObject.spec && inputObject.spec.selector && inputObject.spec.selector[blue_green_helper_2.BLUE_GREEN_VERSION_LABEL]) {
|
||||
return inputObject.spec.selector[blue_green_helper_2.BLUE_GREEN_VERSION_LABEL];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
exports.getServiceSpecLabel = getServiceSpecLabel;
|
||||
@@ -1,184 +0,0 @@
|
||||
'use strict';
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.cleanupSMI = exports.validateTrafficSplitsState = exports.routeBlueGreenSMI = exports.getSMIServiceResource = exports.setupSMI = exports.rejectBlueGreenSMI = exports.promoteBlueGreenSMI = exports.deployBlueGreenSMI = void 0;
|
||||
const kubectlUtils = require("../kubectl-util");
|
||||
const fileHelper = require("../files-helper");
|
||||
const blue_green_helper_1 = require("./blue-green-helper");
|
||||
const blue_green_helper_2 = require("./blue-green-helper");
|
||||
let trafficSplitAPIVersion = "";
|
||||
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit';
|
||||
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit';
|
||||
const MIN_VAL = '0';
|
||||
const MAX_VAL = '100';
|
||||
function deployBlueGreenSMI(kubectl, filePaths) {
|
||||
// get all kubernetes objects defined in manifest files
|
||||
const manifestObjects = blue_green_helper_1.getManifestObjects(filePaths);
|
||||
// creating services and other objects
|
||||
const newObjectsList = manifestObjects.otherObjects.concat(manifestObjects.serviceEntityList).concat(manifestObjects.ingressEntityList).concat(manifestObjects.unroutedServiceEntityList);
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
kubectl.apply(manifestFiles);
|
||||
// make extraservices and trafficsplit
|
||||
setupSMI(kubectl, manifestObjects.serviceEntityList);
|
||||
// create new deloyments
|
||||
const result = blue_green_helper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blue_green_helper_2.GREEN_LABEL_VALUE);
|
||||
// return results to check for manifest stability
|
||||
return result;
|
||||
}
|
||||
exports.deployBlueGreenSMI = deployBlueGreenSMI;
|
||||
function promoteBlueGreenSMI(kubectl, manifestObjects) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// checking if there is something to promote
|
||||
if (!validateTrafficSplitsState(kubectl, manifestObjects.serviceEntityList)) {
|
||||
throw ('NotInPromoteStateSMI');
|
||||
}
|
||||
// create stable deployments with new configuration
|
||||
const result = blue_green_helper_1.createWorkloadsWithLabel(kubectl, manifestObjects.deploymentEntityList, blue_green_helper_2.NONE_LABEL_VALUE);
|
||||
// return result to check for stability
|
||||
return result;
|
||||
});
|
||||
}
|
||||
exports.promoteBlueGreenSMI = promoteBlueGreenSMI;
|
||||
function rejectBlueGreenSMI(kubectl, filePaths) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// get all kubernetes objects defined in manifest files
|
||||
const manifestObjects = blue_green_helper_1.getManifestObjects(filePaths);
|
||||
// routing trafficsplit to stable deploymetns
|
||||
routeBlueGreenSMI(kubectl, blue_green_helper_2.NONE_LABEL_VALUE, manifestObjects.serviceEntityList);
|
||||
// deleting rejected new bluegreen deplyments
|
||||
blue_green_helper_1.deleteWorkloadsWithLabel(kubectl, blue_green_helper_2.GREEN_LABEL_VALUE, manifestObjects.deploymentEntityList);
|
||||
//deleting trafficsplit and extra services
|
||||
cleanupSMI(kubectl, manifestObjects.serviceEntityList);
|
||||
});
|
||||
}
|
||||
exports.rejectBlueGreenSMI = rejectBlueGreenSMI;
|
||||
function setupSMI(kubectl, serviceEntityList) {
|
||||
const newObjectsList = [];
|
||||
const trafficObjectList = [];
|
||||
serviceEntityList.forEach((serviceObject) => {
|
||||
// create a trafficsplit for service
|
||||
trafficObjectList.push(serviceObject);
|
||||
// setting up the services for trafficsplit
|
||||
const newStableService = getSMIServiceResource(serviceObject, blue_green_helper_2.STABLE_SUFFIX);
|
||||
const newGreenService = getSMIServiceResource(serviceObject, blue_green_helper_2.GREEN_SUFFIX);
|
||||
newObjectsList.push(newStableService);
|
||||
newObjectsList.push(newGreenService);
|
||||
});
|
||||
// creating services
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
kubectl.apply(manifestFiles);
|
||||
// route to stable service
|
||||
trafficObjectList.forEach((inputObject) => {
|
||||
createTrafficSplitObject(kubectl, inputObject.metadata.name, blue_green_helper_2.NONE_LABEL_VALUE);
|
||||
});
|
||||
}
|
||||
exports.setupSMI = setupSMI;
|
||||
function createTrafficSplitObject(kubectl, name, nextLabel) {
|
||||
// getting smi spec api version
|
||||
if (!trafficSplitAPIVersion) {
|
||||
trafficSplitAPIVersion = kubectlUtils.getTrafficSplitAPIVersion(kubectl);
|
||||
}
|
||||
// deciding weights based on nextlabel
|
||||
let stableWeight;
|
||||
let greenWeight;
|
||||
if (nextLabel === blue_green_helper_2.GREEN_LABEL_VALUE) {
|
||||
stableWeight = parseInt(MIN_VAL);
|
||||
greenWeight = parseInt(MAX_VAL);
|
||||
}
|
||||
else {
|
||||
stableWeight = parseInt(MAX_VAL);
|
||||
greenWeight = parseInt(MIN_VAL);
|
||||
}
|
||||
//traffic split json
|
||||
const trafficSplitObject = `{
|
||||
"apiVersion": "${trafficSplitAPIVersion}",
|
||||
"kind": "TrafficSplit",
|
||||
"metadata": {
|
||||
"name": "${blue_green_helper_1.getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX)}"
|
||||
},
|
||||
"spec": {
|
||||
"service": "${name}",
|
||||
"backends": [
|
||||
{
|
||||
"service": "${blue_green_helper_1.getBlueGreenResourceName(name, blue_green_helper_2.STABLE_SUFFIX)}",
|
||||
"weight": ${stableWeight}
|
||||
},
|
||||
{
|
||||
"service": "${blue_green_helper_1.getBlueGreenResourceName(name, blue_green_helper_2.GREEN_SUFFIX)}",
|
||||
"weight": ${greenWeight}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`;
|
||||
// creating trafficplit object
|
||||
const trafficSplitManifestFile = fileHelper.writeManifestToFile(trafficSplitObject, TRAFFIC_SPLIT_OBJECT, blue_green_helper_1.getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX));
|
||||
kubectl.apply(trafficSplitManifestFile);
|
||||
}
|
||||
function getSMIServiceResource(inputObject, suffix) {
|
||||
const newObject = JSON.parse(JSON.stringify(inputObject));
|
||||
if (suffix === blue_green_helper_2.STABLE_SUFFIX) {
|
||||
// adding stable suffix to service name
|
||||
newObject.metadata.name = blue_green_helper_1.getBlueGreenResourceName(inputObject.metadata.name, blue_green_helper_2.STABLE_SUFFIX);
|
||||
return blue_green_helper_1.getNewBlueGreenObject(newObject, blue_green_helper_2.NONE_LABEL_VALUE);
|
||||
}
|
||||
else {
|
||||
// green label will be added for these
|
||||
return blue_green_helper_1.getNewBlueGreenObject(newObject, blue_green_helper_2.GREEN_LABEL_VALUE);
|
||||
}
|
||||
}
|
||||
exports.getSMIServiceResource = getSMIServiceResource;
|
||||
function routeBlueGreenSMI(kubectl, nextLabel, serviceEntityList) {
|
||||
serviceEntityList.forEach((serviceObject) => {
|
||||
// routing trafficsplit to given label
|
||||
createTrafficSplitObject(kubectl, serviceObject.metadata.name, nextLabel);
|
||||
});
|
||||
}
|
||||
exports.routeBlueGreenSMI = routeBlueGreenSMI;
|
||||
function validateTrafficSplitsState(kubectl, serviceEntityList) {
|
||||
let areTrafficSplitsInRightState = true;
|
||||
serviceEntityList.forEach((serviceObject) => {
|
||||
const name = serviceObject.metadata.name;
|
||||
let trafficSplitObject = blue_green_helper_1.fetchResource(kubectl, TRAFFIC_SPLIT_OBJECT, blue_green_helper_1.getBlueGreenResourceName(name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX));
|
||||
if (!trafficSplitObject) {
|
||||
// no trafficplit exits
|
||||
areTrafficSplitsInRightState = false;
|
||||
}
|
||||
trafficSplitObject = JSON.parse(JSON.stringify(trafficSplitObject));
|
||||
trafficSplitObject.spec.backends.forEach(element => {
|
||||
// checking if trafficsplit in right state to deploy
|
||||
if (element.service === blue_green_helper_1.getBlueGreenResourceName(name, blue_green_helper_2.GREEN_SUFFIX)) {
|
||||
if (element.weight != MAX_VAL) {
|
||||
// green service should have max weight
|
||||
areTrafficSplitsInRightState = false;
|
||||
}
|
||||
}
|
||||
if (element.service === blue_green_helper_1.getBlueGreenResourceName(name, blue_green_helper_2.STABLE_SUFFIX)) {
|
||||
if (element.weight != MIN_VAL) {
|
||||
// stable service should have 0 weight
|
||||
areTrafficSplitsInRightState = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return areTrafficSplitsInRightState;
|
||||
}
|
||||
exports.validateTrafficSplitsState = validateTrafficSplitsState;
|
||||
function cleanupSMI(kubectl, serviceEntityList) {
|
||||
const deleteList = [];
|
||||
serviceEntityList.forEach((serviceObject) => {
|
||||
deleteList.push({ name: blue_green_helper_1.getBlueGreenResourceName(serviceObject.metadata.name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX), kind: TRAFFIC_SPLIT_OBJECT });
|
||||
deleteList.push({ name: blue_green_helper_1.getBlueGreenResourceName(serviceObject.metadata.name, blue_green_helper_2.GREEN_SUFFIX), kind: serviceObject.kind });
|
||||
deleteList.push({ name: blue_green_helper_1.getBlueGreenResourceName(serviceObject.metadata.name, blue_green_helper_2.STABLE_SUFFIX), kind: serviceObject.kind });
|
||||
});
|
||||
// deleting all objects
|
||||
blue_green_helper_1.deleteObjects(kubectl, deleteList);
|
||||
}
|
||||
exports.cleanupSMI = cleanupSMI;
|
||||
@@ -1,199 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.redirectTrafficToStableDeployment = exports.redirectTrafficToCanaryDeployment = exports.deploySMICanary = void 0;
|
||||
const core = require("@actions/core");
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const util = require("util");
|
||||
const TaskInputParameters = require("../../input-parameters");
|
||||
const fileHelper = require("../files-helper");
|
||||
const helper = require("../resource-object-utility");
|
||||
const utils = require("../manifest-utilities");
|
||||
const kubectlUtils = require("../kubectl-util");
|
||||
const canaryDeploymentHelper = require("./canary-deployment-helper");
|
||||
const utility_1 = require("../utility");
|
||||
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout';
|
||||
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit';
|
||||
let trafficSplitAPIVersion = "";
|
||||
function deploySMICanary(kubectl, filePaths) {
|
||||
const newObjectsList = [];
|
||||
const canaryReplicaCount = parseInt(TaskInputParameters.baselineAndCanaryReplicas);
|
||||
core.debug('Replica count is ' + canaryReplicaCount);
|
||||
filePaths.forEach((filePath) => {
|
||||
const fileContents = fs.readFileSync(filePath);
|
||||
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||
const name = inputObject.metadata.name;
|
||||
const kind = inputObject.kind;
|
||||
if (helper.isDeploymentEntity(kind)) {
|
||||
// Get stable object
|
||||
core.debug('Querying stable object');
|
||||
const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, name);
|
||||
if (!stableObject) {
|
||||
core.debug('Stable object not found. Creating only canary object');
|
||||
// If stable object not found, create canary deployment.
|
||||
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||
newObjectsList.push(newCanaryObject);
|
||||
}
|
||||
else {
|
||||
if (!canaryDeploymentHelper.isResourceMarkedAsStable(stableObject)) {
|
||||
throw (`StableSpecSelectorNotExist : ${name}`);
|
||||
}
|
||||
core.debug('Stable object found. Creating canary and baseline objects');
|
||||
// If canary object not found, create canary and baseline object.
|
||||
const newCanaryObject = canaryDeploymentHelper.getNewCanaryResource(inputObject, canaryReplicaCount);
|
||||
const newBaselineObject = canaryDeploymentHelper.getNewBaselineResource(stableObject, canaryReplicaCount);
|
||||
core.debug('New canary object is: ' + JSON.stringify(newCanaryObject));
|
||||
core.debug('New baseline object is: ' + JSON.stringify(newBaselineObject));
|
||||
newObjectsList.push(newCanaryObject);
|
||||
newObjectsList.push(newBaselineObject);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Updating non deployment entity as it is.
|
||||
newObjectsList.push(inputObject);
|
||||
}
|
||||
});
|
||||
});
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
const result = kubectl.apply(manifestFiles, TaskInputParameters.forceDeployment);
|
||||
createCanaryService(kubectl, filePaths);
|
||||
return { 'result': result, 'newFilePaths': manifestFiles };
|
||||
}
|
||||
exports.deploySMICanary = deploySMICanary;
|
||||
function createCanaryService(kubectl, filePaths) {
|
||||
const newObjectsList = [];
|
||||
const trafficObjectsList = [];
|
||||
filePaths.forEach((filePath) => {
|
||||
const fileContents = fs.readFileSync(filePath);
|
||||
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||
const name = inputObject.metadata.name;
|
||||
const kind = inputObject.kind;
|
||||
if (helper.isServiceEntity(kind)) {
|
||||
const newCanaryServiceObject = canaryDeploymentHelper.getNewCanaryResource(inputObject);
|
||||
core.debug('New canary service object is: ' + JSON.stringify(newCanaryServiceObject));
|
||||
newObjectsList.push(newCanaryServiceObject);
|
||||
const newBaselineServiceObject = canaryDeploymentHelper.getNewBaselineResource(inputObject);
|
||||
core.debug('New baseline object is: ' + JSON.stringify(newBaselineServiceObject));
|
||||
newObjectsList.push(newBaselineServiceObject);
|
||||
core.debug('Querying for stable service object');
|
||||
const stableObject = canaryDeploymentHelper.fetchResource(kubectl, kind, canaryDeploymentHelper.getStableResourceName(name));
|
||||
if (!stableObject) {
|
||||
const newStableServiceObject = canaryDeploymentHelper.getStableResource(inputObject);
|
||||
core.debug('New stable service object is: ' + JSON.stringify(newStableServiceObject));
|
||||
newObjectsList.push(newStableServiceObject);
|
||||
core.debug('Creating the traffic object for service: ' + name);
|
||||
const trafficObject = createTrafficSplitManifestFile(kubectl, name, 0, 0, 1000);
|
||||
core.debug('Creating the traffic object for service: ' + trafficObject);
|
||||
trafficObjectsList.push(trafficObject);
|
||||
}
|
||||
else {
|
||||
let updateTrafficObject = true;
|
||||
const trafficObject = canaryDeploymentHelper.fetchResource(kubectl, TRAFFIC_SPLIT_OBJECT, getTrafficSplitResourceName(name));
|
||||
if (trafficObject) {
|
||||
const trafficJObject = JSON.parse(JSON.stringify(trafficObject));
|
||||
if (trafficJObject && trafficJObject.spec && trafficJObject.spec.backends) {
|
||||
trafficJObject.spec.backends.forEach((s) => {
|
||||
if (s.service === canaryDeploymentHelper.getCanaryResourceName(name) && s.weight === "1000m") {
|
||||
core.debug('Update traffic objcet not required');
|
||||
updateTrafficObject = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (updateTrafficObject) {
|
||||
core.debug('Stable service object present so updating the traffic object for service: ' + name);
|
||||
trafficObjectsList.push(updateTrafficSplitObject(kubectl, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList);
|
||||
manifestFiles.push(...trafficObjectsList);
|
||||
const result = kubectl.apply(manifestFiles, TaskInputParameters.forceDeployment);
|
||||
utility_1.checkForErrors([result]);
|
||||
}
|
||||
function redirectTrafficToCanaryDeployment(kubectl, manifestFilePaths) {
|
||||
adjustTraffic(kubectl, manifestFilePaths, 0, 1000);
|
||||
}
|
||||
exports.redirectTrafficToCanaryDeployment = redirectTrafficToCanaryDeployment;
|
||||
function redirectTrafficToStableDeployment(kubectl, manifestFilePaths) {
|
||||
adjustTraffic(kubectl, manifestFilePaths, 1000, 0);
|
||||
}
|
||||
exports.redirectTrafficToStableDeployment = redirectTrafficToStableDeployment;
|
||||
function adjustTraffic(kubectl, manifestFilePaths, stableWeight, canaryWeight) {
|
||||
// get manifest files
|
||||
const inputManifestFiles = utils.getManifestFiles(manifestFilePaths);
|
||||
if (inputManifestFiles == null || inputManifestFiles.length == 0) {
|
||||
return;
|
||||
}
|
||||
const trafficSplitManifests = [];
|
||||
const serviceObjects = [];
|
||||
inputManifestFiles.forEach((filePath) => {
|
||||
const fileContents = fs.readFileSync(filePath);
|
||||
yaml.safeLoadAll(fileContents, function (inputObject) {
|
||||
const name = inputObject.metadata.name;
|
||||
const kind = inputObject.kind;
|
||||
if (helper.isServiceEntity(kind)) {
|
||||
trafficSplitManifests.push(createTrafficSplitManifestFile(kubectl, name, stableWeight, 0, canaryWeight));
|
||||
serviceObjects.push(name);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (trafficSplitManifests.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const result = kubectl.apply(trafficSplitManifests, TaskInputParameters.forceDeployment);
|
||||
core.debug('serviceObjects:' + serviceObjects.join(',') + ' result:' + result);
|
||||
utility_1.checkForErrors([result]);
|
||||
}
|
||||
function updateTrafficSplitObject(kubectl, serviceName) {
|
||||
const percentage = parseInt(TaskInputParameters.canaryPercentage) * 10;
|
||||
const baselineAndCanaryWeight = percentage / 2;
|
||||
const stableDeploymentWeight = 1000 - percentage;
|
||||
core.debug('Creating the traffic object with canary weight: ' + baselineAndCanaryWeight + ',baseling weight: ' + baselineAndCanaryWeight + ',stable: ' + stableDeploymentWeight);
|
||||
return createTrafficSplitManifestFile(kubectl, serviceName, stableDeploymentWeight, baselineAndCanaryWeight, baselineAndCanaryWeight);
|
||||
}
|
||||
function createTrafficSplitManifestFile(kubectl, serviceName, stableWeight, baselineWeight, canaryWeight) {
|
||||
const smiObjectString = getTrafficSplitObject(kubectl, serviceName, stableWeight, baselineWeight, canaryWeight);
|
||||
const manifestFile = fileHelper.writeManifestToFile(smiObjectString, TRAFFIC_SPLIT_OBJECT, serviceName);
|
||||
if (!manifestFile) {
|
||||
throw new Error('UnableToCreateTrafficSplitManifestFile');
|
||||
}
|
||||
return manifestFile;
|
||||
}
|
||||
function getTrafficSplitObject(kubectl, name, stableWeight, baselineWeight, canaryWeight) {
|
||||
if (!trafficSplitAPIVersion) {
|
||||
trafficSplitAPIVersion = kubectlUtils.getTrafficSplitAPIVersion(kubectl);
|
||||
}
|
||||
const trafficSplitObjectJson = `{
|
||||
"apiVersion": "${trafficSplitAPIVersion}",
|
||||
"kind": "TrafficSplit",
|
||||
"metadata": {
|
||||
"name": "%s"
|
||||
},
|
||||
"spec": {
|
||||
"backends": [
|
||||
{
|
||||
"service": "%s",
|
||||
"weight": "%sm"
|
||||
},
|
||||
{
|
||||
"service": "%s",
|
||||
"weight": "%sm"
|
||||
},
|
||||
{
|
||||
"service": "%s",
|
||||
"weight": "%sm"
|
||||
}
|
||||
],
|
||||
"service": "%s"
|
||||
}
|
||||
}`;
|
||||
const trafficSplitObject = util.format(trafficSplitObjectJson, getTrafficSplitResourceName(name), canaryDeploymentHelper.getStableResourceName(name), stableWeight, canaryDeploymentHelper.getBaselineResourceName(name), baselineWeight, canaryDeploymentHelper.getCanaryResourceName(name), canaryWeight, name);
|
||||
return trafficSplitObject;
|
||||
}
|
||||
function getTrafficSplitResourceName(name) {
|
||||
return name + TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isEqual = exports.StringComparer = void 0;
|
||||
var StringComparer;
|
||||
(function (StringComparer) {
|
||||
StringComparer[StringComparer["Ordinal"] = 0] = "Ordinal";
|
||||
StringComparer[StringComparer["OrdinalIgnoreCase"] = 1] = "OrdinalIgnoreCase";
|
||||
})(StringComparer = exports.StringComparer || (exports.StringComparer = {}));
|
||||
function isEqual(str1, str2, stringComparer) {
|
||||
if (str1 == null && str2 == null) {
|
||||
return true;
|
||||
}
|
||||
if (str1 == null) {
|
||||
return false;
|
||||
}
|
||||
if (str2 == null) {
|
||||
return false;
|
||||
}
|
||||
if (stringComparer == StringComparer.OrdinalIgnoreCase) {
|
||||
return str1.toUpperCase() === str2.toUpperCase();
|
||||
}
|
||||
else {
|
||||
return str1 === str2;
|
||||
}
|
||||
}
|
||||
exports.isEqual = isEqual;
|
||||
@@ -1,527 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ToolRunner = void 0;
|
||||
const os = require("os");
|
||||
const events = require("events");
|
||||
const child = require("child_process");
|
||||
const core = require("@actions/core");
|
||||
class ToolRunner extends events.EventEmitter {
|
||||
constructor(toolPath) {
|
||||
super();
|
||||
if (!toolPath) {
|
||||
throw new Error('Parameter \'toolPath\' cannot be null or empty.');
|
||||
}
|
||||
this.toolPath = toolPath;
|
||||
this.args = [];
|
||||
core.debug('toolRunner toolPath: ' + toolPath);
|
||||
}
|
||||
_debug(message) {
|
||||
this.emit('debug', message);
|
||||
}
|
||||
_argStringToArray(argString) {
|
||||
var args = [];
|
||||
var inQuotes = false;
|
||||
var escaped = false;
|
||||
var lastCharWasSpace = true;
|
||||
var arg = '';
|
||||
var append = function (c) {
|
||||
// we only escape double quotes.
|
||||
if (escaped && c !== '"') {
|
||||
arg += '\\';
|
||||
}
|
||||
arg += c;
|
||||
escaped = false;
|
||||
};
|
||||
for (var i = 0; i < argString.length; i++) {
|
||||
var c = argString.charAt(i);
|
||||
if (c === ' ' && !inQuotes) {
|
||||
if (!lastCharWasSpace) {
|
||||
args.push(arg);
|
||||
arg = '';
|
||||
}
|
||||
lastCharWasSpace = true;
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
lastCharWasSpace = false;
|
||||
}
|
||||
if (c === '"') {
|
||||
if (!escaped) {
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
else {
|
||||
append(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c === "\\" && escaped) {
|
||||
append(c);
|
||||
continue;
|
||||
}
|
||||
if (c === "\\" && inQuotes) {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
append(c);
|
||||
lastCharWasSpace = false;
|
||||
}
|
||||
if (!lastCharWasSpace) {
|
||||
args.push(arg.trim());
|
||||
}
|
||||
return args;
|
||||
}
|
||||
_getCommandString(options, noPrefix) {
|
||||
let toolPath = this._getSpawnFileName();
|
||||
let args = this._getSpawnArgs(options);
|
||||
let cmd = noPrefix ? '' : '[command]'; // omit prefix when piped to a second tool
|
||||
if (process.platform == 'win32') {
|
||||
// Windows + cmd file
|
||||
if (this._isCmdFile()) {
|
||||
cmd += toolPath;
|
||||
args.forEach((a) => {
|
||||
cmd += ` ${a}`;
|
||||
});
|
||||
}
|
||||
// Windows + verbatim
|
||||
else if (options.windowsVerbatimArguments) {
|
||||
cmd += `"${toolPath}"`;
|
||||
args.forEach((a) => {
|
||||
cmd += ` ${a}`;
|
||||
});
|
||||
}
|
||||
// Windows (regular)
|
||||
else {
|
||||
cmd += this._windowsQuoteCmdArg(toolPath);
|
||||
args.forEach((a) => {
|
||||
cmd += ` ${this._windowsQuoteCmdArg(a)}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
// OSX/Linux - this can likely be improved with some form of quoting.
|
||||
// creating processes on Unix is fundamentally different than Windows.
|
||||
// on Unix, execvp() takes an arg array.
|
||||
cmd += toolPath;
|
||||
args.forEach((a) => {
|
||||
cmd += ` ${a}`;
|
||||
});
|
||||
}
|
||||
// append second tool
|
||||
if (this.pipeOutputToTool) {
|
||||
cmd += ' | ' + this.pipeOutputToTool._getCommandString(options, /*noPrefix:*/ true);
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
_getSpawnFileName() {
|
||||
if (process.platform == 'win32') {
|
||||
if (this._isCmdFile()) {
|
||||
return process.env['COMSPEC'] || 'cmd.exe';
|
||||
}
|
||||
}
|
||||
return this.toolPath;
|
||||
}
|
||||
_getSpawnArgs(options) {
|
||||
if (process.platform == 'win32') {
|
||||
if (this._isCmdFile()) {
|
||||
let argline = `/D /S /C "${this._windowsQuoteCmdArg(this.toolPath)}`;
|
||||
for (let i = 0; i < this.args.length; i++) {
|
||||
argline += ' ';
|
||||
argline += options.windowsVerbatimArguments ? this.args[i] : this._windowsQuoteCmdArg(this.args[i]);
|
||||
}
|
||||
argline += '"';
|
||||
return [argline];
|
||||
}
|
||||
if (options.windowsVerbatimArguments) {
|
||||
// note, in Node 6.x options.argv0 can be used instead of overriding args.slice and args.unshift.
|
||||
// for more details, refer to https://github.com/nodejs/node/blob/v6.x/lib/child_process.js
|
||||
let args = this.args.slice(0); // copy the array
|
||||
// override slice to prevent Node from creating a copy of the arg array.
|
||||
// we need Node to use the "unshift" override below.
|
||||
args.slice = function () {
|
||||
if (arguments.length != 1 || arguments[0] != 0) {
|
||||
throw new Error('Unexpected arguments passed to args.slice when windowsVerbatimArguments flag is set.');
|
||||
}
|
||||
return args;
|
||||
};
|
||||
// override unshift
|
||||
//
|
||||
// when using the windowsVerbatimArguments option, Node does not quote the tool path when building
|
||||
// the cmdline parameter for the win32 function CreateProcess(). an unquoted space in the tool path
|
||||
// causes problems for tools when attempting to parse their own command line args. tools typically
|
||||
// assume their arguments begin after arg 0.
|
||||
//
|
||||
// by hijacking unshift, we can quote the tool path when it pushed onto the args array. Node builds
|
||||
// the cmdline parameter from the args array.
|
||||
//
|
||||
// note, we can't simply pass a quoted tool path to Node for multiple reasons:
|
||||
// 1) Node verifies the file exists (calls win32 function GetFileAttributesW) and the check returns
|
||||
// false if the path is quoted.
|
||||
// 2) Node passes the tool path as the application parameter to CreateProcess, which expects the
|
||||
// path to be unquoted.
|
||||
//
|
||||
// also note, in addition to the tool path being embedded within the cmdline parameter, Node also
|
||||
// passes the tool path to CreateProcess via the application parameter (optional parameter). when
|
||||
// present, Windows uses the application parameter to determine which file to run, instead of
|
||||
// interpreting the file from the cmdline parameter.
|
||||
args.unshift = function () {
|
||||
if (arguments.length != 1) {
|
||||
throw new Error('Unexpected arguments passed to args.unshift when windowsVerbatimArguments flag is set.');
|
||||
}
|
||||
return Array.prototype.unshift.call(args, `"${arguments[0]}"`); // quote the file name
|
||||
};
|
||||
return args;
|
||||
}
|
||||
}
|
||||
return this.args;
|
||||
}
|
||||
_isCmdFile() {
|
||||
let upperToolPath = this.toolPath.toUpperCase();
|
||||
return this._endsWith(upperToolPath, '.CMD') || this._endsWith(upperToolPath, '.BAT');
|
||||
}
|
||||
_endsWith(str, end) {
|
||||
return str.slice(-end.length) == end;
|
||||
}
|
||||
_windowsQuoteCmdArg(arg) {
|
||||
// for .exe, apply the normal quoting rules that libuv applies
|
||||
if (!this._isCmdFile()) {
|
||||
return this._uv_quote_cmd_arg(arg);
|
||||
}
|
||||
// otherwise apply quoting rules specific to the cmd.exe command line parser.
|
||||
// the libuv rules are generic and are not designed specifically for cmd.exe
|
||||
// command line parser.
|
||||
//
|
||||
// for a detailed description of the cmd.exe command line parser, refer to
|
||||
// http://stackoverflow.com/questions/4094699/how-does-the-windows-command-interpreter-cmd-exe-parse-scripts/7970912#7970912
|
||||
// need quotes for empty arg
|
||||
if (!arg) {
|
||||
return '""';
|
||||
}
|
||||
// determine whether the arg needs to be quoted
|
||||
const cmdSpecialChars = [' ', '\t', '&', '(', ')', '[', ']', '{', '}', '^', '=', ';', '!', '\'', '+', ',', '`', '~', '|', '<', '>', '"'];
|
||||
let needsQuotes = false;
|
||||
for (let char of arg) {
|
||||
if (cmdSpecialChars.some(x => x == char)) {
|
||||
needsQuotes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// short-circuit if quotes not needed
|
||||
if (!needsQuotes) {
|
||||
return arg;
|
||||
}
|
||||
// the following quoting rules are very similar to the rules that by libuv applies.
|
||||
//
|
||||
// 1) wrap the string in quotes
|
||||
//
|
||||
// 2) double-up quotes - i.e. " => ""
|
||||
//
|
||||
// this is different from the libuv quoting rules. libuv replaces " with \", which unfortunately
|
||||
// doesn't work well with a cmd.exe command line.
|
||||
//
|
||||
// note, replacing " with "" also works well if the arg is passed to a downstream .NET console app.
|
||||
// for example, the command line:
|
||||
// foo.exe "myarg:""my val"""
|
||||
// is parsed by a .NET console app into an arg array:
|
||||
// [ "myarg:\"my val\"" ]
|
||||
// which is the same end result when applying libuv quoting rules. although the actual
|
||||
// command line from libuv quoting rules would look like:
|
||||
// foo.exe "myarg:\"my val\""
|
||||
//
|
||||
// 3) double-up slashes that preceed a quote,
|
||||
// e.g. hello \world => "hello \world"
|
||||
// hello\"world => "hello\\""world"
|
||||
// hello\\"world => "hello\\\\""world"
|
||||
// hello world\ => "hello world\\"
|
||||
//
|
||||
// technically this is not required for a cmd.exe command line, or the batch argument parser.
|
||||
// the reasons for including this as a .cmd quoting rule are:
|
||||
//
|
||||
// a) this is optimized for the scenario where the argument is passed from the .cmd file to an
|
||||
// external program. many programs (e.g. .NET console apps) rely on the slash-doubling rule.
|
||||
//
|
||||
// b) it's what we've been doing previously (by deferring to node default behavior) and we
|
||||
// haven't heard any complaints about that aspect.
|
||||
//
|
||||
// note, a weakness of the quoting rules chosen here, is that % is not escaped. in fact, % cannot be
|
||||
// escaped when used on the command line directly - even though within a .cmd file % can be escaped
|
||||
// by using %%.
|
||||
//
|
||||
// the saving grace is, on the command line, %var% is left as-is if var is not defined. this contrasts
|
||||
// the line parsing rules within a .cmd file, where if var is not defined it is replaced with nothing.
|
||||
//
|
||||
// one option that was explored was replacing % with ^% - i.e. %var% => ^%var^%. this hack would
|
||||
// often work, since it is unlikely that var^ would exist, and the ^ character is removed when the
|
||||
// variable is used. the problem, however, is that ^ is not removed when %* is used to pass the args
|
||||
// to an external program.
|
||||
//
|
||||
// an unexplored potential solution for the % escaping problem, is to create a wrapper .cmd file.
|
||||
// % can be escaped within a .cmd file.
|
||||
let reverse = '"';
|
||||
let quote_hit = true;
|
||||
for (let i = arg.length; i > 0; i--) { // walk the string in reverse
|
||||
reverse += arg[i - 1];
|
||||
if (quote_hit && arg[i - 1] == '\\') {
|
||||
reverse += '\\'; // double the slash
|
||||
}
|
||||
else if (arg[i - 1] == '"') {
|
||||
quote_hit = true;
|
||||
reverse += '"'; // double the quote
|
||||
}
|
||||
else {
|
||||
quote_hit = false;
|
||||
}
|
||||
}
|
||||
reverse += '"';
|
||||
return reverse.split('').reverse().join('');
|
||||
}
|
||||
_uv_quote_cmd_arg(arg) {
|
||||
// Tool runner wraps child_process.spawn() and needs to apply the same quoting as
|
||||
// Node in certain cases where the undocumented spawn option windowsVerbatimArguments
|
||||
// is used.
|
||||
//
|
||||
// Since this function is a port of quote_cmd_arg from Node 4.x (technically, lib UV,
|
||||
// see https://github.com/nodejs/node/blob/v4.x/deps/uv/src/win/process.c for details),
|
||||
// pasting copyright notice from Node within this function:
|
||||
//
|
||||
// Copyright Joyent, Inc. and other Node contributors. All rights reserved.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
if (!arg) {
|
||||
// Need double quotation for empty argument
|
||||
return '""';
|
||||
}
|
||||
if (arg.indexOf(' ') < 0 && arg.indexOf('\t') < 0 && arg.indexOf('"') < 0) {
|
||||
// No quotation needed
|
||||
return arg;
|
||||
}
|
||||
if (arg.indexOf('"') < 0 && arg.indexOf('\\') < 0) {
|
||||
// No embedded double quotes or backslashes, so I can just wrap
|
||||
// quote marks around the whole thing.
|
||||
return `"${arg}"`;
|
||||
}
|
||||
// Expected input/output:
|
||||
// input : hello"world
|
||||
// output: "hello\"world"
|
||||
// input : hello""world
|
||||
// output: "hello\"\"world"
|
||||
// input : hello\world
|
||||
// output: hello\world
|
||||
// input : hello\\world
|
||||
// output: hello\\world
|
||||
// input : hello\"world
|
||||
// output: "hello\\\"world"
|
||||
// input : hello\\"world
|
||||
// output: "hello\\\\\"world"
|
||||
// input : hello world\
|
||||
// output: "hello world\\" - note the comment in libuv actually reads "hello world\"
|
||||
// but it appears the comment is wrong, it should be "hello world\\"
|
||||
let reverse = '"';
|
||||
let quote_hit = true;
|
||||
for (let i = arg.length; i > 0; i--) { // walk the string in reverse
|
||||
reverse += arg[i - 1];
|
||||
if (quote_hit && arg[i - 1] == '\\') {
|
||||
reverse += '\\';
|
||||
}
|
||||
else if (arg[i - 1] == '"') {
|
||||
quote_hit = true;
|
||||
reverse += '\\';
|
||||
}
|
||||
else {
|
||||
quote_hit = false;
|
||||
}
|
||||
}
|
||||
reverse += '"';
|
||||
return reverse.split('').reverse().join('');
|
||||
}
|
||||
_cloneExecOptions(options) {
|
||||
options = options || {};
|
||||
let result = {
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: options.env || process.env,
|
||||
silent: options.silent || false,
|
||||
failOnStdErr: options.failOnStdErr || false,
|
||||
ignoreReturnCode: options.ignoreReturnCode || false,
|
||||
windowsVerbatimArguments: options.windowsVerbatimArguments || false
|
||||
};
|
||||
result.outStream = options.outStream || process.stdout;
|
||||
result.errStream = options.errStream || process.stderr;
|
||||
return result;
|
||||
}
|
||||
_getSpawnSyncOptions(options) {
|
||||
let result = {};
|
||||
result.cwd = options.cwd;
|
||||
result.env = options.env;
|
||||
result['windowsVerbatimArguments'] = options.windowsVerbatimArguments || this._isCmdFile();
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Add argument
|
||||
* Append an argument or an array of arguments
|
||||
* returns ToolRunner for chaining
|
||||
*
|
||||
* @param val string cmdline or array of strings
|
||||
* @returns ToolRunner
|
||||
*/
|
||||
arg(val) {
|
||||
if (!val) {
|
||||
return this;
|
||||
}
|
||||
if (val instanceof Array) {
|
||||
core.debug(this.toolPath + ' arg: ' + JSON.stringify(val));
|
||||
this.args = this.args.concat(val);
|
||||
}
|
||||
else if (typeof (val) === 'string') {
|
||||
core.debug(this.toolPath + ' arg: ' + val);
|
||||
this.args = this.args.concat(val.trim());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Parses an argument line into one or more arguments
|
||||
* e.g. .line('"arg one" two -z') is equivalent to .arg(['arg one', 'two', '-z'])
|
||||
* returns ToolRunner for chaining
|
||||
*
|
||||
* @param val string argument line
|
||||
* @returns ToolRunner
|
||||
*/
|
||||
line(val) {
|
||||
if (!val) {
|
||||
return this;
|
||||
}
|
||||
core.debug(this.toolPath + ' arg: ' + val);
|
||||
this.args = this.args.concat(this._argStringToArray(val));
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Add argument(s) if a condition is met
|
||||
* Wraps arg(). See arg for details
|
||||
* returns ToolRunner for chaining
|
||||
*
|
||||
* @param condition boolean condition
|
||||
* @param val string cmdline or array of strings
|
||||
* @returns ToolRunner
|
||||
*/
|
||||
argIf(condition, val) {
|
||||
if (condition) {
|
||||
this.arg(val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Pipe output of exec() to another tool
|
||||
* @param tool
|
||||
* @param file optional filename to additionally stream the output to.
|
||||
* @returns {ToolRunner}
|
||||
*/
|
||||
pipeExecOutputToTool(tool, file) {
|
||||
this.pipeOutputToTool = tool;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Exec a tool synchronously.
|
||||
* Output will be *not* be streamed to the live console. It will be returned after execution is complete.
|
||||
* Appropriate for short running tools
|
||||
* Returns IExecSyncResult with output and return code
|
||||
*
|
||||
* @param tool path to tool to exec
|
||||
* @param options optional exec options. See IExecSyncOptions
|
||||
* @returns IExecSyncResult
|
||||
*/
|
||||
execSync(options) {
|
||||
core.debug('exec tool: ' + this.toolPath);
|
||||
core.debug('arguments:');
|
||||
this.args.forEach((arg) => {
|
||||
core.debug(' ' + arg);
|
||||
});
|
||||
options = this._cloneExecOptions(options);
|
||||
if (!options.silent) {
|
||||
options.outStream.write(this._getCommandString(options) + os.EOL);
|
||||
}
|
||||
var r = child.spawnSync(this._getSpawnFileName(), this._getSpawnArgs(options), this._getSpawnSyncOptions(options));
|
||||
var res = { code: r.status, error: r.error };
|
||||
if (!options.silent && r.stdout && r.stdout.length > 0) {
|
||||
options.outStream.write(r.stdout);
|
||||
}
|
||||
if (!options.silent && r.stderr && r.stderr.length > 0) {
|
||||
options.errStream.write(r.stderr);
|
||||
}
|
||||
res.stdout = (r.stdout) ? r.stdout.toString() : '';
|
||||
res.stderr = (r.stderr) ? r.stderr.toString() : '';
|
||||
return res;
|
||||
}
|
||||
}
|
||||
exports.ToolRunner = ToolRunner;
|
||||
class ExecState extends events.EventEmitter {
|
||||
constructor(options, toolPath) {
|
||||
super();
|
||||
this.delay = 10000; // 10 seconds
|
||||
this.timeout = null;
|
||||
if (!toolPath) {
|
||||
throw new Error('toolPath must not be empty');
|
||||
}
|
||||
this.options = options;
|
||||
this.toolPath = toolPath;
|
||||
let delay = process.env['TASKLIB_TEST_TOOLRUNNER_EXITDELAY'];
|
||||
if (delay) {
|
||||
this.delay = parseInt(delay);
|
||||
}
|
||||
}
|
||||
CheckComplete() {
|
||||
if (this.done) {
|
||||
return;
|
||||
}
|
||||
if (this.processClosed) {
|
||||
this._setResult();
|
||||
}
|
||||
else if (this.processExited) {
|
||||
this.timeout = setTimeout(ExecState.HandleTimeout, this.delay, this);
|
||||
}
|
||||
}
|
||||
_setResult() {
|
||||
// determine whether there is an error
|
||||
let error;
|
||||
if (this.processExited) {
|
||||
if (this.processError) {
|
||||
error = new Error(`LIB_ProcessError: \n tool: ${this.toolPath} \n error: ${this.processError}`);
|
||||
}
|
||||
else if (this.processExitCode != 0 && !this.options.ignoreReturnCode) {
|
||||
error = new Error(`LIB_ProcessExitCode\n tool: ${this.toolPath} \n Exit Code: ${this.processExitCode}`);
|
||||
}
|
||||
else if (this.processStderr && this.options.failOnStdErr) {
|
||||
error = new Error(`LIB_ProcessStderr', ${this.toolPath}`);
|
||||
}
|
||||
}
|
||||
// clear the timeout
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
this.done = true;
|
||||
this.emit('done', error, this.processExitCode);
|
||||
}
|
||||
static HandleTimeout(state) {
|
||||
if (state.done) {
|
||||
return;
|
||||
}
|
||||
if (!state.processClosed && state.processExited) {
|
||||
core.debug(`LIB_StdioNotClosed`);
|
||||
}
|
||||
state._setResult();
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getNormalizedPath = exports.isHttpUrl = exports.getCurrentTime = exports.getRandomInt = exports.sleep = exports.getDeploymentConfig = exports.annotateChildPods = exports.getWorkflowFilePath = exports.getLastSuccessfulRunSha = exports.checkForErrors = exports.isEqual = exports.getExecutableExtension = void 0;
|
||||
const os = require("os");
|
||||
const core = require("@actions/core");
|
||||
const githubClient_1 = require("../githubClient");
|
||||
const httpClient_1 = require("./httpClient");
|
||||
const inputParams = require("../input-parameters");
|
||||
const docker_object_model_1 = require("../docker-object-model");
|
||||
const io = require("@actions/io");
|
||||
function getExecutableExtension() {
|
||||
if (os.type().match(/^Win/)) {
|
||||
return '.exe';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
exports.getExecutableExtension = getExecutableExtension;
|
||||
function isEqual(str1, str2, ignoreCase) {
|
||||
if (str1 == null && str2 == null) {
|
||||
return true;
|
||||
}
|
||||
if (str1 == null || str2 == null) {
|
||||
return false;
|
||||
}
|
||||
if (ignoreCase) {
|
||||
return str1.toUpperCase() === str2.toUpperCase();
|
||||
}
|
||||
else {
|
||||
return str1 === str2;
|
||||
}
|
||||
}
|
||||
exports.isEqual = isEqual;
|
||||
function checkForErrors(execResults, warnIfError) {
|
||||
if (execResults.length !== 0) {
|
||||
let stderr = '';
|
||||
execResults.forEach(result => {
|
||||
if (result && result.stderr) {
|
||||
if (result.code !== 0) {
|
||||
stderr += result.stderr + '\n';
|
||||
}
|
||||
else {
|
||||
core.warning(result.stderr);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (stderr.length > 0) {
|
||||
if (warnIfError) {
|
||||
core.warning(stderr.trim());
|
||||
}
|
||||
else {
|
||||
throw new Error(stderr.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.checkForErrors = checkForErrors;
|
||||
function getLastSuccessfulRunSha(kubectl, namespaceName, annotationKey) {
|
||||
try {
|
||||
const result = kubectl.getResource('namespace', namespaceName);
|
||||
if (result) {
|
||||
if (result.stderr) {
|
||||
core.warning(`${result.stderr}`);
|
||||
return process.env.GITHUB_SHA;
|
||||
}
|
||||
else if (result.stdout) {
|
||||
const annotationsSet = JSON.parse(result.stdout).metadata.annotations;
|
||||
if (annotationsSet && annotationsSet[annotationKey]) {
|
||||
return JSON.parse(annotationsSet[annotationKey].replace(/'/g, '"')).commit;
|
||||
}
|
||||
else {
|
||||
return 'NA';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
core.warning(`Failed to get commits from cluster. ${JSON.stringify(ex)}`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
exports.getLastSuccessfulRunSha = getLastSuccessfulRunSha;
|
||||
function getWorkflowFilePath(githubToken) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let workflowFilePath = process.env.GITHUB_WORKFLOW;
|
||||
if (!workflowFilePath.startsWith('.github/workflows/')) {
|
||||
const githubClient = new githubClient_1.GitHubClient(process.env.GITHUB_REPOSITORY, githubToken);
|
||||
const response = yield githubClient.getWorkflows();
|
||||
if (response) {
|
||||
if (response.statusCode == httpClient_1.StatusCodes.OK
|
||||
&& response.body
|
||||
&& response.body.total_count) {
|
||||
if (response.body.total_count > 0) {
|
||||
for (let workflow of response.body.workflows) {
|
||||
if (process.env.GITHUB_WORKFLOW === workflow.name) {
|
||||
workflowFilePath = workflow.path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (response.statusCode != httpClient_1.StatusCodes.OK) {
|
||||
core.debug(`An error occured while getting list of workflows on the repo. Statuscode: ${response.statusCode}, StatusMessage: ${response.statusMessage}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
core.warning(`Failed to get response from workflow list API`);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(workflowFilePath);
|
||||
});
|
||||
}
|
||||
exports.getWorkflowFilePath = getWorkflowFilePath;
|
||||
function annotateChildPods(kubectl, resourceType, resourceName, annotationKeyValStr, allPods) {
|
||||
const commandExecutionResults = [];
|
||||
let owner = resourceName;
|
||||
if (resourceType.toLowerCase().indexOf('deployment') > -1) {
|
||||
owner = kubectl.getNewReplicaSet(resourceName);
|
||||
}
|
||||
if (allPods && allPods.items && allPods.items.length > 0) {
|
||||
allPods.items.forEach((pod) => {
|
||||
const owners = pod.metadata.ownerReferences;
|
||||
if (owners) {
|
||||
for (let ownerRef of owners) {
|
||||
if (ownerRef.name === owner) {
|
||||
commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, annotationKeyValStr));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return commandExecutionResults;
|
||||
}
|
||||
exports.annotateChildPods = annotateChildPods;
|
||||
function getDeploymentConfig() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let helmChartPaths = (process.env.HELM_CHART_PATHS && process.env.HELM_CHART_PATHS.split(';').filter(path => path != "")) || [];
|
||||
helmChartPaths = helmChartPaths.map(helmchart => getNormalizedPath(helmchart.trim()));
|
||||
let inputManifestFiles = inputParams.manifests || [];
|
||||
if (!helmChartPaths.length) {
|
||||
inputManifestFiles = inputManifestFiles.map(manifestFile => getNormalizedPath(manifestFile));
|
||||
}
|
||||
const imageNames = inputParams.containers || [];
|
||||
let imageDockerfilePathMap = {};
|
||||
//Fetching from image label if available
|
||||
for (const image of imageNames) {
|
||||
try {
|
||||
imageDockerfilePathMap[image] = yield getDockerfilePath(image);
|
||||
}
|
||||
catch (ex) {
|
||||
core.warning(`Failed to get dockerfile path for image ${image.toString()} | ` + ex);
|
||||
}
|
||||
}
|
||||
const deploymentConfig = {
|
||||
manifestFilePaths: inputManifestFiles,
|
||||
helmChartFilePaths: helmChartPaths,
|
||||
dockerfilePaths: imageDockerfilePathMap
|
||||
};
|
||||
return Promise.resolve(deploymentConfig);
|
||||
});
|
||||
}
|
||||
exports.getDeploymentConfig = getDeploymentConfig;
|
||||
function sleep(timeout) {
|
||||
return new Promise(resolve => setTimeout(resolve, timeout));
|
||||
}
|
||||
exports.sleep = sleep;
|
||||
function getRandomInt(max) {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
}
|
||||
exports.getRandomInt = getRandomInt;
|
||||
function getCurrentTime() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
exports.getCurrentTime = getCurrentTime;
|
||||
function checkDockerPath() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let dockerPath = yield io.which('docker', false);
|
||||
if (!dockerPath) {
|
||||
throw new Error('Docker is not installed.');
|
||||
}
|
||||
});
|
||||
}
|
||||
function getDockerfilePath(image) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
let imageConfig, imageInspectResult;
|
||||
var dockerExec = new docker_object_model_1.DockerExec('docker');
|
||||
yield checkDockerPath();
|
||||
dockerExec.pull(image, [], true);
|
||||
imageInspectResult = dockerExec.inspect(image, [], true);
|
||||
imageConfig = JSON.parse(imageInspectResult)[0];
|
||||
const DOCKERFILE_PATH_LABEL_KEY = 'dockerfile-path';
|
||||
let pathValue = '';
|
||||
if (imageConfig) {
|
||||
if ((imageConfig.Config) && (imageConfig.Config.Labels) && (imageConfig.Config.Labels[DOCKERFILE_PATH_LABEL_KEY])) {
|
||||
const pathLabel = imageConfig.Config.Labels[DOCKERFILE_PATH_LABEL_KEY];
|
||||
pathValue = getNormalizedPath(pathLabel);
|
||||
}
|
||||
}
|
||||
return pathValue;
|
||||
});
|
||||
}
|
||||
function isHttpUrl(url) {
|
||||
const HTTP_REGEX = /^https?:\/\/.*$/;
|
||||
return HTTP_REGEX.test(url);
|
||||
}
|
||||
exports.isHttpUrl = isHttpUrl;
|
||||
function getNormalizedPath(pathValue) {
|
||||
if (!isHttpUrl(pathValue)) { //if it is not an http url then convert to link from current repo and commit
|
||||
return `https://github.com/${process.env.GITHUB_REPOSITORY}/blob/${process.env.GITHUB_SHA}/${pathValue}`;
|
||||
}
|
||||
return pathValue;
|
||||
}
|
||||
exports.getNormalizedPath = getNormalizedPath;
|
||||
-1
@@ -1 +0,0 @@
|
||||
../esprima/bin/esparse.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../esprima/bin/esvalidate.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../js-yaml/bin/js-yaml.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../semver/bin/semver.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../uuid/bin/uuid
|
||||
+6379
File diff suppressed because it is too large
Load Diff
+194
-6
@@ -16,11 +16,14 @@ import * as core from '@actions/core';
|
||||
|
||||
#### Inputs/Outputs
|
||||
|
||||
Action inputs can be read with `getInput`. Outputs can be set with `setOutput` which makes them available to be mapped into inputs of other actions to ensure they are decoupled.
|
||||
Action inputs can be read with `getInput` which returns a `string` or `getBooleanInput` which parses a boolean based on the [yaml 1.2 specification](https://yaml.org/spec/1.2/spec.html#id2804923). If `required` set to be false, the input should have a default value in `action.yml`.
|
||||
|
||||
Outputs can be set with `setOutput` which makes them available to be mapped into inputs of other actions to ensure they are decoupled.
|
||||
|
||||
```js
|
||||
const myInput = core.getInput('inputName', { required: true });
|
||||
|
||||
const myBooleanInput = core.getBooleanInput('booleanInputName', { required: true });
|
||||
const myMultilineInput = core.getMultilineInput('multilineInputName', { required: true });
|
||||
core.setOutput('outputKey', 'outputVal');
|
||||
```
|
||||
|
||||
@@ -62,11 +65,10 @@ catch (err) {
|
||||
// setFailed logs the message and sets a failing exit code
|
||||
core.setFailed(`Action failed with error ${err}`);
|
||||
}
|
||||
```
|
||||
|
||||
Note that `setNeutral` is not yet implemented in actions V2 but equivalent functionality is being planned.
|
||||
|
||||
```
|
||||
|
||||
#### Logging
|
||||
|
||||
Finally, this library provides some utilities for logging. Note that debug logging is hidden from the logs by default. This behavior can be toggled by enabling the [Step Debug Logs](../../docs/action-debugging.md#step-debug-logs).
|
||||
@@ -90,6 +92,8 @@ try {
|
||||
|
||||
// Do stuff
|
||||
core.info('Output to the actions build log')
|
||||
|
||||
core.notice('This is a message that will also emit an annotation')
|
||||
}
|
||||
catch (err) {
|
||||
core.error(`Error ${err}, action may still succeed though`);
|
||||
@@ -113,11 +117,123 @@ const result = await core.group('Do something async', async () => {
|
||||
})
|
||||
```
|
||||
|
||||
#### Annotations
|
||||
|
||||
This library has 3 methods that will produce [annotations](https://docs.github.com/en/rest/reference/checks#create-a-check-run).
|
||||
```js
|
||||
core.error('This is a bad error. This will also fail the build.')
|
||||
|
||||
core.warning('Something went wrong, but it\'s not bad enough to fail the build.')
|
||||
|
||||
core.notice('Something happened that you might want to know about.')
|
||||
```
|
||||
|
||||
These will surface to the UI in the Actions page and on Pull Requests. They look something like this:
|
||||
|
||||

|
||||
|
||||
These annotations can also be attached to particular lines and columns of your source files to show exactly where a problem is occuring.
|
||||
|
||||
These options are:
|
||||
```typescript
|
||||
export interface AnnotationProperties {
|
||||
/**
|
||||
* A title for the annotation.
|
||||
*/
|
||||
title?: string
|
||||
|
||||
/**
|
||||
* The name of the file for which the annotation should be created.
|
||||
*/
|
||||
file?: string
|
||||
|
||||
/**
|
||||
* The start line for the annotation.
|
||||
*/
|
||||
startLine?: number
|
||||
|
||||
/**
|
||||
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
|
||||
*/
|
||||
endLine?: number
|
||||
|
||||
/**
|
||||
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||
*/
|
||||
startColumn?: number
|
||||
|
||||
/**
|
||||
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||
* Defaults to `startColumn` when `startColumn` is provided.
|
||||
*/
|
||||
endColumn?: number
|
||||
}
|
||||
```
|
||||
|
||||
#### Styling output
|
||||
|
||||
Colored output is supported in the Action logs via standard [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 3/4 bit, 8 bit and 24 bit colors are all supported.
|
||||
|
||||
Foreground colors:
|
||||
|
||||
```js
|
||||
// 3/4 bit
|
||||
core.info('\u001b[35mThis foreground will be magenta')
|
||||
|
||||
// 8 bit
|
||||
core.info('\u001b[38;5;6mThis foreground will be cyan')
|
||||
|
||||
// 24 bit
|
||||
core.info('\u001b[38;2;255;0;0mThis foreground will be bright red')
|
||||
```
|
||||
|
||||
Background colors:
|
||||
|
||||
```js
|
||||
// 3/4 bit
|
||||
core.info('\u001b[43mThis background will be yellow');
|
||||
|
||||
// 8 bit
|
||||
core.info('\u001b[48;5;6mThis background will be cyan')
|
||||
|
||||
// 24 bit
|
||||
core.info('\u001b[48;2;255;0;0mThis background will be bright red')
|
||||
```
|
||||
|
||||
Special styles:
|
||||
|
||||
```js
|
||||
core.info('\u001b[1mBold text')
|
||||
core.info('\u001b[3mItalic text')
|
||||
core.info('\u001b[4mUnderlined text')
|
||||
```
|
||||
|
||||
ANSI escape codes can be combined with one another:
|
||||
|
||||
```js
|
||||
core.info('\u001b[31;46mRed foreground with a cyan background and \u001b[1mbold text at the end');
|
||||
```
|
||||
|
||||
> Note: Escape codes reset at the start of each line
|
||||
|
||||
```js
|
||||
core.info('\u001b[35mThis foreground will be magenta')
|
||||
core.info('This foreground will reset to the default')
|
||||
```
|
||||
|
||||
Manually typing escape codes can be a little difficult, but you can use third party modules such as [ansi-styles](https://github.com/chalk/ansi-styles).
|
||||
|
||||
```js
|
||||
const style = require('ansi-styles');
|
||||
core.info(style.color.ansi16m.hex('#abcdef') + 'Hello world!')
|
||||
```
|
||||
|
||||
#### Action state
|
||||
|
||||
You can use this library to save state and get state for sharing information between a given wrapper action:
|
||||
You can use this library to save state and get state for sharing information between a given wrapper action:
|
||||
|
||||
**action.yml**:
|
||||
|
||||
**action.yml**
|
||||
```yaml
|
||||
name: 'Wrapper action sample'
|
||||
inputs:
|
||||
@@ -138,6 +254,7 @@ core.saveState("pidToKill", 12345);
|
||||
```
|
||||
|
||||
In action's `cleanup.js`:
|
||||
|
||||
```js
|
||||
const core = require('@actions/core');
|
||||
|
||||
@@ -145,3 +262,74 @@ var pid = core.getState("pidToKill");
|
||||
|
||||
process.kill(pid);
|
||||
```
|
||||
|
||||
#### OIDC Token
|
||||
|
||||
You can use these methods to interact with the GitHub OIDC provider and get a JWT ID token which would help to get access token from third party cloud providers.
|
||||
|
||||
**Method Name**: getIDToken()
|
||||
|
||||
**Inputs**
|
||||
|
||||
audience : optional
|
||||
|
||||
**Outputs**
|
||||
|
||||
A [JWT](https://jwt.io/) ID Token
|
||||
|
||||
In action's `main.ts`:
|
||||
```js
|
||||
const core = require('@actions/core');
|
||||
async function getIDTokenAction(): Promise<void> {
|
||||
|
||||
const audience = core.getInput('audience', {required: false})
|
||||
|
||||
const id_token1 = await core.getIDToken() // ID Token with default audience
|
||||
const id_token2 = await core.getIDToken(audience) // ID token with custom audience
|
||||
|
||||
// this id_token can be used to get access token from third party cloud providers
|
||||
}
|
||||
getIDTokenAction()
|
||||
```
|
||||
|
||||
In action's `actions.yml`:
|
||||
|
||||
```yaml
|
||||
name: 'GetIDToken'
|
||||
description: 'Get ID token from Github OIDC provider'
|
||||
inputs:
|
||||
audience:
|
||||
description: 'Audience for which the ID token is intended for'
|
||||
required: false
|
||||
outputs:
|
||||
id_token1:
|
||||
description: 'ID token obtained from OIDC provider'
|
||||
id_token2:
|
||||
description: 'ID token obtained from OIDC provider'
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'dist/index.js'
|
||||
```
|
||||
|
||||
#### Filesystem path helpers
|
||||
|
||||
You can use these methods to manipulate file paths across operating systems.
|
||||
|
||||
The `toPosixPath` function converts input paths to Posix-style (Linux) paths.
|
||||
The `toWin32Path` function converts input paths to Windows-style paths. These
|
||||
functions work independently of the underlying runner operating system.
|
||||
|
||||
```js
|
||||
toPosixPath('\\foo\\bar') // => /foo/bar
|
||||
toWin32Path('/foo/bar') // => \foo\bar
|
||||
```
|
||||
|
||||
The `toPlatformPath` function converts input paths to the expected value on the runner's operating system.
|
||||
|
||||
```js
|
||||
// On a Windows runner.
|
||||
toPlatformPath('/foo/bar') // => \foo\bar
|
||||
|
||||
// On a Linux runner.
|
||||
toPlatformPath('\\foo\\bar') // => /foo/bar
|
||||
```
|
||||
|
||||
+1
-7
@@ -1,4 +1,4 @@
|
||||
interface CommandProperties {
|
||||
export interface CommandProperties {
|
||||
[key: string]: any;
|
||||
}
|
||||
/**
|
||||
@@ -13,9 +13,3 @@ interface CommandProperties {
|
||||
*/
|
||||
export declare function issueCommand(command: string, properties: CommandProperties, message: any): void;
|
||||
export declare function issue(name: string, message?: string): void;
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
* @param input input to sanitize into a string
|
||||
*/
|
||||
export declare function toCommandValue(input: any): string;
|
||||
export {};
|
||||
|
||||
+18
-18
@@ -1,13 +1,27 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||
result["default"] = mod;
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.issue = exports.issueCommand = void 0;
|
||||
const os = __importStar(require("os"));
|
||||
const utils_1 = require("./utils");
|
||||
/**
|
||||
* Commands
|
||||
*
|
||||
@@ -61,28 +75,14 @@ class Command {
|
||||
return cmdStr;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
* @param input input to sanitize into a string
|
||||
*/
|
||||
function toCommandValue(input) {
|
||||
if (input === null || input === undefined) {
|
||||
return '';
|
||||
}
|
||||
else if (typeof input === 'string' || input instanceof String) {
|
||||
return input;
|
||||
}
|
||||
return JSON.stringify(input);
|
||||
}
|
||||
exports.toCommandValue = toCommandValue;
|
||||
function escapeData(s) {
|
||||
return toCommandValue(s)
|
||||
return utils_1.toCommandValue(s)
|
||||
.replace(/%/g, '%25')
|
||||
.replace(/\r/g, '%0D')
|
||||
.replace(/\n/g, '%0A');
|
||||
}
|
||||
function escapeProperty(s) {
|
||||
return toCommandValue(s)
|
||||
return utils_1.toCommandValue(s)
|
||||
.replace(/%/g, '%25')
|
||||
.replace(/\r/g, '%0D')
|
||||
.replace(/\n/g, '%0A')
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":";;;;;;;;;AAAA,uCAAwB;AAWxB;;;;;;;;;GASG;AACH,SAAgB,YAAY,CAC1B,OAAe,EACf,UAA6B,EAC7B,OAAY;IAEZ,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;IACrD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;AAC/C,CAAC;AAPD,oCAOC;AAED,SAAgB,KAAK,CAAC,IAAY,EAAE,UAAkB,EAAE;IACtD,YAAY,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;AACjC,CAAC;AAFD,sBAEC;AAED,MAAM,UAAU,GAAG,IAAI,CAAA;AAEvB,MAAM,OAAO;IAKX,YAAY,OAAe,EAAE,UAA6B,EAAE,OAAe;QACzE,IAAI,CAAC,OAAO,EAAE;YACZ,OAAO,GAAG,iBAAiB,CAAA;SAC5B;QAED,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;QAC5B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAED,QAAQ;QACN,IAAI,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC,OAAO,CAAA;QAEtC,IAAI,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;YAC9D,MAAM,IAAI,GAAG,CAAA;YACb,IAAI,KAAK,GAAG,IAAI,CAAA;YAChB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,UAAU,EAAE;gBACjC,IAAI,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE;oBACvC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;oBAChC,IAAI,GAAG,EAAE;wBACP,IAAI,KAAK,EAAE;4BACT,KAAK,GAAG,KAAK,CAAA;yBACd;6BAAM;4BACL,MAAM,IAAI,GAAG,CAAA;yBACd;wBAED,MAAM,IAAI,GAAG,GAAG,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAA;qBAC1C;iBACF;aACF;SACF;QAED,MAAM,IAAI,GAAG,UAAU,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAA;QACpD,OAAO,MAAM,CAAA;IACf,CAAC;CACF;AAED;;;GAGG;AACH,SAAgB,cAAc,CAAC,KAAU;IACvC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE;QACzC,OAAO,EAAE,CAAA;KACV;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,YAAY,MAAM,EAAE;QAC/D,OAAO,KAAe,CAAA;KACvB;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC;AAPD,wCAOC;AAED,SAAS,UAAU,CAAC,CAAM;IACxB,OAAO,cAAc,CAAC,CAAC,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;AAC1B,CAAC;AAED,SAAS,cAAc,CAAC,CAAM;IAC5B,OAAO,cAAc,CAAC,CAAC,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;AACzB,CAAC"}
|
||||
{"version":3,"file":"command.js","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAwB;AACxB,mCAAsC;AAWtC;;;;;;;;;GASG;AACH,SAAgB,YAAY,CAC1B,OAAe,EACf,UAA6B,EAC7B,OAAY;IAEZ,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;IACrD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;AAC/C,CAAC;AAPD,oCAOC;AAED,SAAgB,KAAK,CAAC,IAAY,EAAE,OAAO,GAAG,EAAE;IAC9C,YAAY,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;AACjC,CAAC;AAFD,sBAEC;AAED,MAAM,UAAU,GAAG,IAAI,CAAA;AAEvB,MAAM,OAAO;IAKX,YAAY,OAAe,EAAE,UAA6B,EAAE,OAAe;QACzE,IAAI,CAAC,OAAO,EAAE;YACZ,OAAO,GAAG,iBAAiB,CAAA;SAC5B;QAED,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;QAC5B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAED,QAAQ;QACN,IAAI,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC,OAAO,CAAA;QAEtC,IAAI,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;YAC9D,MAAM,IAAI,GAAG,CAAA;YACb,IAAI,KAAK,GAAG,IAAI,CAAA;YAChB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,UAAU,EAAE;gBACjC,IAAI,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE;oBACvC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;oBAChC,IAAI,GAAG,EAAE;wBACP,IAAI,KAAK,EAAE;4BACT,KAAK,GAAG,KAAK,CAAA;yBACd;6BAAM;4BACL,MAAM,IAAI,GAAG,CAAA;yBACd;wBAED,MAAM,IAAI,GAAG,GAAG,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAA;qBAC1C;iBACF;aACF;SACF;QAED,MAAM,IAAI,GAAG,UAAU,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAA;QACpD,OAAO,MAAM,CAAA;IACf,CAAC;CACF;AAED,SAAS,UAAU,CAAC,CAAM;IACxB,OAAO,sBAAc,CAAC,CAAC,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;AAC1B,CAAC;AAED,SAAS,cAAc,CAAC,CAAM;IAC5B,OAAO,sBAAc,CAAC,CAAC,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;SACpB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;AACzB,CAAC"}
|
||||
+80
-4
@@ -4,6 +4,8 @@
|
||||
export interface InputOptions {
|
||||
/** Optional. Whether the input is required. If required and not present, will throw. Defaults to false */
|
||||
required?: boolean;
|
||||
/** Optional. Whether leading/trailing whitespace will be trimmed for the input. Defaults to true */
|
||||
trimWhitespace?: boolean;
|
||||
}
|
||||
/**
|
||||
* The code to exit an action
|
||||
@@ -18,6 +20,37 @@ export declare enum ExitCode {
|
||||
*/
|
||||
Failure = 1
|
||||
}
|
||||
/**
|
||||
* Optional properties that can be sent with annotatation commands (notice, error, and warning)
|
||||
* See: https://docs.github.com/en/rest/reference/checks#create-a-check-run for more information about annotations.
|
||||
*/
|
||||
export interface AnnotationProperties {
|
||||
/**
|
||||
* A title for the annotation.
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* The path of the file for which the annotation should be created.
|
||||
*/
|
||||
file?: string;
|
||||
/**
|
||||
* The start line for the annotation.
|
||||
*/
|
||||
startLine?: number;
|
||||
/**
|
||||
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
|
||||
*/
|
||||
endLine?: number;
|
||||
/**
|
||||
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||
*/
|
||||
startColumn?: number;
|
||||
/**
|
||||
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||
* Defaults to `startColumn` when `startColumn` is provided.
|
||||
*/
|
||||
endColumn?: number;
|
||||
}
|
||||
/**
|
||||
* Sets env variable for this action and future actions in the job
|
||||
* @param name the name of the variable to set
|
||||
@@ -35,13 +68,35 @@ export declare function setSecret(secret: string): void;
|
||||
*/
|
||||
export declare function addPath(inputPath: string): void;
|
||||
/**
|
||||
* Gets the value of an input. The value is also trimmed.
|
||||
* Gets the value of an input.
|
||||
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
|
||||
* Returns an empty string if the value is not defined.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string
|
||||
*/
|
||||
export declare function getInput(name: string, options?: InputOptions): string;
|
||||
/**
|
||||
* Gets the values of an multiline input. Each value is also trimmed.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string[]
|
||||
*
|
||||
*/
|
||||
export declare function getMultilineInput(name: string, options?: InputOptions): string[];
|
||||
/**
|
||||
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
|
||||
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
|
||||
* The return value is also in boolean type.
|
||||
* ref: https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns boolean
|
||||
*/
|
||||
export declare function getBooleanInput(name: string, options?: InputOptions): boolean;
|
||||
/**
|
||||
* Sets the value of an output.
|
||||
*
|
||||
@@ -73,13 +128,21 @@ export declare function debug(message: string): void;
|
||||
/**
|
||||
* Adds an error issue
|
||||
* @param message error issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
export declare function error(message: string | Error): void;
|
||||
export declare function error(message: string | Error, properties?: AnnotationProperties): void;
|
||||
/**
|
||||
* Adds an warning issue
|
||||
* Adds a warning issue
|
||||
* @param message warning issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
export declare function warning(message: string | Error): void;
|
||||
export declare function warning(message: string | Error, properties?: AnnotationProperties): void;
|
||||
/**
|
||||
* Adds a notice issue
|
||||
* @param message notice issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
export declare function notice(message: string | Error, properties?: AnnotationProperties): void;
|
||||
/**
|
||||
* Writes info to log with console.log.
|
||||
* @param message info message
|
||||
@@ -120,3 +183,16 @@ export declare function saveState(name: string, value: any): void;
|
||||
* @returns string
|
||||
*/
|
||||
export declare function getState(name: string): string;
|
||||
export declare function getIDToken(aud?: string): Promise<string>;
|
||||
/**
|
||||
* Summary exports
|
||||
*/
|
||||
export { summary } from './summary';
|
||||
/**
|
||||
* @deprecated use core.summary
|
||||
*/
|
||||
export { markdownSummary } from './summary';
|
||||
/**
|
||||
* Path exports
|
||||
*/
|
||||
export { toPosixPath, toWin32Path, toPlatformPath } from './path-utils';
|
||||
|
||||
+131
-16
@@ -1,4 +1,23 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
@@ -8,17 +27,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
||||
result["default"] = mod;
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getIDToken = exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0;
|
||||
const command_1 = require("./command");
|
||||
const file_command_1 = require("./file-command");
|
||||
const utils_1 = require("./utils");
|
||||
const os = __importStar(require("os"));
|
||||
const path = __importStar(require("path"));
|
||||
const uuid_1 = require("uuid");
|
||||
const oidc_utils_1 = require("./oidc-utils");
|
||||
/**
|
||||
* The code to exit an action
|
||||
*/
|
||||
@@ -43,9 +60,24 @@ var ExitCode;
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function exportVariable(name, val) {
|
||||
const convertedVal = command_1.toCommandValue(val);
|
||||
const convertedVal = utils_1.toCommandValue(val);
|
||||
process.env[name] = convertedVal;
|
||||
command_1.issueCommand('set-env', { name }, convertedVal);
|
||||
const filePath = process.env['GITHUB_ENV'] || '';
|
||||
if (filePath) {
|
||||
const delimiter = `ghadelimiter_${uuid_1.v4()}`;
|
||||
// These should realistically never happen, but just in case someone finds a way to exploit uuid generation let's not allow keys or values that contain the delimiter.
|
||||
if (name.includes(delimiter)) {
|
||||
throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`);
|
||||
}
|
||||
if (convertedVal.includes(delimiter)) {
|
||||
throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`);
|
||||
}
|
||||
const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`;
|
||||
file_command_1.issueCommand('ENV', commandValue);
|
||||
}
|
||||
else {
|
||||
command_1.issueCommand('set-env', { name }, convertedVal);
|
||||
}
|
||||
}
|
||||
exports.exportVariable = exportVariable;
|
||||
/**
|
||||
@@ -61,12 +93,20 @@ exports.setSecret = setSecret;
|
||||
* @param inputPath
|
||||
*/
|
||||
function addPath(inputPath) {
|
||||
command_1.issueCommand('add-path', {}, inputPath);
|
||||
const filePath = process.env['GITHUB_PATH'] || '';
|
||||
if (filePath) {
|
||||
file_command_1.issueCommand('PATH', inputPath);
|
||||
}
|
||||
else {
|
||||
command_1.issueCommand('add-path', {}, inputPath);
|
||||
}
|
||||
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`;
|
||||
}
|
||||
exports.addPath = addPath;
|
||||
/**
|
||||
* Gets the value of an input. The value is also trimmed.
|
||||
* Gets the value of an input.
|
||||
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
|
||||
* Returns an empty string if the value is not defined.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
@@ -77,9 +117,49 @@ function getInput(name, options) {
|
||||
if (options && options.required && !val) {
|
||||
throw new Error(`Input required and not supplied: ${name}`);
|
||||
}
|
||||
if (options && options.trimWhitespace === false) {
|
||||
return val;
|
||||
}
|
||||
return val.trim();
|
||||
}
|
||||
exports.getInput = getInput;
|
||||
/**
|
||||
* Gets the values of an multiline input. Each value is also trimmed.
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns string[]
|
||||
*
|
||||
*/
|
||||
function getMultilineInput(name, options) {
|
||||
const inputs = getInput(name, options)
|
||||
.split('\n')
|
||||
.filter(x => x !== '');
|
||||
return inputs;
|
||||
}
|
||||
exports.getMultilineInput = getMultilineInput;
|
||||
/**
|
||||
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
|
||||
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
|
||||
* The return value is also in boolean type.
|
||||
* ref: https://yaml.org/spec/1.2/spec.html#id2804923
|
||||
*
|
||||
* @param name name of the input to get
|
||||
* @param options optional. See InputOptions.
|
||||
* @returns boolean
|
||||
*/
|
||||
function getBooleanInput(name, options) {
|
||||
const trueValue = ['true', 'True', 'TRUE'];
|
||||
const falseValue = ['false', 'False', 'FALSE'];
|
||||
const val = getInput(name, options);
|
||||
if (trueValue.includes(val))
|
||||
return true;
|
||||
if (falseValue.includes(val))
|
||||
return false;
|
||||
throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` +
|
||||
`Support boolean input list: \`true | True | TRUE | false | False | FALSE\``);
|
||||
}
|
||||
exports.getBooleanInput = getBooleanInput;
|
||||
/**
|
||||
* Sets the value of an output.
|
||||
*
|
||||
@@ -88,6 +168,7 @@ exports.getInput = getInput;
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function setOutput(name, value) {
|
||||
process.stdout.write(os.EOL);
|
||||
command_1.issueCommand('set-output', { name }, value);
|
||||
}
|
||||
exports.setOutput = setOutput;
|
||||
@@ -134,19 +215,30 @@ exports.debug = debug;
|
||||
/**
|
||||
* Adds an error issue
|
||||
* @param message error issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
function error(message) {
|
||||
command_1.issue('error', message instanceof Error ? message.toString() : message);
|
||||
function error(message, properties = {}) {
|
||||
command_1.issueCommand('error', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message);
|
||||
}
|
||||
exports.error = error;
|
||||
/**
|
||||
* Adds an warning issue
|
||||
* Adds a warning issue
|
||||
* @param message warning issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
function warning(message) {
|
||||
command_1.issue('warning', message instanceof Error ? message.toString() : message);
|
||||
function warning(message, properties = {}) {
|
||||
command_1.issueCommand('warning', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message);
|
||||
}
|
||||
exports.warning = warning;
|
||||
/**
|
||||
* Adds a notice issue
|
||||
* @param message notice issue message. Errors will be converted to string via toString()
|
||||
* @param properties optional properties to add to the annotation.
|
||||
*/
|
||||
function notice(message, properties = {}) {
|
||||
command_1.issueCommand('notice', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message);
|
||||
}
|
||||
exports.notice = notice;
|
||||
/**
|
||||
* Writes info to log with console.log.
|
||||
* @param message info message
|
||||
@@ -219,4 +311,27 @@ function getState(name) {
|
||||
return process.env[`STATE_${name}`] || '';
|
||||
}
|
||||
exports.getState = getState;
|
||||
function getIDToken(aud) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
return yield oidc_utils_1.OidcClient.getIDToken(aud);
|
||||
});
|
||||
}
|
||||
exports.getIDToken = getIDToken;
|
||||
/**
|
||||
* Summary exports
|
||||
*/
|
||||
var summary_1 = require("./summary");
|
||||
Object.defineProperty(exports, "summary", { enumerable: true, get: function () { return summary_1.summary; } });
|
||||
/**
|
||||
* @deprecated use core.summary
|
||||
*/
|
||||
var summary_2 = require("./summary");
|
||||
Object.defineProperty(exports, "markdownSummary", { enumerable: true, get: function () { return summary_2.markdownSummary; } });
|
||||
/**
|
||||
* Path exports
|
||||
*/
|
||||
var path_utils_1 = require("./path-utils");
|
||||
Object.defineProperty(exports, "toPosixPath", { enumerable: true, get: function () { return path_utils_1.toPosixPath; } });
|
||||
Object.defineProperty(exports, "toWin32Path", { enumerable: true, get: function () { return path_utils_1.toWin32Path; } });
|
||||
Object.defineProperty(exports, "toPlatformPath", { enumerable: true, get: function () { return path_utils_1.toPlatformPath; } });
|
||||
//# sourceMappingURL=core.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
export declare function issueCommand(command: string, message: any): void;
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
// For internal use, subject to change.
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.issueCommand = void 0;
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const fs = __importStar(require("fs"));
|
||||
const os = __importStar(require("os"));
|
||||
const utils_1 = require("./utils");
|
||||
function issueCommand(command, message) {
|
||||
const filePath = process.env[`GITHUB_${command}`];
|
||||
if (!filePath) {
|
||||
throw new Error(`Unable to find environment variable for file command ${command}`);
|
||||
}
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Missing file at path: ${filePath}`);
|
||||
}
|
||||
fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
}
|
||||
exports.issueCommand = issueCommand;
|
||||
//# sourceMappingURL=file-command.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"file-command.js","sourceRoot":"","sources":["../src/file-command.ts"],"names":[],"mappings":";AAAA,uCAAuC;;;;;;;;;;;;;;;;;;;;;;AAEvC,mCAAmC;AACnC,uDAAuD;AAEvD,uCAAwB;AACxB,uCAAwB;AACxB,mCAAsC;AAEtC,SAAgB,YAAY,CAAC,OAAe,EAAE,OAAY;IACxD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,OAAO,EAAE,CAAC,CAAA;IACjD,IAAI,CAAC,QAAQ,EAAE;QACb,MAAM,IAAI,KAAK,CACb,wDAAwD,OAAO,EAAE,CAClE,CAAA;KACF;IACD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC5B,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,EAAE,CAAC,CAAA;KACrD;IAED,EAAE,CAAC,cAAc,CAAC,QAAQ,EAAE,GAAG,sBAAc,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE;QACjE,QAAQ,EAAE,MAAM;KACjB,CAAC,CAAA;AACJ,CAAC;AAdD,oCAcC"}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export declare class OidcClient {
|
||||
private static createHttpClient;
|
||||
private static getRequestToken;
|
||||
private static getIDTokenUrl;
|
||||
private static getCall;
|
||||
static getIDToken(audience?: string): Promise<string>;
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OidcClient = void 0;
|
||||
const http_client_1 = require("@actions/http-client");
|
||||
const auth_1 = require("@actions/http-client/lib/auth");
|
||||
const core_1 = require("./core");
|
||||
class OidcClient {
|
||||
static createHttpClient(allowRetry = true, maxRetry = 10) {
|
||||
const requestOptions = {
|
||||
allowRetries: allowRetry,
|
||||
maxRetries: maxRetry
|
||||
};
|
||||
return new http_client_1.HttpClient('actions/oidc-client', [new auth_1.BearerCredentialHandler(OidcClient.getRequestToken())], requestOptions);
|
||||
}
|
||||
static getRequestToken() {
|
||||
const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'];
|
||||
if (!token) {
|
||||
throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
static getIDTokenUrl() {
|
||||
const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL'];
|
||||
if (!runtimeUrl) {
|
||||
throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable');
|
||||
}
|
||||
return runtimeUrl;
|
||||
}
|
||||
static getCall(id_token_url) {
|
||||
var _a;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const httpclient = OidcClient.createHttpClient();
|
||||
const res = yield httpclient
|
||||
.getJson(id_token_url)
|
||||
.catch(error => {
|
||||
throw new Error(`Failed to get ID Token. \n
|
||||
Error Code : ${error.statusCode}\n
|
||||
Error Message: ${error.result.message}`);
|
||||
});
|
||||
const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value;
|
||||
if (!id_token) {
|
||||
throw new Error('Response json body do not have ID Token field');
|
||||
}
|
||||
return id_token;
|
||||
});
|
||||
}
|
||||
static getIDToken(audience) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
try {
|
||||
// New ID Token is requested from action service
|
||||
let id_token_url = OidcClient.getIDTokenUrl();
|
||||
if (audience) {
|
||||
const encodedAudience = encodeURIComponent(audience);
|
||||
id_token_url = `${id_token_url}&audience=${encodedAudience}`;
|
||||
}
|
||||
core_1.debug(`ID token url is ${id_token_url}`);
|
||||
const id_token = yield OidcClient.getCall(id_token_url);
|
||||
core_1.setSecret(id_token);
|
||||
return id_token;
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Error message: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.OidcClient = OidcClient;
|
||||
//# sourceMappingURL=oidc-utils.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"oidc-utils.js","sourceRoot":"","sources":["../src/oidc-utils.ts"],"names":[],"mappings":";;;;;;;;;;;;AAGA,sDAA+C;AAC/C,wDAAqE;AACrE,iCAAuC;AAKvC,MAAa,UAAU;IACb,MAAM,CAAC,gBAAgB,CAC7B,UAAU,GAAG,IAAI,EACjB,QAAQ,GAAG,EAAE;QAEb,MAAM,cAAc,GAAmB;YACrC,YAAY,EAAE,UAAU;YACxB,UAAU,EAAE,QAAQ;SACrB,CAAA;QAED,OAAO,IAAI,wBAAU,CACnB,qBAAqB,EACrB,CAAC,IAAI,8BAAuB,CAAC,UAAU,CAAC,eAAe,EAAE,CAAC,CAAC,EAC3D,cAAc,CACf,CAAA;IACH,CAAC;IAEO,MAAM,CAAC,eAAe;QAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAA;QAC3D,IAAI,CAAC,KAAK,EAAE;YACV,MAAM,IAAI,KAAK,CACb,2DAA2D,CAC5D,CAAA;SACF;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAEO,MAAM,CAAC,aAAa;QAC1B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAA;QAC9D,IAAI,CAAC,UAAU,EAAE;YACf,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAA;SAC3E;QACD,OAAO,UAAU,CAAA;IACnB,CAAC;IAEO,MAAM,CAAO,OAAO,CAAC,YAAoB;;;YAC/C,MAAM,UAAU,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAA;YAEhD,MAAM,GAAG,GAAG,MAAM,UAAU;iBACzB,OAAO,CAAgB,YAAY,CAAC;iBACpC,KAAK,CAAC,KAAK,CAAC,EAAE;gBACb,MAAM,IAAI,KAAK,CACb;uBACa,KAAK,CAAC,UAAU;yBACd,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CACtC,CAAA;YACH,CAAC,CAAC,CAAA;YAEJ,MAAM,QAAQ,SAAG,GAAG,CAAC,MAAM,0CAAE,KAAK,CAAA;YAClC,IAAI,CAAC,QAAQ,EAAE;gBACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;aACjE;YACD,OAAO,QAAQ,CAAA;;KAChB;IAED,MAAM,CAAO,UAAU,CAAC,QAAiB;;YACvC,IAAI;gBACF,gDAAgD;gBAChD,IAAI,YAAY,GAAW,UAAU,CAAC,aAAa,EAAE,CAAA;gBACrD,IAAI,QAAQ,EAAE;oBACZ,MAAM,eAAe,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAA;oBACpD,YAAY,GAAG,GAAG,YAAY,aAAa,eAAe,EAAE,CAAA;iBAC7D;gBAED,YAAK,CAAC,mBAAmB,YAAY,EAAE,CAAC,CAAA;gBAExC,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;gBACvD,gBAAS,CAAC,QAAQ,CAAC,CAAA;gBACnB,OAAO,QAAQ,CAAA;aAChB;YAAC,OAAO,KAAK,EAAE;gBACd,MAAM,IAAI,KAAK,CAAC,kBAAkB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;aACnD;QACH,CAAC;KAAA;CACF;AAzED,gCAyEC"}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* toPosixPath converts the given path to the posix form. On Windows, \\ will be
|
||||
* replaced with /.
|
||||
*
|
||||
* @param pth. Path to transform.
|
||||
* @return string Posix path.
|
||||
*/
|
||||
export declare function toPosixPath(pth: string): string;
|
||||
/**
|
||||
* toWin32Path converts the given path to the win32 form. On Linux, / will be
|
||||
* replaced with \\.
|
||||
*
|
||||
* @param pth. Path to transform.
|
||||
* @return string Win32 path.
|
||||
*/
|
||||
export declare function toWin32Path(pth: string): string;
|
||||
/**
|
||||
* toPlatformPath converts the given path to a platform-specific path. It does
|
||||
* this by replacing instances of / and \ with the platform-specific path
|
||||
* separator.
|
||||
*
|
||||
* @param pth The path to platformize.
|
||||
* @return string The platform-specific path.
|
||||
*/
|
||||
export declare function toPlatformPath(pth: string): string;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = void 0;
|
||||
const path = __importStar(require("path"));
|
||||
/**
|
||||
* toPosixPath converts the given path to the posix form. On Windows, \\ will be
|
||||
* replaced with /.
|
||||
*
|
||||
* @param pth. Path to transform.
|
||||
* @return string Posix path.
|
||||
*/
|
||||
function toPosixPath(pth) {
|
||||
return pth.replace(/[\\]/g, '/');
|
||||
}
|
||||
exports.toPosixPath = toPosixPath;
|
||||
/**
|
||||
* toWin32Path converts the given path to the win32 form. On Linux, / will be
|
||||
* replaced with \\.
|
||||
*
|
||||
* @param pth. Path to transform.
|
||||
* @return string Win32 path.
|
||||
*/
|
||||
function toWin32Path(pth) {
|
||||
return pth.replace(/[/]/g, '\\');
|
||||
}
|
||||
exports.toWin32Path = toWin32Path;
|
||||
/**
|
||||
* toPlatformPath converts the given path to a platform-specific path. It does
|
||||
* this by replacing instances of / and \ with the platform-specific path
|
||||
* separator.
|
||||
*
|
||||
* @param pth The path to platformize.
|
||||
* @return string The platform-specific path.
|
||||
*/
|
||||
function toPlatformPath(pth) {
|
||||
return pth.replace(/[/\\]/g, path.sep);
|
||||
}
|
||||
exports.toPlatformPath = toPlatformPath;
|
||||
//# sourceMappingURL=path-utils.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA,2CAA4B;AAE5B;;;;;;GAMG;AACH,SAAgB,WAAW,CAAC,GAAW;IACrC,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAClC,CAAC;AAFD,kCAEC;AAED;;;;;;GAMG;AACH,SAAgB,WAAW,CAAC,GAAW;IACrC,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;AAClC,CAAC;AAFD,kCAEC;AAED;;;;;;;GAOG;AACH,SAAgB,cAAc,CAAC,GAAW;IACxC,OAAO,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;AACxC,CAAC;AAFD,wCAEC"}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
export declare const SUMMARY_ENV_VAR = "GITHUB_STEP_SUMMARY";
|
||||
export declare const SUMMARY_DOCS_URL = "https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary";
|
||||
export declare type SummaryTableRow = (SummaryTableCell | string)[];
|
||||
export interface SummaryTableCell {
|
||||
/**
|
||||
* Cell content
|
||||
*/
|
||||
data: string;
|
||||
/**
|
||||
* Render cell as header
|
||||
* (optional) default: false
|
||||
*/
|
||||
header?: boolean;
|
||||
/**
|
||||
* Number of columns the cell extends
|
||||
* (optional) default: '1'
|
||||
*/
|
||||
colspan?: string;
|
||||
/**
|
||||
* Number of rows the cell extends
|
||||
* (optional) default: '1'
|
||||
*/
|
||||
rowspan?: string;
|
||||
}
|
||||
export interface SummaryImageOptions {
|
||||
/**
|
||||
* The width of the image in pixels. Must be an integer without a unit.
|
||||
* (optional)
|
||||
*/
|
||||
width?: string;
|
||||
/**
|
||||
* The height of the image in pixels. Must be an integer without a unit.
|
||||
* (optional)
|
||||
*/
|
||||
height?: string;
|
||||
}
|
||||
export interface SummaryWriteOptions {
|
||||
/**
|
||||
* Replace all existing content in summary file with buffer contents
|
||||
* (optional) default: false
|
||||
*/
|
||||
overwrite?: boolean;
|
||||
}
|
||||
declare class Summary {
|
||||
private _buffer;
|
||||
private _filePath?;
|
||||
constructor();
|
||||
/**
|
||||
* Finds the summary file path from the environment, rejects if env var is not found or file does not exist
|
||||
* Also checks r/w permissions.
|
||||
*
|
||||
* @returns step summary file path
|
||||
*/
|
||||
private filePath;
|
||||
/**
|
||||
* Wraps content in an HTML tag, adding any HTML attributes
|
||||
*
|
||||
* @param {string} tag HTML tag to wrap
|
||||
* @param {string | null} content content within the tag
|
||||
* @param {[attribute: string]: string} attrs key-value list of HTML attributes to add
|
||||
*
|
||||
* @returns {string} content wrapped in HTML element
|
||||
*/
|
||||
private wrap;
|
||||
/**
|
||||
* Writes text in the buffer to the summary buffer file and empties buffer. Will append by default.
|
||||
*
|
||||
* @param {SummaryWriteOptions} [options] (optional) options for write operation
|
||||
*
|
||||
* @returns {Promise<Summary>} summary instance
|
||||
*/
|
||||
write(options?: SummaryWriteOptions): Promise<Summary>;
|
||||
/**
|
||||
* Clears the summary buffer and wipes the summary file
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
clear(): Promise<Summary>;
|
||||
/**
|
||||
* Returns the current summary buffer as a string
|
||||
*
|
||||
* @returns {string} string of summary buffer
|
||||
*/
|
||||
stringify(): string;
|
||||
/**
|
||||
* If the summary buffer is empty
|
||||
*
|
||||
* @returns {boolen} true if the buffer is empty
|
||||
*/
|
||||
isEmptyBuffer(): boolean;
|
||||
/**
|
||||
* Resets the summary buffer without writing to summary file
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
emptyBuffer(): Summary;
|
||||
/**
|
||||
* Adds raw text to the summary buffer
|
||||
*
|
||||
* @param {string} text content to add
|
||||
* @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false)
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addRaw(text: string, addEOL?: boolean): Summary;
|
||||
/**
|
||||
* Adds the operating system-specific end-of-line marker to the buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addEOL(): Summary;
|
||||
/**
|
||||
* Adds an HTML codeblock to the summary buffer
|
||||
*
|
||||
* @param {string} code content to render within fenced code block
|
||||
* @param {string} lang (optional) language to syntax highlight code
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addCodeBlock(code: string, lang?: string): Summary;
|
||||
/**
|
||||
* Adds an HTML list to the summary buffer
|
||||
*
|
||||
* @param {string[]} items list of items to render
|
||||
* @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false)
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addList(items: string[], ordered?: boolean): Summary;
|
||||
/**
|
||||
* Adds an HTML table to the summary buffer
|
||||
*
|
||||
* @param {SummaryTableCell[]} rows table rows
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addTable(rows: SummaryTableRow[]): Summary;
|
||||
/**
|
||||
* Adds a collapsable HTML details element to the summary buffer
|
||||
*
|
||||
* @param {string} label text for the closed state
|
||||
* @param {string} content collapsable content
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addDetails(label: string, content: string): Summary;
|
||||
/**
|
||||
* Adds an HTML image tag to the summary buffer
|
||||
*
|
||||
* @param {string} src path to the image you to embed
|
||||
* @param {string} alt text description of the image
|
||||
* @param {SummaryImageOptions} options (optional) addition image attributes
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addImage(src: string, alt: string, options?: SummaryImageOptions): Summary;
|
||||
/**
|
||||
* Adds an HTML section heading element
|
||||
*
|
||||
* @param {string} text heading text
|
||||
* @param {number | string} [level=1] (optional) the heading level, default: 1
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addHeading(text: string, level?: number | string): Summary;
|
||||
/**
|
||||
* Adds an HTML thematic break (<hr>) to the summary buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addSeparator(): Summary;
|
||||
/**
|
||||
* Adds an HTML line break (<br>) to the summary buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addBreak(): Summary;
|
||||
/**
|
||||
* Adds an HTML blockquote to the summary buffer
|
||||
*
|
||||
* @param {string} text quote text
|
||||
* @param {string} cite (optional) citation url
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addQuote(text: string, cite?: string): Summary;
|
||||
/**
|
||||
* Adds an HTML anchor tag to the summary buffer
|
||||
*
|
||||
* @param {string} text link text/content
|
||||
* @param {string} href hyperlink
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addLink(text: string, href: string): Summary;
|
||||
}
|
||||
/**
|
||||
* @deprecated use `core.summary`
|
||||
*/
|
||||
export declare const markdownSummary: Summary;
|
||||
export declare const summary: Summary;
|
||||
export {};
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0;
|
||||
const os_1 = require("os");
|
||||
const fs_1 = require("fs");
|
||||
const { access, appendFile, writeFile } = fs_1.promises;
|
||||
exports.SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY';
|
||||
exports.SUMMARY_DOCS_URL = 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary';
|
||||
class Summary {
|
||||
constructor() {
|
||||
this._buffer = '';
|
||||
}
|
||||
/**
|
||||
* Finds the summary file path from the environment, rejects if env var is not found or file does not exist
|
||||
* Also checks r/w permissions.
|
||||
*
|
||||
* @returns step summary file path
|
||||
*/
|
||||
filePath() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (this._filePath) {
|
||||
return this._filePath;
|
||||
}
|
||||
const pathFromEnv = process.env[exports.SUMMARY_ENV_VAR];
|
||||
if (!pathFromEnv) {
|
||||
throw new Error(`Unable to find environment variable for $${exports.SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`);
|
||||
}
|
||||
try {
|
||||
yield access(pathFromEnv, fs_1.constants.R_OK | fs_1.constants.W_OK);
|
||||
}
|
||||
catch (_a) {
|
||||
throw new Error(`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`);
|
||||
}
|
||||
this._filePath = pathFromEnv;
|
||||
return this._filePath;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Wraps content in an HTML tag, adding any HTML attributes
|
||||
*
|
||||
* @param {string} tag HTML tag to wrap
|
||||
* @param {string | null} content content within the tag
|
||||
* @param {[attribute: string]: string} attrs key-value list of HTML attributes to add
|
||||
*
|
||||
* @returns {string} content wrapped in HTML element
|
||||
*/
|
||||
wrap(tag, content, attrs = {}) {
|
||||
const htmlAttrs = Object.entries(attrs)
|
||||
.map(([key, value]) => ` ${key}="${value}"`)
|
||||
.join('');
|
||||
if (!content) {
|
||||
return `<${tag}${htmlAttrs}>`;
|
||||
}
|
||||
return `<${tag}${htmlAttrs}>${content}</${tag}>`;
|
||||
}
|
||||
/**
|
||||
* Writes text in the buffer to the summary buffer file and empties buffer. Will append by default.
|
||||
*
|
||||
* @param {SummaryWriteOptions} [options] (optional) options for write operation
|
||||
*
|
||||
* @returns {Promise<Summary>} summary instance
|
||||
*/
|
||||
write(options) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite);
|
||||
const filePath = yield this.filePath();
|
||||
const writeFunc = overwrite ? writeFile : appendFile;
|
||||
yield writeFunc(filePath, this._buffer, { encoding: 'utf8' });
|
||||
return this.emptyBuffer();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Clears the summary buffer and wipes the summary file
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
clear() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
return this.emptyBuffer().write({ overwrite: true });
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns the current summary buffer as a string
|
||||
*
|
||||
* @returns {string} string of summary buffer
|
||||
*/
|
||||
stringify() {
|
||||
return this._buffer;
|
||||
}
|
||||
/**
|
||||
* If the summary buffer is empty
|
||||
*
|
||||
* @returns {boolen} true if the buffer is empty
|
||||
*/
|
||||
isEmptyBuffer() {
|
||||
return this._buffer.length === 0;
|
||||
}
|
||||
/**
|
||||
* Resets the summary buffer without writing to summary file
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
emptyBuffer() {
|
||||
this._buffer = '';
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Adds raw text to the summary buffer
|
||||
*
|
||||
* @param {string} text content to add
|
||||
* @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false)
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addRaw(text, addEOL = false) {
|
||||
this._buffer += text;
|
||||
return addEOL ? this.addEOL() : this;
|
||||
}
|
||||
/**
|
||||
* Adds the operating system-specific end-of-line marker to the buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addEOL() {
|
||||
return this.addRaw(os_1.EOL);
|
||||
}
|
||||
/**
|
||||
* Adds an HTML codeblock to the summary buffer
|
||||
*
|
||||
* @param {string} code content to render within fenced code block
|
||||
* @param {string} lang (optional) language to syntax highlight code
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addCodeBlock(code, lang) {
|
||||
const attrs = Object.assign({}, (lang && { lang }));
|
||||
const element = this.wrap('pre', this.wrap('code', code), attrs);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML list to the summary buffer
|
||||
*
|
||||
* @param {string[]} items list of items to render
|
||||
* @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false)
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addList(items, ordered = false) {
|
||||
const tag = ordered ? 'ol' : 'ul';
|
||||
const listItems = items.map(item => this.wrap('li', item)).join('');
|
||||
const element = this.wrap(tag, listItems);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML table to the summary buffer
|
||||
*
|
||||
* @param {SummaryTableCell[]} rows table rows
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addTable(rows) {
|
||||
const tableBody = rows
|
||||
.map(row => {
|
||||
const cells = row
|
||||
.map(cell => {
|
||||
if (typeof cell === 'string') {
|
||||
return this.wrap('td', cell);
|
||||
}
|
||||
const { header, data, colspan, rowspan } = cell;
|
||||
const tag = header ? 'th' : 'td';
|
||||
const attrs = Object.assign(Object.assign({}, (colspan && { colspan })), (rowspan && { rowspan }));
|
||||
return this.wrap(tag, data, attrs);
|
||||
})
|
||||
.join('');
|
||||
return this.wrap('tr', cells);
|
||||
})
|
||||
.join('');
|
||||
const element = this.wrap('table', tableBody);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds a collapsable HTML details element to the summary buffer
|
||||
*
|
||||
* @param {string} label text for the closed state
|
||||
* @param {string} content collapsable content
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addDetails(label, content) {
|
||||
const element = this.wrap('details', this.wrap('summary', label) + content);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML image tag to the summary buffer
|
||||
*
|
||||
* @param {string} src path to the image you to embed
|
||||
* @param {string} alt text description of the image
|
||||
* @param {SummaryImageOptions} options (optional) addition image attributes
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addImage(src, alt, options) {
|
||||
const { width, height } = options || {};
|
||||
const attrs = Object.assign(Object.assign({}, (width && { width })), (height && { height }));
|
||||
const element = this.wrap('img', null, Object.assign({ src, alt }, attrs));
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML section heading element
|
||||
*
|
||||
* @param {string} text heading text
|
||||
* @param {number | string} [level=1] (optional) the heading level, default: 1
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addHeading(text, level) {
|
||||
const tag = `h${level}`;
|
||||
const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)
|
||||
? tag
|
||||
: 'h1';
|
||||
const element = this.wrap(allowedTag, text);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML thematic break (<hr>) to the summary buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addSeparator() {
|
||||
const element = this.wrap('hr', null);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML line break (<br>) to the summary buffer
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addBreak() {
|
||||
const element = this.wrap('br', null);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML blockquote to the summary buffer
|
||||
*
|
||||
* @param {string} text quote text
|
||||
* @param {string} cite (optional) citation url
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addQuote(text, cite) {
|
||||
const attrs = Object.assign({}, (cite && { cite }));
|
||||
const element = this.wrap('blockquote', text, attrs);
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
/**
|
||||
* Adds an HTML anchor tag to the summary buffer
|
||||
*
|
||||
* @param {string} text link text/content
|
||||
* @param {string} href hyperlink
|
||||
*
|
||||
* @returns {Summary} summary instance
|
||||
*/
|
||||
addLink(text, href) {
|
||||
const element = this.wrap('a', text, { href });
|
||||
return this.addRaw(element).addEOL();
|
||||
}
|
||||
}
|
||||
const _summary = new Summary();
|
||||
/**
|
||||
* @deprecated use `core.summary`
|
||||
*/
|
||||
exports.markdownSummary = _summary;
|
||||
exports.summary = _summary;
|
||||
//# sourceMappingURL=summary.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+14
@@ -0,0 +1,14 @@
|
||||
import { AnnotationProperties } from './core';
|
||||
import { CommandProperties } from './command';
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
* @param input input to sanitize into a string
|
||||
*/
|
||||
export declare function toCommandValue(input: any): string;
|
||||
/**
|
||||
*
|
||||
* @param annotationProperties
|
||||
* @returns The command properties to send with the actual annotation command
|
||||
* See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646
|
||||
*/
|
||||
export declare function toCommandProperties(annotationProperties: AnnotationProperties): CommandProperties;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
"use strict";
|
||||
// We use any as a valid input type
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.toCommandProperties = exports.toCommandValue = void 0;
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
* @param input input to sanitize into a string
|
||||
*/
|
||||
function toCommandValue(input) {
|
||||
if (input === null || input === undefined) {
|
||||
return '';
|
||||
}
|
||||
else if (typeof input === 'string' || input instanceof String) {
|
||||
return input;
|
||||
}
|
||||
return JSON.stringify(input);
|
||||
}
|
||||
exports.toCommandValue = toCommandValue;
|
||||
/**
|
||||
*
|
||||
* @param annotationProperties
|
||||
* @returns The command properties to send with the actual annotation command
|
||||
* See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646
|
||||
*/
|
||||
function toCommandProperties(annotationProperties) {
|
||||
if (!Object.keys(annotationProperties).length) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
title: annotationProperties.title,
|
||||
file: annotationProperties.file,
|
||||
line: annotationProperties.startLine,
|
||||
endLine: annotationProperties.endLine,
|
||||
col: annotationProperties.startColumn,
|
||||
endColumn: annotationProperties.endColumn
|
||||
};
|
||||
}
|
||||
exports.toCommandProperties = toCommandProperties;
|
||||
//# sourceMappingURL=utils.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";AAAA,mCAAmC;AACnC,uDAAuD;;;AAKvD;;;GAGG;AACH,SAAgB,cAAc,CAAC,KAAU;IACvC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE;QACzC,OAAO,EAAE,CAAA;KACV;SAAM,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,YAAY,MAAM,EAAE;QAC/D,OAAO,KAAe,CAAA;KACvB;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC;AAPD,wCAOC;AAED;;;;;GAKG;AACH,SAAgB,mBAAmB,CACjC,oBAA0C;IAE1C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,MAAM,EAAE;QAC7C,OAAO,EAAE,CAAA;KACV;IAED,OAAO;QACL,KAAK,EAAE,oBAAoB,CAAC,KAAK;QACjC,IAAI,EAAE,oBAAoB,CAAC,IAAI;QAC/B,IAAI,EAAE,oBAAoB,CAAC,SAAS;QACpC,OAAO,EAAE,oBAAoB,CAAC,OAAO;QACrC,GAAG,EAAE,oBAAoB,CAAC,WAAW;QACrC,SAAS,EAAE,oBAAoB,CAAC,SAAS;KAC1C,CAAA;AACH,CAAC;AAfD,kDAeC"}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [8.3.2](https://github.com/uuidjs/uuid/compare/v8.3.1...v8.3.2) (2020-12-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- lazy load getRandomValues ([#537](https://github.com/uuidjs/uuid/issues/537)) ([16c8f6d](https://github.com/uuidjs/uuid/commit/16c8f6df2f6b09b4d6235602d6a591188320a82e)), closes [#536](https://github.com/uuidjs/uuid/issues/536)
|
||||
|
||||
### [8.3.1](https://github.com/uuidjs/uuid/compare/v8.3.0...v8.3.1) (2020-10-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- support expo>=39.0.0 ([#515](https://github.com/uuidjs/uuid/issues/515)) ([c65a0f3](https://github.com/uuidjs/uuid/commit/c65a0f3fa73b901959d638d1e3591dfacdbed867)), closes [#375](https://github.com/uuidjs/uuid/issues/375)
|
||||
|
||||
## [8.3.0](https://github.com/uuidjs/uuid/compare/v8.2.0...v8.3.0) (2020-07-27)
|
||||
|
||||
### Features
|
||||
|
||||
- add parse/stringify/validate/version/NIL APIs ([#479](https://github.com/uuidjs/uuid/issues/479)) ([0e6c10b](https://github.com/uuidjs/uuid/commit/0e6c10ba1bf9517796ff23c052fc0468eedfd5f4)), closes [#475](https://github.com/uuidjs/uuid/issues/475) [#478](https://github.com/uuidjs/uuid/issues/478) [#480](https://github.com/uuidjs/uuid/issues/480) [#481](https://github.com/uuidjs/uuid/issues/481) [#180](https://github.com/uuidjs/uuid/issues/180)
|
||||
|
||||
## [8.2.0](https://github.com/uuidjs/uuid/compare/v8.1.0...v8.2.0) (2020-06-23)
|
||||
|
||||
### Features
|
||||
|
||||
- improve performance of v1 string representation ([#453](https://github.com/uuidjs/uuid/issues/453)) ([0ee0b67](https://github.com/uuidjs/uuid/commit/0ee0b67c37846529c66089880414d29f3ae132d5))
|
||||
- remove deprecated v4 string parameter ([#454](https://github.com/uuidjs/uuid/issues/454)) ([88ce3ca](https://github.com/uuidjs/uuid/commit/88ce3ca0ba046f60856de62c7ce03f7ba98ba46c)), closes [#437](https://github.com/uuidjs/uuid/issues/437)
|
||||
- support jspm ([#473](https://github.com/uuidjs/uuid/issues/473)) ([e9f2587](https://github.com/uuidjs/uuid/commit/e9f2587a92575cac31bc1d4ae944e17c09756659))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- prepare package exports for webpack 5 ([#468](https://github.com/uuidjs/uuid/issues/468)) ([8d6e6a5](https://github.com/uuidjs/uuid/commit/8d6e6a5f8965ca9575eb4d92e99a43435f4a58a8))
|
||||
|
||||
## [8.1.0](https://github.com/uuidjs/uuid/compare/v8.0.0...v8.1.0) (2020-05-20)
|
||||
|
||||
### Features
|
||||
|
||||
- improve v4 performance by reusing random number array ([#435](https://github.com/uuidjs/uuid/issues/435)) ([bf4af0d](https://github.com/uuidjs/uuid/commit/bf4af0d711b4d2ed03d1f74fd12ad0baa87dc79d))
|
||||
- optimize V8 performance of bytesToUuid ([#434](https://github.com/uuidjs/uuid/issues/434)) ([e156415](https://github.com/uuidjs/uuid/commit/e156415448ec1af2351fa0b6660cfb22581971f2))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export package.json required by react-native and bundlers ([#449](https://github.com/uuidjs/uuid/issues/449)) ([be1c8fe](https://github.com/uuidjs/uuid/commit/be1c8fe9a3206c358e0059b52fafd7213aa48a52)), closes [ai/nanoevents#44](https://github.com/ai/nanoevents/issues/44#issuecomment-602010343) [#444](https://github.com/uuidjs/uuid/issues/444)
|
||||
|
||||
## [8.0.0](https://github.com/uuidjs/uuid/compare/v7.0.3...v8.0.0) (2020-04-29)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
- For native ECMAScript Module (ESM) usage in Node.js only named exports are exposed, there is no more default export.
|
||||
|
||||
```diff
|
||||
-import uuid from 'uuid';
|
||||
-console.log(uuid.v4()); // -> 'cd6c3b08-0adc-4f4b-a6ef-36087a1c9869'
|
||||
+import { v4 as uuidv4 } from 'uuid';
|
||||
+uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
|
||||
```
|
||||
|
||||
- Deep requiring specific algorithms of this library like `require('uuid/v4')`, which has been deprecated in `uuid@7`, is no longer supported.
|
||||
|
||||
Instead use the named exports that this module exports.
|
||||
|
||||
For ECMAScript Modules (ESM):
|
||||
|
||||
```diff
|
||||
-import uuidv4 from 'uuid/v4';
|
||||
+import { v4 as uuidv4 } from 'uuid';
|
||||
uuidv4();
|
||||
```
|
||||
|
||||
For CommonJS:
|
||||
|
||||
```diff
|
||||
-const uuidv4 = require('uuid/v4');
|
||||
+const { v4: uuidv4 } = require('uuid');
|
||||
uuidv4();
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- native Node.js ES Modules (wrapper approach) ([#423](https://github.com/uuidjs/uuid/issues/423)) ([2d9f590](https://github.com/uuidjs/uuid/commit/2d9f590ad9701d692625c07ed62f0a0f91227991)), closes [#245](https://github.com/uuidjs/uuid/issues/245) [#419](https://github.com/uuidjs/uuid/issues/419) [#342](https://github.com/uuidjs/uuid/issues/342)
|
||||
- remove deep requires ([#426](https://github.com/uuidjs/uuid/issues/426)) ([daf72b8](https://github.com/uuidjs/uuid/commit/daf72b84ceb20272a81bb5fbddb05dd95922cbba))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add CommonJS syntax example to README quickstart section ([#417](https://github.com/uuidjs/uuid/issues/417)) ([e0ec840](https://github.com/uuidjs/uuid/commit/e0ec8402c7ad44b7ef0453036c612f5db513fda0))
|
||||
|
||||
### [7.0.3](https://github.com/uuidjs/uuid/compare/v7.0.2...v7.0.3) (2020-03-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- make deep require deprecation warning work in browsers ([#409](https://github.com/uuidjs/uuid/issues/409)) ([4b71107](https://github.com/uuidjs/uuid/commit/4b71107d8c0d2ef56861ede6403fc9dc35a1e6bf)), closes [#408](https://github.com/uuidjs/uuid/issues/408)
|
||||
|
||||
### [7.0.2](https://github.com/uuidjs/uuid/compare/v7.0.1...v7.0.2) (2020-03-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- make access to msCrypto consistent ([#393](https://github.com/uuidjs/uuid/issues/393)) ([8bf2a20](https://github.com/uuidjs/uuid/commit/8bf2a20f3565df743da7215eebdbada9d2df118c))
|
||||
- simplify link in deprecation warning ([#391](https://github.com/uuidjs/uuid/issues/391)) ([bb2c8e4](https://github.com/uuidjs/uuid/commit/bb2c8e4e9f4c5f9c1eaaf3ea59710c633cd90cb7))
|
||||
- update links to match content in readme ([#386](https://github.com/uuidjs/uuid/issues/386)) ([44f2f86](https://github.com/uuidjs/uuid/commit/44f2f86e9d2bbf14ee5f0f00f72a3db1292666d4))
|
||||
|
||||
### [7.0.1](https://github.com/uuidjs/uuid/compare/v7.0.0...v7.0.1) (2020-02-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- clean up esm builds for node and browser ([#383](https://github.com/uuidjs/uuid/issues/383)) ([59e6a49](https://github.com/uuidjs/uuid/commit/59e6a49e7ce7b3e8fb0f3ee52b9daae72af467dc))
|
||||
- provide browser versions independent from module system ([#380](https://github.com/uuidjs/uuid/issues/380)) ([4344a22](https://github.com/uuidjs/uuid/commit/4344a22e7aed33be8627eeaaf05360f256a21753)), closes [#378](https://github.com/uuidjs/uuid/issues/378)
|
||||
|
||||
## [7.0.0](https://github.com/uuidjs/uuid/compare/v3.4.0...v7.0.0) (2020-02-24)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
- The default export, which used to be the v4() method but which was already discouraged in v3.x of this library, has been removed.
|
||||
- Explicitly note that deep imports of the different uuid version functions are deprecated and no longer encouraged and that ECMAScript module named imports should be used instead. Emit a deprecation warning for people who deep-require the different algorithm variants.
|
||||
- Remove builtin support for insecure random number generators in the browser. Users who want that will have to supply their own random number generator function.
|
||||
- Remove support for generating v3 and v5 UUIDs in Node.js<4.x
|
||||
- Convert code base to ECMAScript Modules (ESM) and release CommonJS build for node and ESM build for browser bundlers.
|
||||
|
||||
### Features
|
||||
|
||||
- add UMD build to npm package ([#357](https://github.com/uuidjs/uuid/issues/357)) ([4e75adf](https://github.com/uuidjs/uuid/commit/4e75adf435196f28e3fbbe0185d654b5ded7ca2c)), closes [#345](https://github.com/uuidjs/uuid/issues/345)
|
||||
- add various es module and CommonJS examples ([b238510](https://github.com/uuidjs/uuid/commit/b238510bf352463521f74bab175a3af9b7a42555))
|
||||
- ensure that docs are up-to-date in CI ([ee5e77d](https://github.com/uuidjs/uuid/commit/ee5e77db547474f5a8f23d6c857a6d399209986b))
|
||||
- hybrid CommonJS & ECMAScript modules build ([a3f078f](https://github.com/uuidjs/uuid/commit/a3f078faa0baff69ab41aed08e041f8f9c8993d0))
|
||||
- remove insecure fallback random number generator ([3a5842b](https://github.com/uuidjs/uuid/commit/3a5842b141a6e5de0ae338f391661e6b84b167c9)), closes [#173](https://github.com/uuidjs/uuid/issues/173)
|
||||
- remove support for pre Node.js v4 Buffer API ([#356](https://github.com/uuidjs/uuid/issues/356)) ([b59b5c5](https://github.com/uuidjs/uuid/commit/b59b5c5ecad271c5453f1a156f011671f6d35627))
|
||||
- rename repository to github:uuidjs/uuid ([#351](https://github.com/uuidjs/uuid/issues/351)) ([c37a518](https://github.com/uuidjs/uuid/commit/c37a518e367ac4b6d0aa62dba1bc6ce9e85020f7)), closes [#338](https://github.com/uuidjs/uuid/issues/338)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add deep-require proxies for local testing and adjust tests ([#365](https://github.com/uuidjs/uuid/issues/365)) ([7fedc79](https://github.com/uuidjs/uuid/commit/7fedc79ac8fda4bfd1c566c7f05ef4ac13b2db48))
|
||||
- add note about removal of default export ([#372](https://github.com/uuidjs/uuid/issues/372)) ([12749b7](https://github.com/uuidjs/uuid/commit/12749b700eb49db8a9759fd306d8be05dbfbd58c)), closes [#370](https://github.com/uuidjs/uuid/issues/370)
|
||||
- deprecated deep requiring of the different algorithm versions ([#361](https://github.com/uuidjs/uuid/issues/361)) ([c0bdf15](https://github.com/uuidjs/uuid/commit/c0bdf15e417639b1aeb0b247b2fb11f7a0a26b23))
|
||||
|
||||
## [3.4.0](https://github.com/uuidjs/uuid/compare/v3.3.3...v3.4.0) (2020-01-16)
|
||||
|
||||
### Features
|
||||
|
||||
- rename repository to github:uuidjs/uuid ([#351](https://github.com/uuidjs/uuid/issues/351)) ([e2d7314](https://github.com/uuidjs/uuid/commit/e2d7314)), closes [#338](https://github.com/uuidjs/uuid/issues/338)
|
||||
|
||||
## [3.3.3](https://github.com/uuidjs/uuid/compare/v3.3.2...v3.3.3) (2019-08-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- no longer run ci tests on node v4
|
||||
- upgrade dependencies
|
||||
|
||||
## [3.3.2](https://github.com/uuidjs/uuid/compare/v3.3.1...v3.3.2) (2018-06-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- typo ([305d877](https://github.com/uuidjs/uuid/commit/305d877))
|
||||
|
||||
## [3.3.1](https://github.com/uuidjs/uuid/compare/v3.3.0...v3.3.1) (2018-06-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix [#284](https://github.com/uuidjs/uuid/issues/284) by setting function name in try-catch ([f2a60f2](https://github.com/uuidjs/uuid/commit/f2a60f2))
|
||||
|
||||
# [3.3.0](https://github.com/uuidjs/uuid/compare/v3.2.1...v3.3.0) (2018-06-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- assignment to readonly property to allow running in strict mode ([#270](https://github.com/uuidjs/uuid/issues/270)) ([d062fdc](https://github.com/uuidjs/uuid/commit/d062fdc))
|
||||
- fix [#229](https://github.com/uuidjs/uuid/issues/229) ([c9684d4](https://github.com/uuidjs/uuid/commit/c9684d4))
|
||||
- Get correct version of IE11 crypto ([#274](https://github.com/uuidjs/uuid/issues/274)) ([153d331](https://github.com/uuidjs/uuid/commit/153d331))
|
||||
- mem issue when generating uuid ([#267](https://github.com/uuidjs/uuid/issues/267)) ([c47702c](https://github.com/uuidjs/uuid/commit/c47702c))
|
||||
|
||||
### Features
|
||||
|
||||
- enforce Conventional Commit style commit messages ([#282](https://github.com/uuidjs/uuid/issues/282)) ([cc9a182](https://github.com/uuidjs/uuid/commit/cc9a182))
|
||||
|
||||
## [3.2.1](https://github.com/uuidjs/uuid/compare/v3.2.0...v3.2.1) (2018-01-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- use msCrypto if available. Fixes [#241](https://github.com/uuidjs/uuid/issues/241) ([#247](https://github.com/uuidjs/uuid/issues/247)) ([1fef18b](https://github.com/uuidjs/uuid/commit/1fef18b))
|
||||
|
||||
# [3.2.0](https://github.com/uuidjs/uuid/compare/v3.1.0...v3.2.0) (2018-01-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- remove mistakenly added typescript dependency, rollback version (standard-version will auto-increment) ([09fa824](https://github.com/uuidjs/uuid/commit/09fa824))
|
||||
- use msCrypto if available. Fixes [#241](https://github.com/uuidjs/uuid/issues/241) ([#247](https://github.com/uuidjs/uuid/issues/247)) ([1fef18b](https://github.com/uuidjs/uuid/commit/1fef18b))
|
||||
|
||||
### Features
|
||||
|
||||
- Add v3 Support ([#217](https://github.com/uuidjs/uuid/issues/217)) ([d94f726](https://github.com/uuidjs/uuid/commit/d94f726))
|
||||
|
||||
# [3.1.0](https://github.com/uuidjs/uuid/compare/v3.1.0...v3.0.1) (2017-06-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- (fix) Add .npmignore file to exclude test/ and other non-essential files from packing. (#183)
|
||||
- Fix typo (#178)
|
||||
- Simple typo fix (#165)
|
||||
|
||||
### Features
|
||||
|
||||
- v5 support in CLI (#197)
|
||||
- V5 support (#188)
|
||||
|
||||
# 3.0.1 (2016-11-28)
|
||||
|
||||
- split uuid versions into separate files
|
||||
|
||||
# 3.0.0 (2016-11-17)
|
||||
|
||||
- remove .parse and .unparse
|
||||
|
||||
# 2.0.0
|
||||
|
||||
- Removed uuid.BufferClass
|
||||
|
||||
# 1.4.0
|
||||
|
||||
- Improved module context detection
|
||||
- Removed public RNG functions
|
||||
|
||||
# 1.3.2
|
||||
|
||||
- Improve tests and handling of v1() options (Issue #24)
|
||||
- Expose RNG option to allow for perf testing with different generators
|
||||
|
||||
# 1.3.0
|
||||
|
||||
- Support for version 1 ids, thanks to [@ctavan](https://github.com/ctavan)!
|
||||
- Support for node.js crypto API
|
||||
- De-emphasizing performance in favor of a) cryptographic quality PRNGs where available and b) more manageable code
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Contributing
|
||||
|
||||
Please feel free to file GitHub Issues or propose Pull Requests. We're always happy to discuss improvements to this library!
|
||||
|
||||
## Testing
|
||||
|
||||
```shell
|
||||
npm test
|
||||
```
|
||||
|
||||
## Releasing
|
||||
|
||||
Releases are supposed to be done from master, version bumping is automated through [`standard-version`](https://github.com/conventional-changelog/standard-version):
|
||||
|
||||
```shell
|
||||
npm run release -- --dry-run # verify output manually
|
||||
npm run release # follow the instructions from the output of this command
|
||||
```
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2010-2020 Robert Kieffer and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+505
@@ -0,0 +1,505 @@
|
||||
<!--
|
||||
-- This file is auto-generated from README_js.md. Changes should be made there.
|
||||
-->
|
||||
|
||||
# uuid [](https://github.com/uuidjs/uuid/actions?query=workflow%3ACI) [](https://github.com/uuidjs/uuid/actions?query=workflow%3ABrowser)
|
||||
|
||||
For the creation of [RFC4122](http://www.ietf.org/rfc/rfc4122.txt) UUIDs
|
||||
|
||||
- **Complete** - Support for RFC4122 version 1, 3, 4, and 5 UUIDs
|
||||
- **Cross-platform** - Support for ...
|
||||
- CommonJS, [ECMAScript Modules](#ecmascript-modules) and [CDN builds](#cdn-builds)
|
||||
- Node 8, 10, 12, 14
|
||||
- Chrome, Safari, Firefox, Edge, IE 11 browsers
|
||||
- Webpack and rollup.js module bundlers
|
||||
- [React Native / Expo](#react-native--expo)
|
||||
- **Secure** - Cryptographically-strong random values
|
||||
- **Small** - Zero-dependency, small footprint, plays nice with "tree shaking" packagers
|
||||
- **CLI** - Includes the [`uuid` command line](#command-line) utility
|
||||
|
||||
**Upgrading from `uuid@3.x`?** Your code is probably okay, but check out [Upgrading From `uuid@3.x`](#upgrading-from-uuid3x) for details.
|
||||
|
||||
## Quickstart
|
||||
|
||||
To create a random UUID...
|
||||
|
||||
**1. Install**
|
||||
|
||||
```shell
|
||||
npm install uuid
|
||||
```
|
||||
|
||||
**2. Create a UUID** (ES6 module syntax)
|
||||
|
||||
```javascript
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
|
||||
```
|
||||
|
||||
... or using CommonJS syntax:
|
||||
|
||||
```javascript
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
|
||||
```
|
||||
|
||||
For timestamp UUIDs, namespace UUIDs, and other options read on ...
|
||||
|
||||
## API Summary
|
||||
|
||||
| | | |
|
||||
| --- | --- | --- |
|
||||
| [`uuid.NIL`](#uuidnil) | The nil UUID string (all zeros) | New in `uuid@8.3` |
|
||||
| [`uuid.parse()`](#uuidparsestr) | Convert UUID string to array of bytes | New in `uuid@8.3` |
|
||||
| [`uuid.stringify()`](#uuidstringifyarr-offset) | Convert array of bytes to UUID string | New in `uuid@8.3` |
|
||||
| [`uuid.v1()`](#uuidv1options-buffer-offset) | Create a version 1 (timestamp) UUID | |
|
||||
| [`uuid.v3()`](#uuidv3name-namespace-buffer-offset) | Create a version 3 (namespace w/ MD5) UUID | |
|
||||
| [`uuid.v4()`](#uuidv4options-buffer-offset) | Create a version 4 (random) UUID | |
|
||||
| [`uuid.v5()`](#uuidv5name-namespace-buffer-offset) | Create a version 5 (namespace w/ SHA-1) UUID | |
|
||||
| [`uuid.validate()`](#uuidvalidatestr) | Test a string to see if it is a valid UUID | New in `uuid@8.3` |
|
||||
| [`uuid.version()`](#uuidversionstr) | Detect RFC version of a UUID | New in `uuid@8.3` |
|
||||
|
||||
## API
|
||||
|
||||
### uuid.NIL
|
||||
|
||||
The nil UUID string (all zeros).
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
import { NIL as NIL_UUID } from 'uuid';
|
||||
|
||||
NIL_UUID; // ⇨ '00000000-0000-0000-0000-000000000000'
|
||||
```
|
||||
|
||||
### uuid.parse(str)
|
||||
|
||||
Convert UUID string to array of bytes
|
||||
|
||||
| | |
|
||||
| --------- | ---------------------------------------- |
|
||||
| `str` | A valid UUID `String` |
|
||||
| _returns_ | `Uint8Array[16]` |
|
||||
| _throws_ | `TypeError` if `str` is not a valid UUID |
|
||||
|
||||
Note: Ordering of values in the byte arrays used by `parse()` and `stringify()` follows the left ↠ right order of hex-pairs in UUID strings. As shown in the example below.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
import { parse as uuidParse } from 'uuid';
|
||||
|
||||
// Parse a UUID
|
||||
const bytes = uuidParse('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b');
|
||||
|
||||
// Convert to hex strings to show byte order (for documentation purposes)
|
||||
[...bytes].map((v) => v.toString(16).padStart(2, '0')); // ⇨
|
||||
// [
|
||||
// '6e', 'c0', 'bd', '7f',
|
||||
// '11', 'c0', '43', 'da',
|
||||
// '97', '5e', '2a', '8a',
|
||||
// 'd9', 'eb', 'ae', '0b'
|
||||
// ]
|
||||
```
|
||||
|
||||
### uuid.stringify(arr[, offset])
|
||||
|
||||
Convert array of bytes to UUID string
|
||||
|
||||
| | |
|
||||
| -------------- | ---------------------------------------------------------------------------- |
|
||||
| `arr` | `Array`-like collection of 16 values (starting from `offset`) between 0-255. |
|
||||
| [`offset` = 0] | `Number` Starting index in the Array |
|
||||
| _returns_ | `String` |
|
||||
| _throws_ | `TypeError` if a valid UUID string cannot be generated |
|
||||
|
||||
Note: Ordering of values in the byte arrays used by `parse()` and `stringify()` follows the left ↠ right order of hex-pairs in UUID strings. As shown in the example below.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
import { stringify as uuidStringify } from 'uuid';
|
||||
|
||||
const uuidBytes = [
|
||||
0x6e,
|
||||
0xc0,
|
||||
0xbd,
|
||||
0x7f,
|
||||
0x11,
|
||||
0xc0,
|
||||
0x43,
|
||||
0xda,
|
||||
0x97,
|
||||
0x5e,
|
||||
0x2a,
|
||||
0x8a,
|
||||
0xd9,
|
||||
0xeb,
|
||||
0xae,
|
||||
0x0b,
|
||||
];
|
||||
|
||||
uuidStringify(uuidBytes); // ⇨ '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b'
|
||||
```
|
||||
|
||||
### uuid.v1([options[, buffer[, offset]]])
|
||||
|
||||
Create an RFC version 1 (timestamp) UUID
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| [`options`] | `Object` with one or more of the following properties: |
|
||||
| [`options.node` ] | RFC "node" field as an `Array[6]` of byte values (per 4.1.6) |
|
||||
| [`options.clockseq`] | RFC "clock sequence" as a `Number` between 0 - 0x3fff |
|
||||
| [`options.msecs`] | RFC "timestamp" field (`Number` of milliseconds, unix epoch) |
|
||||
| [`options.nsecs`] | RFC "timestamp" field (`Number` of nanseconds to add to `msecs`, should be 0-10,000) |
|
||||
| [`options.random`] | `Array` of 16 random bytes (0-255) |
|
||||
| [`options.rng`] | Alternative to `options.random`, a `Function` that returns an `Array` of 16 random bytes (0-255) |
|
||||
| [`buffer`] | `Array \| Buffer` If specified, uuid will be written here in byte-form, starting at `offset` |
|
||||
| [`offset` = 0] | `Number` Index to start writing UUID bytes in `buffer` |
|
||||
| _returns_ | UUID `String` if no `buffer` is specified, otherwise returns `buffer` |
|
||||
| _throws_ | `Error` if more than 10M UUIDs/sec are requested |
|
||||
|
||||
Note: The default [node id](https://tools.ietf.org/html/rfc4122#section-4.1.6) (the last 12 digits in the UUID) is generated once, randomly, on process startup, and then remains unchanged for the duration of the process.
|
||||
|
||||
Note: `options.random` and `options.rng` are only meaningful on the very first call to `v1()`, where they may be passed to initialize the internal `node` and `clockseq` fields.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
|
||||
uuidv1(); // ⇨ '2c5ea4c0-4067-11e9-8bad-9b1deb4d3b7d'
|
||||
```
|
||||
|
||||
Example using `options`:
|
||||
|
||||
```javascript
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
|
||||
const v1options = {
|
||||
node: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab],
|
||||
clockseq: 0x1234,
|
||||
msecs: new Date('2011-11-01').getTime(),
|
||||
nsecs: 5678,
|
||||
};
|
||||
uuidv1(v1options); // ⇨ '710b962e-041c-11e1-9234-0123456789ab'
|
||||
```
|
||||
|
||||
### uuid.v3(name, namespace[, buffer[, offset]])
|
||||
|
||||
Create an RFC version 3 (namespace w/ MD5) UUID
|
||||
|
||||
API is identical to `v5()`, but uses "v3" instead.
|
||||
|
||||
⚠️ Note: Per the RFC, "_If backward compatibility is not an issue, SHA-1 [Version 5] is preferred_."
|
||||
|
||||
### uuid.v4([options[, buffer[, offset]]])
|
||||
|
||||
Create an RFC version 4 (random) UUID
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| [`options`] | `Object` with one or more of the following properties: |
|
||||
| [`options.random`] | `Array` of 16 random bytes (0-255) |
|
||||
| [`options.rng`] | Alternative to `options.random`, a `Function` that returns an `Array` of 16 random bytes (0-255) |
|
||||
| [`buffer`] | `Array \| Buffer` If specified, uuid will be written here in byte-form, starting at `offset` |
|
||||
| [`offset` = 0] | `Number` Index to start writing UUID bytes in `buffer` |
|
||||
| _returns_ | UUID `String` if no `buffer` is specified, otherwise returns `buffer` |
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
|
||||
```
|
||||
|
||||
Example using predefined `random` values:
|
||||
|
||||
```javascript
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const v4options = {
|
||||
random: [
|
||||
0x10,
|
||||
0x91,
|
||||
0x56,
|
||||
0xbe,
|
||||
0xc4,
|
||||
0xfb,
|
||||
0xc1,
|
||||
0xea,
|
||||
0x71,
|
||||
0xb4,
|
||||
0xef,
|
||||
0xe1,
|
||||
0x67,
|
||||
0x1c,
|
||||
0x58,
|
||||
0x36,
|
||||
],
|
||||
};
|
||||
uuidv4(v4options); // ⇨ '109156be-c4fb-41ea-b1b4-efe1671c5836'
|
||||
```
|
||||
|
||||
### uuid.v5(name, namespace[, buffer[, offset]])
|
||||
|
||||
Create an RFC version 5 (namespace w/ SHA-1) UUID
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| `name` | `String \| Array` |
|
||||
| `namespace` | `String \| Array[16]` Namespace UUID |
|
||||
| [`buffer`] | `Array \| Buffer` If specified, uuid will be written here in byte-form, starting at `offset` |
|
||||
| [`offset` = 0] | `Number` Index to start writing UUID bytes in `buffer` |
|
||||
| _returns_ | UUID `String` if no `buffer` is specified, otherwise returns `buffer` |
|
||||
|
||||
Note: The RFC `DNS` and `URL` namespaces are available as `v5.DNS` and `v5.URL`.
|
||||
|
||||
Example with custom namespace:
|
||||
|
||||
```javascript
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
|
||||
// Define a custom namespace. Readers, create your own using something like
|
||||
// https://www.uuidgenerator.net/
|
||||
const MY_NAMESPACE = '1b671a64-40d5-491e-99b0-da01ff1f3341';
|
||||
|
||||
uuidv5('Hello, World!', MY_NAMESPACE); // ⇨ '630eb68f-e0fa-5ecc-887a-7c7a62614681'
|
||||
```
|
||||
|
||||
Example with RFC `URL` namespace:
|
||||
|
||||
```javascript
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
|
||||
uuidv5('https://www.w3.org/', uuidv5.URL); // ⇨ 'c106a26a-21bb-5538-8bf2-57095d1976c1'
|
||||
```
|
||||
|
||||
### uuid.validate(str)
|
||||
|
||||
Test a string to see if it is a valid UUID
|
||||
|
||||
| | |
|
||||
| --------- | --------------------------------------------------- |
|
||||
| `str` | `String` to validate |
|
||||
| _returns_ | `true` if string is a valid UUID, `false` otherwise |
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
|
||||
uuidValidate('not a UUID'); // ⇨ false
|
||||
uuidValidate('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b'); // ⇨ true
|
||||
```
|
||||
|
||||
Using `validate` and `version` together it is possible to do per-version validation, e.g. validate for only v4 UUIds.
|
||||
|
||||
```javascript
|
||||
import { version as uuidVersion } from 'uuid';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
|
||||
function uuidValidateV4(uuid) {
|
||||
return uuidValidate(uuid) && uuidVersion(uuid) === 4;
|
||||
}
|
||||
|
||||
const v1Uuid = 'd9428888-122b-11e1-b85c-61cd3cbb3210';
|
||||
const v4Uuid = '109156be-c4fb-41ea-b1b4-efe1671c5836';
|
||||
|
||||
uuidValidateV4(v4Uuid); // ⇨ true
|
||||
uuidValidateV4(v1Uuid); // ⇨ false
|
||||
```
|
||||
|
||||
### uuid.version(str)
|
||||
|
||||
Detect RFC version of a UUID
|
||||
|
||||
| | |
|
||||
| --------- | ---------------------------------------- |
|
||||
| `str` | A valid UUID `String` |
|
||||
| _returns_ | `Number` The RFC version of the UUID |
|
||||
| _throws_ | `TypeError` if `str` is not a valid UUID |
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
import { version as uuidVersion } from 'uuid';
|
||||
|
||||
uuidVersion('45637ec4-c85f-11ea-87d0-0242ac130003'); // ⇨ 1
|
||||
uuidVersion('6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b'); // ⇨ 4
|
||||
```
|
||||
|
||||
## Command Line
|
||||
|
||||
UUIDs can be generated from the command line using `uuid`.
|
||||
|
||||
```shell
|
||||
$ uuid
|
||||
ddeb27fb-d9a0-4624-be4d-4615062daed4
|
||||
```
|
||||
|
||||
The default is to generate version 4 UUIDS, however the other versions are supported. Type `uuid --help` for details:
|
||||
|
||||
```shell
|
||||
$ uuid --help
|
||||
|
||||
Usage:
|
||||
uuid
|
||||
uuid v1
|
||||
uuid v3 <name> <namespace uuid>
|
||||
uuid v4
|
||||
uuid v5 <name> <namespace uuid>
|
||||
uuid --help
|
||||
|
||||
Note: <namespace uuid> may be "URL" or "DNS" to use the corresponding UUIDs
|
||||
defined by RFC4122
|
||||
```
|
||||
|
||||
## ECMAScript Modules
|
||||
|
||||
This library comes with [ECMAScript Modules](https://www.ecma-international.org/ecma-262/6.0/#sec-modules) (ESM) support for Node.js versions that support it ([example](./examples/node-esmodules/)) as well as bundlers like [rollup.js](https://rollupjs.org/guide/en/#tree-shaking) ([example](./examples/browser-rollup/)) and [webpack](https://webpack.js.org/guides/tree-shaking/) ([example](./examples/browser-webpack/)) (targeting both, Node.js and browser environments).
|
||||
|
||||
```javascript
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
|
||||
```
|
||||
|
||||
To run the examples you must first create a dist build of this library in the module root:
|
||||
|
||||
```shell
|
||||
npm run build
|
||||
```
|
||||
|
||||
## CDN Builds
|
||||
|
||||
### ECMAScript Modules
|
||||
|
||||
To load this module directly into modern browsers that [support loading ECMAScript Modules](https://caniuse.com/#feat=es6-module) you can make use of [jspm](https://jspm.org/):
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { v4 as uuidv4 } from 'https://jspm.dev/uuid';
|
||||
console.log(uuidv4()); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
|
||||
</script>
|
||||
```
|
||||
|
||||
### UMD
|
||||
|
||||
To load this module directly into older browsers you can use the [UMD (Universal Module Definition)](https://github.com/umdjs/umd) builds from any of the following CDNs:
|
||||
|
||||
**Using [UNPKG](https://unpkg.com/uuid@latest/dist/umd/)**:
|
||||
|
||||
```html
|
||||
<script src="https://unpkg.com/uuid@latest/dist/umd/uuidv4.min.js"></script>
|
||||
```
|
||||
|
||||
**Using [jsDelivr](https://cdn.jsdelivr.net/npm/uuid@latest/dist/umd/)**:
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/uuid@latest/dist/umd/uuidv4.min.js"></script>
|
||||
```
|
||||
|
||||
**Using [cdnjs](https://cdnjs.com/libraries/uuid)**:
|
||||
|
||||
```html
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/uuid/8.1.0/uuidv4.min.js"></script>
|
||||
```
|
||||
|
||||
These CDNs all provide the same [`uuidv4()`](#uuidv4options-buffer-offset) method:
|
||||
|
||||
```html
|
||||
<script>
|
||||
uuidv4(); // ⇨ '55af1e37-0734-46d8-b070-a1e42e4fc392'
|
||||
</script>
|
||||
```
|
||||
|
||||
Methods for the other algorithms ([`uuidv1()`](#uuidv1options-buffer-offset), [`uuidv3()`](#uuidv3name-namespace-buffer-offset) and [`uuidv5()`](#uuidv5name-namespace-buffer-offset)) are available from the files `uuidv1.min.js`, `uuidv3.min.js` and `uuidv5.min.js` respectively.
|
||||
|
||||
## "getRandomValues() not supported"
|
||||
|
||||
This error occurs in environments where the standard [`crypto.getRandomValues()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) API is not supported. This issue can be resolved by adding an appropriate polyfill:
|
||||
|
||||
### React Native / Expo
|
||||
|
||||
1. Install [`react-native-get-random-values`](https://github.com/LinusU/react-native-get-random-values#readme)
|
||||
1. Import it _before_ `uuid`. Since `uuid` might also appear as a transitive dependency of some other imports it's safest to just import `react-native-get-random-values` as the very first thing in your entry point:
|
||||
|
||||
```javascript
|
||||
import 'react-native-get-random-values';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
```
|
||||
|
||||
Note: If you are using Expo, you must be using at least `react-native-get-random-values@1.5.0` and `expo@39.0.0`.
|
||||
|
||||
### Web Workers / Service Workers (Edge <= 18)
|
||||
|
||||
[In Edge <= 18, Web Crypto is not supported in Web Workers or Service Workers](https://caniuse.com/#feat=cryptography) and we are not aware of a polyfill (let us know if you find one, please).
|
||||
|
||||
## Upgrading From `uuid@7.x`
|
||||
|
||||
### Only Named Exports Supported When Using with Node.js ESM
|
||||
|
||||
`uuid@7.x` did not come with native ECMAScript Module (ESM) support for Node.js. Importing it in Node.js ESM consequently imported the CommonJS source with a default export. This library now comes with true Node.js ESM support and only provides named exports.
|
||||
|
||||
Instead of doing:
|
||||
|
||||
```javascript
|
||||
import uuid from 'uuid';
|
||||
uuid.v4();
|
||||
```
|
||||
|
||||
you will now have to use the named exports:
|
||||
|
||||
```javascript
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
uuidv4();
|
||||
```
|
||||
|
||||
### Deep Requires No Longer Supported
|
||||
|
||||
Deep requires like `require('uuid/v4')` [which have been deprecated in `uuid@7.x`](#deep-requires-now-deprecated) are no longer supported.
|
||||
|
||||
## Upgrading From `uuid@3.x`
|
||||
|
||||
"_Wait... what happened to `uuid@4.x` - `uuid@6.x`?!?_"
|
||||
|
||||
In order to avoid confusion with RFC [version 4](#uuidv4options-buffer-offset) and [version 5](#uuidv5name-namespace-buffer-offset) UUIDs, and a possible [version 6](http://gh.peabody.io/uuidv6/), releases 4 thru 6 of this module have been skipped.
|
||||
|
||||
### Deep Requires Now Deprecated
|
||||
|
||||
`uuid@3.x` encouraged the use of deep requires to minimize the bundle size of browser builds:
|
||||
|
||||
```javascript
|
||||
const uuidv4 = require('uuid/v4'); // <== NOW DEPRECATED!
|
||||
uuidv4();
|
||||
```
|
||||
|
||||
As of `uuid@7.x` this library now provides ECMAScript modules builds, which allow packagers like Webpack and Rollup to do "tree-shaking" to remove dead code. Instead, use the `import` syntax:
|
||||
|
||||
```javascript
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
uuidv4();
|
||||
```
|
||||
|
||||
... or for CommonJS:
|
||||
|
||||
```javascript
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
uuidv4();
|
||||
```
|
||||
|
||||
### Default Export Removed
|
||||
|
||||
`uuid@3.x` was exporting the Version 4 UUID method as a default export:
|
||||
|
||||
```javascript
|
||||
const uuid = require('uuid'); // <== REMOVED!
|
||||
```
|
||||
|
||||
This usage pattern was already discouraged in `uuid@3.x` and has been removed in `uuid@7.x`.
|
||||
|
||||
----
|
||||
Markdown generated from [README_js.md](README_js.md) by [](https://github.com/broofa/runmd)
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
require('../uuid-bin');
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export { default as v1 } from './v1.js';
|
||||
export { default as v3 } from './v3.js';
|
||||
export { default as v4 } from './v4.js';
|
||||
export { default as v5 } from './v5.js';
|
||||
export { default as NIL } from './nil.js';
|
||||
export { default as version } from './version.js';
|
||||
export { default as validate } from './validate.js';
|
||||
export { default as stringify } from './stringify.js';
|
||||
export { default as parse } from './parse.js';
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Browser-compatible JavaScript MD5
|
||||
*
|
||||
* Modification of JavaScript MD5
|
||||
* https://github.com/blueimp/JavaScript-MD5
|
||||
*
|
||||
* Copyright 2011, Sebastian Tschan
|
||||
* https://blueimp.net
|
||||
*
|
||||
* Licensed under the MIT license:
|
||||
* https://opensource.org/licenses/MIT
|
||||
*
|
||||
* Based on
|
||||
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
|
||||
* Digest Algorithm, as defined in RFC 1321.
|
||||
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
|
||||
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
|
||||
* Distributed under the BSD License
|
||||
* See http://pajhome.org.uk/crypt/md5 for more info.
|
||||
*/
|
||||
function md5(bytes) {
|
||||
if (typeof bytes === 'string') {
|
||||
var msg = unescape(encodeURIComponent(bytes)); // UTF8 escape
|
||||
|
||||
bytes = new Uint8Array(msg.length);
|
||||
|
||||
for (var i = 0; i < msg.length; ++i) {
|
||||
bytes[i] = msg.charCodeAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
return md5ToHexEncodedArray(wordsToMd5(bytesToWords(bytes), bytes.length * 8));
|
||||
}
|
||||
/*
|
||||
* Convert an array of little-endian words to an array of bytes
|
||||
*/
|
||||
|
||||
|
||||
function md5ToHexEncodedArray(input) {
|
||||
var output = [];
|
||||
var length32 = input.length * 32;
|
||||
var hexTab = '0123456789abcdef';
|
||||
|
||||
for (var i = 0; i < length32; i += 8) {
|
||||
var x = input[i >> 5] >>> i % 32 & 0xff;
|
||||
var hex = parseInt(hexTab.charAt(x >>> 4 & 0x0f) + hexTab.charAt(x & 0x0f), 16);
|
||||
output.push(hex);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
/**
|
||||
* Calculate output length with padding and bit length
|
||||
*/
|
||||
|
||||
|
||||
function getOutputLength(inputLength8) {
|
||||
return (inputLength8 + 64 >>> 9 << 4) + 14 + 1;
|
||||
}
|
||||
/*
|
||||
* Calculate the MD5 of an array of little-endian words, and a bit length.
|
||||
*/
|
||||
|
||||
|
||||
function wordsToMd5(x, len) {
|
||||
/* append padding */
|
||||
x[len >> 5] |= 0x80 << len % 32;
|
||||
x[getOutputLength(len) - 1] = len;
|
||||
var a = 1732584193;
|
||||
var b = -271733879;
|
||||
var c = -1732584194;
|
||||
var d = 271733878;
|
||||
|
||||
for (var i = 0; i < x.length; i += 16) {
|
||||
var olda = a;
|
||||
var oldb = b;
|
||||
var oldc = c;
|
||||
var oldd = d;
|
||||
a = md5ff(a, b, c, d, x[i], 7, -680876936);
|
||||
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
|
||||
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
|
||||
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
|
||||
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
|
||||
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
|
||||
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
|
||||
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
|
||||
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
|
||||
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
|
||||
c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
|
||||
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
|
||||
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
|
||||
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
|
||||
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
|
||||
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
|
||||
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
|
||||
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
|
||||
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
|
||||
b = md5gg(b, c, d, a, x[i], 20, -373897302);
|
||||
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
|
||||
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
|
||||
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
|
||||
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
|
||||
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
|
||||
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
|
||||
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
|
||||
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
|
||||
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
|
||||
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
|
||||
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
|
||||
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
|
||||
a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
|
||||
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
|
||||
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
|
||||
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
|
||||
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
|
||||
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
|
||||
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
|
||||
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
|
||||
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
|
||||
d = md5hh(d, a, b, c, x[i], 11, -358537222);
|
||||
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
|
||||
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
|
||||
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
|
||||
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
|
||||
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
|
||||
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
|
||||
a = md5ii(a, b, c, d, x[i], 6, -198630844);
|
||||
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
|
||||
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
|
||||
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
|
||||
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
|
||||
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
|
||||
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
|
||||
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
|
||||
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
|
||||
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
|
||||
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
|
||||
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
|
||||
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
|
||||
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
|
||||
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
|
||||
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
|
||||
a = safeAdd(a, olda);
|
||||
b = safeAdd(b, oldb);
|
||||
c = safeAdd(c, oldc);
|
||||
d = safeAdd(d, oldd);
|
||||
}
|
||||
|
||||
return [a, b, c, d];
|
||||
}
|
||||
/*
|
||||
* Convert an array bytes to an array of little-endian words
|
||||
* Characters >255 have their high-byte silently ignored.
|
||||
*/
|
||||
|
||||
|
||||
function bytesToWords(input) {
|
||||
if (input.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var length8 = input.length * 8;
|
||||
var output = new Uint32Array(getOutputLength(length8));
|
||||
|
||||
for (var i = 0; i < length8; i += 8) {
|
||||
output[i >> 5] |= (input[i / 8] & 0xff) << i % 32;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
/*
|
||||
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
|
||||
* to work around bugs in some JS interpreters.
|
||||
*/
|
||||
|
||||
|
||||
function safeAdd(x, y) {
|
||||
var lsw = (x & 0xffff) + (y & 0xffff);
|
||||
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||
return msw << 16 | lsw & 0xffff;
|
||||
}
|
||||
/*
|
||||
* Bitwise rotate a 32-bit number to the left.
|
||||
*/
|
||||
|
||||
|
||||
function bitRotateLeft(num, cnt) {
|
||||
return num << cnt | num >>> 32 - cnt;
|
||||
}
|
||||
/*
|
||||
* These functions implement the four basic operations the algorithm uses.
|
||||
*/
|
||||
|
||||
|
||||
function md5cmn(q, a, b, x, s, t) {
|
||||
return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
|
||||
}
|
||||
|
||||
function md5ff(a, b, c, d, x, s, t) {
|
||||
return md5cmn(b & c | ~b & d, a, b, x, s, t);
|
||||
}
|
||||
|
||||
function md5gg(a, b, c, d, x, s, t) {
|
||||
return md5cmn(b & d | c & ~d, a, b, x, s, t);
|
||||
}
|
||||
|
||||
function md5hh(a, b, c, d, x, s, t) {
|
||||
return md5cmn(b ^ c ^ d, a, b, x, s, t);
|
||||
}
|
||||
|
||||
function md5ii(a, b, c, d, x, s, t) {
|
||||
return md5cmn(c ^ (b | ~d), a, b, x, s, t);
|
||||
}
|
||||
|
||||
export default md5;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default '00000000-0000-0000-0000-000000000000';
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import validate from './validate.js';
|
||||
|
||||
function parse(uuid) {
|
||||
if (!validate(uuid)) {
|
||||
throw TypeError('Invalid UUID');
|
||||
}
|
||||
|
||||
var v;
|
||||
var arr = new Uint8Array(16); // Parse ########-....-....-....-............
|
||||
|
||||
arr[0] = (v = parseInt(uuid.slice(0, 8), 16)) >>> 24;
|
||||
arr[1] = v >>> 16 & 0xff;
|
||||
arr[2] = v >>> 8 & 0xff;
|
||||
arr[3] = v & 0xff; // Parse ........-####-....-....-............
|
||||
|
||||
arr[4] = (v = parseInt(uuid.slice(9, 13), 16)) >>> 8;
|
||||
arr[5] = v & 0xff; // Parse ........-....-####-....-............
|
||||
|
||||
arr[6] = (v = parseInt(uuid.slice(14, 18), 16)) >>> 8;
|
||||
arr[7] = v & 0xff; // Parse ........-....-....-####-............
|
||||
|
||||
arr[8] = (v = parseInt(uuid.slice(19, 23), 16)) >>> 8;
|
||||
arr[9] = v & 0xff; // Parse ........-....-....-....-############
|
||||
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
|
||||
|
||||
arr[10] = (v = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000 & 0xff;
|
||||
arr[11] = v / 0x100000000 & 0xff;
|
||||
arr[12] = v >>> 24 & 0xff;
|
||||
arr[13] = v >>> 16 & 0xff;
|
||||
arr[14] = v >>> 8 & 0xff;
|
||||
arr[15] = v & 0xff;
|
||||
return arr;
|
||||
}
|
||||
|
||||
export default parse;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Unique ID creation requires a high quality random # generator. In the browser we therefore
|
||||
// require the crypto API and do not support built-in fallback to lower quality random number
|
||||
// generators (like Math.random()).
|
||||
var getRandomValues;
|
||||
var rnds8 = new Uint8Array(16);
|
||||
export default function rng() {
|
||||
// lazy load so that environments that need to polyfill have a chance to do so
|
||||
if (!getRandomValues) {
|
||||
// getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. Also,
|
||||
// find the complete implementation of crypto (msCrypto) on IE11.
|
||||
getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || typeof msCrypto !== 'undefined' && typeof msCrypto.getRandomValues === 'function' && msCrypto.getRandomValues.bind(msCrypto);
|
||||
|
||||
if (!getRandomValues) {
|
||||
throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');
|
||||
}
|
||||
}
|
||||
|
||||
return getRandomValues(rnds8);
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
// Adapted from Chris Veness' SHA1 code at
|
||||
// http://www.movable-type.co.uk/scripts/sha1.html
|
||||
function f(s, x, y, z) {
|
||||
switch (s) {
|
||||
case 0:
|
||||
return x & y ^ ~x & z;
|
||||
|
||||
case 1:
|
||||
return x ^ y ^ z;
|
||||
|
||||
case 2:
|
||||
return x & y ^ x & z ^ y & z;
|
||||
|
||||
case 3:
|
||||
return x ^ y ^ z;
|
||||
}
|
||||
}
|
||||
|
||||
function ROTL(x, n) {
|
||||
return x << n | x >>> 32 - n;
|
||||
}
|
||||
|
||||
function sha1(bytes) {
|
||||
var K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
|
||||
var H = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
|
||||
|
||||
if (typeof bytes === 'string') {
|
||||
var msg = unescape(encodeURIComponent(bytes)); // UTF8 escape
|
||||
|
||||
bytes = [];
|
||||
|
||||
for (var i = 0; i < msg.length; ++i) {
|
||||
bytes.push(msg.charCodeAt(i));
|
||||
}
|
||||
} else if (!Array.isArray(bytes)) {
|
||||
// Convert Array-like to Array
|
||||
bytes = Array.prototype.slice.call(bytes);
|
||||
}
|
||||
|
||||
bytes.push(0x80);
|
||||
var l = bytes.length / 4 + 2;
|
||||
var N = Math.ceil(l / 16);
|
||||
var M = new Array(N);
|
||||
|
||||
for (var _i = 0; _i < N; ++_i) {
|
||||
var arr = new Uint32Array(16);
|
||||
|
||||
for (var j = 0; j < 16; ++j) {
|
||||
arr[j] = bytes[_i * 64 + j * 4] << 24 | bytes[_i * 64 + j * 4 + 1] << 16 | bytes[_i * 64 + j * 4 + 2] << 8 | bytes[_i * 64 + j * 4 + 3];
|
||||
}
|
||||
|
||||
M[_i] = arr;
|
||||
}
|
||||
|
||||
M[N - 1][14] = (bytes.length - 1) * 8 / Math.pow(2, 32);
|
||||
M[N - 1][14] = Math.floor(M[N - 1][14]);
|
||||
M[N - 1][15] = (bytes.length - 1) * 8 & 0xffffffff;
|
||||
|
||||
for (var _i2 = 0; _i2 < N; ++_i2) {
|
||||
var W = new Uint32Array(80);
|
||||
|
||||
for (var t = 0; t < 16; ++t) {
|
||||
W[t] = M[_i2][t];
|
||||
}
|
||||
|
||||
for (var _t = 16; _t < 80; ++_t) {
|
||||
W[_t] = ROTL(W[_t - 3] ^ W[_t - 8] ^ W[_t - 14] ^ W[_t - 16], 1);
|
||||
}
|
||||
|
||||
var a = H[0];
|
||||
var b = H[1];
|
||||
var c = H[2];
|
||||
var d = H[3];
|
||||
var e = H[4];
|
||||
|
||||
for (var _t2 = 0; _t2 < 80; ++_t2) {
|
||||
var s = Math.floor(_t2 / 20);
|
||||
var T = ROTL(a, 5) + f(s, b, c, d) + e + K[s] + W[_t2] >>> 0;
|
||||
e = d;
|
||||
d = c;
|
||||
c = ROTL(b, 30) >>> 0;
|
||||
b = a;
|
||||
a = T;
|
||||
}
|
||||
|
||||
H[0] = H[0] + a >>> 0;
|
||||
H[1] = H[1] + b >>> 0;
|
||||
H[2] = H[2] + c >>> 0;
|
||||
H[3] = H[3] + d >>> 0;
|
||||
H[4] = H[4] + e >>> 0;
|
||||
}
|
||||
|
||||
return [H[0] >> 24 & 0xff, H[0] >> 16 & 0xff, H[0] >> 8 & 0xff, H[0] & 0xff, H[1] >> 24 & 0xff, H[1] >> 16 & 0xff, H[1] >> 8 & 0xff, H[1] & 0xff, H[2] >> 24 & 0xff, H[2] >> 16 & 0xff, H[2] >> 8 & 0xff, H[2] & 0xff, H[3] >> 24 & 0xff, H[3] >> 16 & 0xff, H[3] >> 8 & 0xff, H[3] & 0xff, H[4] >> 24 & 0xff, H[4] >> 16 & 0xff, H[4] >> 8 & 0xff, H[4] & 0xff];
|
||||
}
|
||||
|
||||
export default sha1;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import validate from './validate.js';
|
||||
/**
|
||||
* Convert array of 16 byte values to UUID string format of the form:
|
||||
* XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|
||||
*/
|
||||
|
||||
var byteToHex = [];
|
||||
|
||||
for (var i = 0; i < 256; ++i) {
|
||||
byteToHex.push((i + 0x100).toString(16).substr(1));
|
||||
}
|
||||
|
||||
function stringify(arr) {
|
||||
var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
|
||||
// Note: Be careful editing this code! It's been tuned for performance
|
||||
// and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434
|
||||
var uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one
|
||||
// of the following:
|
||||
// - One or more input array values don't map to a hex octet (leading to
|
||||
// "undefined" in the uuid)
|
||||
// - Invalid input values for the RFC `version` or `variant` fields
|
||||
|
||||
if (!validate(uuid)) {
|
||||
throw TypeError('Stringified UUID is invalid');
|
||||
}
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
export default stringify;
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import rng from './rng.js';
|
||||
import stringify from './stringify.js'; // **`v1()` - Generate time-based UUID**
|
||||
//
|
||||
// Inspired by https://github.com/LiosK/UUID.js
|
||||
// and http://docs.python.org/library/uuid.html
|
||||
|
||||
var _nodeId;
|
||||
|
||||
var _clockseq; // Previous uuid creation time
|
||||
|
||||
|
||||
var _lastMSecs = 0;
|
||||
var _lastNSecs = 0; // See https://github.com/uuidjs/uuid for API details
|
||||
|
||||
function v1(options, buf, offset) {
|
||||
var i = buf && offset || 0;
|
||||
var b = buf || new Array(16);
|
||||
options = options || {};
|
||||
var node = options.node || _nodeId;
|
||||
var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; // node and clockseq need to be initialized to random values if they're not
|
||||
// specified. We do this lazily to minimize issues related to insufficient
|
||||
// system entropy. See #189
|
||||
|
||||
if (node == null || clockseq == null) {
|
||||
var seedBytes = options.random || (options.rng || rng)();
|
||||
|
||||
if (node == null) {
|
||||
// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
|
||||
node = _nodeId = [seedBytes[0] | 0x01, seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]];
|
||||
}
|
||||
|
||||
if (clockseq == null) {
|
||||
// Per 4.2.2, randomize (14 bit) clockseq
|
||||
clockseq = _clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff;
|
||||
}
|
||||
} // UUID timestamps are 100 nano-second units since the Gregorian epoch,
|
||||
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
|
||||
// time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
|
||||
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
|
||||
|
||||
|
||||
var msecs = options.msecs !== undefined ? options.msecs : Date.now(); // Per 4.2.1.2, use count of uuid's generated during the current clock
|
||||
// cycle to simulate higher resolution clock
|
||||
|
||||
var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; // Time since last uuid creation (in msecs)
|
||||
|
||||
var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; // Per 4.2.1.2, Bump clockseq on clock regression
|
||||
|
||||
if (dt < 0 && options.clockseq === undefined) {
|
||||
clockseq = clockseq + 1 & 0x3fff;
|
||||
} // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
|
||||
// time interval
|
||||
|
||||
|
||||
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
|
||||
nsecs = 0;
|
||||
} // Per 4.2.1.2 Throw error if too many uuids are requested
|
||||
|
||||
|
||||
if (nsecs >= 10000) {
|
||||
throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");
|
||||
}
|
||||
|
||||
_lastMSecs = msecs;
|
||||
_lastNSecs = nsecs;
|
||||
_clockseq = clockseq; // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
|
||||
|
||||
msecs += 12219292800000; // `time_low`
|
||||
|
||||
var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
|
||||
b[i++] = tl >>> 24 & 0xff;
|
||||
b[i++] = tl >>> 16 & 0xff;
|
||||
b[i++] = tl >>> 8 & 0xff;
|
||||
b[i++] = tl & 0xff; // `time_mid`
|
||||
|
||||
var tmh = msecs / 0x100000000 * 10000 & 0xfffffff;
|
||||
b[i++] = tmh >>> 8 & 0xff;
|
||||
b[i++] = tmh & 0xff; // `time_high_and_version`
|
||||
|
||||
b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
|
||||
|
||||
b[i++] = tmh >>> 16 & 0xff; // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
|
||||
|
||||
b[i++] = clockseq >>> 8 | 0x80; // `clock_seq_low`
|
||||
|
||||
b[i++] = clockseq & 0xff; // `node`
|
||||
|
||||
for (var n = 0; n < 6; ++n) {
|
||||
b[i + n] = node[n];
|
||||
}
|
||||
|
||||
return buf || stringify(b);
|
||||
}
|
||||
|
||||
export default v1;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import v35 from './v35.js';
|
||||
import md5 from './md5.js';
|
||||
var v3 = v35('v3', 0x30, md5);
|
||||
export default v3;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import stringify from './stringify.js';
|
||||
import parse from './parse.js';
|
||||
|
||||
function stringToBytes(str) {
|
||||
str = unescape(encodeURIComponent(str)); // UTF8 escape
|
||||
|
||||
var bytes = [];
|
||||
|
||||
for (var i = 0; i < str.length; ++i) {
|
||||
bytes.push(str.charCodeAt(i));
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export var DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
||||
export var URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
|
||||
export default function (name, version, hashfunc) {
|
||||
function generateUUID(value, namespace, buf, offset) {
|
||||
if (typeof value === 'string') {
|
||||
value = stringToBytes(value);
|
||||
}
|
||||
|
||||
if (typeof namespace === 'string') {
|
||||
namespace = parse(namespace);
|
||||
}
|
||||
|
||||
if (namespace.length !== 16) {
|
||||
throw TypeError('Namespace must be array-like (16 iterable integer values, 0-255)');
|
||||
} // Compute hash of namespace and value, Per 4.3
|
||||
// Future: Use spread syntax when supported on all platforms, e.g. `bytes =
|
||||
// hashfunc([...namespace, ... value])`
|
||||
|
||||
|
||||
var bytes = new Uint8Array(16 + value.length);
|
||||
bytes.set(namespace);
|
||||
bytes.set(value, namespace.length);
|
||||
bytes = hashfunc(bytes);
|
||||
bytes[6] = bytes[6] & 0x0f | version;
|
||||
bytes[8] = bytes[8] & 0x3f | 0x80;
|
||||
|
||||
if (buf) {
|
||||
offset = offset || 0;
|
||||
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
buf[offset + i] = bytes[i];
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
return stringify(bytes);
|
||||
} // Function#name is not settable on some platforms (#270)
|
||||
|
||||
|
||||
try {
|
||||
generateUUID.name = name; // eslint-disable-next-line no-empty
|
||||
} catch (err) {} // For CommonJS default export support
|
||||
|
||||
|
||||
generateUUID.DNS = DNS;
|
||||
generateUUID.URL = URL;
|
||||
return generateUUID;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import rng from './rng.js';
|
||||
import stringify from './stringify.js';
|
||||
|
||||
function v4(options, buf, offset) {
|
||||
options = options || {};
|
||||
var rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
|
||||
|
||||
rnds[6] = rnds[6] & 0x0f | 0x40;
|
||||
rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided
|
||||
|
||||
if (buf) {
|
||||
offset = offset || 0;
|
||||
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
buf[offset + i] = rnds[i];
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
return stringify(rnds);
|
||||
}
|
||||
|
||||
export default v4;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import v35 from './v35.js';
|
||||
import sha1 from './sha1.js';
|
||||
var v5 = v35('v5', 0x50, sha1);
|
||||
export default v5;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user