Compare commits

...

36 Commits

Author SHA1 Message Date
Oliver King 2e569e818b fix misses string template 2021-11-30 09:29:27 -05:00
Oliver King adc1ea014e build js 2021-11-29 15:32:31 -05:00
Oliver King 8898d95f4f Fix smi weight bug (#150)
Co-authored-by: Oliver King <kingoliver@microsoft.com>
2021-11-29 15:26:58 -05:00
Koushik Dey 33608d18f7 Sending a warning instead of throwing error when KUBECONFIG is missing. (#144) 2021-08-17 16:39:38 +05:30
Koushik Dey acd12a4705 Create defaultLabels.yml (#140) 2021-06-27 08:54:24 +05:30
Koushik Dey 81557b8633 Bump @actions/tool-cache from 1.1.1 to 1.1.2 (#138) 2021-06-18 11:50:08 +05:30
dependabot[bot] e1b9842236 Bump ws from 7.4.4 to 7.4.6 (#137)
Bumps [ws](https://github.com/websockets/ws) from 7.4.4 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.4.4...7.4.6)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-15 22:57:14 +05:30
dependabot[bot] 26cb2cdb5f Bump browserslist from 4.16.3 to 4.16.6 (#136)
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.3 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.3...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-15 22:00:46 +05:30
dependabot[bot] 5ebbfbbefe Bump hosted-git-info from 2.8.8 to 2.8.9 (#135)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-26 12:47:36 +05:30
Koushik Dey ac49626466 Fixing label values of workflow names with spaces (#134) 2021-05-05 15:55:02 +05:30
Ganesh S d332939666 Merge pull request #131 from Azure/ganeshrockz/delimiterFix
Supporting both comma and new line as delimiters for manifests
2021-04-15 11:51:30 +05:30
Ganeshrockz 2577009bcb Adding ; as delimiter 2021-04-15 11:35:18 +05:30
Ganeshrockz a58ad23e7f PR comments 2021-04-14 20:05:45 +05:30
Ganeshrockz ee4b5d33e0 Comments 2021-04-14 15:31:19 +05:30
Ganeshrockz 625898f6eb Removing whitespace from manifests 2021-04-14 14:31:04 +05:30
Ganeshrockz 8d257fed50 Supporting both comma and new line as delimiters for manifests 2021-04-14 14:14:06 +05:30
Sundar 4f6b70e29a Updated trigger for L2 tests. (#128) 2021-03-31 17:30:09 +05:30
Deepak Sattiraju 202bacc71b Update integration-tests.yml 2021-03-16 15:54:57 +05:30
Deepak Sattiraju 2c09684db9 Removing js files from main branch (#122) 2021-03-15 14:28:53 +05:30
Gennady Trubach 282a81e1fc Added arch detection to kubectl download (#117) 2021-03-15 12:46:34 +05:30
Deepak Sattiraju ce7c8f066f Fixing depandabot alert node-notifier --> 8.0.2 (#121) 2021-03-12 11:34:20 +05:30
Maxime Guerreiro 25ded46b9d Update readme to use v1.4 (#116)
Version 1.4 was released in December 2020. Update the readme to use this new version.
2021-03-10 13:20:41 +05:30
Jyotsna 56e4abca5e Users/jysin/pm feedback changes for master (#99)
* Changed dockerfile, Manifests, helmcharts links and README
2021-01-07 12:51:44 +05:30
Jyotsna 4bd69f56a9 Bug fix for dockerfile path for default (#96)
* Bug fix for dockerfile path link
2020-12-30 21:55:39 +05:30
Jyotsna 04921d7d06 New traceability fields added to annotations (#90)
* New traceability fields
2020-12-30 15:03:39 +05:30
Jyotsna 51b95a5ca2 Readme updated with traceability changes (#84)
* Updated new features in Readme with sample workflows - updated changes.
2020-12-28 14:50:00 +05:30
Jyotsna 49257c6f33 Updated new features in Readme with sample workflows (#82)
* Updated new changes in Readme for annotations with sample workflows for v1.2
2020-12-11 12:08:11 +05:30
Thomas Seljen Tvedt 895952654c Fixing version in samples (#80)
*editing in github to fix diff?*
Update README.md, changing from Azure/k8s-deploy@v1 to newest version Azure/k8s-deploy@v1.3.

Using @v1 I got an error message "Error: TypeError: Cannot read property 'trim' of null". When I finally realized I was on the wrong version it worked smoothly 🥳

Hopefully this will save someone else some troubleshooting?
2020-11-25 22:50:50 +05:30
Zainudeen V K 0fd84a1b0d Update integration-tests.yml 2020-11-09 13:49:00 +05:30
Ajinkya d35174fe93 Fixing issues found in bug bash. (#71) 2020-10-30 13:10:27 +05:30
Koushik Dey f80ed6c460 Merge pull request #66 from Azure/dependabot/npm_and_yarn/actions/core-1.2.6
Bump @actions/core from 1.1.0 to 1.2.6
2020-10-06 12:16:02 +05:30
Koushik Dey 72bc167726 BumpUp @actions/core, fixed failing test case 2020-10-05 18:45:39 +05:30
dependabot[bot] 21d3af2857 Bump @actions/core from 1.1.0 to 1.2.6
Bumps [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) from 1.1.0 to 1.2.6.
- [Release notes](https://github.com/actions/toolkit/releases)
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-01 17:20:23 +00:00
Koushik Dey a80355209a Merge pull request #64 from Azure/users/koushdey/workflowAnnotations-master
(M)Annotation and Label changes to handle multiple workflows across branches
2020-09-21 14:53:50 +05:30
Koushik Dey 92589546e8 Annotation and Label changes to handle multiple workflows across branches(master) 2020-09-21 14:30:51 +05:30
Ajinkya b9146889f3 Update issue templates (#58)
* Update issue templates

* Add label as well
2020-09-17 14:04:13 +05:30
48 changed files with 14325 additions and 3138 deletions
@@ -0,0 +1,20 @@
---
name: Blue Green Bug Bash
about: Issues found in blue-green strategy bug bash
title: "[Blue-Green Bug Bash] "
labels: 'blue-green-bug-bash'
assignees: ajinkya599
---
Repro steps:
- Add steps to repro the issue here
-
Current behaviour:
- What is the current behaviour?
-
Expected behaviour:
- What is the expected behaviour?
-
+3 -3
View File
@@ -4,14 +4,14 @@ repository=$3
prNumber=$4 prNumber=$4
frombranch=$5 frombranch=$5
tobranch=$6 tobranch=$6
patUser=$7
getPayLoad() { getPayLoad() {
cat <<EOF cat <<EOF
{ {
"event_type": "K8sDeployActionPR", "event_type": "K8sDeployActionPR",
"client_payload": "client_payload":
{ {
"action": "CreateSecret", "action": "K8sDeploy",
"commit": "$commit", "commit": "$commit",
"repository": "$repository", "repository": "$repository",
"prNumber": "$prNumber", "prNumber": "$prNumber",
@@ -22,7 +22,7 @@ getPayLoad() {
EOF EOF
} }
response=$(curl -X POST -H "Authorization: token $token" https://api.github.com/repos/Azure/azure-actions-integration-tests/dispatches --data "$(getPayLoad)") response=$(curl -u $patUser:$token -X POST https://api.github.com/repos/Azure/azure-actions-integration-tests/dispatches --data "$(getPayLoad)")
if [ "$response" == "" ]; then if [ "$response" == "" ]; then
echo "Integration tests triggered successfully" echo "Integration tests triggered successfully"
+36
View File
@@ -0,0 +1,36 @@
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
+2 -6
View File
@@ -1,12 +1,8 @@
name: "Trigger Integration tests" name: "Trigger Integration tests"
on: on:
# push:
# branches:
# - master
# - 'releases/*'
pull_request: pull_request:
branches: branches:
- master - main
- 'releases/*' - 'releases/*'
jobs: jobs:
trigger-integration-tests: trigger-integration-tests:
@@ -20,4 +16,4 @@ jobs:
- name: Trigger Test run - name: Trigger Test run
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 }} 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 }} ${{ secrets.L2_REPO_USER }}
@@ -1,4 +1,4 @@
name: "build-test" name: "Run unit tests."
on: # rebuild any PRs and main branch changes on: # rebuild any PRs and main branch changes
pull_request: pull_request:
branches: branches:
@@ -16,5 +16,4 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- run: | - run: |
npm install npm install
npm build
npm test npm test
+1 -329
View File
@@ -1,329 +1 @@
## Ignore Visual Studio temporary files, build results, and node_modules
## 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
# 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/
+142 -10
View File
@@ -103,7 +103,7 @@ Following are the key capabilities of this action:
### Basic deployment (without any deployment strategy) ### Basic deployment (without any deployment strategy)
```yaml ```yaml
- uses: Azure/k8s-deploy@v1 - uses: Azure/k8s-deploy@v1.4
with: with:
namespace: 'myapp' namespace: 'myapp'
manifests: | manifests: |
@@ -119,7 +119,7 @@ Following are the key capabilities of this action:
### Deployment Strategies - Canary deployment without service mesh ### Deployment Strategies - Canary deployment without service mesh
```yaml ```yaml
- uses: Azure/k8s-deploy@v1 - uses: Azure/k8s-deploy@v1.4
with: with:
namespace: 'myapp' namespace: 'myapp'
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}' images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
@@ -136,7 +136,7 @@ Following are the key capabilities of this action:
### To promote/reject the canary created by the above snippet, the following YAML snippet could be used: ### To promote/reject the canary created by the above snippet, the following YAML snippet could be used:
```yaml ```yaml
- uses: Azure/k8s-deploy@v1 - uses: Azure/k8s-deploy@v1.4
with: with:
namespace: 'myapp' namespace: 'myapp'
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}' images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
@@ -153,7 +153,7 @@ Following are the key capabilities of this action:
### Deployment Strategies - Canary deployment based on Service Mesh Interface ### Deployment Strategies - Canary deployment based on Service Mesh Interface
```yaml ```yaml
- uses: Azure/k8s-deploy@v1 - uses: Azure/k8s-deploy@v1.4
with: with:
namespace: 'myapp' namespace: 'myapp'
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}' images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
@@ -170,7 +170,7 @@ Following are the key capabilities of this action:
``` ```
### To promote/reject the canary created by the above snippet, the following YAML snippet could be used: ### To promote/reject the canary created by the above snippet, the following YAML snippet could be used:
```yaml ```yaml
- uses: Azure/k8s-deploy@v1 - uses: Azure/k8s-deploy@v1.4
with: with:
namespace: 'myapp' namespace: 'myapp'
images: 'contoso.azurecr.io/myapp:${{ event.run_id }} ' images: 'contoso.azurecr.io/myapp:${{ event.run_id }} '
@@ -187,7 +187,7 @@ Following are the key capabilities of this action:
### Deployment Strategies - Blue-Green deployment with different route methods ### Deployment Strategies - Blue-Green deployment with different route methods
```yaml ```yaml
- uses: Azure/k8s-deploy@v1 - uses: Azure/k8s-deploy@v1.4
with: with:
namespace: 'myapp' namespace: 'myapp'
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}' images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
@@ -206,7 +206,7 @@ Following are the key capabilities of this action:
### **To promote/reject the green workload created by the above snippet, the following YAML snippet could be used:** ### **To promote/reject the green workload created by the above snippet, the following YAML snippet could be used:**
```yaml ```yaml
- uses: Azure/k8s-deploy@v1 - uses: Azure/k8s-deploy@v1.4
with: with:
namespace: 'myapp' namespace: 'myapp'
images: 'contoso.azurecr.io/myapp:${{ event.run_id }}' images: 'contoso.azurecr.io/myapp:${{ event.run_id }}'
@@ -218,7 +218,7 @@ Following are the key capabilities of this action:
service.yaml service.yaml
ingress-yml ingress-yml
strategy: blue-green strategy: blue-green
strategy: ingress # should be the same as the value when action was deploy route-method: ingress # should be the same as the value when action was deploy
action: promote # substitute reject if you want to reject action: promote # substitute reject if you want to reject
``` ```
@@ -261,7 +261,7 @@ jobs:
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }} container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
secret-name: demo-k8s-secret secret-name: demo-k8s-secret
- uses: Azure/k8s-deploy@v1 - uses: Azure/k8s-deploy@v1.4
with: with:
manifests: | manifests: |
manifests/deployment.yml manifests/deployment.yml
@@ -304,7 +304,7 @@ jobs:
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }} container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
secret-name: demo-k8s-secret secret-name: demo-k8s-secret
- uses: Azure/k8s-deploy@v1 - uses: Azure/k8s-deploy@v1.4
with: with:
manifests: | manifests: |
manifests/deployment.yml manifests/deployment.yml
@@ -314,6 +314,138 @@ jobs:
imagepullsecrets: | imagepullsecrets: |
demo-k8s-secret demo-k8s-secret
``` ```
## Sample workflows for new 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 2 distinct jobs in the same or separate workflows.
### End to end workflow for building and deploying container images
```yaml
on: [push]
env:
NAMESPACE: demo-ns2
jobs:
build-and-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 }}
- run: |
docker build . -t contoso.azurecr.io/k8sdemo:${{ github.sha }} --label dockerfile-path=./Dockerfile
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.2
with:
manifests: |
manifests/deployment.yml
manifests/service.yml
images: |
contoso.azurecr.io/k8sdemo:${{ github.sha }}
imagepullsecrets: |
demo-k8s-secret
```
### CI workflow to build image and add `dockerfile-path` label to it. This image can then be used in another CD workflow.
```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 }}
```
### CD workflow using 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 }}
# 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:
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@v1
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:
manifests: ${{ steps.bake.outputs.manifestsBundle }}
images: |
contoso.azurecr.io/k8sdemo:${{ github.sha }}
imagepullsecrets: |
demo-k8s-secret
```
# Contributing # Contributing
+215 -20
View File
@@ -6,11 +6,14 @@ import * as deployment from '../src/utilities/strategy-helpers/deployment-helper
import * as fs from 'fs'; import * as fs from 'fs';
import * as io from '@actions/io'; import * as io from '@actions/io';
import * as toolCache from '@actions/tool-cache'; import * as toolCache from '@actions/tool-cache';
import * as util from 'util';
import * as fileHelper from '../src/utilities/files-helper'; import * as fileHelper from '../src/utilities/files-helper';
import { workflowAnnotations } from '../src/constants'; import { getWorkflowAnnotationKeyLabel, getWorkflowAnnotationsJson } from '../src/constants';
import * as inputParam from '../src/input-parameters'; import * as inputParam from '../src/input-parameters';
import { Kubectl, Resource } from '../src/kubectl-object-model'; 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 { getkubectlDownloadURL } from "../src/utilities/kubectl-util";
import { mocked } from 'ts-jest/utils'; import { mocked } from 'ts-jest/utils';
@@ -21,6 +24,7 @@ const os = require("os");
const coreMock = mocked(core, true); const coreMock = mocked(core, true);
const ioMock = mocked(io, true); const ioMock = mocked(io, true);
const inputParamMock = mocked(inputParam, true); const inputParamMock = mocked(inputParam, true);
const osMock = mocked(os, true);
const toolCacheMock = mocked(toolCache, true); const toolCacheMock = mocked(toolCache, true);
const fileUtility = mocked(fs, true); const fileUtility = mocked(fs, true);
@@ -36,13 +40,47 @@ const getAllPodsMock = {
const getNamespaceMock = { const getNamespaceMock = {
'code': 0, 'code': 0,
'stdout': '{"apiVersion": "v1","kind": "Namespace","metadata": {"annotations": {"workflow": ".github/workflows/workflow.yml","runUri": "https://github.com/testRepo/actions/runs/12345"}},"spec": {"finalizers": ["kubernetes"]},"status": {"phase": "Active"}}' '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" }]; const resources: Resource[] = [{ type: "Deployment", name: "AppName" }];
beforeEach(() => { beforeEach(() => {
deploymentYaml = fs.readFileSync(path.join(__dirname, 'manifests', 'deployment.yml'), 'utf8'); deploymentYaml = fs.readFileSync(path.join(__dirname, 'manifests', 'deployment.yml'), 'utf8');
jest.spyOn(Date, 'now').mockImplementation(() => 1234561234567);
process.env["KUBECONFIG"] = 'kubeConfig'; process.env["KUBECONFIG"] = 'kubeConfig';
process.env['GITHUB_RUN_ID'] = '12345'; process.env['GITHUB_RUN_ID'] = '12345';
@@ -52,12 +90,18 @@ beforeEach(() => {
process.env['GITHUB_REPOSITORY'] = 'testRepo'; process.env['GITHUB_REPOSITORY'] = 'testRepo';
process.env['GITHUB_SHA'] = 'testCommit'; process.env['GITHUB_SHA'] = 'testCommit';
process.env['GITHUB_REF'] = 'testBranch'; process.env['GITHUB_REF'] = 'testBranch';
process.env['GITHUB_TOKEN'] = 'testToken';
}) })
test("setKubectlPath() - install a particular version", async () => { test.each([
['arm', 'arm'],
['arm64', 'arm64'],
['x64', 'amd64']
])("setKubectlPath() - install a particular version on %s", async (osArch, kubectlArch) => {
const kubectlVersion = 'v1.18.0' const kubectlVersion = 'v1.18.0'
//Mocks //Mocks
coreMock.getInput = jest.fn().mockReturnValue(kubectlVersion); coreMock.getInput = jest.fn().mockReturnValue(kubectlVersion);
osMock.arch = jest.fn().mockReturnValue(osArch);
toolCacheMock.find = jest.fn().mockReturnValue(undefined); toolCacheMock.find = jest.fn().mockReturnValue(undefined);
toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath'); toolCacheMock.downloadTool = jest.fn().mockReturnValue('downloadpath');
toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath'); toolCacheMock.cacheFile = jest.fn().mockReturnValue('cachepath');
@@ -66,7 +110,7 @@ test("setKubectlPath() - install a particular version", async () => {
//Invoke and assert //Invoke and assert
await expect(action.run()).resolves.not.toThrow(); await expect(action.run()).resolves.not.toThrow();
expect(toolCacheMock.find).toBeCalledWith('kubectl', kubectlVersion); expect(toolCacheMock.find).toBeCalledWith('kubectl', kubectlVersion);
expect(toolCacheMock.downloadTool).toBeCalledWith(getkubectlDownloadURL(kubectlVersion)); expect(toolCacheMock.downloadTool).toBeCalledWith(getkubectlDownloadURL(kubectlVersion, kubectlArch));
}); });
test("setKubectlPath() - install a latest version", async () => { test("setKubectlPath() - install a latest version", async () => {
@@ -166,6 +210,111 @@ test("run() - deploy - Manifiest not provided", async () => {
expect(coreMock.setFailed).toBeCalledWith('No manifests supplied to deploy'); expect(coreMock.setFailed).toBeCalledWith('No manifests supplied to deploy');
}); });
test("run() - deploy - Only one manifest with no delimiters", async () => {
const kubectlVersion = 'v1.18.0'
coreMock.getInput = jest.fn().mockImplementation((name) => {
if (name == 'manifests') {
return "bg-smi.yml";
}
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();
});
test("run() - deploy - Manifests provided by new line delimiter", async () => {
const kubectlVersion = 'v1.18.0'
coreMock.getInput = jest.fn().mockImplementation((name) => {
if (name == 'manifests') {
return "bg-smi.yml\n bg.yml\ndeployment.yml";
}
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();
});
test("run() - deploy - Manifests provided by comma as a delimiter", async () => {
const kubectlVersion = 'v1.18.0'
coreMock.getInput = jest.fn().mockImplementation((name) => {
if (name == 'manifests') {
return "bg-smi.yml, bg.yml, deployment.yml";
}
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();
});
test("run() - deploy - Manifests provided by both new line and comma as a delimiter", async () => {
const kubectlVersion = 'v1.18.0'
coreMock.getInput = jest.fn().mockImplementation((name) => {
if (name == 'manifests') {
return "bg-smi.yml\nbg.yml,deployment.yml";
}
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();
});
test("run() - deploy - Manifests provided by both new line and comma and semi-colon as a delimiter", async () => {
const kubectlVersion = 'v1.18.0'
coreMock.getInput = jest.fn().mockImplementation((name) => {
if (name == 'manifests') {
return "bg-smi.yml\nbg.yml,deployment.yml;bg.yml";
}
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();
});
test("deployment - deploy() - Invokes with no manifestfiles", async () => { test("deployment - deploy() - Invokes with no manifestfiles", async () => {
const kubeCtl: jest.Mocked<Kubectl> = new Kubectl("") as any; const kubeCtl: jest.Mocked<Kubectl> = new Kubectl("") as any;
@@ -213,9 +362,11 @@ test("deployment - deploy() - Invokes with manifestfiles", async () => {
kubeCtl.describe = jest.fn().mockReturnValue(""); kubeCtl.describe = jest.fn().mockReturnValue("");
kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotateFiles = jest.fn().mockReturnValue("");
kubeCtl.annotate = jest.fn().mockReturnValue(""); kubeCtl.annotate = jest.fn().mockReturnValue("");
kubeCtl.labelFiles = jest.fn().mockReturnValue("");
KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue("");
const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml); const readFileSpy = jest.spyOn(fs, 'readFileSync').mockImplementation(() => deploymentYaml);
jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(getWorkflowsUrlResponse));
//Invoke and assert //Invoke and assert
await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError();
@@ -241,9 +392,11 @@ test("deployment - deploy() - deploy force flag on", async () => {
kubeCtl.describe = jest.fn().mockReturnValue(""); kubeCtl.describe = jest.fn().mockReturnValue("");
kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotateFiles = jest.fn().mockReturnValue("");
kubeCtl.annotate = jest.fn().mockReturnValue(""); kubeCtl.annotate = jest.fn().mockReturnValue("");
kubeCtl.labelFiles = jest.fn().mockReturnValue("");
KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue("");
const deploySpy = jest.spyOn(kubeCtl, 'apply').mockImplementation(() => applyResMock); const deploySpy = jest.spyOn(kubeCtl, 'apply').mockImplementation(() => applyResMock);
jest.spyOn(httpClient, 'sendRequest').mockImplementation(() => Promise.resolve(getWorkflowsUrlResponse));
//Invoke and assert //Invoke and assert
await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError();
@@ -251,15 +404,20 @@ test("deployment - deploy() - deploy force flag on", async () => {
deploySpy.mockRestore(); deploySpy.mockRestore();
}); });
test("deployment - deploy() - Annotate resources", async () => { test("deployment - deploy() - Annotate & label resources", async () => {
let 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); const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true);
KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue("");
const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true);
KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources); KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources);
const fileHelperMock = mocked(fileHelper, true); const fileHelperMock = mocked(fileHelper, true);
const fsMock = (mocked(fs, true)); const fsMock = (mocked(fs, true));
fileHelperMock.getTempDirectory = jest.fn().mockReturnValue("Local/Temp/"); fileHelperMock.getTempDirectory = jest.fn().mockReturnValue("~/Deployment_testapp_currentTimestamp");
fsMock.writeFileSync =jest.fn().mockReturnValue(""); 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; const kubeCtl: jest.Mocked<Kubectl> = new Kubectl("") as any;
kubeCtl.apply = jest.fn().mockReturnValue(""); kubeCtl.apply = jest.fn().mockReturnValue("");
kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock); kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock);
@@ -267,23 +425,31 @@ test("deployment - deploy() - Annotate resources", async () => {
kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9"); kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9");
kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotateFiles = jest.fn().mockReturnValue("");
kubeCtl.annotate = jest.fn().mockReturnValue(""); kubeCtl.annotate = jest.fn().mockReturnValue("");
kubeCtl.labelFiles = jest.fn();
//Invoke and assert //Invoke and assert
await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError();
expect(kubeCtl.annotateFiles).toBeCalledWith(["Local/Temp/deployment.yaml"], workflowAnnotations, true); 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.annotate).toBeCalledTimes(2);
expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp/deployment.yaml"],
[`workflowFriendlyName=workflow.yml`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`]);
}); });
test("deployment - deploy() - Skip Annotate namespace", async () => { test("deployment - deploy() - Annotate & label resources for a new workflow", async () => {
process.env['GITHUB_REPOSITORY'] = 'test1Repo'; process.env.GITHUB_WORKFLOW = '.github/workflows/New Workflow.yml';
let deploymentConfig: utility.DeploymentConfig = { manifestFilePaths: ['manifests/deployment.yaml'], helmChartFilePaths: [], dockerfilePaths: {} }
let annotationKeyValStr = getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW) + '=' + getWorkflowAnnotationsJson('NA', '.github/workflows/New Workflow.yml', deploymentConfig);
const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true); const KubernetesManifestUtilityMock = mocked(KubernetesManifestUtility, true);
KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue("");
const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true); const KubernetesObjectUtilityMock = mocked(KubernetesObjectUtility, true);
KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources); KubernetesObjectUtilityMock.getResources = jest.fn().mockReturnValue(resources);
const fileHelperMock = mocked(fileHelper, true); const fileHelperMock = mocked(fileHelper, true);
const fsMock = (mocked(fs, true)); const fsMock = (mocked(fs, true));
fileHelperMock.getTempDirectory = jest.fn().mockReturnValue("Local/Temp/"); fileHelperMock.getTempDirectory = jest.fn().mockReturnValue("~/Deployment_testapp_currentTimestamp");
fsMock.writeFileSync =jest.fn().mockReturnValue(""); 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; const kubeCtl: jest.Mocked<Kubectl> = new Kubectl("") as any;
kubeCtl.apply = jest.fn().mockReturnValue(""); kubeCtl.apply = jest.fn().mockReturnValue("");
kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock); kubeCtl.getResource = jest.fn().mockReturnValue(getNamespaceMock);
@@ -291,14 +457,14 @@ test("deployment - deploy() - Skip Annotate namespace", async () => {
kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9"); kubeCtl.getNewReplicaSet = jest.fn().mockReturnValue("testpod-776cbc86f9");
kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotateFiles = jest.fn().mockReturnValue("");
kubeCtl.annotate = jest.fn().mockReturnValue(""); kubeCtl.annotate = jest.fn().mockReturnValue("");
kubeCtl.labelFiles = jest.fn();
const consoleOutputSpy = jest.spyOn(process.stdout, "write").mockImplementation();
//Invoke and assert //Invoke and assert
await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError();
expect(kubeCtl.annotateFiles).toBeCalledWith(["Local/Temp/deployment.yaml"], workflowAnnotations, true); expect(kubeCtl.annotate).toHaveBeenNthCalledWith(1, 'namespace', 'default', annotationKeyValStr);
expect(kubeCtl.annotate).toBeCalledTimes(1); expect(kubeCtl.annotateFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp/deployment.yaml"], annotationKeyValStr);
expect(consoleOutputSpy).toHaveBeenNthCalledWith(1, `##[debug]Skipping 'annotate namespace' as namespace annotated by other workflow` + os.EOL) expect(kubeCtl.annotate).toBeCalledTimes(2);
expect(kubeCtl.labelFiles).toBeCalledWith(["~/Deployment_testapp_currentTimestamp/deployment.yaml"],
[`workflowFriendlyName=New_Workflow.yml`, `workflow=${getWorkflowAnnotationKeyLabel(process.env.GITHUB_WORKFLOW)}`]);
}); });
test("deployment - deploy() - Annotate resources failed", async () => { test("deployment - deploy() - Annotate resources failed", async () => {
@@ -320,10 +486,39 @@ test("deployment - deploy() - Annotate resources failed", async () => {
kubeCtl.describe = jest.fn().mockReturnValue(""); kubeCtl.describe = jest.fn().mockReturnValue("");
kubeCtl.annotateFiles = jest.fn().mockReturnValue(""); kubeCtl.annotateFiles = jest.fn().mockReturnValue("");
kubeCtl.annotate = jest.fn().mockReturnValue(annotateMock); kubeCtl.annotate = jest.fn().mockReturnValue(annotateMock);
kubeCtl.labelFiles = jest.fn().mockReturnValue("");
KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue(""); KubernetesManifestUtilityMock.checkManifestStability = jest.fn().mockReturnValue("");
const consoleOutputSpy = jest.spyOn(process.stdout, "write").mockImplementation(); const consoleOutputSpy = jest.spyOn(process.stdout, "write").mockImplementation();
//Invoke and assert //Invoke and assert
await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError(); await expect(deployment.deploy(kubeCtl, ['manifests/deployment.yaml'], undefined)).resolves.not.toThrowError();
expect(consoleOutputSpy).toHaveBeenNthCalledWith(1, '##[warning]kubectl annotate failed' + os.EOL) 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);
});
test("action - run() - Throw kubectl error on 404 response", async () => {
const kubectlVersion = 'v1.18.0'
const arch = 'arm128';
// Mock
coreMock.getInput = jest.fn().mockReturnValue(kubectlVersion);
osMock.arch = jest.fn().mockReturnValue(arch);
toolCacheMock.find = jest.fn().mockReturnValue(undefined);
toolCacheMock.downloadTool = jest.fn().mockImplementation(_ => {
throw new toolCache.HTTPError(httpClient.StatusCodes.NOT_FOUND);
});
//Invoke and assert
await expect(action.run()).rejects.toThrow(util.format("Kubectl '%s' for '%s' arch not found.", kubectlVersion, arch));
}); });
+4
View File
@@ -53,6 +53,10 @@ inputs:
description: 'Deploy when a previous deployment already exists. If true then --force argument is added to the apply command' description: 'Deploy when a previous deployment already exists. If true then --force argument is added to the apply command'
required: false required: false
default: false default: false
token:
description: 'Github token'
default: ${{ github.token }}
required: true
branding: branding:
color: 'green' # optional, decorates the entry in the GitHub Marketplace color: 'green' # optional, decorates the entry in the GitHub Marketplace
+28 -13
View File
@@ -1,6 +1,6 @@
'use strict'; 'use strict';
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.workflowAnnotations = exports.workloadTypesWithRolloutStatus = exports.workloadTypes = exports.deploymentTypes = exports.ServiceTypes = exports.DiscoveryAndLoadBalancerResource = exports.KubernetesWorkload = void 0; exports.getWorkflowAnnotationKeyLabel = exports.getWorkflowAnnotationsJson = exports.workloadTypesWithRolloutStatus = exports.workloadTypes = exports.deploymentTypes = exports.ServiceTypes = exports.DiscoveryAndLoadBalancerResource = exports.KubernetesWorkload = void 0;
class KubernetesWorkload { class KubernetesWorkload {
} }
exports.KubernetesWorkload = KubernetesWorkload; exports.KubernetesWorkload = KubernetesWorkload;
@@ -25,15 +25,30 @@ ServiceTypes.clusterIP = 'ClusterIP';
exports.deploymentTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset']; exports.deploymentTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset'];
exports.workloadTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob']; exports.workloadTypes = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob'];
exports.workloadTypesWithRolloutStatus = ['deployment', 'daemonset', 'statefulset']; exports.workloadTypesWithRolloutStatus = ['deployment', 'daemonset', 'statefulset'];
exports.workflowAnnotations = [ function getWorkflowAnnotationsJson(lastSuccessRunSha, workflowFilePath, deploymentConfig) {
`run=${process.env['GITHUB_RUN_ID']}`, let annotationObject = {};
`repository=${process.env['GITHUB_REPOSITORY']}`, annotationObject["run"] = process.env.GITHUB_RUN_ID;
`workflow=${process.env['GITHUB_WORKFLOW']}`, annotationObject["repository"] = process.env.GITHUB_REPOSITORY;
`jobName=${process.env['GITHUB_JOB']}`, annotationObject["workflow"] = process.env.GITHUB_WORKFLOW;
`createdBy=${process.env['GITHUB_ACTOR']}`, annotationObject["workflowFileName"] = workflowFilePath.replace(".github/workflows/", "");
`runUri=https://github.com/${process.env['GITHUB_REPOSITORY']}/actions/runs/${process.env['GITHUB_RUN_ID']}`, annotationObject["jobName"] = process.env.GITHUB_JOB;
`commit=${process.env['GITHUB_SHA']}`, annotationObject["createdBy"] = process.env.GITHUB_ACTOR;
`branch=${process.env['GITHUB_REF']}`, annotationObject["runUri"] = `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
`deployTimestamp=${Date.now()}`, annotationObject["commit"] = process.env.GITHUB_SHA;
`provider=GitHub` 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;
+31
View File
@@ -0,0 +1,31 @@
"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;
+35
View File
@@ -0,0 +1,35 @@
"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;
+6 -2
View File
@@ -1,11 +1,11 @@
'use strict'; 'use strict';
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.forceDeployment = exports.args = exports.baselineAndCanaryReplicas = exports.versionSwitchBuffer = exports.routeMethod = exports.trafficSplitMethod = exports.deploymentStrategy = exports.canaryPercentage = exports.manifests = exports.imagePullSecrets = exports.containers = exports.namespace = void 0; exports.githubToken = exports.forceDeployment = exports.args = 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"); const core = require("@actions/core");
exports.namespace = core.getInput('namespace'); exports.namespace = core.getInput('namespace');
exports.containers = core.getInput('images').split('\n'); exports.containers = core.getInput('images').split('\n');
exports.imagePullSecrets = core.getInput('imagepullsecrets').split('\n').filter(secret => secret.trim().length > 0); exports.imagePullSecrets = core.getInput('imagepullsecrets').split('\n').filter(secret => secret.trim().length > 0);
exports.manifests = core.getInput('manifests').split('\n'); exports.manifests = core.getInput('manifests').split(/[\n,;]+/).filter(manifest => manifest.trim().length > 0);
exports.canaryPercentage = core.getInput('percentage'); exports.canaryPercentage = core.getInput('percentage');
exports.deploymentStrategy = core.getInput('strategy'); exports.deploymentStrategy = core.getInput('strategy');
exports.trafficSplitMethod = core.getInput('traffic-split-method'); exports.trafficSplitMethod = core.getInput('traffic-split-method');
@@ -14,10 +14,14 @@ exports.versionSwitchBuffer = core.getInput('version-switch-buffer');
exports.baselineAndCanaryReplicas = core.getInput('baseline-and-canary-replicas'); exports.baselineAndCanaryReplicas = core.getInput('baseline-and-canary-replicas');
exports.args = core.getInput('arguments'); exports.args = core.getInput('arguments');
exports.forceDeployment = core.getInput('force').toLowerCase() == 'true'; exports.forceDeployment = core.getInput('force').toLowerCase() == 'true';
exports.githubToken = core.getInput("token");
if (!exports.namespace) { if (!exports.namespace) {
core.debug('Namespace was not supplied; using "default" namespace instead.'); core.debug('Namespace was not supplied; using "default" namespace instead.');
exports.namespace = 'default'; exports.namespace = 'default';
} }
if (!exports.githubToken) {
core.error("'token' input is not supplied. Set it to a PAT/GITHUB_TOKEN");
}
try { try {
const pe = parseInt(exports.canaryPercentage); const pe = parseInt(exports.canaryPercentage);
if (pe < 0 || pe > 100) { if (pe < 0 || pe > 100) {
+13 -10
View File
@@ -37,21 +37,24 @@ class Kubectl {
} }
return newReplicaSet; return newReplicaSet;
} }
annotate(resourceType, resourceName, annotations, overwrite) { annotate(resourceType, resourceName, annotation) {
let args = ['annotate', resourceType, resourceName]; let args = ['annotate', resourceType, resourceName];
args = args.concat(annotations); args.push(annotation);
if (!!overwrite) { args.push(`--overwrite`);
args.push(`--overwrite`);
}
return this.execute(args); return this.execute(args);
} }
annotateFiles(files, annotations, overwrite) { annotateFiles(files, annotation) {
let args = ['annotate']; let args = ['annotate'];
args = args.concat(['-f', this.createInlineArray(files)]); args = args.concat(['-f', this.createInlineArray(files)]);
args = args.concat(annotations); args.push(annotation);
if (!!overwrite) { args.push(`--overwrite`);
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); return this.execute(args);
} }
getAllPods() { getAllPods() {
-80
View File
@@ -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;
+7 -2
View File
@@ -53,7 +53,7 @@ function installKubectl(version) {
} }
function checkClusterContext() { function checkClusterContext() {
if (!process.env["KUBECONFIG"]) { if (!process.env["KUBECONFIG"]) {
throw new Error('Cluster context not set. Use k8ssetcontext action to set cluster context'); core.warning('KUBECONFIG env is not explicitly set. Ensure cluster context is set by using k8s-set-context / aks-set-context action.');
} }
} }
function run() { function run() {
@@ -70,7 +70,12 @@ function run() {
namespace = 'default'; namespace = 'default';
} }
let action = core.getInput('action'); let action = core.getInput('action');
let manifests = manifestsInput.split('\n'); let manifests = manifestsInput.split(/[\n,;]+/).filter(manifest => manifest.trim().length > 0);
if (manifests.length > 0) {
manifests = manifests.map(manifest => {
return manifest.trim();
});
}
if (action === 'deploy') { if (action === 'deploy') {
let strategy = core.getInput('strategy'); let strategy = core.getInput('strategy');
console.log("strategy: ", strategy); console.log("strategy: ", strategy);
+111
View File
@@ -0,0 +1,111 @@
"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;
});
}
+20 -6
View File
@@ -16,6 +16,7 @@ const os = require("os");
const path = require("path"); const path = require("path");
const toolCache = require("@actions/tool-cache"); const toolCache = require("@actions/tool-cache");
const util = require("util"); const util = require("util");
const httpClient_1 = require("./httpClient");
const kubectlToolName = 'kubectl'; const kubectlToolName = 'kubectl';
const stableKubectlVersion = 'v1.15.0'; const stableKubectlVersion = 'v1.15.0';
const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/release/stable.txt'; const stableVersionUrl = 'https://storage.googleapis.com/kubernetes-release/release/stable.txt';
@@ -26,15 +27,22 @@ function getExecutableExtension() {
} }
return ''; return '';
} }
function getkubectlDownloadURL(version) { function getKubectlArch() {
let arch = os.arch();
if (arch === 'x64') {
return 'amd64';
}
return arch;
}
function getkubectlDownloadURL(version, arch) {
switch (os.type()) { switch (os.type()) {
case 'Linux': case 'Linux':
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/linux/amd64/kubectl', version); return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/linux/%s/kubectl', version, arch);
case 'Darwin': case 'Darwin':
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/darwin/amd64/kubectl', version); return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/darwin/%s/kubectl', version, arch);
case 'Windows_NT': case 'Windows_NT':
default: default:
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/windows/amd64/kubectl.exe', version); return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/windows/%s/kubectl.exe', version, arch);
} }
} }
exports.getkubectlDownloadURL = getkubectlDownloadURL; exports.getkubectlDownloadURL = getkubectlDownloadURL;
@@ -58,12 +66,18 @@ function downloadKubectl(version) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
let cachedToolpath = toolCache.find(kubectlToolName, version); let cachedToolpath = toolCache.find(kubectlToolName, version);
let kubectlDownloadPath = ''; let kubectlDownloadPath = '';
let arch = getKubectlArch();
if (!cachedToolpath) { if (!cachedToolpath) {
try { try {
kubectlDownloadPath = yield toolCache.downloadTool(getkubectlDownloadURL(version)); kubectlDownloadPath = yield toolCache.downloadTool(getkubectlDownloadURL(version, arch));
} }
catch (exception) { catch (exception) {
throw new Error('DownloadKubectlFailed'); if (exception instanceof toolCache.HTTPError && exception.httpStatusCode === httpClient_1.StatusCodes.NOT_FOUND) {
throw new Error(util.format("Kubectl '%s' for '%s' arch not found.", version, arch));
}
else {
throw new Error('DownloadKubectlFailed');
}
} }
cachedToolpath = yield toolCache.cacheFile(kubectlDownloadPath, kubectlToolName + getExecutableExtension(), kubectlToolName, version); cachedToolpath = yield toolCache.cacheFile(kubectlDownloadPath, kubectlToolName + getExecutableExtension(), kubectlToolName, version);
} }
@@ -55,7 +55,7 @@ function deploy(kubectl, manifestFilePaths, deploymentStrategy) {
catch (e) { catch (e) {
core.debug("Unable to parse pods; Error: " + e); core.debug("Unable to parse pods; Error: " + e);
} }
annotateResources(deployedManifestFiles, kubectl, resourceTypes, allPods); annotateAndLabelResources(deployedManifestFiles, kubectl, resourceTypes, allPods);
}); });
} }
exports.deploy = deploy; exports.deploy = deploy;
@@ -131,18 +131,33 @@ function checkManifestStability(kubectl, resources) {
yield KubernetesManifestUtility.checkManifestStability(kubectl, resources); yield KubernetesManifestUtility.checkManifestStability(kubectl, resources);
}); });
} }
function annotateResources(files, kubectl, resourceTypes, allPods) { 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 annotateResults = [];
annotateResults.push(utility_1.annotateNamespace(kubectl, TaskInputParameters.namespace)); const lastSuccessSha = utility_1.getLastSuccessfulRunSha(kubectl, TaskInputParameters.namespace, annotationKey);
annotateResults.push(kubectl.annotateFiles(files, models.workflowAnnotations, true)); 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 => { resourceTypes.forEach(resource => {
if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) { if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) {
utility_1.annotateChildPods(kubectl, resource.type, resource.name, allPods) utility_1.annotateChildPods(kubectl, resource.type, resource.name, annotationKeyValStr, allPods)
.forEach(execResult => annotateResults.push(execResult)); .forEach(execResult => annotateResults.push(execResult));
} }
}); });
utility_1.checkForErrors(annotateResults, true); utility_1.checkForErrors(annotateResults, true);
} }
function labelResources(files, kubectl, label) {
const labels = [`workflowFriendlyName=${utility_1.normaliseWorkflowStrLabel(process.env.GITHUB_WORKFLOW)}`, `workflow=${label}`];
utility_1.checkForErrors([kubectl.labelFiles(files, labels)], true);
}
function isCanaryDeploymentStrategy(deploymentStrategy) { function isCanaryDeploymentStrategy(deploymentStrategy) {
return deploymentStrategy != null && deploymentStrategy.toUpperCase() === canaryDeploymentHelper.CANARY_DEPLOYMENT_STRATEGY.toUpperCase(); return deploymentStrategy != null && deploymentStrategy.toUpperCase() === canaryDeploymentHelper.CANARY_DEPLOYMENT_STRATEGY.toUpperCase();
} }
@@ -71,7 +71,7 @@ exports.rejectBlueGreenIngress = rejectBlueGreenIngress;
function routeBlueGreenIngress(kubectl, nextLabel, serviceNameMap, ingressEntityList) { function routeBlueGreenIngress(kubectl, nextLabel, serviceNameMap, ingressEntityList) {
let newObjectsList = []; let newObjectsList = [];
if (!nextLabel) { if (!nextLabel) {
newObjectsList = newObjectsList.concat(ingressEntityList); newObjectsList = ingressEntityList.filter(ingress => isIngressRouted(ingress, serviceNameMap));
} }
else { else {
ingressEntityList.forEach((inputObject) => { ingressEntityList.forEach((inputObject) => {
@@ -174,9 +174,9 @@ exports.validateTrafficSplitsState = validateTrafficSplitsState;
function cleanupSMI(kubectl, serviceEntityList) { function cleanupSMI(kubectl, serviceEntityList) {
const deleteList = []; const deleteList = [];
serviceEntityList.forEach((serviceObject) => { 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.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 }); deleteList.push({ name: blue_green_helper_1.getBlueGreenResourceName(serviceObject.metadata.name, blue_green_helper_2.STABLE_SUFFIX), kind: serviceObject.kind });
deleteList.push({ name: blue_green_helper_1.getBlueGreenResourceName(serviceObject.metadata.name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX), kind: TRAFFIC_SPLIT_OBJECT });
}); });
// deleting all objects // deleting all objects
blue_green_helper_1.deleteObjects(kubectl, deleteList); blue_green_helper_1.deleteObjects(kubectl, deleteList);
@@ -4,7 +4,6 @@ exports.redirectTrafficToStableDeployment = exports.redirectTrafficToCanaryDeplo
const core = require("@actions/core"); const core = require("@actions/core");
const fs = require("fs"); const fs = require("fs");
const yaml = require("js-yaml"); const yaml = require("js-yaml");
const util = require("util");
const TaskInputParameters = require("../../input-parameters"); const TaskInputParameters = require("../../input-parameters");
const fileHelper = require("../files-helper"); const fileHelper = require("../files-helper");
const helper = require("../resource-object-utility"); const helper = require("../resource-object-utility");
@@ -167,32 +166,30 @@ function getTrafficSplitObject(kubectl, name, stableWeight, baselineWeight, cana
if (!trafficSplitAPIVersion) { if (!trafficSplitAPIVersion) {
trafficSplitAPIVersion = kubectlUtils.getTrafficSplitAPIVersion(kubectl); trafficSplitAPIVersion = kubectlUtils.getTrafficSplitAPIVersion(kubectl);
} }
const trafficSplitObjectJson = `{ return `{
"apiVersion": "${trafficSplitAPIVersion}", "apiVersion": "${trafficSplitAPIVersion}",
"kind": "TrafficSplit", "kind": "TrafficSplit",
"metadata": { "metadata": {
"name": "%s" "name": "${getTrafficSplitResourceName(name)}"
}, },
"spec": { "spec": {
"backends": [ "backends": [
{ {
"service": "%s", "service": "${canaryDeploymentHelper.getStableResourceName(name)}",
"weight": "%sm" "weight": "${stableWeight}"
}, },
{ {
"service": "%s", "service": "${canaryDeploymentHelper.getBaselineResourceName(name)}",
"weight": "%sm" "weight": "${baselineWeight}"
}, },
{ {
"service": "%s", "service": "${canaryDeploymentHelper.getCanaryResourceName(name)}",
"weight": "%sm" "weight": "${canaryWeight}"
} }
], ],
"service": "%s" "service": "${name}"
} }
}`; }`;
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) { function getTrafficSplitResourceName(name) {
return name + TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX; return name + TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX;
+142 -22
View File
@@ -1,9 +1,22 @@
"use strict"; "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 }); Object.defineProperty(exports, "__esModule", { value: true });
exports.getCurrentTime = exports.getRandomInt = exports.sleep = exports.annotateNamespace = exports.annotateChildPods = exports.checkForErrors = exports.isEqual = exports.getExecutableExtension = void 0; exports.getNormalizedPath = exports.isHttpUrl = exports.getCurrentTime = exports.getRandomInt = exports.sleep = exports.normaliseWorkflowStrLabel = exports.getDeploymentConfig = exports.annotateChildPods = exports.getWorkflowFilePath = exports.getLastSuccessfulRunSha = exports.checkForErrors = exports.isEqual = exports.getExecutableExtension = void 0;
const os = require("os"); const os = require("os");
const core = require("@actions/core"); const core = require("@actions/core");
const constants_1 = require("../constants"); 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() { function getExecutableExtension() {
if (os.type().match(/^Win/)) { if (os.type().match(/^Win/)) {
return '.exe'; return '.exe';
@@ -50,7 +63,63 @@ function checkForErrors(execResults, warnIfError) {
} }
} }
exports.checkForErrors = checkForErrors; exports.checkForErrors = checkForErrors;
function annotateChildPods(kubectl, resourceType, resourceName, allPods) { 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 = []; const commandExecutionResults = [];
let owner = resourceName; let owner = resourceName;
if (resourceType.toLowerCase().indexOf('deployment') > -1) { if (resourceType.toLowerCase().indexOf('deployment') > -1) {
@@ -62,7 +131,7 @@ function annotateChildPods(kubectl, resourceType, resourceName, allPods) {
if (owners) { if (owners) {
for (let ownerRef of owners) { for (let ownerRef of owners) {
if (ownerRef.name === owner) { if (ownerRef.name === owner) {
commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, constants_1.workflowAnnotations, true)); commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, annotationKeyValStr));
break; break;
} }
} }
@@ -72,28 +141,40 @@ function annotateChildPods(kubectl, resourceType, resourceName, allPods) {
return commandExecutionResults; return commandExecutionResults;
} }
exports.annotateChildPods = annotateChildPods; exports.annotateChildPods = annotateChildPods;
function annotateNamespace(kubectl, namespaceName) { function getDeploymentConfig() {
const result = kubectl.getResource('namespace', namespaceName); return __awaiter(this, void 0, void 0, function* () {
if (!result) { let helmChartPaths = (process.env.HELM_CHART_PATHS && process.env.HELM_CHART_PATHS.split(';').filter(path => path != "")) || [];
return { code: 1, stderr: 'Failed to get resource' }; helmChartPaths = helmChartPaths.map(helmchart => getNormalizedPath(helmchart.trim()));
} let inputManifestFiles = inputParams.manifests || [];
else { if (!helmChartPaths || helmChartPaths.length == 0) {
if (result.stderr) { inputManifestFiles = inputManifestFiles.map(manifestFile => getNormalizedPath(manifestFile));
return result;
} }
else if (result.stdout) { const imageNames = inputParams.containers || [];
const annotationsSet = JSON.parse(result.stdout).metadata.annotations; let imageDockerfilePathMap = {};
if (annotationsSet && annotationsSet.runUri) { //Fetching from image label if available
if (annotationsSet.runUri.indexOf(process.env['GITHUB_REPOSITORY']) == -1) { for (const image of imageNames) {
core.debug(`Skipping 'annotate namespace' as namespace annotated by other workflow`); try {
return { code: 0, stdout: '' }; imageDockerfilePathMap[image] = yield getDockerfilePath(image);
} }
catch (ex) {
core.warning(`Failed to get dockerfile path for image ${image.toString()} | ` + ex);
} }
return kubectl.annotate('namespace', namespaceName, constants_1.workflowAnnotations, true);
} }
} const deploymentConfig = {
manifestFilePaths: inputManifestFiles,
helmChartFilePaths: helmChartPaths,
dockerfilePaths: imageDockerfilePathMap
};
return Promise.resolve(deploymentConfig);
});
} }
exports.annotateNamespace = annotateNamespace; exports.getDeploymentConfig = getDeploymentConfig;
function normaliseWorkflowStrLabel(workflowName) {
workflowName = workflowName.startsWith('.github/workflows/') ?
workflowName.replace(".github/workflows/", "") : workflowName;
return workflowName.replace(/ /g, "_");
}
exports.normaliseWorkflowStrLabel = normaliseWorkflowStrLabel;
function sleep(timeout) { function sleep(timeout) {
return new Promise(resolve => setTimeout(resolve, timeout)); return new Promise(resolve => setTimeout(resolve, timeout));
} }
@@ -106,3 +187,42 @@ function getCurrentTime() {
return new Date().getTime(); return new Date().getTime();
} }
exports.getCurrentTime = getCurrentTime; 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 Promise.resolve(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;
+10547 -34
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -8,16 +8,16 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@actions/tool-cache": "^1.0.0", "@actions/tool-cache": "1.1.2",
"@actions/io": "^1.0.0", "@actions/io": "^1.0.0",
"@actions/core": "^1.0.0", "@actions/core": "^1.2.6",
"@actions/exec": "^1.0.0", "@actions/exec": "^1.0.0",
"js-yaml": "3.13.1" "js-yaml": "3.13.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^12.0.10", "@types/node": "^12.0.10",
"jest": "^25.0.0", "jest": "^26.0.0",
"@types/jest": "^25.2.2", "@types/jest": "^26.0.0",
"ts-jest": "^25.5.1", "ts-jest": "^25.5.1",
"typescript": "3.9.5" "typescript": "3.9.5"
} }
+28 -12
View File
@@ -1,4 +1,5 @@
'use strict'; 'use strict';
import { DeploymentConfig } from "./utilities/utility";
export class KubernetesWorkload { export class KubernetesWorkload {
public static pod: string = 'Pod'; public static pod: string = 'Pod';
@@ -25,15 +26,30 @@ export const deploymentTypes: string[] = ['deployment', 'replicaset', 'daemonset
export const workloadTypes: string[] = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob']; export const workloadTypes: string[] = ['deployment', 'replicaset', 'daemonset', 'pod', 'statefulset', 'job', 'cronjob'];
export const workloadTypesWithRolloutStatus: string[] = ['deployment', 'daemonset', 'statefulset']; export const workloadTypesWithRolloutStatus: string[] = ['deployment', 'daemonset', 'statefulset'];
export const workflowAnnotations = [ export function getWorkflowAnnotationsJson(lastSuccessRunSha: string, workflowFilePath: string, deploymentConfig: DeploymentConfig): string {
`run=${process.env['GITHUB_RUN_ID']}`, let annotationObject: any = {};
`repository=${process.env['GITHUB_REPOSITORY']}`, annotationObject["run"] = process.env.GITHUB_RUN_ID;
`workflow=${process.env['GITHUB_WORKFLOW']}`, annotationObject["repository"] = process.env.GITHUB_REPOSITORY;
`jobName=${process.env['GITHUB_JOB']}`, annotationObject["workflow"] = process.env.GITHUB_WORKFLOW;
`createdBy=${process.env['GITHUB_ACTOR']}`, annotationObject["workflowFileName"] = workflowFilePath.replace(".github/workflows/", "");
`runUri=https://github.com/${process.env['GITHUB_REPOSITORY']}/actions/runs/${process.env['GITHUB_RUN_ID']}`, annotationObject["jobName"] = process.env.GITHUB_JOB;
`commit=${process.env['GITHUB_SHA']}`, annotationObject["createdBy"] = process.env.GITHUB_ACTOR;
`branch=${process.env['GITHUB_REF']}`, annotationObject["runUri"] = `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
`deployTimestamp=${Date.now()}`, annotationObject["commit"] = process.env.GITHUB_SHA;
`provider=GitHub` 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);
}
export function getWorkflowAnnotationKeyLabel(workflowFilePath: string): string {
const hashKey = require("crypto").createHash("MD5")
.update(`${process.env.GITHUB_REPOSITORY}/${workflowFilePath}`)
.digest("hex");
return `githubWorkflow_${hashKey}`;
}
+33
View File
@@ -0,0 +1,33 @@
import { ToolRunner, IExecOptions, IExecSyncResult } from "./utilities/tool-runner";
export class DockerExec {
private dockerPath: string;
constructor(dockerPath: string) {
this.dockerPath = dockerPath;
};
public pull(image: string, args: string[], silent?: boolean) {
args = ['pull', image, ...args];
let result: IExecSyncResult = this.execute(args, silent);
if (result.stderr != '' && result.code != 0) {
throw new Error(`docker images pull failed with: ${result.error}`);
}
}
public inspect(image: string, args: string[], silent?: boolean): any {
args = ['inspect', image, ...args];
let result: IExecSyncResult = this.execute(args, silent);
if (result.stderr != '' && result.code != 0) {
throw new Error(`docker inspect call failed with: ${result.error}`);
}
return result.stdout;
}
private execute(args: string[], silent?: boolean) {
const command = new ToolRunner(this.dockerPath);
command.arg(args);
return command.execSync({ silent: !!silent } as IExecOptions);
}
}
+26
View File
@@ -0,0 +1,26 @@
import * as core from '@actions/core';
import { WebRequest, WebResponse, sendRequest } from "./utilities/httpClient";
export class GitHubClient {
constructor(repository: string, token: string) {
this._repository = repository;
this._token = token;
}
public async getWorkflows(): Promise<any> {
const getWorkflowFileNameUrl = `https://api.github.com/repos/${this._repository}/actions/workflows`;
const webRequest = new WebRequest();
webRequest.method = "GET";
webRequest.uri = getWorkflowFileNameUrl;
webRequest.headers = {
Authorization: `Bearer ${this._token}`
};
core.debug(`Getting workflows for repo: ${this._repository}`);
const response: WebResponse = await sendRequest(webRequest);
return Promise.resolve(response);
}
private _repository: string;
private _token: string;
}
+6 -1
View File
@@ -5,7 +5,7 @@ import * as core from '@actions/core';
export let namespace: string = core.getInput('namespace'); export let namespace: string = core.getInput('namespace');
export const containers: string[] = core.getInput('images').split('\n'); export const containers: string[] = core.getInput('images').split('\n');
export const imagePullSecrets: string[] = core.getInput('imagepullsecrets').split('\n').filter(secret => secret.trim().length > 0); export const imagePullSecrets: string[] = core.getInput('imagepullsecrets').split('\n').filter(secret => secret.trim().length > 0);
export const manifests = core.getInput('manifests').split('\n'); export const manifests = core.getInput('manifests').split(/[\n,;]+/).filter(manifest => manifest.trim().length > 0);
export const canaryPercentage: string = core.getInput('percentage'); export const canaryPercentage: string = core.getInput('percentage');
export const deploymentStrategy: string = core.getInput('strategy'); export const deploymentStrategy: string = core.getInput('strategy');
export const trafficSplitMethod: string = core.getInput('traffic-split-method'); export const trafficSplitMethod: string = core.getInput('traffic-split-method');
@@ -14,12 +14,17 @@ export const versionSwitchBuffer: string = core.getInput('version-switch-buffer'
export const baselineAndCanaryReplicas: string = core.getInput('baseline-and-canary-replicas'); export const baselineAndCanaryReplicas: string = core.getInput('baseline-and-canary-replicas');
export const args: string = core.getInput('arguments'); export const args: string = core.getInput('arguments');
export const forceDeployment: boolean = core.getInput('force').toLowerCase() == 'true'; export const forceDeployment: boolean = core.getInput('force').toLowerCase() == 'true';
export const githubToken = core.getInput("token");
if (!namespace) { if (!namespace) {
core.debug('Namespace was not supplied; using "default" namespace instead.'); core.debug('Namespace was not supplied; using "default" namespace instead.');
namespace = 'default'; namespace = 'default';
} }
if (!githubToken) {
core.error("'token' input is not supplied. Set it to a PAT/GITHUB_TOKEN");
}
try { try {
const pe = parseInt(canaryPercentage); const pe = parseInt(canaryPercentage);
if (pe < 0 || pe > 100) { if (pe < 0 || pe > 100) {
+14 -6
View File
@@ -50,18 +50,26 @@ export class Kubectl {
return newReplicaSet; return newReplicaSet;
} }
public annotate(resourceType: string, resourceName: string, annotations: string[], overwrite?: boolean): IExecSyncResult { public annotate(resourceType: string, resourceName: string, annotation: string): IExecSyncResult {
let args = ['annotate', resourceType, resourceName]; let args = ['annotate', resourceType, resourceName];
args = args.concat(annotations); args.push(annotation);
if (!!overwrite) { args.push(`--overwrite`); } args.push(`--overwrite`);
return this.execute(args); return this.execute(args);
} }
public annotateFiles(files: string | string[], annotations: string[], overwrite?: boolean): IExecSyncResult { public annotateFiles(files: string | string[], annotation: string): IExecSyncResult {
let args = ['annotate']; let args = ['annotate'];
args = args.concat(['-f', this.createInlineArray(files)]); args = args.concat(['-f', this.createInlineArray(files)]);
args = args.concat(annotations); args.push(annotation);
if (!!overwrite) { args.push(`--overwrite`); } args.push(`--overwrite`);
return this.execute(args);
}
public labelFiles(files: string | string[], labels: string[]): IExecSyncResult {
let args = ['label'];
args = args.concat(['-f', this.createInlineArray(files)]);
args = args.concat(labels);
args.push(`--overwrite`);
return this.execute(args); return this.execute(args);
} }
+8 -2
View File
@@ -42,7 +42,7 @@ async function installKubectl(version: string) {
function checkClusterContext() { function checkClusterContext() {
if (!process.env["KUBECONFIG"]) { if (!process.env["KUBECONFIG"]) {
throw new Error('Cluster context not set. Use k8ssetcontext action to set cluster context'); core.warning('KUBECONFIG env is not explicitly set. Ensure cluster context is set by using k8s-set-context / aks-set-context action.');
} }
} }
@@ -59,7 +59,13 @@ export async function run() {
namespace = 'default'; namespace = 'default';
} }
let action = core.getInput('action'); let action = core.getInput('action');
let manifests = manifestsInput.split('\n'); let manifests = manifestsInput.split(/[\n,;]+/).filter(manifest => manifest.trim().length > 0);
if (manifests.length > 0) {
manifests = manifests.map(manifest => {
return manifest.trim();
});
}
if (action === 'deploy') { if (action === 'deploy') {
let strategy = core.getInput('strategy'); let strategy = core.getInput('strategy');
+114
View File
@@ -0,0 +1,114 @@
// Taken from https://github.com/Azure/aks-set-context/blob/master/src/client.ts
import util = require("util");
import fs = require('fs');
import httpClient = require("typed-rest-client/HttpClient");
import * as core from '@actions/core';
var httpCallbackClient = new httpClient.HttpClient('GITHUB_RUNNER', null, {});
export enum StatusCodes {
OK = 200,
CREATED = 201,
ACCEPTED = 202,
UNAUTHORIZED = 401,
NOT_FOUND = 404,
INTERNAL_SERVER_ERROR = 500,
SERVICE_UNAVAILABLE = 503
}
export class WebRequest {
public method: string;
public uri: string;
// body can be string or ReadableStream
public body: string | NodeJS.ReadableStream;
public headers: any;
}
export class WebResponse {
public statusCode: number;
public statusMessage: string;
public headers: any;
public body: any;
}
export class WebRequestOptions {
public retriableErrorCodes?: string[];
public retryCount?: number;
public retryIntervalInSeconds?: number;
public retriableStatusCodes?: number[];
public retryRequestTimedout?: boolean;
}
export async function sendRequest(request: WebRequest, options?: WebRequestOptions): Promise<WebResponse> {
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: number = retryIntervalInSeconds;
while (true) {
try {
if (request.body && typeof (request.body) !== 'string' && !request.body["readable"]) {
request.body = fs.createReadStream(request.body["path"]);
}
let response: WebResponse = await 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));
await 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));
await sleepFor(timeToWait);
timeToWait = timeToWait * retryIntervalInSeconds + retryIntervalInSeconds;
}
else {
if (error.code) {
core.debug("error code =" + error.code);
}
throw error;
}
}
}
}
export function sleepFor(sleepDurationInSeconds: number): Promise<any> {
return new Promise((resolve, reject) => {
setTimeout(resolve, sleepDurationInSeconds * 1000);
});
}
async function sendRequestInternal(request: WebRequest): Promise<WebResponse> {
core.debug(util.format("[%s]%s", request.method, request.uri));
var response: httpClient.HttpClientResponse = await httpCallbackClient.request(request.method, request.uri, request.body, request.headers);
return await toWebResponse(response);
}
async function toWebResponse(response: httpClient.HttpClientResponse): Promise<WebResponse> {
var res = new WebResponse();
if (response) {
res.statusCode = response.message.statusCode;
res.statusMessage = response.message.statusMessage;
res.headers = response.message.headers;
var body = await 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;
}
+20 -6
View File
@@ -6,6 +6,7 @@ import * as toolCache from '@actions/tool-cache';
import * as util from 'util'; import * as util from 'util';
import { Kubectl } from '../kubectl-object-model'; import { Kubectl } from '../kubectl-object-model';
import { StatusCodes } from "./httpClient"
const kubectlToolName = 'kubectl'; const kubectlToolName = 'kubectl';
const stableKubectlVersion = 'v1.15.0'; const stableKubectlVersion = 'v1.15.0';
@@ -19,17 +20,25 @@ function getExecutableExtension(): string {
return ''; return '';
} }
export function getkubectlDownloadURL(version: string): string { function getKubectlArch(): string {
let arch = os.arch();
if (arch === 'x64') {
return 'amd64';
}
return arch;
}
export function getkubectlDownloadURL(version: string, arch: string): string {
switch (os.type()) { switch (os.type()) {
case 'Linux': case 'Linux':
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/linux/amd64/kubectl', version); return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/linux/%s/kubectl', version, arch);
case 'Darwin': case 'Darwin':
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/darwin/amd64/kubectl', version); return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/darwin/%s/kubectl', version, arch);
case 'Windows_NT': case 'Windows_NT':
default: default:
return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/windows/amd64/kubectl.exe', version); return util.format('https://storage.googleapis.com/kubernetes-release/release/%s/bin/windows/%s/kubectl.exe', version, arch);
} }
} }
@@ -51,11 +60,16 @@ export async function getStableKubectlVersion(): Promise<string> {
export async function downloadKubectl(version: string): Promise<string> { export async function downloadKubectl(version: string): Promise<string> {
let cachedToolpath = toolCache.find(kubectlToolName, version); let cachedToolpath = toolCache.find(kubectlToolName, version);
let kubectlDownloadPath = ''; let kubectlDownloadPath = '';
let arch = getKubectlArch();
if (!cachedToolpath) { if (!cachedToolpath) {
try { try {
kubectlDownloadPath = await toolCache.downloadTool(getkubectlDownloadURL(version)); kubectlDownloadPath = await toolCache.downloadTool(getkubectlDownloadURL(version, arch));
} catch (exception) { } catch (exception) {
throw new Error('DownloadKubectlFailed'); if (exception instanceof toolCache.HTTPError && exception.httpStatusCode === StatusCodes.NOT_FOUND) {
throw new Error(util.format("Kubectl '%s' for '%s' arch not found.", version, arch));
} else {
throw new Error('DownloadKubectlFailed');
}
} }
cachedToolpath = await toolCache.cacheFile(kubectlDownloadPath, kubectlToolName + getExecutableExtension(), kubectlToolName, version); cachedToolpath = await toolCache.cacheFile(kubectlDownloadPath, kubectlToolName + getExecutableExtension(), kubectlToolName, version);
@@ -17,7 +17,7 @@ import { IExecSyncResult } from '../../utilities/tool-runner';
import { deployPodCanary } from './pod-canary-deployment-helper'; import { deployPodCanary } from './pod-canary-deployment-helper';
import { deploySMICanary } from './smi-canary-deployment-helper'; import { deploySMICanary } from './smi-canary-deployment-helper';
import { checkForErrors, annotateChildPods, annotateNamespace } from "../utility"; import { checkForErrors, annotateChildPods, getWorkflowFilePath, getLastSuccessfulRunSha, DeploymentConfig, getDeploymentConfig, normaliseWorkflowStrLabel } from "../utility";
import { isBlueGreenDeploymentStrategy, isIngressRoute, isSMIRoute, routeBlueGreen } from './blue-green-helper'; import { isBlueGreenDeploymentStrategy, isIngressRoute, isSMIRoute, routeBlueGreen } from './blue-green-helper';
import { deployBlueGreenService } from './service-blue-green-helper'; import { deployBlueGreenService } from './service-blue-green-helper';
import { deployBlueGreenIngress } from './ingress-blue-green-helper'; import { deployBlueGreenIngress } from './ingress-blue-green-helper';
@@ -54,7 +54,7 @@ export async function deploy(kubectl: Kubectl, manifestFilePaths: string[], depl
core.debug("Unable to parse pods; Error: " + e); core.debug("Unable to parse pods; Error: " + e);
} }
annotateResources(deployedManifestFiles, kubectl, resourceTypes, allPods); annotateAndLabelResources(deployedManifestFiles, kubectl, resourceTypes, allPods);
} }
export function getManifestFiles(manifestFilePaths: string[]): string[] { export function getManifestFiles(manifestFilePaths: string[]): string[] {
@@ -128,19 +128,34 @@ async function checkManifestStability(kubectl: Kubectl, resources: Resource[]):
await KubernetesManifestUtility.checkManifestStability(kubectl, resources); await KubernetesManifestUtility.checkManifestStability(kubectl, resources);
} }
function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any) { async function annotateAndLabelResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any) {
const workflowFilePath = await getWorkflowFilePath(TaskInputParameters.githubToken);
const deploymentConfig = await getDeploymentConfig();
const annotationKeyLabel = models.getWorkflowAnnotationKeyLabel(workflowFilePath);
annotateResources(files, kubectl, resourceTypes, allPods, annotationKeyLabel, workflowFilePath, deploymentConfig);
labelResources(files, kubectl, annotationKeyLabel);
}
function annotateResources(files: string[], kubectl: Kubectl, resourceTypes: Resource[], allPods: any, annotationKey: string, workflowFilePath: string, deploymentConfig: DeploymentConfig) {
const annotateResults: IExecSyncResult[] = []; const annotateResults: IExecSyncResult[] = [];
annotateResults.push(annotateNamespace(kubectl, TaskInputParameters.namespace)); const lastSuccessSha = getLastSuccessfulRunSha(kubectl, TaskInputParameters.namespace, annotationKey);
annotateResults.push(kubectl.annotateFiles(files, models.workflowAnnotations, true)); 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 => { resourceTypes.forEach(resource => {
if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) { if (resource.type.toUpperCase() !== models.KubernetesWorkload.pod.toUpperCase()) {
annotateChildPods(kubectl, resource.type, resource.name, allPods) annotateChildPods(kubectl, resource.type, resource.name, annotationKeyValStr, allPods)
.forEach(execResult => annotateResults.push(execResult)); .forEach(execResult => annotateResults.push(execResult));
} }
}); });
checkForErrors(annotateResults, true); checkForErrors(annotateResults, true);
} }
function labelResources(files: string[], kubectl: Kubectl, label: string) {
const labels = [`workflowFriendlyName=${normaliseWorkflowStrLabel(process.env.GITHUB_WORKFLOW)}`, `workflow=${label}`];
checkForErrors([kubectl.labelFiles(files, labels)], true);
}
function isCanaryDeploymentStrategy(deploymentStrategy: string): boolean { function isCanaryDeploymentStrategy(deploymentStrategy: string): boolean {
return deploymentStrategy != null && deploymentStrategy.toUpperCase() === canaryDeploymentHelper.CANARY_DEPLOYMENT_STRATEGY.toUpperCase(); return deploymentStrategy != null && deploymentStrategy.toUpperCase() === canaryDeploymentHelper.CANARY_DEPLOYMENT_STRATEGY.toUpperCase();
} }
@@ -68,7 +68,7 @@ export async function rejectBlueGreenIngress(kubectl: Kubectl, filePaths: string
export function routeBlueGreenIngress(kubectl: Kubectl, nextLabel: string, serviceNameMap: Map<string, string>, ingressEntityList: any[]) { export function routeBlueGreenIngress(kubectl: Kubectl, nextLabel: string, serviceNameMap: Map<string, string>, ingressEntityList: any[]) {
let newObjectsList = []; let newObjectsList = [];
if (!nextLabel) { if (!nextLabel) {
newObjectsList = newObjectsList.concat(ingressEntityList); newObjectsList = ingressEntityList.filter(ingress => isIngressRouted(ingress, serviceNameMap));
} else { } else {
ingressEntityList.forEach((inputObject) => { ingressEntityList.forEach((inputObject) => {
if (isIngressRouted(inputObject, serviceNameMap)) { if (isIngressRouted(inputObject, serviceNameMap)) {
@@ -177,9 +177,9 @@ export function validateTrafficSplitsState(kubectl: Kubectl, serviceEntityList:
export function cleanupSMI(kubectl: Kubectl, serviceEntityList: any[]) { export function cleanupSMI(kubectl: Kubectl, serviceEntityList: any[]) {
const deleteList = []; const deleteList = [];
serviceEntityList.forEach((serviceObject) => { serviceEntityList.forEach((serviceObject) => {
deleteList.push({ name: getBlueGreenResourceName(serviceObject.metadata.name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX), kind: TRAFFIC_SPLIT_OBJECT });
deleteList.push({name: getBlueGreenResourceName(serviceObject.metadata.name, GREEN_SUFFIX), kind: serviceObject.kind}); deleteList.push({name: getBlueGreenResourceName(serviceObject.metadata.name, GREEN_SUFFIX), kind: serviceObject.kind});
deleteList.push({name: getBlueGreenResourceName(serviceObject.metadata.name, STABLE_SUFFIX), kind: serviceObject.kind}); deleteList.push({name: getBlueGreenResourceName(serviceObject.metadata.name, STABLE_SUFFIX), kind: serviceObject.kind});
deleteList.push({name: getBlueGreenResourceName(serviceObject.metadata.name, TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX), kind: TRAFFIC_SPLIT_OBJECT});
}); });
// deleting all objects // deleting all objects
@@ -186,33 +186,31 @@ function getTrafficSplitObject(kubectl: Kubectl, name: string, stableWeight: num
if (!trafficSplitAPIVersion) { if (!trafficSplitAPIVersion) {
trafficSplitAPIVersion = kubectlUtils.getTrafficSplitAPIVersion(kubectl); trafficSplitAPIVersion = kubectlUtils.getTrafficSplitAPIVersion(kubectl);
} }
const trafficSplitObjectJson = `{
return `{
"apiVersion": "${trafficSplitAPIVersion}", "apiVersion": "${trafficSplitAPIVersion}",
"kind": "TrafficSplit", "kind": "TrafficSplit",
"metadata": { "metadata": {
"name": "%s" "name": "${getTrafficSplitResourceName(name)}"
}, },
"spec": { "spec": {
"backends": [ "backends": [
{ {
"service": "%s", "service": "${canaryDeploymentHelper.getStableResourceName(name)}",
"weight": "%sm" "weight": "${stableWeight}"
}, },
{ {
"service": "%s", "service": "${canaryDeploymentHelper.getBaselineResourceName(name)}",
"weight": "%sm" "weight": "${baselineWeight}"
}, },
{ {
"service": "%s", "service": "${canaryDeploymentHelper.getCanaryResourceName(name)}",
"weight": "%sm" "weight": "${canaryWeight}"
} }
], ],
"service": "%s" "service": "${name}"
} }
}`; }`;
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: string) { function getTrafficSplitResourceName(name: string) {
+136 -21
View File
@@ -2,7 +2,17 @@ import * as os from 'os';
import * as core from '@actions/core'; import * as core from '@actions/core';
import { IExecSyncResult } from './tool-runner'; import { IExecSyncResult } from './tool-runner';
import { Kubectl } from '../kubectl-object-model'; import { Kubectl } from '../kubectl-object-model';
import { workflowAnnotations } from '../constants'; import { GitHubClient } from '../githubClient';
import { StatusCodes } from "./httpClient";
import * as inputParams from "../input-parameters";
import { DockerExec } from '../docker-object-model';
import * as io from '@actions/io';
export interface DeploymentConfig {
manifestFilePaths: string[];
helmChartFilePaths: string[];
dockerfilePaths: any;
}
export function getExecutableExtension(): string { export function getExecutableExtension(): string {
if (os.type().match(/^Win/)) { if (os.type().match(/^Win/)) {
@@ -50,7 +60,61 @@ export function checkForErrors(execResults: IExecSyncResult[], warnIfError?: boo
} }
} }
export function annotateChildPods(kubectl: Kubectl, resourceType: string, resourceName: string, allPods): IExecSyncResult[] { export function getLastSuccessfulRunSha(kubectl: Kubectl, namespaceName: string, annotationKey: string): string {
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 '';
}
}
export async function getWorkflowFilePath(githubToken: string): Promise<string> {
let workflowFilePath = process.env.GITHUB_WORKFLOW;
if (!workflowFilePath.startsWith('.github/workflows/')) {
const githubClient = new GitHubClient(process.env.GITHUB_REPOSITORY, githubToken);
const response = await githubClient.getWorkflows();
if (response) {
if (response.statusCode == 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 != 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);
}
export function annotateChildPods(kubectl: Kubectl, resourceType: string, resourceName: string, annotationKeyValStr: string, allPods): IExecSyncResult[] {
const commandExecutionResults = []; const commandExecutionResults = [];
let owner = resourceName; let owner = resourceName;
if (resourceType.toLowerCase().indexOf('deployment') > -1) { if (resourceType.toLowerCase().indexOf('deployment') > -1) {
@@ -63,7 +127,7 @@ export function annotateChildPods(kubectl: Kubectl, resourceType: string, resour
if (owners) { if (owners) {
for (let ownerRef of owners) { for (let ownerRef of owners) {
if (ownerRef.name === owner) { if (ownerRef.name === owner) {
commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, workflowAnnotations, true)); commandExecutionResults.push(kubectl.annotate('pod', pod.metadata.name, annotationKeyValStr));
break; break;
} }
} }
@@ -74,27 +138,41 @@ export function annotateChildPods(kubectl: Kubectl, resourceType: string, resour
return commandExecutionResults; return commandExecutionResults;
} }
export function annotateNamespace(kubectl: Kubectl, namespaceName: string): IExecSyncResult { export async function getDeploymentConfig(): Promise<DeploymentConfig> {
const result = kubectl.getResource('namespace', namespaceName);
if (!result) {
return { code: 1, stderr: 'Failed to get resource' } as IExecSyncResult;
}
else {
if (result.stderr) {
return result;
}
else if (result.stdout) { let helmChartPaths: string[] = (process.env.HELM_CHART_PATHS && process.env.HELM_CHART_PATHS.split(';').filter(path => path != "")) || [];
const annotationsSet = JSON.parse(result.stdout).metadata.annotations; helmChartPaths = helmChartPaths.map(helmchart => getNormalizedPath(helmchart.trim()));
if (annotationsSet && annotationsSet.runUri) {
if (annotationsSet.runUri.indexOf(process.env['GITHUB_REPOSITORY']) == -1) { let inputManifestFiles: string[] = inputParams.manifests || [];
core.debug(`Skipping 'annotate namespace' as namespace annotated by other workflow`); if (!helmChartPaths || helmChartPaths.length == 0) {
return { code: 0, stdout: '' } as IExecSyncResult; inputManifestFiles = inputManifestFiles.map(manifestFile => getNormalizedPath(manifestFile));
} }
}
return kubectl.annotate('namespace', namespaceName, workflowAnnotations, true); const imageNames = inputParams.containers || [];
let imageDockerfilePathMap: { [id: string]: string; } = {};
//Fetching from image label if available
for (const image of imageNames) {
try {
imageDockerfilePathMap[image] = await getDockerfilePath(image);
}
catch (ex) {
core.warning(`Failed to get dockerfile path for image ${image.toString()} | ` + ex);
} }
} }
const deploymentConfig = <DeploymentConfig>{
manifestFilePaths: inputManifestFiles,
helmChartFilePaths: helmChartPaths,
dockerfilePaths: imageDockerfilePathMap
};
return Promise.resolve(deploymentConfig);
}
export function normaliseWorkflowStrLabel(workflowName: string): string {
workflowName = workflowName.startsWith('.github/workflows/') ?
workflowName.replace(".github/workflows/", "") : workflowName;
return workflowName.replace(/ /g, "_");
} }
export function sleep(timeout: number) { export function sleep(timeout: number) {
@@ -108,3 +186,40 @@ export function getRandomInt(max: number) {
export function getCurrentTime(): number { export function getCurrentTime(): number {
return new Date().getTime(); return new Date().getTime();
} }
async function checkDockerPath() {
let dockerPath = await io.which('docker', false);
if (!dockerPath) {
throw new Error('Docker is not installed.');
}
}
async function getDockerfilePath(image: any): Promise<string> {
let imageConfig: any, imageInspectResult: string;
var dockerExec: DockerExec = new DockerExec('docker');
await checkDockerPath();
dockerExec.pull(image, [], true);
imageInspectResult = dockerExec.inspect(image, [], true);
imageConfig = JSON.parse(imageInspectResult)[0];
const DOCKERFILE_PATH_LABEL_KEY = 'dockerfile-path';
let pathValue: string = '';
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 Promise.resolve(pathValue);
}
export function isHttpUrl(url: string) {
const HTTP_REGEX = /^https?:\/\/.*$/;
return HTTP_REGEX.test(url);
}
export function getNormalizedPath(pathValue: string) {
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;
}