Refactor entire project (#39)
- Use more modern TypeScript conventions - Use JavaScript Kubernetes Client - Add unit tests - Add integration tests - Add TypeScript compile verify workflow - Switch codeowners
This commit is contained in:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -1 +1 @@
|
|||||||
* @ds-ms
|
* @Azure/aks-atlanta
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: 'Issue: Bug Report / Feature Request'
|
name: "Issue: Bug Report / Feature Request"
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ""
|
||||||
labels: need-to-triage
|
labels: need-to-triage
|
||||||
assignees: 'shigupt202'
|
assignees: "@Azure/aks-atlanta"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
33
.github/workflows/TriggerIntegrationTests.sh
vendored
33
.github/workflows/TriggerIntegrationTests.sh
vendored
@ -1,33 +0,0 @@
|
|||||||
token=$1
|
|
||||||
commit=$2
|
|
||||||
repository=$3
|
|
||||||
prNumber=$4
|
|
||||||
frombranch=$5
|
|
||||||
tobranch=$6
|
|
||||||
patUser=$7
|
|
||||||
|
|
||||||
getPayLoad() {
|
|
||||||
cat <<EOF
|
|
||||||
{
|
|
||||||
"event_type": "K8sSetContextPR",
|
|
||||||
"client_payload":
|
|
||||||
{
|
|
||||||
"action": "K8sSetContext",
|
|
||||||
"commit": "$commit",
|
|
||||||
"repository": "$repository",
|
|
||||||
"prNumber": "$prNumber",
|
|
||||||
"tobranch": "$tobranch",
|
|
||||||
"frombranch": "$frombranch"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
response=$(curl -u $patUser:$token -X POST https://api.github.com/repos/Azure/azure-actions-integration-tests/dispatches --data "$(getPayLoad)")
|
|
||||||
|
|
||||||
if [ "$response" == "" ]; then
|
|
||||||
echo "Integration tests triggered successfully"
|
|
||||||
else
|
|
||||||
echo "Triggering integration tests failed with: '$response'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
@ -1,9 +1,9 @@
|
|||||||
name: setting-default-labels
|
name: Setting Default Labels
|
||||||
|
|
||||||
# Controls when the action will run.
|
# Controls when the action will run.
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0/3 * * *"
|
- cron: "0 0/3 * * *"
|
||||||
|
|
||||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
jobs:
|
jobs:
|
||||||
@ -13,24 +13,23 @@ jobs:
|
|||||||
|
|
||||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/stale@v3
|
- uses: actions/stale@v3
|
||||||
name: Setting issue as idle
|
name: Setting Issue as Idle
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
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-message: "This issue is idle because it has been open for 14 days with no activity."
|
||||||
stale-issue-label: 'idle'
|
stale-issue-label: "idle"
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: -1
|
days-before-close: -1
|
||||||
operations-per-run: 100
|
operations-per-run: 100
|
||||||
exempt-issue-labels: 'backlog'
|
exempt-issue-labels: "backlog"
|
||||||
|
|
||||||
- uses: actions/stale@v3
|
- uses: actions/stale@v3
|
||||||
name: Setting PR as idle
|
name: Setting PR as Idle
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
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-message: "This PR is idle because it has been open for 14 days with no activity."
|
||||||
stale-pr-label: 'idle'
|
stale-pr-label: "idle"
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: -1
|
days-before-close: -1
|
||||||
operations-per-run: 100
|
operations-per-run: 100
|
154
.github/workflows/integration-tests.yml
vendored
154
.github/workflows/integration-tests.yml
vendored
@ -1,19 +1,143 @@
|
|||||||
name: "Trigger Integration tests"
|
name: Run Integration Tests
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- 'releases/*'
|
- "releases/*"
|
||||||
jobs:
|
push:
|
||||||
trigger-integration-tests:
|
branches:
|
||||||
name: Trigger Integration tests
|
- master
|
||||||
runs-on: ubuntu-latest
|
- "releases/*"
|
||||||
steps:
|
|
||||||
- name: Check out repository
|
jobs:
|
||||||
uses: actions/checkout@v2
|
kubeconfig-method-integration-test:
|
||||||
with:
|
runs-on: ubuntu-latest
|
||||||
path: IntegrationTests
|
steps:
|
||||||
|
- name: Checkout Source Code
|
||||||
- name: Trigger Test run
|
id: checkout-code
|
||||||
run: |
|
uses: actions/checkout@v2
|
||||||
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 }}
|
- name: Npm Install and Build
|
||||||
|
id: npm-build
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
- name: Set Context
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
method: kubeconfig
|
||||||
|
context: exp-scratch
|
||||||
|
kubeconfig: |
|
||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: fake-ca-file
|
||||||
|
server: https://1.2.3.4
|
||||||
|
name: development
|
||||||
|
- cluster:
|
||||||
|
insecure-skip-tls-verify: true
|
||||||
|
server: https://5.6.7.8
|
||||||
|
name: scratch
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: development
|
||||||
|
namespace: frontend
|
||||||
|
user: developer
|
||||||
|
name: dev-frontend
|
||||||
|
- context:
|
||||||
|
cluster: development
|
||||||
|
namespace: storage
|
||||||
|
user: developer
|
||||||
|
name: dev-storage
|
||||||
|
- context:
|
||||||
|
cluster: scratch
|
||||||
|
namespace: default
|
||||||
|
user: experimenter
|
||||||
|
name: exp-scratch
|
||||||
|
current-context: ""
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
||||||
|
users:
|
||||||
|
- name: developer
|
||||||
|
user:
|
||||||
|
client-certificate: fake-cert-file
|
||||||
|
client-key: fake-key-file
|
||||||
|
- name: experimenter
|
||||||
|
user:
|
||||||
|
password: some-password
|
||||||
|
username: exp
|
||||||
|
- name: Vertify Results
|
||||||
|
run: |
|
||||||
|
echo "$EXPECTED_KC" > /tmp/expected_kc.json
|
||||||
|
DIFF=$(diff <(jq -S -c . $KUBECONFIG) <(jq -S -c . /tmp/expected_kc.json))
|
||||||
|
if [ "$DIFF" != "" ]; then exit 1; else echo -e "Kubeconfig matches expected"; fi
|
||||||
|
env:
|
||||||
|
EXPECTED_KC: |
|
||||||
|
{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"clusters": [
|
||||||
|
{
|
||||||
|
"cluster": {
|
||||||
|
"certificate-authority": "fake-ca-file",
|
||||||
|
"insecure-skip-tls-verify": false,
|
||||||
|
"server": "https://1.2.3.4"
|
||||||
|
},
|
||||||
|
"name": "development"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cluster": {
|
||||||
|
"insecure-skip-tls-verify": true,
|
||||||
|
"server": "https://5.6.7.8"
|
||||||
|
},
|
||||||
|
"name": "scratch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contexts": [
|
||||||
|
{
|
||||||
|
"context": {
|
||||||
|
"cluster": "development",
|
||||||
|
"name": "dev-frontend",
|
||||||
|
"namespace": "frontend",
|
||||||
|
"user": "developer"
|
||||||
|
},
|
||||||
|
"name": "dev-frontend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": {
|
||||||
|
"cluster": "development",
|
||||||
|
"name": "dev-storage",
|
||||||
|
"namespace": "storage",
|
||||||
|
"user": "developer"
|
||||||
|
},
|
||||||
|
"name": "dev-storage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": {
|
||||||
|
"cluster": "scratch",
|
||||||
|
"name": "exp-scratch",
|
||||||
|
"namespace": "default",
|
||||||
|
"user": "experimenter"
|
||||||
|
},
|
||||||
|
"name": "exp-scratch"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"current-context": "exp-scratch",
|
||||||
|
"kind": "Config",
|
||||||
|
"preferences": {
|
||||||
|
},
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "developer",
|
||||||
|
"user": {
|
||||||
|
"client-certificate": "fake-cert-file",
|
||||||
|
"client-key": "fake-key-file"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "experimenter",
|
||||||
|
"user": {
|
||||||
|
"password": "some-password",
|
||||||
|
"username": "exp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
41
.github/workflows/ts-build-check.yml
vendored
Normal file
41
.github/workflows/ts-build-check.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
name: TypeScript Build Check
|
||||||
|
|
||||||
|
on: pull_request
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ts-build-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Pull Request
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{github.event.pull_request.head.ref}}
|
||||||
|
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||||
|
path: original-pr
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
- name: Clone and Build Pull Request
|
||||||
|
run: |
|
||||||
|
cp $GITHUB_WORKSPACE/original-pr/ $GITHUB_WORKSPACE/built-pr -r
|
||||||
|
cd $GITHUB_WORKSPACE/built-pr/
|
||||||
|
npm i
|
||||||
|
npm run build
|
||||||
|
- name: Compare Built Directories
|
||||||
|
id: diff
|
||||||
|
run: |
|
||||||
|
DIFF=$(diff $GITHUB_WORKSPACE/original-pr/lib $GITHUB_WORKSPACE/built-pr/lib -rqiEZbwBd)
|
||||||
|
if [ "$DIFF" != "" ]; then exit 1; else echo -e "PR contains up-to-date compiled JavaScript."; fi
|
||||||
|
- name: Comment Unbuilt TypeScript
|
||||||
|
if: failure() && steps.diff.outcome == 'failure'
|
||||||
|
uses: actions/github-script@v2
|
||||||
|
with:
|
||||||
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
|
script: |
|
||||||
|
github.issues.createComment({
|
||||||
|
issue_number: ${{ github.event.number }},
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: 'Please compile the TypeScript code with `npm run build`. The compiled JavaScript is not up-to-date.'
|
||||||
|
})
|
21
.github/workflows/unit-tests.yml
vendored
21
.github/workflows/unit-tests.yml
vendored
@ -1,21 +1,20 @@
|
|||||||
name: "Run L0 tests."
|
name: Run Unit Tests
|
||||||
on: # rebuild any PRs and main branch changes
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- 'releases/*'
|
- "releases/*"
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- 'releases/*'
|
- "releases/*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build: # make sure build/ci works properly
|
unit-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
- name: Run Unit Tests
|
||||||
- name: Run L0 tests.
|
run: |
|
||||||
run: |
|
npm install
|
||||||
npm install
|
npm test
|
||||||
npm test
|
|
||||||
|
97
README.md
97
README.md
@ -1,86 +1,38 @@
|
|||||||
# Kubernetes set context
|
# Kubernetes set context
|
||||||
|
|
||||||
This action can be used to set cluster context before other actions like [`azure/k8s-deploy`](https://github.com/Azure/k8s-deploy/tree/master), [`azure/k8s-create-secret`](https://github.com/Azure/k8s-create-secret/tree/master) or any kubectl commands (in script) can be run subsequently in the workflow.
|
This action can be used to set cluster context before other actions like [`azure/k8s-deploy`](https://github.com/Azure/k8s-deploy/tree/master) and [`azure/k8s-create-secret`](https://github.com/Azure/k8s-create-secret/tree/master). It should also be used before `kubectl` commands (in script) are run subsequently in the workflow.
|
||||||
|
|
||||||
It is a requirement to use [`azure/login`](https://github.com/Azure/login/tree/master) in your workflow before using this action.
|
It is a requirement to use [`azure/login`](https://github.com/Azure/login/tree/master) in your workflow before using this action when using the `service-account` or `service-principal` methods.
|
||||||
|
|
||||||
There are three approaches for specifying the deployment target:
|
There are three approaches for specifying the deployment target:
|
||||||
|
|
||||||
- Kubeconfig file provided as input to the action
|
- Kubeconfig file provided as input to the action
|
||||||
- Service account approach where the secret associated with the service account is provided as input to the action
|
- Service account approach where the secret associated with the service account is provided as input to the action
|
||||||
- Service principal approach(only applicable for arc cluster) where service principal provided with 'creds' is used as input to action
|
- Service principal approach (only applicable for arc cluster) where service principal provided with 'creds' is used as input to action
|
||||||
|
|
||||||
If inputs related to all these approaches are provided, kubeconfig approach related inputs are given precedence.
|
In all these approaches it is recommended to store these contents (kubeconfig file content or secret content) in a [secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets/).
|
||||||
|
|
||||||
In all these approaches it is recommended to store these contents (kubeconfig file content or secret content) in a [secret](https://developer.github.com/actions/managing-workflows/storing-secrets/) which could be referenced later in the action.
|
Refer to the [action metadata file](./action.yml) for details about inputs. Note that different inputs are required for different method and cluster types. Use the below examples as a reference.
|
||||||
|
|
||||||
## Action inputs
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Action inputs</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tr>
|
|
||||||
<td><code>method</code><br/>Method</td>
|
|
||||||
<td>(Optional) Acceptable values: kubeconfig/service-account/service-principal. Default value: kubeconfig</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>kubeconfig</code><br/>Kubectl config</td>
|
|
||||||
<td>(Relevant for kubeconfig approach) Contents of the configuration file to be used with kubectl (e.g. can be pulled from a secret)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>context</code><br/>Context</td>
|
|
||||||
<td>(Relevant for kubeconfig approach) Context to be used within the provided kubeconfig file</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>k8s-url</code><br/>API server URL</td>
|
|
||||||
<td>(Relevant for service account approach) API Server URL for the K8s cluster</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>k8s-secret</code><br/>Secret</td>
|
|
||||||
<td>(Relevant for service account approach) Secret associated with the service account to be used for deployments</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>cluster-type</code><br/>Type of cluster</td>
|
|
||||||
<td>Type of cluster. Acceptable values: generic/arc</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>cluster-name</code><br/>Name of arc cluster</td>
|
|
||||||
<td>Name of Azure Arc enabled Kubernetes cluster. Required only if cluster-type is 'arc'.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>resource-group</code><br/>resource group</td>
|
|
||||||
<td>Resource group containing the Azure Arc enabled Kubernetes cluster. Required only if cluster-type is 'arc'.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>token</code><br/>Service account token</td>
|
|
||||||
<td>Applicable for 'service-account' method.</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## Example usage
|
## Example usage
|
||||||
|
|
||||||
### Kubeconfig approach
|
### Kubeconfig approach
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: azure/k8s-set-context@v1
|
- uses: azure/k8s-set-context@v2
|
||||||
with:
|
with:
|
||||||
method: kubeconfig
|
method: kubeconfig
|
||||||
kubeconfig: <your kubeconfig> # Use secret (https://developer.github.com/actions/managing-workflows/storing-secrets/)
|
kubeconfig: <your kubeconfig>
|
||||||
context: <context name> #If left unspecified, current-context from kubeconfig is used as default
|
context: <context name> # current-context from kubeconfig is used as default
|
||||||
id: setcontext
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Please note** that the input requires the _contents_ of the kubeconfig file, and not its path.
|
**Please note** that the input requires the _contents_ of the kubeconfig file, and not its path.
|
||||||
|
|
||||||
Following are the ways to fetch kubeconfig file onto your local development machine so that the same can be used in the action input shown above:
|
Following are the ways to fetch kubeconfig file onto your local development machine so that the same can be used in the action input shown above.
|
||||||
|
|
||||||
#### For Azure Kubernetes Service cluster
|
#### Azure Kubernetes Service cluster
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
az aks get-credentials --name
|
az aks get-credentials --name
|
||||||
--resource-group
|
--resource-group
|
||||||
[--admin]
|
[--admin]
|
||||||
@ -91,65 +43,60 @@ az aks get-credentials --name
|
|||||||
|
|
||||||
Further details can be found in [az aks get-credentials documentation](https://docs.microsoft.com/en-us/cli/azure/aks?view=azure-cli-latest#az-aks-get-credentials).
|
Further details can be found in [az aks get-credentials documentation](https://docs.microsoft.com/en-us/cli/azure/aks?view=azure-cli-latest#az-aks-get-credentials).
|
||||||
|
|
||||||
#### For any generic Kubernetes cluster
|
#### Generic Kubernetes cluster
|
||||||
|
|
||||||
Please refer to documentation on fetching [kubeconfig for any generic K8s cluster](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/)
|
Please refer to documentation on fetching [kubeconfig for any generic K8s cluster](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/)
|
||||||
|
|
||||||
### Service account approach
|
### Service account approach
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: azure/k8s-set-context@v1
|
- uses: azure/k8s-set-context@v2
|
||||||
with:
|
with:
|
||||||
method: service-account
|
method: service-account
|
||||||
k8s-url: <URL of the clsuter's API server>
|
k8s-url: <URL of the cluster's API server>
|
||||||
k8s-secret: <secret associated with the service account>
|
k8s-secret: <secret associated with the service account>
|
||||||
id: setcontext
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For fetching Server URL, execute the following command on your shell:
|
For fetching Server URL, execute the following command on your shell:
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
kubectl config view --minify -o 'jsonpath={.clusters[0].cluster.server}'
|
kubectl config view --minify -o 'jsonpath={.clusters[0].cluster.server}'
|
||||||
```
|
```
|
||||||
|
|
||||||
For fetching Secret object required to connect and authenticate with the cluster, the following sequence of commands need to be run:
|
For fetching Secret object required to connect and authenticate with the cluster, the following sequence of commands need to be run:
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
kubectl get serviceAccounts <service-account-name> -n <namespace> -o 'jsonpath={.secrets[*].name}'
|
kubectl get serviceAccounts <service-account-name> -n <namespace> -o 'jsonpath={.secrets[*].name}'
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
kubectl get secret <service-account-secret-name> -n <namespace> -o yaml
|
kubectl get secret <service-account-secret-name> -n <namespace> -o yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service account approach for arc cluster
|
### Service account approach for arc cluster
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: azure/k8s-set-context@v1
|
- uses: azure/k8s-set-context@v2
|
||||||
with:
|
with:
|
||||||
method: service-account
|
method: service-account
|
||||||
cluster-type: 'arc'
|
cluster-type: arc
|
||||||
cluster-name: <cluster-name>
|
cluster-name: <cluster-name>
|
||||||
resource-group: <resource-group>
|
resource-group: <resource-group>
|
||||||
token: '${{ secrets.SA_TOKEN }}'
|
token: "${{ secrets.SA_TOKEN }}"
|
||||||
id: setcontext
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service principal approach for arc cluster
|
### Service principal approach for arc cluster
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: azure/k8s-set-context@v1
|
- uses: azure/k8s-set-context@v2
|
||||||
with:
|
with:
|
||||||
method: service-principal
|
method: service-principal
|
||||||
cluster-type: 'arc'
|
cluster-type: arc
|
||||||
cluster-name: <cluster-name>
|
cluster-name: <cluster-name>
|
||||||
resource-group: <resource-group>
|
resource-group: <resource-group>
|
||||||
id: setcontext
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
||||||
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
||||||
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
|
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
|
||||||
|
|
||||||
|
@ -1,328 +0,0 @@
|
|||||||
import * as run from '../src/login'
|
|
||||||
import * as arc from '../src/arc-login'
|
|
||||||
import * as os from 'os';
|
|
||||||
import * as io from '@actions/io';
|
|
||||||
import * as toolCache from '@actions/tool-cache';
|
|
||||||
import * as core from '@actions/core';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as jsyaml from 'js-yaml';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as child_process from 'child_process';
|
|
||||||
import * as exec from '@actions/exec';
|
|
||||||
var mockStatusCode;
|
|
||||||
const mockExecFn = jest.fn().mockImplementation(() => mockStatusCode);
|
|
||||||
jest.mock('@actions/exec/lib/toolrunner', () => {
|
|
||||||
return {
|
|
||||||
ToolRunner: jest.fn().mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
exec: mockExecFn
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('Testing all functions.', () => {
|
|
||||||
|
|
||||||
test('getArcKubeconfig() - checking execution of service-account scenario',async () =>{
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'service-account';
|
|
||||||
if (inputName == 'resource-group') return 'testrg';
|
|
||||||
if (inputName == 'cluster-name') return 'testcluster';
|
|
||||||
if (inputName == 'token') return 'token';
|
|
||||||
});
|
|
||||||
jest.spyOn(io, 'which').mockResolvedValue('az');
|
|
||||||
process.env['RUNNER_TEMP'] = 'tempDirPath';
|
|
||||||
jest.spyOn(Date, 'now').mockImplementation(() => 1234561234567);
|
|
||||||
const dummy_process=await child_process.spawn('ls',[]);
|
|
||||||
const dummy_sleep_promise = new Promise(resolve => setTimeout(resolve, 0));
|
|
||||||
jest.spyOn(child_process,'spawn').mockImplementation((commands, arg, option)=>{
|
|
||||||
return dummy_process;
|
|
||||||
});
|
|
||||||
jest.spyOn(arc,'sleep').mockImplementation((ms)=>{
|
|
||||||
return dummy_sleep_promise;
|
|
||||||
});
|
|
||||||
jest.spyOn(fs, 'chmodSync').mockImplementation();
|
|
||||||
jest.spyOn(core, 'exportVariable').mockImplementation();
|
|
||||||
jest.spyOn(exec,'exec').mockImplementation();
|
|
||||||
await arc.getArcKubeconfig();
|
|
||||||
expect(core.getInput).toBeCalledTimes(4);
|
|
||||||
expect(io.which).toHaveBeenCalledWith("az",true);
|
|
||||||
expect(child_process.spawn).toHaveBeenCalledWith('az',['connectedk8s','proxy','-n','testcluster','-g','testrg','-f',path.join('tempDirPath', 'kubeconfig_1234561234567'),'--token','token'], {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore'
|
|
||||||
});
|
|
||||||
expect(fs.chmodSync).toHaveBeenCalledWith(path.join('tempDirPath', 'kubeconfig_1234561234567'), '600');
|
|
||||||
expect(core.exportVariable).toHaveBeenCalledWith('KUBECONFIG', path.join('tempDirPath', 'kubeconfig_1234561234567'));
|
|
||||||
},180000);
|
|
||||||
|
|
||||||
test('getArcKubeconfig() - checking execution of service-principal scenario',async () =>{
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'service-principal';
|
|
||||||
if (inputName == 'resource-group') return 'testrg';
|
|
||||||
if (inputName == 'cluster-name') return 'testcluster';
|
|
||||||
});
|
|
||||||
jest.spyOn(io, 'which').mockResolvedValue('az');
|
|
||||||
process.env['RUNNER_TEMP'] = 'tempDirPath';
|
|
||||||
jest.spyOn(Date, 'now').mockImplementation(() => 1234561234567);
|
|
||||||
const dummy_process=await child_process.spawn('ls',[]);
|
|
||||||
const dummy_sleep_promise = new Promise(resolve => setTimeout(resolve, 0));
|
|
||||||
jest.spyOn(child_process,'spawn').mockImplementation((commands, arg, option)=>{
|
|
||||||
return dummy_process;
|
|
||||||
});
|
|
||||||
jest.spyOn(arc,'sleep').mockImplementation((ms)=>{
|
|
||||||
return dummy_sleep_promise;
|
|
||||||
});
|
|
||||||
jest.spyOn(fs, 'chmodSync').mockImplementation();
|
|
||||||
jest.spyOn(core, 'exportVariable').mockImplementation();
|
|
||||||
jest.spyOn(exec,'exec').mockImplementation();
|
|
||||||
await arc.getArcKubeconfig();
|
|
||||||
expect(core.getInput).toBeCalledTimes(3);
|
|
||||||
expect(io.which).toHaveBeenCalledWith("az",true);
|
|
||||||
expect(child_process.spawn).toHaveBeenCalledWith('az',['connectedk8s','proxy','-n','testcluster','-g','testrg','-f',path.join('tempDirPath', 'kubeconfig_1234561234567')], {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore'
|
|
||||||
});
|
|
||||||
expect(fs.chmodSync).toHaveBeenCalledWith(path.join('tempDirPath', 'kubeconfig_1234561234567'), '600');
|
|
||||||
expect(core.exportVariable).toHaveBeenCalledWith('KUBECONFIG', path.join('tempDirPath', 'kubeconfig_1234561234567'));
|
|
||||||
},180000);
|
|
||||||
|
|
||||||
test('getArcKubeconfig() - wrong method passed',async ()=>{
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'someMethod';
|
|
||||||
});
|
|
||||||
await expect(arc.getArcKubeconfig()).rejects.toThrow("Supported methods for arc cluster are 'service-account' and 'service-principal'.");
|
|
||||||
expect(core.getInput).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getArcKubeconfig() - resource group not passed',async ()=>{
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'service-account';
|
|
||||||
if (inputName == 'resource-group') return '';
|
|
||||||
});
|
|
||||||
await expect(arc.getArcKubeconfig()).rejects.toThrow("'resourceGroupName' is not passed for arc cluster.");
|
|
||||||
expect(core.getInput).toBeCalledTimes(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getArcKubeconfig() - cluster name not passed',async ()=>{
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'service-account';
|
|
||||||
if (inputName == 'resource-group') return 'testrg';
|
|
||||||
if (inputName == 'cluster-name') return '';
|
|
||||||
});
|
|
||||||
await expect(arc.getArcKubeconfig()).rejects.toThrow("'clusterName' is not passed for arc cluster.");
|
|
||||||
expect(core.getInput).toBeCalledTimes(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getArcKubeconfig() - token not passed',async ()=>{
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'service-account';
|
|
||||||
if (inputName == 'resource-group') return 'testrg';
|
|
||||||
if (inputName == 'cluster-name') return 'testcluster';
|
|
||||||
if (inputName == 'token') return '';
|
|
||||||
});
|
|
||||||
jest.spyOn(io, 'which').mockResolvedValue('az');
|
|
||||||
jest.spyOn(exec,'exec').mockImplementation();
|
|
||||||
process.env['RUNNER_TEMP'] = 'tempDirPath';
|
|
||||||
jest.spyOn(Date, 'now').mockImplementation(() => 1234561234567);
|
|
||||||
|
|
||||||
await expect(arc.getArcKubeconfig()).rejects.toThrow("'saToken' is not passed for 'service-account' method.");
|
|
||||||
expect(core.getInput).toBeCalledTimes(4);
|
|
||||||
expect(io.which).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('executeAzCliCommand() - testing execution of function',()=>{
|
|
||||||
jest.spyOn(exec,'exec').mockImplementation();
|
|
||||||
var azPath = "az";
|
|
||||||
expect(arc.executeAzCliCommand("some command",false));
|
|
||||||
expect(exec.exec).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getExecutableExtension() - return .exe when os is Windows', () => {
|
|
||||||
jest.spyOn(os, 'type').mockReturnValue('Windows_NT');
|
|
||||||
|
|
||||||
expect(run.getExecutableExtension()).toBe('.exe');
|
|
||||||
expect(os.type).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getExecutableExtension() - return empty string for non-windows OS', () => {
|
|
||||||
jest.spyOn(os, 'type').mockReturnValue('Darwin');
|
|
||||||
|
|
||||||
expect(run.getExecutableExtension()).toBe('');
|
|
||||||
expect(os.type).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getKubectlPath() - return path to existing kubectl', async () => {
|
|
||||||
jest.spyOn(io, 'which').mockResolvedValue('pathToKubectl');
|
|
||||||
|
|
||||||
expect(await run.getKubectlPath()).toBe('pathToKubectl');
|
|
||||||
expect(io.which).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getKubectlPath() - throw error when kubectl not installed', async () => {
|
|
||||||
jest.spyOn(io, 'which').mockResolvedValue('');
|
|
||||||
jest.spyOn(toolCache, 'findAllVersions').mockReturnValue([]);
|
|
||||||
|
|
||||||
await expect(run.getKubectlPath()).rejects.toThrow('Kubectl is not installed');
|
|
||||||
expect(io.which).toBeCalled();
|
|
||||||
expect(toolCache.findAllVersions).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getKubectlPath() - return path to kubectl in toolCache', async () => {
|
|
||||||
jest.spyOn(io, 'which').mockResolvedValue('');
|
|
||||||
jest.spyOn(toolCache, 'findAllVersions').mockReturnValue(['v1.15.0']);
|
|
||||||
jest.spyOn(toolCache, 'find').mockReturnValue('pathToTool');
|
|
||||||
jest.spyOn(os, 'type').mockReturnValue('Windows_NT');
|
|
||||||
|
|
||||||
expect(await run.getKubectlPath()).toBe(path.join('pathToTool', 'kubectl.exe'));
|
|
||||||
expect(io.which).toBeCalled();
|
|
||||||
expect(toolCache.findAllVersions).toBeCalled();
|
|
||||||
expect(toolCache.find).toBeCalledWith('kubectl', 'v1.15.0');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getKubeconfig() - throw error on invalid input', () => {
|
|
||||||
jest.spyOn(core, 'getInput').mockReturnValue('invalid');
|
|
||||||
|
|
||||||
expect(() => run.getKubeconfig()).toThrow('Invalid method specified. Acceptable values are kubeconfig and service-account.');
|
|
||||||
expect(core.getInput).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getKubeconfig() - return kubeconfig from input', () => {
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'kubeconfig';
|
|
||||||
if (inputName == 'kubeconfig') return '###';
|
|
||||||
});
|
|
||||||
jest.spyOn(core, 'debug').mockImplementation();
|
|
||||||
|
|
||||||
expect(run.getKubeconfig()).toBe('###');
|
|
||||||
expect(core.getInput).toBeCalledWith('method', { required: true });
|
|
||||||
expect(core.getInput).toBeCalledWith('kubeconfig', { required: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getKubeconfig() - create kubeconfig from secret provided and return it', () => {
|
|
||||||
jest.spyOn(core, 'debug').mockImplementation();
|
|
||||||
const k8Secret = fs.readFileSync('__tests__/sample-secret.yml').toString();
|
|
||||||
const kubeconfig = JSON.stringify({
|
|
||||||
"apiVersion": "v1",
|
|
||||||
"kind": "Config",
|
|
||||||
"clusters": [
|
|
||||||
{
|
|
||||||
"cluster": {
|
|
||||||
"certificate-authority-data": 'LS0tLS1CRUdJTiBDRWyUSUZJQ',
|
|
||||||
"server": 'https://testing-dns-4za.hfp.earth.azmk8s.io:443'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"token": Buffer.from('ZXlKaGJHY2lPcUpTVXpJMU5pSX=', 'base64').toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'service-account';
|
|
||||||
if (inputName == 'k8s-url') return 'https://testing-dns-4za.hfp.earth.azmk8s.io:443';
|
|
||||||
if (inputName == 'k8s-secret') return k8Secret;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(run.getKubeconfig()).toBe(kubeconfig);
|
|
||||||
expect(core.getInput).toBeCalledTimes(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getKubeconfig() - throw error if empty config provided', () => {
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'service-account';
|
|
||||||
if (inputName == 'k8s-url') return 'https://testing-dns-4da.hcp.earth.azmk8s.io:443';
|
|
||||||
if (inputName == 'k8s-secret') return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => run.getKubeconfig()).toThrow('The service account secret yaml specified is invalid. Make sure that its a valid yaml and try again.');
|
|
||||||
expect(core.getInput).toBeCalledTimes(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getKubeconfig() - throw error if data field doesn\'t exist', () => {
|
|
||||||
var k8SecretYaml = fs.readFileSync('__tests__/sample-secret.yml').toString();
|
|
||||||
var k8SecretObject = jsyaml.safeLoad(k8SecretYaml);
|
|
||||||
delete k8SecretObject['data'];
|
|
||||||
k8SecretYaml = jsyaml.dump(k8SecretObject);
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'service-account';
|
|
||||||
if (inputName == 'k8s-url') return 'https://testing-dns-4da.hcp.earth.azmk8s.io:443';
|
|
||||||
if (inputName == 'k8s-secret') return k8SecretYaml;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => run.getKubeconfig()).toThrow('The service account secret yaml does not contain data; field. Make sure that its present and try again.');
|
|
||||||
expect(core.getInput).toBeCalledTimes(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getKubeconfig() - throw error if data.token field doesn\'t exist', () => {
|
|
||||||
var k8SecretYaml = fs.readFileSync('__tests__/sample-secret.yml').toString();
|
|
||||||
var k8SecretObject = jsyaml.safeLoad(k8SecretYaml);
|
|
||||||
delete k8SecretObject['data']['token'];
|
|
||||||
k8SecretYaml = jsyaml.dump(k8SecretObject);
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'service-account';
|
|
||||||
if (inputName == 'k8s-url') return 'https://testing-dns-4da.hcp.earth.azmk8s.io:443';
|
|
||||||
if (inputName == 'k8s-secret') return k8SecretYaml;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => run.getKubeconfig()).toThrow('The service account secret yaml does not contain data.token; field. Make sure that its present and try again.');
|
|
||||||
expect(core.getInput).toBeCalledTimes(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getKubeconfig() - throw error if data[ca.crt] field doesn\'t exist', () => {
|
|
||||||
var k8SecretYaml = fs.readFileSync('__tests__/sample-secret.yml').toString();
|
|
||||||
var k8SecretObject = jsyaml.safeLoad(k8SecretYaml);
|
|
||||||
delete k8SecretObject['data']['ca.crt'];
|
|
||||||
k8SecretYaml = jsyaml.dump(k8SecretObject);
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'service-account';
|
|
||||||
if (inputName == 'k8s-url') return 'https://testing-dns-4da.hcp.earth.azmk8s.io:443';
|
|
||||||
if (inputName == 'k8s-secret') return k8SecretYaml;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(() => run.getKubeconfig()).toThrow('The service account secret yaml does not contain data[ca.crt]; field. Make sure that its present and try again.');
|
|
||||||
expect(core.getInput).toBeCalledTimes(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setContext() - set context using kubectl', async () => {
|
|
||||||
jest.spyOn(core, 'getInput').mockReturnValue('abc');
|
|
||||||
jest.spyOn(io, 'which').mockResolvedValue('pathToKubectl');
|
|
||||||
mockStatusCode = 0;
|
|
||||||
|
|
||||||
expect(await run.setContext('/pathToKubeconfig'));
|
|
||||||
expect(mockExecFn).toBeCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('run() - create kubeconfig, exportvariable and give appropriate access', async () => {
|
|
||||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
|
||||||
jest.spyOn(fs, 'chmodSync').mockImplementation(() => {});
|
|
||||||
jest.spyOn(core, 'exportVariable').mockImplementation(() => {});
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'method') return 'kubeconfig';
|
|
||||||
if (inputName == 'kubeconfig') return '###';
|
|
||||||
if (inputName == 'context') return '';
|
|
||||||
if (inputName == 'cluster-type') return 'generic';
|
|
||||||
});
|
|
||||||
process.env['RUNNER_TEMP'] = 'tempDirPath'
|
|
||||||
jest.spyOn(Date, 'now').mockImplementation(() => 1234561234567);
|
|
||||||
|
|
||||||
expect(run.run());
|
|
||||||
expect(fs.writeFileSync).toHaveBeenCalledWith(path.join('tempDirPath', 'kubeconfig_1234561234567'), '###');
|
|
||||||
expect(fs.chmodSync).toHaveBeenCalledWith(path.join('tempDirPath', 'kubeconfig_1234561234567'), '600');
|
|
||||||
expect(core.exportVariable).toHaveBeenCalledWith('KUBECONFIG', path.join('tempDirPath', 'kubeconfig_1234561234567'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('run() - check if arc scenario is getting triggered', async () =>{
|
|
||||||
jest.spyOn(arc,'getArcKubeconfig').mockImplementation();
|
|
||||||
jest.spyOn(core, 'getInput').mockImplementation((inputName, options) => {
|
|
||||||
if (inputName == 'cluster-type') return 'arc';
|
|
||||||
});
|
|
||||||
expect(run.run());
|
|
||||||
expect(arc.getArcKubeconfig).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
46
action.yml
46
action.yml
@ -1,47 +1,39 @@
|
|||||||
name: 'Kubernetes set context'
|
name: "Kubernetes Set Context"
|
||||||
description: 'Kubernetes set context'
|
description: "Set the context of a target Kubernetes cluster and export the kubeconfig which will be used by other actions like azure/k8s-deploy"
|
||||||
inputs:
|
inputs:
|
||||||
# Used for setting the target K8s cluster context which will be used by other actions like azure/k8s-actions/k8s-deploy or azure/k8s-actions/k8s-create-secret
|
# Please ensure you have used azure/login in the workflow before this action
|
||||||
cluster-type:
|
cluster-type:
|
||||||
description: 'Acceptable values: generic or arc'
|
description: "Acceptable values: generic or arc"
|
||||||
required: true
|
required: true
|
||||||
default: 'generic'
|
default: "generic"
|
||||||
method:
|
method:
|
||||||
description: 'Acceptable values: kubeconfig or service-account or service-principal'
|
description: "Acceptable values: kubeconfig or service-account or service-principal"
|
||||||
required: true
|
required: true
|
||||||
default: 'kubeconfig'
|
default: "kubeconfig"
|
||||||
kubeconfig:
|
kubeconfig:
|
||||||
description: 'Contents of kubeconfig file'
|
description: "Contents of kubeconfig file"
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
|
||||||
context:
|
context:
|
||||||
description: 'If your kubeconfig has multiple contexts, use this field to use a specific context, otherwise the default one would be chosen'
|
description: "If your kubeconfig has multiple contexts, use this field to use a specific context, otherwise the default one would be chosen"
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
|
||||||
|
|
||||||
k8s-url:
|
k8s-url:
|
||||||
description: 'Cluster Url'
|
description: "Cluster Url"
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
|
||||||
k8s-secret:
|
k8s-secret:
|
||||||
description: 'Service account secret. Run kubectl get serviceaccounts <service-account-name> -o yaml and copy the service-account-secret-name. Copy the ouptut of kubectl get secret <service-account-secret-name> -o yaml'
|
description: "Service account secret (run kubectl get serviceaccounts <service-account-name> -o yaml and copy the service-account-secret-name)"
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
|
||||||
token:
|
token:
|
||||||
description: 'Token extracted from the secret of service account (should be base 64 decoded)'
|
description: "Token extracted from the secret of service account (should be base 64 decoded)"
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
|
||||||
resource-group:
|
resource-group:
|
||||||
description: 'Azure resource group name'
|
description: "Azure resource group name"
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
|
||||||
cluster-name:
|
cluster-name:
|
||||||
description: 'Azure connected cluster name'
|
description: "Azure connected cluster name"
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
|
||||||
|
|
||||||
branding:
|
branding:
|
||||||
color: 'green' # optional, decorates the entry in the GitHub Marketplace
|
color: "blue"
|
||||||
runs:
|
runs:
|
||||||
using: 'node12'
|
using: "node12"
|
||||||
main: 'lib/login.js'
|
main: "lib/run.js"
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
restoreMocks: true,
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
moduleFileExtensions: ['js', 'ts'],
|
resetMocks: true,
|
||||||
testEnvironment: 'node',
|
moduleFileExtensions: ["js", "ts"],
|
||||||
testMatch: ['**/*.test.ts'],
|
testEnvironment: "node",
|
||||||
|
testMatch: ["**/*.test.ts"],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.ts$': 'ts-jest'
|
"^.+\\.ts$": "ts-jest",
|
||||||
},
|
},
|
||||||
verbose: true,
|
verbose: true,
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
"global": {
|
global: {
|
||||||
"branches": 0,
|
branches: 0,
|
||||||
"functions": 40,
|
functions: 40,
|
||||||
"lines": 22,
|
lines: 22,
|
||||||
"statements": 22
|
statements: 22,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
||||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
||||||
return new (P || (P = Promise))(function (resolve, reject) {
|
|
||||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
||||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
||||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
const core = require("@actions/core");
|
|
||||||
const path = require("path");
|
|
||||||
const child_process_1 = require("child_process");
|
|
||||||
const fs = require("fs");
|
|
||||||
const io = require("@actions/io");
|
|
||||||
const exec = require("@actions/exec");
|
|
||||||
var azPath;
|
|
||||||
const kubeconfig_timeout = 120; //timeout in seconds
|
|
||||||
function getArcKubeconfig() {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
try {
|
|
||||||
let method = core.getInput('method');
|
|
||||||
if (method != 'service-account' && method != 'service-principal') {
|
|
||||||
throw Error("Supported methods for arc cluster are 'service-account' and 'service-principal'.");
|
|
||||||
}
|
|
||||||
let resourceGroupName = core.getInput('resource-group');
|
|
||||||
let clusterName = core.getInput('cluster-name');
|
|
||||||
if (!resourceGroupName) {
|
|
||||||
throw Error("'resourceGroupName' is not passed for arc cluster.");
|
|
||||||
}
|
|
||||||
if (!clusterName) {
|
|
||||||
throw Error("'clusterName' is not passed for arc cluster.");
|
|
||||||
}
|
|
||||||
azPath = yield io.which("az", true);
|
|
||||||
yield executeAzCliCommand(`account show`, false);
|
|
||||||
try {
|
|
||||||
yield executeAzCliCommand(`extension remove -n connectedk8s`, false);
|
|
||||||
}
|
|
||||||
catch (_a) {
|
|
||||||
//ignore if this causes an error
|
|
||||||
}
|
|
||||||
yield executeAzCliCommand(`extension add -n connectedk8s`, false);
|
|
||||||
yield executeAzCliCommand(`extension list`, false);
|
|
||||||
const runnerTempDirectory = process.env['RUNNER_TEMP']; // Using process.env until the core libs are updated
|
|
||||||
const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`);
|
|
||||||
if (method == 'service-account') {
|
|
||||||
let saToken = core.getInput('token');
|
|
||||||
if (!saToken) {
|
|
||||||
throw Error("'saToken' is not passed for 'service-account' method.");
|
|
||||||
}
|
|
||||||
console.log("using 'service-account' method for authenticating to arc cluster.");
|
|
||||||
const proc = child_process_1.spawn(azPath, ['connectedk8s', 'proxy', '-n', clusterName, '-g', resourceGroupName, '-f', kubeconfigPath, '--token', saToken], {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore'
|
|
||||||
});
|
|
||||||
proc.unref();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log("using 'service-principal' method for authenticating to arc cluster.");
|
|
||||||
const proc = child_process_1.spawn(azPath, ['connectedk8s', 'proxy', '-n', clusterName, '-g', resourceGroupName, '-f', kubeconfigPath], {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore'
|
|
||||||
});
|
|
||||||
proc.unref();
|
|
||||||
}
|
|
||||||
console.log(`Waiting for ${kubeconfig_timeout} seconds for kubeconfig to be merged....`);
|
|
||||||
yield sleep(kubeconfig_timeout * 1000); //sleeping for 2 minutes to allow kubeconfig to be merged
|
|
||||||
fs.chmodSync(kubeconfigPath, '600');
|
|
||||||
core.exportVariable('KUBECONFIG', kubeconfigPath);
|
|
||||||
console.log('KUBECONFIG environment variable is set');
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
return Promise.reject(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
exports.getArcKubeconfig = getArcKubeconfig;
|
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
exports.sleep = sleep;
|
|
||||||
function executeAzCliCommand(command, silent, execOptions = {}, args = []) {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
execOptions.silent = !!silent;
|
|
||||||
try {
|
|
||||||
yield exec.exec(`"${azPath}" ${command}`, args, execOptions);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
exports.executeAzCliCommand = executeAzCliCommand;
|
|
84
lib/kubeconfigs/arc.js
Normal file
84
lib/kubeconfigs/arc.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
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.getArcKubeconfig = exports.KUBECONFIG_LOCATION = void 0;
|
||||||
|
const core = __importStar(require("@actions/core"));
|
||||||
|
const io = __importStar(require("@actions/io"));
|
||||||
|
const method_1 = require("../types/method");
|
||||||
|
const path = __importStar(require("path"));
|
||||||
|
const azCommands_1 = require("./azCommands");
|
||||||
|
const RUNNER_TEMP = process.env["RUNNER_TEMP"] || "";
|
||||||
|
exports.KUBECONFIG_LOCATION = path.join(RUNNER_TEMP, `arc_kubeconfig_${Date.now()}`);
|
||||||
|
/**
|
||||||
|
* Gets the kubeconfig based on provided method for an Arc Kubernetes cluster
|
||||||
|
* @returns The kubeconfig wrapped in a Promise
|
||||||
|
*/
|
||||||
|
function getArcKubeconfig() {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const resourceGroupName = core.getInput("resource-group", { required: true });
|
||||||
|
const clusterName = core.getInput("cluster-name", { required: true });
|
||||||
|
const azPath = yield io.which("az", true);
|
||||||
|
const method = method_1.parseMethod(core.getInput("method", { required: true }));
|
||||||
|
yield azCommands_1.runAzCliCommand(azPath, ["extension", "add", "-n", "connectedk8s"]);
|
||||||
|
switch (method) {
|
||||||
|
case method_1.Method.SERVICE_ACCOUNT:
|
||||||
|
const saToken = core.getInput("token", { required: true });
|
||||||
|
return yield azCommands_1.runAzKubeconfigCommandBlocking(azPath, [
|
||||||
|
"connectedk8s",
|
||||||
|
"proxy",
|
||||||
|
"-n",
|
||||||
|
clusterName,
|
||||||
|
"-g",
|
||||||
|
resourceGroupName,
|
||||||
|
"--token",
|
||||||
|
saToken,
|
||||||
|
"-f",
|
||||||
|
exports.KUBECONFIG_LOCATION,
|
||||||
|
], exports.KUBECONFIG_LOCATION);
|
||||||
|
case method_1.Method.SERVICE_PRINCIPAL:
|
||||||
|
return yield azCommands_1.runAzKubeconfigCommandBlocking(azPath, [
|
||||||
|
"connectedk8s",
|
||||||
|
"proxy",
|
||||||
|
"-n",
|
||||||
|
clusterName,
|
||||||
|
"-g",
|
||||||
|
resourceGroupName,
|
||||||
|
"-f",
|
||||||
|
exports.KUBECONFIG_LOCATION,
|
||||||
|
], exports.KUBECONFIG_LOCATION);
|
||||||
|
case undefined:
|
||||||
|
core.warning("Defaulting to kubeconfig method");
|
||||||
|
case method_1.Method.KUBECONFIG:
|
||||||
|
default:
|
||||||
|
throw Error("Kubeconfig method not supported for Arc cluster");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.getArcKubeconfig = getArcKubeconfig;
|
67
lib/kubeconfigs/azCommands.js
Normal file
67
lib/kubeconfigs/azCommands.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
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.runAzKubeconfigCommandBlocking = exports.runAzCliCommand = void 0;
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
|
const exec_1 = require("@actions/exec");
|
||||||
|
const child_process_1 = require("child_process");
|
||||||
|
const AZ_TIMEOUT_SECONDS = 120;
|
||||||
|
/**
|
||||||
|
* Executes an az cli command
|
||||||
|
* @param azPath The path to the az tool
|
||||||
|
* @param args The arguments to be invoked
|
||||||
|
* @param options Optional options for the command execution
|
||||||
|
*/
|
||||||
|
function runAzCliCommand(azPath, args, options = {}) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
yield exec_1.exec(azPath, args, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.runAzCliCommand = runAzCliCommand;
|
||||||
|
/**
|
||||||
|
* Executes an az cli command that will set the kubeconfig
|
||||||
|
* @param azPath The path to the az tool
|
||||||
|
* @param args The arguments to be be invoked
|
||||||
|
* @param kubeconfigPath The path to the kubeconfig that is updated by the command
|
||||||
|
* @returns The contents of the kubeconfig
|
||||||
|
*/
|
||||||
|
function runAzKubeconfigCommandBlocking(azPath, args, kubeconfigPath) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const proc = child_process_1.spawn(azPath, args, {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
proc.unref();
|
||||||
|
yield sleep(AZ_TIMEOUT_SECONDS);
|
||||||
|
return fs.readFileSync(kubeconfigPath).toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.runAzKubeconfigCommandBlocking = runAzKubeconfigCommandBlocking;
|
||||||
|
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
87
lib/kubeconfigs/default.js
Normal file
87
lib/kubeconfigs/default.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.createKubeconfig = exports.getDefaultKubeconfig = void 0;
|
||||||
|
const core = __importStar(require("@actions/core"));
|
||||||
|
const jsyaml = __importStar(require("js-yaml"));
|
||||||
|
const k8sSecret_1 = require("../types/k8sSecret");
|
||||||
|
const method_1 = require("../types/method");
|
||||||
|
/**
|
||||||
|
* Gets the kubeconfig based on provided method for a default Kubernetes cluster
|
||||||
|
* @returns The kubeconfig
|
||||||
|
*/
|
||||||
|
function getDefaultKubeconfig() {
|
||||||
|
const method = method_1.parseMethod(core.getInput("method", { required: true }));
|
||||||
|
switch (method) {
|
||||||
|
case method_1.Method.SERVICE_ACCOUNT: {
|
||||||
|
const clusterUrl = core.getInput("k8s-url", { required: true });
|
||||||
|
core.debug("Found clusterUrl. Creating kubeconfig using certificate and token");
|
||||||
|
const k8sSecret = core.getInput("k8s-secret", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const parsedK8sSecret = k8sSecret_1.parseK8sSecret(jsyaml.load(k8sSecret));
|
||||||
|
const certAuth = parsedK8sSecret.data["ca.crt"];
|
||||||
|
const token = Buffer.from(parsedK8sSecret.data.token, "base64").toString();
|
||||||
|
return createKubeconfig(certAuth, token, clusterUrl);
|
||||||
|
}
|
||||||
|
case method_1.Method.SERVICE_PRINCIPAL: {
|
||||||
|
core.warning("Service Principal method not supported for default cluster type");
|
||||||
|
}
|
||||||
|
case undefined: {
|
||||||
|
core.warning("Defaulting to kubeconfig method");
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
core.debug("Setting context using kubeconfig");
|
||||||
|
return core.getInput("kubeconfig", { required: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.getDefaultKubeconfig = getDefaultKubeconfig;
|
||||||
|
/**
|
||||||
|
* Creates a kubeconfig and returns the string representation
|
||||||
|
* @param certAuth The certificate authentication of the cluster
|
||||||
|
* @param token The user token
|
||||||
|
* @param clusterUrl The server url of the cluster
|
||||||
|
* @returns The kubeconfig as a string
|
||||||
|
*/
|
||||||
|
function createKubeconfig(certAuth, token, clusterUrl) {
|
||||||
|
const kubeconfig = {
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "Config",
|
||||||
|
clusters: [
|
||||||
|
{
|
||||||
|
cluster: {
|
||||||
|
"certificate-authority-data": certAuth,
|
||||||
|
server: clusterUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
token: token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return JSON.stringify(kubeconfig);
|
||||||
|
}
|
||||||
|
exports.createKubeconfig = createKubeconfig;
|
140
lib/login.js
140
lib/login.js
@ -1,140 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
||||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
||||||
return new (P || (P = Promise))(function (resolve, reject) {
|
|
||||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
||||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
||||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
||||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
const core = require("@actions/core");
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
const io = require("@actions/io");
|
|
||||||
const toolCache = require("@actions/tool-cache");
|
|
||||||
const os = require("os");
|
|
||||||
const toolrunner_1 = require("@actions/exec/lib/toolrunner");
|
|
||||||
const jsyaml = require("js-yaml");
|
|
||||||
const util = require("util");
|
|
||||||
const arc_login_1 = require("./arc-login");
|
|
||||||
function getKubeconfig() {
|
|
||||||
const method = core.getInput('method', { required: true });
|
|
||||||
if (method == 'kubeconfig') {
|
|
||||||
const kubeconfig = core.getInput('kubeconfig', { required: true });
|
|
||||||
core.debug("Setting context using kubeconfig");
|
|
||||||
return kubeconfig;
|
|
||||||
}
|
|
||||||
else if (method == 'service-account') {
|
|
||||||
const clusterUrl = core.getInput('k8s-url', { required: true });
|
|
||||||
core.debug("Found clusterUrl, creating kubeconfig using certificate and token");
|
|
||||||
let k8sSecret = core.getInput('k8s-secret', { required: true });
|
|
||||||
var parsedk8sSecret = jsyaml.safeLoad(k8sSecret);
|
|
||||||
let kubernetesServiceAccountSecretFieldNotPresent = 'The service account secret yaml does not contain %s; field. Make sure that its present and try again.';
|
|
||||||
if (!parsedk8sSecret) {
|
|
||||||
throw Error("The service account secret yaml specified is invalid. Make sure that its a valid yaml and try again.");
|
|
||||||
}
|
|
||||||
if (!parsedk8sSecret.data) {
|
|
||||||
throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data"));
|
|
||||||
}
|
|
||||||
if (!parsedk8sSecret.data.token) {
|
|
||||||
throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data.token"));
|
|
||||||
}
|
|
||||||
if (!parsedk8sSecret.data["ca.crt"]) {
|
|
||||||
throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data[ca.crt]"));
|
|
||||||
}
|
|
||||||
const certAuth = parsedk8sSecret.data["ca.crt"];
|
|
||||||
const token = Buffer.from(parsedk8sSecret.data.token, 'base64').toString();
|
|
||||||
const kubeconfigObject = {
|
|
||||||
"apiVersion": "v1",
|
|
||||||
"kind": "Config",
|
|
||||||
"clusters": [
|
|
||||||
{
|
|
||||||
"cluster": {
|
|
||||||
"certificate-authority-data": certAuth,
|
|
||||||
"server": clusterUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"token": token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
return JSON.stringify(kubeconfigObject);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw Error("Invalid method specified. Acceptable values are kubeconfig and service-account.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exports.getKubeconfig = getKubeconfig;
|
|
||||||
function getExecutableExtension() {
|
|
||||||
if (os.type().match(/^Win/)) {
|
|
||||||
return '.exe';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
exports.getExecutableExtension = getExecutableExtension;
|
|
||||||
function getKubectlPath() {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
let kubectlPath = yield io.which('kubectl', false);
|
|
||||||
if (!kubectlPath) {
|
|
||||||
const allVersions = toolCache.findAllVersions('kubectl');
|
|
||||||
kubectlPath = allVersions.length > 0 ? toolCache.find('kubectl', allVersions[0]) : '';
|
|
||||||
if (!kubectlPath) {
|
|
||||||
throw new Error('Kubectl is not installed');
|
|
||||||
}
|
|
||||||
kubectlPath = path.join(kubectlPath, `kubectl${getExecutableExtension()}`);
|
|
||||||
}
|
|
||||||
return kubectlPath;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
exports.getKubectlPath = getKubectlPath;
|
|
||||||
function setContext(kubeconfigPath) {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
let context = core.getInput('context');
|
|
||||||
if (context) {
|
|
||||||
//To use kubectl commands, the environment variable KUBECONFIG needs to be set for this step
|
|
||||||
process.env['KUBECONFIG'] = kubeconfigPath;
|
|
||||||
const kubectlPath = yield getKubectlPath();
|
|
||||||
let toolRunner = new toolrunner_1.ToolRunner(kubectlPath, ['config', 'use-context', context]);
|
|
||||||
yield toolRunner.exec();
|
|
||||||
toolRunner = new toolrunner_1.ToolRunner(kubectlPath, ['config', 'current-context']);
|
|
||||||
yield toolRunner.exec();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
exports.setContext = setContext;
|
|
||||||
function run() {
|
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
|
||||||
try {
|
|
||||||
let kubeconfig = '';
|
|
||||||
const cluster_type = core.getInput('cluster-type', { required: true });
|
|
||||||
if (cluster_type == 'arc') {
|
|
||||||
yield arc_login_1.getArcKubeconfig().catch(ex => {
|
|
||||||
throw new Error('Error: Could not get the KUBECONFIG for arc cluster: ' + ex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const runnerTempDirectory = process.env['RUNNER_TEMP']; // Using process.env until the core libs are updated
|
|
||||||
const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`);
|
|
||||||
kubeconfig = getKubeconfig();
|
|
||||||
core.debug(`Writing kubeconfig contents to ${kubeconfigPath}`);
|
|
||||||
fs.writeFileSync(kubeconfigPath, kubeconfig);
|
|
||||||
fs.chmodSync(kubeconfigPath, '600');
|
|
||||||
core.exportVariable('KUBECONFIG', kubeconfigPath);
|
|
||||||
console.log('KUBECONFIG environment variable is set');
|
|
||||||
yield setContext(kubeconfigPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
return Promise.reject(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
exports.run = run;
|
|
||||||
run().catch(core.setFailed);
|
|
61
lib/run.js
Normal file
61
lib/run.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.run = void 0;
|
||||||
|
const core = __importStar(require("@actions/core"));
|
||||||
|
const path = __importStar(require("path"));
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
|
const cluster_1 = require("./types/cluster");
|
||||||
|
const utils_1 = require("./utils");
|
||||||
|
/**
|
||||||
|
* Sets the Kubernetes context based on supplied action inputs
|
||||||
|
*/
|
||||||
|
function run() {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
// get inputs
|
||||||
|
const clusterType = cluster_1.parseCluster(core.getInput("cluster-type", {
|
||||||
|
required: true,
|
||||||
|
}));
|
||||||
|
const runnerTempDirectory = process.env["RUNNER_TEMP"];
|
||||||
|
const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`);
|
||||||
|
// get kubeconfig and update context
|
||||||
|
const kubeconfig = yield utils_1.getKubeconfig(clusterType);
|
||||||
|
const kubeconfigWithContext = utils_1.setContext(kubeconfig);
|
||||||
|
// output kubeconfig
|
||||||
|
core.debug(`Writing kubeconfig contents to ${kubeconfigPath}`);
|
||||||
|
fs.writeFileSync(kubeconfigPath, kubeconfigWithContext);
|
||||||
|
fs.chmodSync(kubeconfigPath, "600");
|
||||||
|
core.debug("Setting KUBECONFIG environment variable");
|
||||||
|
core.exportVariable("KUBECONFIG", kubeconfigPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.run = run;
|
||||||
|
// Run the application
|
||||||
|
run().catch(core.setFailed);
|
14
lib/types/cluster.js
Normal file
14
lib/types/cluster.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.parseCluster = exports.Cluster = void 0;
|
||||||
|
var Cluster;
|
||||||
|
(function (Cluster) {
|
||||||
|
Cluster["ARC"] = "arc";
|
||||||
|
Cluster["GENERIC"] = "generic";
|
||||||
|
})(Cluster = exports.Cluster || (exports.Cluster = {}));
|
||||||
|
/**
|
||||||
|
* Converts a string to the Cluster enum
|
||||||
|
* @param str The cluster type (case insensitive)
|
||||||
|
* @returns The Cluster enum or undefined if it can't be parsed
|
||||||
|
*/
|
||||||
|
exports.parseCluster = (str) => Cluster[Object.keys(Cluster).filter((k) => Cluster[k].toString().toLowerCase() === str.toLowerCase())[0]];
|
41
lib/types/k8sSecret.js
Normal file
41
lib/types/k8sSecret.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.parseK8sSecret = void 0;
|
||||||
|
const util = __importStar(require("util"));
|
||||||
|
/**
|
||||||
|
* Throws an error if an object does not have all required fields to be a K8sSecret
|
||||||
|
* @param secret
|
||||||
|
* @returns A type guarded K8sSecret
|
||||||
|
*/
|
||||||
|
function parseK8sSecret(secret) {
|
||||||
|
if (!secret)
|
||||||
|
throw Error("K8s secret yaml is invalid");
|
||||||
|
if (!secret.data)
|
||||||
|
throw k8sSecretMissingFieldError("data");
|
||||||
|
if (!secret.data.token)
|
||||||
|
throw k8sSecretMissingFieldError("token");
|
||||||
|
if (!secret.data["ca.crt"])
|
||||||
|
throw k8sSecretMissingFieldError("ca.crt");
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
exports.parseK8sSecret = parseK8sSecret;
|
||||||
|
const k8sSecretMissingFieldError = (field) => Error(util.format("K8s secret yaml does not contain %s field", field));
|
15
lib/types/method.js
Normal file
15
lib/types/method.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.parseMethod = exports.Method = void 0;
|
||||||
|
var Method;
|
||||||
|
(function (Method) {
|
||||||
|
Method["KUBECONFIG"] = "kubeconfig";
|
||||||
|
Method["SERVICE_ACCOUNT"] = "service-account";
|
||||||
|
Method["SERVICE_PRINCIPAL"] = "service-principal";
|
||||||
|
})(Method = exports.Method || (exports.Method = {}));
|
||||||
|
/**
|
||||||
|
* Converts a string to the Method enum
|
||||||
|
* @param str The method (case insensitive)
|
||||||
|
* @returns The Method enum or undefined if it can't be parsed
|
||||||
|
*/
|
||||||
|
exports.parseMethod = (str) => Method[Object.keys(Method).filter((k) => Method[k].toString().toLowerCase() === str.toLowerCase())[0]];
|
76
lib/utils.js
Normal file
76
lib/utils.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
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.setContext = exports.getKubeconfig = void 0;
|
||||||
|
const core = __importStar(require("@actions/core"));
|
||||||
|
const client_node_1 = require("@kubernetes/client-node");
|
||||||
|
const default_1 = require("./kubeconfigs/default");
|
||||||
|
const arc_1 = require("./kubeconfigs/arc");
|
||||||
|
const cluster_1 = require("./types/cluster");
|
||||||
|
/**
|
||||||
|
* Gets the kubeconfig based on Kubernetes cluster type
|
||||||
|
* @param type The cluster type for the kubeconfig (defaults to generic)
|
||||||
|
* @returns A promise of the kubeconfig
|
||||||
|
*/
|
||||||
|
function getKubeconfig(type) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
switch (type) {
|
||||||
|
case cluster_1.Cluster.ARC: {
|
||||||
|
return yield arc_1.getArcKubeconfig();
|
||||||
|
}
|
||||||
|
case undefined: {
|
||||||
|
core.warning("Cluster type not recognized. Defaulting to generic.");
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return default_1.getDefaultKubeconfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.getKubeconfig = getKubeconfig;
|
||||||
|
/**
|
||||||
|
* Sets the context by updating the kubeconfig
|
||||||
|
* @param kubeconfig The kubeconfig
|
||||||
|
* @returns Updated kubeconfig with the context
|
||||||
|
*/
|
||||||
|
function setContext(kubeconfig) {
|
||||||
|
const context = core.getInput("context");
|
||||||
|
if (!context) {
|
||||||
|
core.debug("Can't set context because context is unspecified.");
|
||||||
|
return kubeconfig;
|
||||||
|
}
|
||||||
|
// load current kubeconfig
|
||||||
|
const kc = new client_node_1.KubeConfig();
|
||||||
|
kc.loadFromString(kubeconfig);
|
||||||
|
// update kubeconfig
|
||||||
|
kc.setCurrentContext(context);
|
||||||
|
return kc.exportConfig();
|
||||||
|
}
|
||||||
|
exports.setContext = setContext;
|
20856
package-lock.json
generated
20856
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
60
package.json
60
package.json
@ -1,31 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "k8s-set-context-action",
|
"name": "k8s-set-context-action",
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "lib/run.js",
|
"main": "lib/run.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --outDir ./lib --rootDir ./src",
|
"build": "tsc --outDir ./lib --rootDir ./src",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test-coverage": "jest --coverage"
|
"test-coverage": "jest --coverage"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"actions",
|
"actions",
|
||||||
"node",
|
"node",
|
||||||
"setup"
|
"setup"
|
||||||
],
|
],
|
||||||
"author": "GitHub",
|
"author": "GitHub",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.2.6",
|
"@actions/core": "^1.2.6",
|
||||||
"@actions/exec": "^1.0.0",
|
"@actions/exec": "^1.0.0",
|
||||||
"@actions/tool-cache": "^1.0.0",
|
"@actions/tool-cache": "^1.0.0",
|
||||||
"js-yaml": "^3.13.1"
|
"@kubernetes/client-node": "^0.16.0",
|
||||||
},
|
"js-yaml": "^3.13.1"
|
||||||
"devDependencies": {
|
},
|
||||||
"@types/jest": "^25.2.2",
|
"devDependencies": {
|
||||||
"@types/node": "^12.0.4",
|
"@types/jest": "^25.2.2",
|
||||||
"jest": "^26.6.3",
|
"@types/js-yaml": "^4.0.4",
|
||||||
"ts-jest": "^25.5.1",
|
"@types/node": "^12.0.4",
|
||||||
"typescript": "3.9.2"
|
"jest": "^26.6.3",
|
||||||
}
|
"ts-jest": "^25.5.1",
|
||||||
|
"typescript": "3.9.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
import * as core from '@actions/core';
|
|
||||||
import * as path from 'path';
|
|
||||||
import {spawn} from 'child_process';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as io from '@actions/io';
|
|
||||||
import * as exec from '@actions/exec';
|
|
||||||
var azPath: string;
|
|
||||||
|
|
||||||
const kubeconfig_timeout = 120;//timeout in seconds
|
|
||||||
|
|
||||||
export async function getArcKubeconfig(): Promise<string> {
|
|
||||||
try {
|
|
||||||
let method = core.getInput('method');
|
|
||||||
if (method != 'service-account' && method != 'service-principal'){
|
|
||||||
throw Error("Supported methods for arc cluster are 'service-account' and 'service-principal'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let resourceGroupName = core.getInput('resource-group');
|
|
||||||
let clusterName = core.getInput('cluster-name');
|
|
||||||
if(!resourceGroupName){
|
|
||||||
throw Error("'resourceGroupName' is not passed for arc cluster.")
|
|
||||||
}
|
|
||||||
if(!clusterName){
|
|
||||||
throw Error("'clusterName' is not passed for arc cluster.")
|
|
||||||
}
|
|
||||||
azPath = await io.which("az", true);
|
|
||||||
await executeAzCliCommand(`account show`, false);
|
|
||||||
try{
|
|
||||||
await executeAzCliCommand(`extension remove -n connectedk8s`, false);
|
|
||||||
}
|
|
||||||
catch{
|
|
||||||
//ignore if this causes an error
|
|
||||||
}
|
|
||||||
await executeAzCliCommand(`extension add -n connectedk8s`, false);
|
|
||||||
await executeAzCliCommand(`extension list`, false);
|
|
||||||
const runnerTempDirectory = process.env['RUNNER_TEMP']; // Using process.env until the core libs are updated
|
|
||||||
const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`);
|
|
||||||
if (method == 'service-account'){
|
|
||||||
let saToken = core.getInput('token');
|
|
||||||
if(!saToken){
|
|
||||||
throw Error("'saToken' is not passed for 'service-account' method.")
|
|
||||||
}
|
|
||||||
console.log("using 'service-account' method for authenticating to arc cluster.")
|
|
||||||
const proc=spawn(azPath,['connectedk8s','proxy','-n',clusterName,'-g',resourceGroupName,'-f',kubeconfigPath,'--token',saToken], {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore'
|
|
||||||
});
|
|
||||||
proc.unref();
|
|
||||||
} else{
|
|
||||||
console.log("using 'service-principal' method for authenticating to arc cluster.")
|
|
||||||
const proc=spawn(azPath,['connectedk8s','proxy','-n',clusterName,'-g',resourceGroupName,'-f',kubeconfigPath], {
|
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore'
|
|
||||||
});
|
|
||||||
proc.unref();
|
|
||||||
}
|
|
||||||
console.log(`Waiting for ${kubeconfig_timeout} seconds for kubeconfig to be merged....`)
|
|
||||||
await sleep(kubeconfig_timeout*1000) //sleeping for 2 minutes to allow kubeconfig to be merged
|
|
||||||
fs.chmodSync(kubeconfigPath, '600');
|
|
||||||
core.exportVariable('KUBECONFIG', kubeconfigPath);
|
|
||||||
console.log('KUBECONFIG environment variable is set');
|
|
||||||
} catch (ex) {
|
|
||||||
return Promise.reject(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sleep(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function executeAzCliCommand(
|
|
||||||
command: string,
|
|
||||||
silent?: boolean,
|
|
||||||
execOptions: any = {},
|
|
||||||
args: any = []) {
|
|
||||||
execOptions.silent = !!silent;
|
|
||||||
try {
|
|
||||||
await exec.exec(`"${azPath}" ${command}`, args, execOptions);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
}
|
|
103
src/kubeconfigs/arc.test.ts
Normal file
103
src/kubeconfigs/arc.test.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import * as actions from "@actions/exec";
|
||||||
|
import * as io from "@actions/io";
|
||||||
|
import { getRequiredInputError } from "../../tests/util";
|
||||||
|
import { getArcKubeconfig, KUBECONFIG_LOCATION } from "./arc";
|
||||||
|
import * as az from "./azCommands";
|
||||||
|
|
||||||
|
describe("Arc kubeconfig", () => {
|
||||||
|
test("it throws error without resource group", async () => {
|
||||||
|
await expect(getArcKubeconfig()).rejects.toThrow(
|
||||||
|
getRequiredInputError("resource-group")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it throws error without cluster name", async () => {
|
||||||
|
process.env["INPUT_RESOURCE-GROUP"] = "group";
|
||||||
|
await expect(getArcKubeconfig()).rejects.toThrow(
|
||||||
|
getRequiredInputError("cluster-name")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("runs az cli commands", () => {
|
||||||
|
const group = "group";
|
||||||
|
const name = "name";
|
||||||
|
const path = "path";
|
||||||
|
const kubeconfig = "kubeconfig";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env["INPUT_RESOURCE-GROUP"] = group;
|
||||||
|
process.env["INPUT_CLUSTER-NAME"] = name;
|
||||||
|
|
||||||
|
jest.spyOn(io, "which").mockImplementation(async () => path);
|
||||||
|
jest.spyOn(az, "runAzCliCommand").mockImplementation(async () => {});
|
||||||
|
jest
|
||||||
|
.spyOn(az, "runAzKubeconfigCommandBlocking")
|
||||||
|
.mockImplementation(async () => kubeconfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error without method", async () => {
|
||||||
|
await expect(getArcKubeconfig()).rejects.toThrow(
|
||||||
|
getRequiredInputError("method")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("service account method", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env["INPUT_METHOD"] = "service-account";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error without token", async () => {
|
||||||
|
await expect(getArcKubeconfig()).rejects.toThrow(
|
||||||
|
getRequiredInputError("token")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets the kubeconfig", async () => {
|
||||||
|
const token = "token";
|
||||||
|
process.env["INPUT_TOKEN"] = token;
|
||||||
|
|
||||||
|
expect(await getArcKubeconfig()).toBe(kubeconfig);
|
||||||
|
expect(az.runAzKubeconfigCommandBlocking).toHaveBeenCalledWith(
|
||||||
|
path,
|
||||||
|
[
|
||||||
|
"connectedk8s",
|
||||||
|
"proxy",
|
||||||
|
"-n",
|
||||||
|
name,
|
||||||
|
"-g",
|
||||||
|
group,
|
||||||
|
"--token",
|
||||||
|
token,
|
||||||
|
"-f",
|
||||||
|
KUBECONFIG_LOCATION,
|
||||||
|
],
|
||||||
|
KUBECONFIG_LOCATION
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("service principal method", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env["INPUT_METHOD"] = "service-principal";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets the kubeconfig", async () => {
|
||||||
|
expect(await getArcKubeconfig()).toBe(kubeconfig);
|
||||||
|
expect(az.runAzKubeconfigCommandBlocking).toHaveBeenCalledWith(
|
||||||
|
path,
|
||||||
|
[
|
||||||
|
"connectedk8s",
|
||||||
|
"proxy",
|
||||||
|
"-n",
|
||||||
|
name,
|
||||||
|
"-g",
|
||||||
|
group,
|
||||||
|
"-f",
|
||||||
|
KUBECONFIG_LOCATION,
|
||||||
|
],
|
||||||
|
KUBECONFIG_LOCATION
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
68
src/kubeconfigs/arc.ts
Normal file
68
src/kubeconfigs/arc.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
import * as io from "@actions/io";
|
||||||
|
import { Method, parseMethod } from "../types/method";
|
||||||
|
import * as path from "path";
|
||||||
|
import { runAzCliCommand, runAzKubeconfigCommandBlocking } from "./azCommands";
|
||||||
|
|
||||||
|
const RUNNER_TEMP: string = process.env["RUNNER_TEMP"] || "";
|
||||||
|
export const KUBECONFIG_LOCATION: string = path.join(
|
||||||
|
RUNNER_TEMP,
|
||||||
|
`arc_kubeconfig_${Date.now()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the kubeconfig based on provided method for an Arc Kubernetes cluster
|
||||||
|
* @returns The kubeconfig wrapped in a Promise
|
||||||
|
*/
|
||||||
|
export async function getArcKubeconfig(): Promise<string> {
|
||||||
|
const resourceGroupName = core.getInput("resource-group", { required: true });
|
||||||
|
const clusterName = core.getInput("cluster-name", { required: true });
|
||||||
|
const azPath = await io.which("az", true);
|
||||||
|
|
||||||
|
const method: Method | undefined = parseMethod(
|
||||||
|
core.getInput("method", { required: true })
|
||||||
|
);
|
||||||
|
|
||||||
|
await runAzCliCommand(azPath, ["extension", "add", "-n", "connectedk8s"]);
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case Method.SERVICE_ACCOUNT:
|
||||||
|
const saToken = core.getInput("token", { required: true });
|
||||||
|
return await runAzKubeconfigCommandBlocking(
|
||||||
|
azPath,
|
||||||
|
[
|
||||||
|
"connectedk8s",
|
||||||
|
"proxy",
|
||||||
|
"-n",
|
||||||
|
clusterName,
|
||||||
|
"-g",
|
||||||
|
resourceGroupName,
|
||||||
|
"--token",
|
||||||
|
saToken,
|
||||||
|
"-f",
|
||||||
|
KUBECONFIG_LOCATION,
|
||||||
|
],
|
||||||
|
KUBECONFIG_LOCATION
|
||||||
|
);
|
||||||
|
case Method.SERVICE_PRINCIPAL:
|
||||||
|
return await runAzKubeconfigCommandBlocking(
|
||||||
|
azPath,
|
||||||
|
[
|
||||||
|
"connectedk8s",
|
||||||
|
"proxy",
|
||||||
|
"-n",
|
||||||
|
clusterName,
|
||||||
|
"-g",
|
||||||
|
resourceGroupName,
|
||||||
|
"-f",
|
||||||
|
KUBECONFIG_LOCATION,
|
||||||
|
],
|
||||||
|
KUBECONFIG_LOCATION
|
||||||
|
);
|
||||||
|
case undefined:
|
||||||
|
core.warning("Defaulting to kubeconfig method");
|
||||||
|
case Method.KUBECONFIG:
|
||||||
|
default:
|
||||||
|
throw Error("Kubeconfig method not supported for Arc cluster");
|
||||||
|
}
|
||||||
|
}
|
14
src/kubeconfigs/azCommands.test.ts
Normal file
14
src/kubeconfigs/azCommands.test.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import * as actions from "@actions/exec";
|
||||||
|
import { runAzCliCommand } from "./azCommands";
|
||||||
|
|
||||||
|
describe("Az commands", () => {
|
||||||
|
test("it runs an az cli command", async () => {
|
||||||
|
const path = "path";
|
||||||
|
const args = ["args"];
|
||||||
|
|
||||||
|
jest.spyOn(actions, "exec").mockImplementation(async () => 0);
|
||||||
|
|
||||||
|
expect(await runAzCliCommand(path, args));
|
||||||
|
expect(actions.exec).toBeCalledWith(path, args, {});
|
||||||
|
});
|
||||||
|
});
|
44
src/kubeconfigs/azCommands.ts
Normal file
44
src/kubeconfigs/azCommands.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { ExecOptions } from "@actions/exec/lib/interfaces";
|
||||||
|
import { exec } from "@actions/exec";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
|
const AZ_TIMEOUT_SECONDS: number = 120;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes an az cli command
|
||||||
|
* @param azPath The path to the az tool
|
||||||
|
* @param args The arguments to be invoked
|
||||||
|
* @param options Optional options for the command execution
|
||||||
|
*/
|
||||||
|
export async function runAzCliCommand(
|
||||||
|
azPath: string,
|
||||||
|
args: string[],
|
||||||
|
options: ExecOptions = {}
|
||||||
|
) {
|
||||||
|
await exec(azPath, args, options);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Executes an az cli command that will set the kubeconfig
|
||||||
|
* @param azPath The path to the az tool
|
||||||
|
* @param args The arguments to be be invoked
|
||||||
|
* @param kubeconfigPath The path to the kubeconfig that is updated by the command
|
||||||
|
* @returns The contents of the kubeconfig
|
||||||
|
*/
|
||||||
|
export async function runAzKubeconfigCommandBlocking(
|
||||||
|
azPath: string,
|
||||||
|
args: string[],
|
||||||
|
kubeconfigPath: string
|
||||||
|
): Promise<string> {
|
||||||
|
const proc = spawn(azPath, args, {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
proc.unref();
|
||||||
|
|
||||||
|
await sleep(AZ_TIMEOUT_SECONDS);
|
||||||
|
return fs.readFileSync(kubeconfigPath).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (seconds: number) =>
|
||||||
|
new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
128
src/kubeconfigs/default.test.ts
Normal file
128
src/kubeconfigs/default.test.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { getRequiredInputError } from "../../tests/util";
|
||||||
|
import { createKubeconfig, getDefaultKubeconfig } from "./default";
|
||||||
|
|
||||||
|
describe("Default kubeconfig", () => {
|
||||||
|
test("it creates a kubeconfig with proper format", () => {
|
||||||
|
const certAuth = "certAuth";
|
||||||
|
const token = "token";
|
||||||
|
const clusterUrl = "clusterUrl";
|
||||||
|
|
||||||
|
const kc = createKubeconfig(certAuth, token, clusterUrl);
|
||||||
|
const expected = JSON.stringify({
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "Config",
|
||||||
|
clusters: [
|
||||||
|
{
|
||||||
|
cluster: {
|
||||||
|
"certificate-authority-data": certAuth,
|
||||||
|
server: clusterUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
token: token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(kc).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it throws error without method", () => {
|
||||||
|
expect(() => getDefaultKubeconfig()).toThrow(
|
||||||
|
getRequiredInputError("method")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("default method", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env["INPUT_METHOD"] = "default";
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it throws error without kubeconfig", () => {
|
||||||
|
expect(() => getDefaultKubeconfig()).toThrow(
|
||||||
|
getRequiredInputError("kubeconfig")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it gets default config through kubeconfig input", () => {
|
||||||
|
const kc = "example kc";
|
||||||
|
process.env["INPUT_KUBECONFIG"] = kc;
|
||||||
|
|
||||||
|
expect(getDefaultKubeconfig()).toBe(kc);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it defaults to default method", () => {
|
||||||
|
process.env["INPUT_METHOD"] = "unknown";
|
||||||
|
|
||||||
|
const kc = "example kc";
|
||||||
|
process.env["INPUT_KUBECONFIG"] = kc;
|
||||||
|
|
||||||
|
expect(getDefaultKubeconfig()).toBe(kc);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it defaults to default method from service-principal", () => {
|
||||||
|
process.env["INPUT_METHOD"] = "service-principal";
|
||||||
|
|
||||||
|
const kc = "example kc";
|
||||||
|
process.env["INPUT_KUBECONFIG"] = kc;
|
||||||
|
|
||||||
|
expect(getDefaultKubeconfig()).toBe(kc);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("service-account method", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env["INPUT_METHOD"] = "service-account";
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it throws error without cluster url", () => {
|
||||||
|
expect(() => getDefaultKubeconfig()).toThrow(
|
||||||
|
getRequiredInputError("k8s-url")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it throws error without k8s secret", () => {
|
||||||
|
process.env["INPUT_K8S-URL"] = "url";
|
||||||
|
|
||||||
|
expect(() => getDefaultKubeconfig()).toThrow(
|
||||||
|
getRequiredInputError("k8s-secret")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it gets kubeconfig through service-account", () => {
|
||||||
|
const k8sUrl = "https://testing-dns-4za.hfp.earth.azmk8s.io:443";
|
||||||
|
const token = "ZXlKaGJHY2lPcUpTVXpJMU5pSX=";
|
||||||
|
const cert = "LS0tLS1CRUdJTiBDRWyUSUZJQ";
|
||||||
|
const k8sSecret = fs.readFileSync("tests/sample-secret.yml").toString();
|
||||||
|
|
||||||
|
process.env["INPUT_K8S-URL"] = k8sUrl;
|
||||||
|
process.env["INPUT_K8S-SECRET"] = k8sSecret;
|
||||||
|
|
||||||
|
const expectedConfig = JSON.stringify({
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "Config",
|
||||||
|
clusters: [
|
||||||
|
{
|
||||||
|
cluster: {
|
||||||
|
"certificate-authority-data": cert,
|
||||||
|
server: k8sUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
token: Buffer.from(token, "base64").toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(getDefaultKubeconfig()).toBe(expectedConfig);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
82
src/kubeconfigs/default.ts
Normal file
82
src/kubeconfigs/default.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
import * as jsyaml from "js-yaml";
|
||||||
|
import { K8sSecret, parseK8sSecret } from "../types/k8sSecret";
|
||||||
|
import { Method, parseMethod } from "../types/method";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the kubeconfig based on provided method for a default Kubernetes cluster
|
||||||
|
* @returns The kubeconfig
|
||||||
|
*/
|
||||||
|
export function getDefaultKubeconfig(): string {
|
||||||
|
const method: Method | undefined = parseMethod(
|
||||||
|
core.getInput("method", { required: true })
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case Method.SERVICE_ACCOUNT: {
|
||||||
|
const clusterUrl = core.getInput("k8s-url", { required: true });
|
||||||
|
core.debug(
|
||||||
|
"Found clusterUrl. Creating kubeconfig using certificate and token"
|
||||||
|
);
|
||||||
|
|
||||||
|
const k8sSecret: string = core.getInput("k8s-secret", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const parsedK8sSecret: K8sSecret = parseK8sSecret(jsyaml.load(k8sSecret));
|
||||||
|
const certAuth: string = parsedK8sSecret.data["ca.crt"];
|
||||||
|
const token: string = Buffer.from(
|
||||||
|
parsedK8sSecret.data.token,
|
||||||
|
"base64"
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
return createKubeconfig(certAuth, token, clusterUrl);
|
||||||
|
}
|
||||||
|
case Method.SERVICE_PRINCIPAL: {
|
||||||
|
core.warning(
|
||||||
|
"Service Principal method not supported for default cluster type"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case undefined: {
|
||||||
|
core.warning("Defaulting to kubeconfig method");
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
core.debug("Setting context using kubeconfig");
|
||||||
|
return core.getInput("kubeconfig", { required: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a kubeconfig and returns the string representation
|
||||||
|
* @param certAuth The certificate authentication of the cluster
|
||||||
|
* @param token The user token
|
||||||
|
* @param clusterUrl The server url of the cluster
|
||||||
|
* @returns The kubeconfig as a string
|
||||||
|
*/
|
||||||
|
export function createKubeconfig(
|
||||||
|
certAuth: string,
|
||||||
|
token: string,
|
||||||
|
clusterUrl: string
|
||||||
|
): string {
|
||||||
|
const kubeconfig = {
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "Config",
|
||||||
|
clusters: [
|
||||||
|
{
|
||||||
|
cluster: {
|
||||||
|
"certificate-authority-data": certAuth,
|
||||||
|
server: clusterUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
token: token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(kubeconfig);
|
||||||
|
}
|
133
src/login.ts
133
src/login.ts
@ -1,133 +0,0 @@
|
|||||||
import * as core from '@actions/core';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as io from '@actions/io';
|
|
||||||
import * as toolCache from '@actions/tool-cache';
|
|
||||||
import * as os from 'os';
|
|
||||||
import { ToolRunner } from "@actions/exec/lib/toolrunner";
|
|
||||||
import * as jsyaml from 'js-yaml';
|
|
||||||
import * as util from 'util';
|
|
||||||
import { getArcKubeconfig } from './arc-login';
|
|
||||||
|
|
||||||
export function getKubeconfig(): string {
|
|
||||||
const method = core.getInput('method', { required: true });
|
|
||||||
if (method == 'kubeconfig') {
|
|
||||||
const kubeconfig = core.getInput('kubeconfig', { required: true });
|
|
||||||
core.debug("Setting context using kubeconfig");
|
|
||||||
return kubeconfig;
|
|
||||||
}
|
|
||||||
else if (method == 'service-account') {
|
|
||||||
const clusterUrl = core.getInput('k8s-url', { required: true });
|
|
||||||
core.debug("Found clusterUrl, creating kubeconfig using certificate and token");
|
|
||||||
let k8sSecret = core.getInput('k8s-secret', { required: true });
|
|
||||||
var parsedk8sSecret = jsyaml.safeLoad(k8sSecret);
|
|
||||||
let kubernetesServiceAccountSecretFieldNotPresent = 'The service account secret yaml does not contain %s; field. Make sure that its present and try again.';
|
|
||||||
if (!parsedk8sSecret) {
|
|
||||||
throw Error("The service account secret yaml specified is invalid. Make sure that its a valid yaml and try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedk8sSecret.data) {
|
|
||||||
throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedk8sSecret.data.token) {
|
|
||||||
throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data.token"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedk8sSecret.data["ca.crt"]) {
|
|
||||||
throw Error(util.format(kubernetesServiceAccountSecretFieldNotPresent, "data[ca.crt]"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const certAuth = parsedk8sSecret.data["ca.crt"];
|
|
||||||
const token = Buffer.from(parsedk8sSecret.data.token, 'base64').toString();
|
|
||||||
const kubeconfigObject = {
|
|
||||||
"apiVersion": "v1",
|
|
||||||
"kind": "Config",
|
|
||||||
"clusters": [
|
|
||||||
{
|
|
||||||
"cluster": {
|
|
||||||
"certificate-authority-data": certAuth,
|
|
||||||
"server": clusterUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"user": {
|
|
||||||
"token": token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
return JSON.stringify(kubeconfigObject);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw Error("Invalid method specified. Acceptable values are kubeconfig and service-account.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getExecutableExtension(): string {
|
|
||||||
if (os.type().match(/^Win/)) {
|
|
||||||
return '.exe';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getKubectlPath() {
|
|
||||||
let kubectlPath = await io.which('kubectl', false);
|
|
||||||
if (!kubectlPath) {
|
|
||||||
const allVersions = toolCache.findAllVersions('kubectl');
|
|
||||||
kubectlPath = allVersions.length > 0 ? toolCache.find('kubectl', allVersions[0]) : '';
|
|
||||||
if (!kubectlPath) {
|
|
||||||
throw new Error('Kubectl is not installed');
|
|
||||||
}
|
|
||||||
|
|
||||||
kubectlPath = path.join(kubectlPath, `kubectl${getExecutableExtension()}`);
|
|
||||||
}
|
|
||||||
return kubectlPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setContext(kubeconfigPath: string) {
|
|
||||||
let context = core.getInput('context');
|
|
||||||
if (context) {
|
|
||||||
//To use kubectl commands, the environment variable KUBECONFIG needs to be set for this step
|
|
||||||
process.env['KUBECONFIG'] = kubeconfigPath;
|
|
||||||
const kubectlPath = await getKubectlPath();
|
|
||||||
let toolRunner = new ToolRunner(kubectlPath, ['config', 'use-context', context]);
|
|
||||||
await toolRunner.exec();
|
|
||||||
toolRunner = new ToolRunner(kubectlPath, ['config', 'current-context']);
|
|
||||||
await toolRunner.exec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function run() {
|
|
||||||
try {
|
|
||||||
let kubeconfig = '';
|
|
||||||
const cluster_type = core.getInput('cluster-type', { required: true });
|
|
||||||
if (cluster_type == 'arc') {
|
|
||||||
try{
|
|
||||||
await getArcKubeconfig();
|
|
||||||
}
|
|
||||||
catch (ex){
|
|
||||||
throw new Error('Error: Could not get the KUBECONFIG for arc cluster: ' + ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const runnerTempDirectory = process.env['RUNNER_TEMP']; // Using process.env until the core libs are updated
|
|
||||||
const kubeconfigPath = path.join(runnerTempDirectory, `kubeconfig_${Date.now()}`);
|
|
||||||
kubeconfig = getKubeconfig();
|
|
||||||
core.debug(`Writing kubeconfig contents to ${kubeconfigPath}`);
|
|
||||||
fs.writeFileSync(kubeconfigPath, kubeconfig);
|
|
||||||
fs.chmodSync(kubeconfigPath, '600');
|
|
||||||
core.exportVariable('KUBECONFIG', kubeconfigPath);
|
|
||||||
console.log('KUBECONFIG environment variable is set');
|
|
||||||
await setContext(kubeconfigPath);
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
return Promise.reject(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run().catch(core.setFailed);
|
|
30
src/run.test.ts
Normal file
30
src/run.test.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { getRequiredInputError } from "../tests/util";
|
||||||
|
import { run } from "./run";
|
||||||
|
import fs from "fs";
|
||||||
|
import * as utils from "./utils";
|
||||||
|
|
||||||
|
describe("Run", () => {
|
||||||
|
it("throws error without cluster type", async () => {
|
||||||
|
await expect(run()).rejects.toThrow(getRequiredInputError("cluster-type"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes kubeconfig and sets context", async () => {
|
||||||
|
const kubeconfig = "kubeconfig";
|
||||||
|
|
||||||
|
process.env["INPUT_CLUSTER-TYPE"] = "default";
|
||||||
|
process.env["RUNNER_TEMP"] = "/sample/path";
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(utils, "getKubeconfig")
|
||||||
|
.mockImplementation(async () => kubeconfig);
|
||||||
|
jest.spyOn(fs, "writeFileSync").mockImplementation(() => {});
|
||||||
|
jest.spyOn(fs, "chmodSync").mockImplementation(() => {});
|
||||||
|
jest.spyOn(utils, "setContext").mockImplementation(() => kubeconfig);
|
||||||
|
|
||||||
|
expect(await run());
|
||||||
|
expect(utils.getKubeconfig).toHaveBeenCalled();
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
|
expect(fs.chmodSync).toHaveBeenCalled();
|
||||||
|
expect(utils.setContext).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
36
src/run.ts
Normal file
36
src/run.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { Cluster, parseCluster } from "./types/cluster";
|
||||||
|
import { setContext, getKubeconfig } from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Kubernetes context based on supplied action inputs
|
||||||
|
*/
|
||||||
|
export async function run() {
|
||||||
|
// get inputs
|
||||||
|
const clusterType: Cluster | undefined = parseCluster(
|
||||||
|
core.getInput("cluster-type", {
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const runnerTempDirectory: string = process.env["RUNNER_TEMP"];
|
||||||
|
const kubeconfigPath: string = path.join(
|
||||||
|
runnerTempDirectory,
|
||||||
|
`kubeconfig_${Date.now()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// get kubeconfig and update context
|
||||||
|
const kubeconfig: string = await getKubeconfig(clusterType);
|
||||||
|
const kubeconfigWithContext: string = setContext(kubeconfig);
|
||||||
|
|
||||||
|
// output kubeconfig
|
||||||
|
core.debug(`Writing kubeconfig contents to ${kubeconfigPath}`);
|
||||||
|
fs.writeFileSync(kubeconfigPath, kubeconfigWithContext);
|
||||||
|
fs.chmodSync(kubeconfigPath, "600");
|
||||||
|
core.debug("Setting KUBECONFIG environment variable");
|
||||||
|
core.exportVariable("KUBECONFIG", kubeconfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the application
|
||||||
|
run().catch(core.setFailed);
|
24
src/types/cluster.test.ts
Normal file
24
src/types/cluster.test.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Cluster, parseCluster } from "./cluster";
|
||||||
|
|
||||||
|
describe("Cluster type", () => {
|
||||||
|
test("it has required values", () => {
|
||||||
|
const vals = <any>Object.values(Cluster);
|
||||||
|
expect(vals.includes("arc")).toBe(true);
|
||||||
|
expect(vals.includes("generic")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it can parse valid values from a string", () => {
|
||||||
|
expect(parseCluster("arc")).toBe(Cluster.ARC);
|
||||||
|
expect(parseCluster("Arc")).toBe(Cluster.ARC);
|
||||||
|
expect(parseCluster("ARC")).toBe(Cluster.ARC);
|
||||||
|
|
||||||
|
expect(parseCluster("generic")).toBe(Cluster.GENERIC);
|
||||||
|
expect(parseCluster("Generic")).toBe(Cluster.GENERIC);
|
||||||
|
expect(parseCluster("GENERIC")).toBe(Cluster.GENERIC);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it will return undefined if it can't parse values from a string", () => {
|
||||||
|
expect(parseCluster("invalid")).toBe(undefined);
|
||||||
|
expect(parseCluster("unsupportedType")).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
16
src/types/cluster.ts
Normal file
16
src/types/cluster.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export enum Cluster {
|
||||||
|
ARC = "arc",
|
||||||
|
GENERIC = "generic",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to the Cluster enum
|
||||||
|
* @param str The cluster type (case insensitive)
|
||||||
|
* @returns The Cluster enum or undefined if it can't be parsed
|
||||||
|
*/
|
||||||
|
export const parseCluster = (str: string): Cluster | undefined =>
|
||||||
|
Cluster[
|
||||||
|
Object.keys(Cluster).filter(
|
||||||
|
(k) => Cluster[k].toString().toLowerCase() === str.toLowerCase()
|
||||||
|
)[0] as keyof typeof Cluster
|
||||||
|
];
|
33
src/types/k8sSecret.test.ts
Normal file
33
src/types/k8sSecret.test.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { parseK8sSecret, K8sSecret } from "./k8sSecret";
|
||||||
|
|
||||||
|
describe("K8sSecret type", () => {
|
||||||
|
describe("Parsing from any", () => {
|
||||||
|
test("it returns a type guarded secret", () => {
|
||||||
|
const secret = { data: { token: "token", "ca.crt": "cert" } };
|
||||||
|
expect(() => parseK8sSecret(secret)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it throws an error when secret not provided", () => {
|
||||||
|
expect(() => parseK8sSecret(undefined)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it throws an error when there is no data field", () => {
|
||||||
|
const secret = {};
|
||||||
|
expect(() => parseK8sSecret(secret)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it throws an error when there is no token", () => {
|
||||||
|
const secret = {
|
||||||
|
data: {
|
||||||
|
"ca.crt": "cert",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(() => parseK8sSecret(secret)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it throws an error when there is no ca.crt field", () => {
|
||||||
|
const secret = { data: { token: "token" } };
|
||||||
|
expect(() => parseK8sSecret(secret)).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
25
src/types/k8sSecret.ts
Normal file
25
src/types/k8sSecret.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as util from "util";
|
||||||
|
|
||||||
|
export interface K8sSecret {
|
||||||
|
data: {
|
||||||
|
token: string;
|
||||||
|
"ca.crt": string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an error if an object does not have all required fields to be a K8sSecret
|
||||||
|
* @param secret
|
||||||
|
* @returns A type guarded K8sSecret
|
||||||
|
*/
|
||||||
|
export function parseK8sSecret(secret: any): K8sSecret {
|
||||||
|
if (!secret) throw Error("K8s secret yaml is invalid");
|
||||||
|
if (!secret.data) throw k8sSecretMissingFieldError("data");
|
||||||
|
if (!secret.data.token) throw k8sSecretMissingFieldError("token");
|
||||||
|
if (!secret.data["ca.crt"]) throw k8sSecretMissingFieldError("ca.crt");
|
||||||
|
|
||||||
|
return secret as K8sSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const k8sSecretMissingFieldError = (field: string): Error =>
|
||||||
|
Error(util.format("K8s secret yaml does not contain %s field", field));
|
29
src/types/method.test.ts
Normal file
29
src/types/method.test.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Method, parseMethod } from "./method";
|
||||||
|
|
||||||
|
describe("Method type", () => {
|
||||||
|
test("it has required values", () => {
|
||||||
|
const vals = <any>Object.values(Method);
|
||||||
|
expect(vals.includes("kubeconfig")).toBe(true);
|
||||||
|
expect(vals.includes("service-account")).toBe(true);
|
||||||
|
expect(vals.includes("service-principal")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it can parse valid values from a string", () => {
|
||||||
|
expect(parseMethod("kubeconfig")).toBe(Method.KUBECONFIG);
|
||||||
|
expect(parseMethod("Kubeconfig")).toBe(Method.KUBECONFIG);
|
||||||
|
expect(parseMethod("KUBECONFIG")).toBe(Method.KUBECONFIG);
|
||||||
|
|
||||||
|
expect(parseMethod("service-account")).toBe(Method.SERVICE_ACCOUNT);
|
||||||
|
expect(parseMethod("Service-Account")).toBe(Method.SERVICE_ACCOUNT);
|
||||||
|
expect(parseMethod("SERVICE-ACCOUNT")).toBe(Method.SERVICE_ACCOUNT);
|
||||||
|
|
||||||
|
expect(parseMethod("service-principal")).toBe(Method.SERVICE_PRINCIPAL);
|
||||||
|
expect(parseMethod("Service-Principal")).toBe(Method.SERVICE_PRINCIPAL);
|
||||||
|
expect(parseMethod("SERVICE-PRINCIPAL")).toBe(Method.SERVICE_PRINCIPAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it will return undefined if it can't parse values from a string", () => {
|
||||||
|
expect(parseMethod("invalid")).toBe(undefined);
|
||||||
|
expect(parseMethod("unsupportedType")).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
17
src/types/method.ts
Normal file
17
src/types/method.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export enum Method {
|
||||||
|
KUBECONFIG = "kubeconfig",
|
||||||
|
SERVICE_ACCOUNT = "service-account",
|
||||||
|
SERVICE_PRINCIPAL = "service-principal",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to the Method enum
|
||||||
|
* @param str The method (case insensitive)
|
||||||
|
* @returns The Method enum or undefined if it can't be parsed
|
||||||
|
*/
|
||||||
|
export const parseMethod = (str: string): Method | undefined =>
|
||||||
|
Method[
|
||||||
|
Object.keys(Method).filter(
|
||||||
|
(k) => Method[k].toString().toLowerCase() === str.toLowerCase()
|
||||||
|
)[0] as keyof typeof Method
|
||||||
|
];
|
47
src/utils.test.ts
Normal file
47
src/utils.test.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import * as arc from "./kubeconfigs/arc";
|
||||||
|
import * as def from "./kubeconfigs/default";
|
||||||
|
import { Cluster } from "./types/cluster";
|
||||||
|
import { getKubeconfig, setContext } from "./utils";
|
||||||
|
|
||||||
|
describe("Utils", () => {
|
||||||
|
describe("get kubeconfig", () => {
|
||||||
|
test("it gets arc kubeconfig when type is arc", async () => {
|
||||||
|
const arcKubeconfig = "arckubeconfig";
|
||||||
|
jest
|
||||||
|
.spyOn(arc, "getArcKubeconfig")
|
||||||
|
.mockImplementation(async () => arcKubeconfig);
|
||||||
|
|
||||||
|
expect(await getKubeconfig(Cluster.ARC)).toBe(arcKubeconfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it defaults to default kubeconfig", async () => {
|
||||||
|
const defaultKubeconfig = "arckubeconfig";
|
||||||
|
jest
|
||||||
|
.spyOn(def, "getDefaultKubeconfig")
|
||||||
|
.mockImplementation(() => defaultKubeconfig);
|
||||||
|
|
||||||
|
expect(await getKubeconfig(undefined)).toBe(defaultKubeconfig);
|
||||||
|
expect(await getKubeconfig(Cluster.GENERIC)).toBe(defaultKubeconfig);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("set context", () => {
|
||||||
|
const kc = fs.readFileSync("tests/sample-kubeconfig.yml").toString();
|
||||||
|
|
||||||
|
test("it doesn't change kubeconfig without context", () => {
|
||||||
|
expect(setContext(kc)).toBe(kc);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it writes the context to the kubeconfig", () => {
|
||||||
|
process.env["INPUT_CONTEXT"] = "example";
|
||||||
|
|
||||||
|
const received = JSON.parse(setContext(kc));
|
||||||
|
const expectedKc = JSON.parse(
|
||||||
|
fs.readFileSync("tests/expected-kubeconfig.json").toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(received).toMatchObject(expectedKc);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
48
src/utils.ts
Normal file
48
src/utils.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
|
import { getDefaultKubeconfig } from "./kubeconfigs/default";
|
||||||
|
import { getArcKubeconfig } from "./kubeconfigs/arc";
|
||||||
|
import { Cluster } from "./types/cluster";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the kubeconfig based on Kubernetes cluster type
|
||||||
|
* @param type The cluster type for the kubeconfig (defaults to generic)
|
||||||
|
* @returns A promise of the kubeconfig
|
||||||
|
*/
|
||||||
|
export async function getKubeconfig(
|
||||||
|
type: Cluster | undefined
|
||||||
|
): Promise<string> {
|
||||||
|
switch (type) {
|
||||||
|
case Cluster.ARC: {
|
||||||
|
return await getArcKubeconfig();
|
||||||
|
}
|
||||||
|
case undefined: {
|
||||||
|
core.warning("Cluster type not recognized. Defaulting to generic.");
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return getDefaultKubeconfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the context by updating the kubeconfig
|
||||||
|
* @param kubeconfig The kubeconfig
|
||||||
|
* @returns Updated kubeconfig with the context
|
||||||
|
*/
|
||||||
|
export function setContext(kubeconfig: string): string {
|
||||||
|
const context: string = core.getInput("context");
|
||||||
|
if (!context) {
|
||||||
|
core.debug("Can't set context because context is unspecified.");
|
||||||
|
return kubeconfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load current kubeconfig
|
||||||
|
const kc = new KubeConfig();
|
||||||
|
kc.loadFromString(kubeconfig);
|
||||||
|
|
||||||
|
// update kubeconfig
|
||||||
|
kc.setCurrentContext(context);
|
||||||
|
return kc.exportConfig();
|
||||||
|
}
|
27
tests/expected-kubeconfig.json
Normal file
27
tests/expected-kubeconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Config",
|
||||||
|
"clusters": [
|
||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"cluster": {
|
||||||
|
"server": "http://example.com:8080",
|
||||||
|
"insecure-skip-tls-verify": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"users": [],
|
||||||
|
"contexts": [
|
||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"context": {
|
||||||
|
"cluster": "example",
|
||||||
|
"name": "example",
|
||||||
|
"user": "example",
|
||||||
|
"namespace": "example"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"preferences": {},
|
||||||
|
"current-context": "example"
|
||||||
|
}
|
12
tests/sample-kubeconfig.yml
Normal file
12
tests/sample-kubeconfig.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Config
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
server: http://example.com:8080
|
||||||
|
name: example
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: example
|
||||||
|
namespace: example
|
||||||
|
user: example
|
||||||
|
name: example
|
7
tests/util.ts
Normal file
7
tests/util.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Returns error thrown by core.getInput when input required but not found
|
||||||
|
* @param inputName Name of input
|
||||||
|
* @returns Error with explanation message
|
||||||
|
*/
|
||||||
|
export const getRequiredInputError = (inputName) =>
|
||||||
|
Error(`Input required and not supplied: ${inputName}`);
|
@ -1,10 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"module": "commonjs"
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["node_modules", "tests", "src/**/*.test.ts"]
|
||||||
"node_modules",
|
|
||||||
"__tests__"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user