mirror of
https://github.com/bartvdbraak/keyweave.git
synced 2025-04-28 23:31:20 +00:00
Compare commits
No commits in common. "main" and "v0.2.0" have entirely different histories.
16 changed files with 594 additions and 2010 deletions
12
.github/renovate.json
vendored
12
.github/renovate.json
vendored
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": ["config:base"],
|
|
||||||
"reviewers": ["bartvdbraak"],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchPackagePrefixes": ["azure"],
|
|
||||||
"groupName": "Azure Dependencies",
|
|
||||||
"groupSlug": "azure-dependencies"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
11
.github/workflows/checks.yml
vendored
11
.github/workflows/checks.yml
vendored
|
@ -1,12 +1,9 @@
|
||||||
name: Checks
|
name: Checks
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
paths: [ 'src/**', 'Cargo.toml', 'Cargo.lock' ]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches:
|
||||||
paths: [ 'src/**', 'Cargo.toml', 'Cargo.lock' ]
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fmt:
|
fmt:
|
||||||
|
@ -39,8 +36,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Run unit tests
|
- name: Run tests
|
||||||
run: cargo test --bins
|
run: cargo test --all
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
123
.github/workflows/release.yml
vendored
123
.github/workflows/release.yml
vendored
|
@ -10,21 +10,14 @@ jobs:
|
||||||
pre-check:
|
pre-check:
|
||||||
name: Pre-check
|
name: Pre-check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
version: ${{ steps.version-check.outputs.version }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- id: version-check
|
- run: |
|
||||||
run: |
|
if [[ "$(git describe --tags --abbrev=0)" != "v$(grep -m1 -F 'version =' Cargo.toml | cut -d\" -f2)" ]]; then
|
||||||
version_tag="$(git describe --tags --abbrev=0 | sed 's/^v//')"
|
|
||||||
version_toml="$(grep -m1 -F 'version =' Cargo.toml | cut -d\" -f2)"
|
|
||||||
|
|
||||||
if [[ "$version_tag" != "$version_toml" ]]; then
|
|
||||||
echo "Error: The git tag does not match the Cargo.toml version."
|
echo "Error: The git tag does not match the Cargo.toml version."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Success: The git tag matches the Cargo.toml version."
|
echo "Success: The git tag matches the Cargo.toml version."
|
||||||
echo "version=$version_toml" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: pre-check
|
needs: pre-check
|
||||||
|
@ -70,7 +63,7 @@ jobs:
|
||||||
experimental: false
|
experimental: false
|
||||||
|
|
||||||
- name: mac-arm64
|
- name: mac-arm64
|
||||||
os: macos-latest
|
os: macos-11.0
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
cross: true
|
cross: true
|
||||||
experimental: true
|
experimental: true
|
||||||
|
@ -87,12 +80,12 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v3
|
||||||
if: startsWith(matrix.name, 'linux-')
|
if: startsWith(matrix.name, 'linux-')
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/bin
|
path: ~/.cargo/bin
|
||||||
|
@ -104,6 +97,7 @@ jobs:
|
||||||
|
|
||||||
- uses: taiki-e/setup-cross-toolchain-action@v1
|
- uses: taiki-e/setup-cross-toolchain-action@v1
|
||||||
with:
|
with:
|
||||||
|
# NB: sets CARGO_BUILD_TARGET evar - do not need --target flag in build
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
- uses: taiki-e/install-action@cross
|
- uses: taiki-e/install-action@cross
|
||||||
|
@ -114,9 +108,18 @@ jobs:
|
||||||
- name: Extract version
|
- name: Extract version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "${{ needs.pre-check.outputs.version }}" > VERSION
|
set -euxo pipefail
|
||||||
|
|
||||||
- name: Archive and Package
|
version=$(grep -m1 -F 'version =' Cargo.toml | cut -d\" -f2)
|
||||||
|
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
echo "Error: no version :("
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$version" > VERSION
|
||||||
|
|
||||||
|
- name: Package
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
@ -125,49 +128,49 @@ jobs:
|
||||||
bin="target/${{ matrix.target }}/release/keyweave${ext}"
|
bin="target/${{ matrix.target }}/release/keyweave${ext}"
|
||||||
strip "$bin" || true
|
strip "$bin" || true
|
||||||
dst="keyweave-${{ matrix.target }}"
|
dst="keyweave-${{ matrix.target }}"
|
||||||
mkdir -p "$dst" dist
|
mkdir "$dst"
|
||||||
cp "$bin" "$dst/"
|
cp "$bin" "$dst/"
|
||||||
if [[ "${{ matrix.name }}" == windows-* ]] ; then
|
|
||||||
mv "$dst/keyweave${ext}" dist/keyweave-${{ matrix.target }}.exe
|
|
||||||
else
|
|
||||||
tar cavf "$dst.tar.xz" "$dst"
|
|
||||||
mv "$dst.tar.xz" dist/
|
|
||||||
fi
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- name: Archive (tar)
|
||||||
|
if: '! startsWith(matrix.name, ''windows-'')'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
dst="keyweave-${{ matrix.target }}"
|
||||||
|
tar cavf "$dst.tar.xz" "$dst"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: dist-${{ matrix.target }}
|
name: builds
|
||||||
path: dist
|
retention-days: 1
|
||||||
|
path: |
|
||||||
|
keyweave-*.tar.xz
|
||||||
|
keyweave-x86_64-pc-windows-gnu/keyweave.exe
|
||||||
|
|
||||||
release:
|
sign:
|
||||||
needs: build
|
needs: build
|
||||||
name: Sign and Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
sha256sums: ${{ steps.homebrew-inputs.outputs.sha256sums }}
|
|
||||||
|
|
||||||
|
name: Checksum and sign
|
||||||
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/bin
|
path: ~/.cargo/bin
|
||||||
key: sign-tools-${{ hashFiles('.github/workflows/release.yml') }}
|
key: sign-tools-${{ hashFiles('.github/workflows/release.yml') }}
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
pattern: dist-*
|
name: builds
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Checksums with SHA512 and SHA256
|
- name: Checksums with SHA512
|
||||||
run: |
|
run: sha512sum keyweave-* | tee SHA512SUMS
|
||||||
sha512sum keyweave-* | tee SHA512SUMS
|
|
||||||
sha256sum keyweave-* | tee SHA256SUMS
|
|
||||||
|
|
||||||
- uses: softprops/action-gh-release@v2
|
- uses: softprops/action-gh-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
@ -175,45 +178,5 @@ jobs:
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
files: |
|
files: |
|
||||||
keyweave-*.tar.xz
|
keyweave-*.tar.xz
|
||||||
keyweave-*.exe
|
keyweave-*/keyweave.exe
|
||||||
*SUMS*
|
*SUMS*
|
||||||
|
|
||||||
- name: Generate SHA256SUM input for Homebrew
|
|
||||||
id: homebrew-inputs
|
|
||||||
run: |
|
|
||||||
sha256sums="{$(awk '{printf "%s '\''%s'\'': '\''%s'\''", (NR>1 ? "," : ""), $2, $1} END {print ""}' SHA256SUMS)}"
|
|
||||||
echo "sha256sums=$sha256sums" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
publish-brew:
|
|
||||||
needs: [release, pre-check]
|
|
||||||
name: Publish brew formula
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/github-script@v7
|
|
||||||
name: Dispatch Homebrew release
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.PAT_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const sha256sums = ${{ needs.release.outputs.sha256sums }}
|
|
||||||
await github.rest.actions.createWorkflowDispatch({
|
|
||||||
owner: 'bartvdbraak',
|
|
||||||
repo: 'homebrew-keyweave',
|
|
||||||
workflow_id: 'release.yml',
|
|
||||||
ref: 'main',
|
|
||||||
inputs: {
|
|
||||||
version: '${{ needs.pre-check.outputs.version }}',
|
|
||||||
sha256sums: JSON.stringify(sha256sums)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
publish-crate:
|
|
||||||
needs: release
|
|
||||||
name: Publish rust crate
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
- name: Publish to crates.io
|
|
||||||
run: cargo publish --token ${CARGO_REGISTRY_TOKEN}
|
|
||||||
env:
|
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
104
.github/workflows/tests.yml
vendored
104
.github/workflows/tests.yml
vendored
|
@ -1,104 +0,0 @@
|
||||||
name: Tests
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
bicep-pre-check:
|
|
||||||
name: Bicep Pre-check
|
|
||||||
environment: bicep
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
deployed_tag_exists: ${{ steps.check_tag.outputs.DEPLOYED_TAG_EXISTS }}
|
|
||||||
no_changes: ${{ steps.check_changes.outputs.NO_CHANGES }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Fetch complete history
|
|
||||||
run: |
|
|
||||||
git fetch --prune --unshallow --tags
|
|
||||||
- name: Check for deployed tag
|
|
||||||
id: check_tag
|
|
||||||
run: |
|
|
||||||
if git rev-parse --verify deployed >/dev/null 2>&1; then
|
|
||||||
echo "DEPLOYED_TAG_EXISTS=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "LAST_DEPLOYED_COMMIT=$(git rev-list -n 1 deployed)" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "DEPLOYED_TAG_EXISTS=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
- name: Check for changes in bicep folder
|
|
||||||
id: check_changes
|
|
||||||
if: steps.check_tag.outputs.DEPLOYED_TAG_EXISTS == 'true'
|
|
||||||
run: |
|
|
||||||
if git diff --quiet ${{ steps.check_tag.outputs.LAST_DEPLOYED_COMMIT }} HEAD -- bicep/ ; then
|
|
||||||
echo "NO_CHANGES=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "NO_CHANGES=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
bicep:
|
|
||||||
name: Deploy Azure resources
|
|
||||||
needs: bicep-pre-check
|
|
||||||
if: needs.bicep-pre-check.outputs.deployed_tag_exists == 'false' || needs.bicep-pre-check.outputs.no_changes == 'false'
|
|
||||||
environment: bicep
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
concurrency:
|
|
||||||
group: bicep
|
|
||||||
env:
|
|
||||||
LOCATION: eastus
|
|
||||||
DEPLOYMENT_NAME: keyweave-${{ github.run_id }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: azure/login@v2
|
|
||||||
with:
|
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID_BICEP }}
|
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
|
|
||||||
- name: Deploy Bicep template
|
|
||||||
uses: azure/arm-deploy@v2
|
|
||||||
with:
|
|
||||||
scope: subscription
|
|
||||||
region: ${{ env.LOCATION }}
|
|
||||||
template: bicep/main.bicep
|
|
||||||
deploymentName: ${{ env.DEPLOYMENT_NAME }}
|
|
||||||
- name: Tag Deployment
|
|
||||||
run: |
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git tag -fa deployed -m "Deployed to Azure"
|
|
||||||
git push origin --tags --force
|
|
||||||
|
|
||||||
tests:
|
|
||||||
name: Run End-to-End Tests
|
|
||||||
needs: bicep
|
|
||||||
if: always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- filter: no_access
|
|
||||||
client-id-ref: AZURE_CLIENT_ID_NO_ACCESS
|
|
||||||
- filter: only_get
|
|
||||||
client-id-ref: AZURE_CLIENT_ID_GET
|
|
||||||
- filter: only_list
|
|
||||||
client-id-ref: AZURE_CLIENT_ID_LIST
|
|
||||||
- filter: get_and_list_access
|
|
||||||
client-id-ref: AZURE_CLIENT_ID_GET_LIST
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: test
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
- name: Azure Login
|
|
||||||
uses: azure/login@v2
|
|
||||||
with:
|
|
||||||
client-id: ${{ secrets[matrix.client-id-ref] }}
|
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
|
||||||
- name: Run ${{ matrix.filter }} tests
|
|
||||||
run: cargo test ${{ matrix.filter }}
|
|
|
@ -1,128 +0,0 @@
|
||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We as members, contributors, and leaders pledge to make participation in our
|
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
|
||||||
and orientation.
|
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
||||||
diverse, inclusive, and healthy community.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment for our
|
|
||||||
community include:
|
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
|
||||||
* Giving and gracefully accepting constructive feedback
|
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
||||||
and learning from the experience
|
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
|
||||||
overall community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
|
||||||
advances of any kind
|
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or email
|
|
||||||
address, without their explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
|
||||||
|
|
||||||
Community leaders are responsible for clarifying and enforcing our standards of
|
|
||||||
acceptable behavior and will take appropriate and fair corrective action in
|
|
||||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
||||||
or harmful.
|
|
||||||
|
|
||||||
Community leaders have the right and responsibility to remove, edit, or reject
|
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
||||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
||||||
decisions when appropriate.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also applies when
|
|
||||||
an individual is officially representing the community in public spaces.
|
|
||||||
Examples of representing our community include using an official e-mail address,
|
|
||||||
posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported to the community leaders responsible for enforcement at
|
|
||||||
[bart@vanderbraak.nl](mailto:bart@vanderbraak.nl).
|
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
|
||||||
reporter of any incident.
|
|
||||||
|
|
||||||
## Enforcement Guidelines
|
|
||||||
|
|
||||||
Community leaders will follow these Community Impact Guidelines in determining
|
|
||||||
the consequences for any action they deem in violation of this Code of Conduct:
|
|
||||||
|
|
||||||
### 1. Correction
|
|
||||||
|
|
||||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
||||||
unprofessional or unwelcome in the community.
|
|
||||||
|
|
||||||
**Consequence**: A private, written warning from community leaders, providing
|
|
||||||
clarity around the nature of the violation and an explanation of why the
|
|
||||||
behavior was inappropriate. A public apology may be requested.
|
|
||||||
|
|
||||||
### 2. Warning
|
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
|
||||||
of actions.
|
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
|
||||||
interaction with the people involved, including unsolicited interaction with
|
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
|
||||||
like social media. Violating these terms may lead to a temporary or
|
|
||||||
permanent ban.
|
|
||||||
|
|
||||||
### 3. Temporary Ban
|
|
||||||
|
|
||||||
**Community Impact**: A serious violation of community standards, including
|
|
||||||
sustained inappropriate behavior.
|
|
||||||
|
|
||||||
**Consequence**: A temporary ban from any sort of interaction or public
|
|
||||||
communication with the community for a specified period of time. No public or
|
|
||||||
private interaction with the people involved, including unsolicited interaction
|
|
||||||
with those enforcing the Code of Conduct, is allowed during this period.
|
|
||||||
Violating these terms may lead to a permanent ban.
|
|
||||||
|
|
||||||
### 4. Permanent Ban
|
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
|
||||||
standards, including sustained inappropriate behavior, harassment of an
|
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
|
||||||
the community.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
||||||
version 2.0, available at
|
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
|
||||||
https://www.contributor-covenant.org/translations.
|
|
1370
Cargo.lock
generated
1370
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
28
Cargo.toml
28
Cargo.toml
|
@ -1,29 +1,15 @@
|
||||||
[package]
|
[package]
|
||||||
name = "keyweave"
|
name = "keyweave"
|
||||||
version = "0.3.1"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Bart van der Braak <bart@vanderbraak.nl>"]
|
authors = ["Bart van der Braak <bart@vanderbraak.nl>"]
|
||||||
keywords = ["azure", "keyvault", "env"]
|
|
||||||
description = "Fetches secrets from Azure Key Vault and weaves them into a convenient .env file"
|
|
||||||
license = "GPL-3.0"
|
|
||||||
documentation = "https://docs.rs/keyweave"
|
|
||||||
repository = "https://github.com/bartvdbraak/keyweave/"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.82"
|
azure_identity = "0.17.0"
|
||||||
azure_core = "0.21.0"
|
azure_security_keyvault = "0.17.0"
|
||||||
azure_identity = "0.21.0"
|
clap = { version = "4.4.7", features = ["derive"] }
|
||||||
azure_security_keyvault = "0.21.0"
|
futures = "0.3.29"
|
||||||
clap = { version = "4.5.4", features = ["derive"] }
|
tokio = {version = "1.33.0", features = ["full"]}
|
||||||
futures = "0.3.30"
|
|
||||||
paris = { version = "1.5.15", features = ["macros"] }
|
|
||||||
tokio = {version = "1.37.0", features = ["full"]}
|
|
||||||
|
|
||||||
[target.'cfg(all(target_os = "linux", any(target_env = "musl", target_arch = "arm", target_arch = "aarch64")))'.dependencies]
|
[target.'cfg(all(target_os = "linux", any(target_env = "musl", target_arch = "arm", target_arch = "aarch64")))'.dependencies]
|
||||||
openssl = { version = "0.10", features = ["vendored"] }
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
assert_cmd = "2.0.14"
|
|
||||||
assert_fs = "1.1.1"
|
|
||||||
predicates = "3.1.0"
|
|
||||||
serial_test = "3.1.0"
|
|
75
README.md
75
README.md
|
@ -1,14 +1,8 @@
|
||||||
# Keyweave
|
# Keyweave
|
||||||
|
|
||||||
[<img alt="github" src="https://img.shields.io/badge/github-bartvdbraak/keyweave-8da0cb?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/bartvdbraak/keyweave)
|
<img align="right" src="https://github.com/bartvdbraak/keyweave/assets/3996360/bed7f004-e897-46e5-98a4-c654251c0e17" alt="Cluster" height="256">
|
||||||
[<img alt="crates.io" src="https://img.shields.io/crates/v/keyweave.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/keyweave)
|
|
||||||
[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-keyweave-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/keyweave)
|
|
||||||
[<img alt="build status" src="https://img.shields.io/github/actions/workflow/status/bartvdbraak/keyweave/checks.yml?style=for-the-badge&branch=main" height="20">](https://github.com/bartvdbraak/keyweave/actions/workflows/checks.yml)
|
|
||||||
[<img alt="test status" src="https://img.shields.io/github/actions/workflow/status/bartvdbraak/keyweave/tests.yml?style=for-the-badge&label=tests&branch=main" height="20">](https://github.com/bartvdbraak/keyweave/actions/workflows/tests.yml)
|
|
||||||
|
|
||||||
<img align="right" src="https://github.com/bartvdbraak/keyweave/assets/3996360/5461f53a-5cef-4bde-908a-b8d3bc1c71c5" alt="Keyweave" width="30%">
|
Keyweave is an open-source tool designed to seamlessly fetch secrets from Azure Key Vault and weave them into a convenient `.env` file. Developed in Rust, Keyweave is efficient and easy to use, making it an ideal choice for managing your application's secrets.
|
||||||
|
|
||||||
Keyweave is an open-source tool crafted to seamlessly fetch secrets from Azure Key Vault and weave them into a convenient `.env` file. Developed in Rust, Keyweave stands out for its efficiency and user-friendly design, making it an ideal choice for managing your application's secrets.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -19,85 +13,46 @@ Keyweave is an open-source tool crafted to seamlessly fetch secrets from Azure K
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Before diving into Keyweave, ensure you have the following prerequisites:
|
- **Rust**: Ensure you have Rust installed on your system. If not, you can install it using [rustup](https://rustup.rs/).
|
||||||
|
- **Azure Account**: Log into your Azure tenant and set up the right subscription.
|
||||||
|
|
||||||
- Logged into the right Azure tenant:
|
## Installation
|
||||||
|
|
||||||
```bash
|
Clone the repository to your local machine:
|
||||||
az login --tenant "your-tenant-guid"
|
|
||||||
```
|
|
||||||
|
|
||||||
- The identity you logged in with has `Get` and `List` Secret Permissions in the Access Policies of the Key Vault.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Cargo
|
|
||||||
|
|
||||||
Keyweave is built with [Cargo](https://doc.rust-lang.org/cargo/), the Rust package manager. It can also be used to install from [crates.io](https://crates.io/crates/keyweave):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install keyweave
|
|
||||||
```
|
|
||||||
|
|
||||||
### Homebrew (MacOS, Linux)
|
|
||||||
|
|
||||||
For MacOS and Linux systems, installation is a breeze with [Homebrew](https://brew.sh/). Simply run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew tap bartvdbraak/keyweave
|
|
||||||
brew install keyweave
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Download
|
|
||||||
|
|
||||||
If you prefer manual installation or need binaries for different platforms (including an executable for Windows), visit the [Releases](/releases) page of this GitHub repository.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Invoke-WebRequest -Uri 'https://github.com/bartvdbraak/keyweave/releases/latest/download/keyweave.exe' -OutFile 'keyweave.exe'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building from Source
|
|
||||||
|
|
||||||
To build Keyweave from source, follow these steps:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/bartvdbraak/keyweave.git
|
git clone https://github.com/bartvdbraak/keyweave.git
|
||||||
cd keyweave
|
cd keyweave
|
||||||
cargo build --release
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Once built, run Keyweave using Cargo:
|
Build the project:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run -- --vault-name <VAULT_NAME> [--output <FILE>] [--filter <FILTER>]
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
With the binary on your `PATH`, run Keyweave as follows:
|
After building the project, you can run Keyweave using the following command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
keyweave --vault-name <VAULT_NAME> [--output <FILE>] [--filter <FILTER>]
|
cargo run -- --vault_name <VAULT_NAME> [--output <FILE>] [--filter <FILTER>]
|
||||||
```
|
```
|
||||||
|
|
||||||
- `--vault-name <VAULT_NAME>`: Sets the name of the Azure Key Vault.
|
- `--vault_name <VAULT_NAME>`: Sets the name of the Azure Key Vault.
|
||||||
- `--output <FILE>`: (Optional) Sets the name of the output file (default: `.env`).
|
- `--output <FILE>`: (Optional) Sets the name of the output file (default: `.env`).
|
||||||
- `--filter <FILTER>`: (Optional) Filters the secrets to be retrieved by name.
|
- `--filter <FILTER>`: (Optional) Filters the secrets to be retrieved by name.
|
||||||
|
|
||||||
### Example
|
## Example
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
keyweave --vault-name my-key-vault --output my-env-file.env --filter my-secret
|
cargo run -- --vault_name my-key-vault --output my-env-file.env --filter my-secret
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Additional documentation for this package can be found on [docs.rs](https://docs.rs/keyweave).
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Keyweave is licensed under the GPLv3 License. See [LICENSE](LICENSE) for more details.
|
Keyweave is licensed under the GLPv3 License. See [LICENSE](LICENSE) for more details.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We welcome contributions! Feel free to submit pull requests, report issues, or suggest new features. Your input helps make Keyweave even better.
|
We welcome contributions! Please feel free to submit pull requests, report issues, or suggest new features.
|
||||||
|
|
31
SECURITY.md
31
SECURITY.md
|
@ -1,31 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Use the latest version of Keyweave for the latest security updates.
|
|
||||||
|
|
||||||
## Reporting Vulnerabilities
|
|
||||||
|
|
||||||
To report a security issue, please email [bart@vanderbraak.nl](mailto:bart@vanderbraak.nl) with a detailed description and steps to reproduce. Do not file a public issue for security vulnerabilities.
|
|
||||||
|
|
||||||
### Response Timeline
|
|
||||||
|
|
||||||
We aim to respond to security reports within 48 hours, and to patch the issue within a reasonable timeframe depending on the severity.
|
|
||||||
|
|
||||||
### Responsible Disclosure
|
|
||||||
|
|
||||||
Please allow us a reasonable timeframe to address the issue before publicly disclosing it.
|
|
||||||
|
|
||||||
### Acknowledgements
|
|
||||||
|
|
||||||
We appreciate the responsible disclosure of issues by our users and will acknowledge contributors in our release notes.
|
|
||||||
|
|
||||||
## Security Best Practices
|
|
||||||
|
|
||||||
- Ensure you are running the latest version of Keyweave.
|
|
||||||
- Follow secure password and authentication practices.
|
|
||||||
|
|
||||||
## Contact Alternatives
|
|
||||||
|
|
||||||
If you are unable to send an email, please open an issue on GitHub without disclosing details such that we can establish a alternative form of communication.
|
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
targetScope = 'subscription'
|
|
||||||
|
|
||||||
/*
|
|
||||||
Parameters
|
|
||||||
*/
|
|
||||||
|
|
||||||
@allowed([
|
|
||||||
'D' // Development
|
|
||||||
'T' // Test
|
|
||||||
'A' // Acceptance
|
|
||||||
'P' // Production
|
|
||||||
])
|
|
||||||
param environment string = 'T'
|
|
||||||
param location string = 'westeurope'
|
|
||||||
param name object = {
|
|
||||||
tenantId: 'BVDB'
|
|
||||||
projectId: 'KEYWEAVE'
|
|
||||||
region: 'WEU'
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Variables
|
|
||||||
*/
|
|
||||||
|
|
||||||
var tags = {
|
|
||||||
project: 'keyweave'
|
|
||||||
}
|
|
||||||
var nameFormat = '${name.tenantId}-${name.projectId}-${environment}-${name.region}-{0}-{1:N0}'
|
|
||||||
|
|
||||||
/*
|
|
||||||
Resource Group
|
|
||||||
*/
|
|
||||||
|
|
||||||
resource ResourceGroup 'Microsoft.Resources/resourceGroups@2024-03-01' = {
|
|
||||||
name: format(nameFormat, 'RG', 1)
|
|
||||||
location: location
|
|
||||||
tags: tags
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Module for Log Analytics Workspace
|
|
||||||
*/
|
|
||||||
|
|
||||||
module LogAnalyticsWorkspace 'modules/law.bicep' = {
|
|
||||||
name: 'LogAnalyticsWorkspace'
|
|
||||||
scope: ResourceGroup
|
|
||||||
params: {
|
|
||||||
nameFormat: nameFormat
|
|
||||||
location: location
|
|
||||||
tags: tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Module for Managed Identities
|
|
||||||
*/
|
|
||||||
|
|
||||||
module ManagedIdentities 'modules/id.bicep' = {
|
|
||||||
name: 'ManagedIdentities'
|
|
||||||
scope: ResourceGroup
|
|
||||||
params: {
|
|
||||||
nameFormat: nameFormat
|
|
||||||
location: location
|
|
||||||
tags: tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Module for KeyVault
|
|
||||||
*/
|
|
||||||
|
|
||||||
module KeyVault 'modules/kv.bicep' = {
|
|
||||||
name: 'KeyVault'
|
|
||||||
scope: ResourceGroup
|
|
||||||
dependsOn: [
|
|
||||||
LogAnalyticsWorkspace
|
|
||||||
]
|
|
||||||
params: {
|
|
||||||
nameFormat: nameFormat
|
|
||||||
location: location
|
|
||||||
tags: tags
|
|
||||||
|
|
||||||
identities: ManagedIdentities.outputs.identities
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
param nameFormat string
|
|
||||||
param location string
|
|
||||||
param tags object
|
|
||||||
|
|
||||||
param identityEnvironments array = [
|
|
||||||
'none'
|
|
||||||
'get'
|
|
||||||
'list'
|
|
||||||
'getlist'
|
|
||||||
]
|
|
||||||
|
|
||||||
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = [for (environment, index) in identityEnvironments: {
|
|
||||||
name: format(nameFormat, 'ID', index+1)
|
|
||||||
location: location
|
|
||||||
tags: tags
|
|
||||||
}]
|
|
||||||
|
|
||||||
resource federatedCredential 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2023-01-31' = [for (environment, index) in identityEnvironments: {
|
|
||||||
name: environment
|
|
||||||
parent: managedIdentity[index]
|
|
||||||
properties: {
|
|
||||||
issuer: 'https://token.actions.githubusercontent.com'
|
|
||||||
subject: 'repo:bartvdbraak/keyweave:environment:test'
|
|
||||||
audiences: [
|
|
||||||
'api://AzureADTokenExchange'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
|
|
||||||
output identities array = [for (environment, index) in identityEnvironments: {
|
|
||||||
name: environment
|
|
||||||
id: managedIdentity[index].properties.principalId
|
|
||||||
}]
|
|
|
@ -1,121 +0,0 @@
|
||||||
param nameFormat string
|
|
||||||
param location string
|
|
||||||
param tags object
|
|
||||||
|
|
||||||
param identities array
|
|
||||||
|
|
||||||
var accessPolicies = [for identity in identities: {
|
|
||||||
tenantId: tenant().tenantId
|
|
||||||
objectId: identity.id
|
|
||||||
permissions: {
|
|
||||||
secrets: contains(identity.name, 'get') && contains(identity.name, 'list') ? ['Get', 'List'] : contains(identity.name, 'get') ? ['Get'] : contains(identity.name, 'list') ? ['List'] : []
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
|
|
||||||
/*
|
|
||||||
Log Analytics Workspace (existing)
|
|
||||||
*/
|
|
||||||
|
|
||||||
resource _logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = {
|
|
||||||
name: format(nameFormat, 'LAW', 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Key Vault
|
|
||||||
*/
|
|
||||||
|
|
||||||
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
|
|
||||||
name: replace(toLower(format(nameFormat, 'KVT', 1)), '-', '')
|
|
||||||
location: location
|
|
||||||
tags: tags
|
|
||||||
properties: {
|
|
||||||
sku: {
|
|
||||||
family: 'A'
|
|
||||||
name: 'standard'
|
|
||||||
}
|
|
||||||
tenantId: tenant().tenantId
|
|
||||||
enableSoftDelete: true
|
|
||||||
enablePurgeProtection: true
|
|
||||||
accessPolicies: accessPolicies
|
|
||||||
}
|
|
||||||
resource testSecret 'secrets' = {
|
|
||||||
name: 'testSecret'
|
|
||||||
properties: {
|
|
||||||
value: 'testSecretValue'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resource filterTestSecret 'secrets' = {
|
|
||||||
name: 'filterTestSecret'
|
|
||||||
properties: {
|
|
||||||
value: 'filterTestSecretValue'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Key Vault
|
|
||||||
*/
|
|
||||||
|
|
||||||
resource keyVaultWithFirewall 'Microsoft.KeyVault/vaults@2023-07-01' = {
|
|
||||||
name: replace(toLower(format(nameFormat, 'KVT', 2)), '-', '')
|
|
||||||
location: location
|
|
||||||
tags: tags
|
|
||||||
properties: {
|
|
||||||
sku: {
|
|
||||||
family: 'A'
|
|
||||||
name: 'standard'
|
|
||||||
}
|
|
||||||
tenantId: tenant().tenantId
|
|
||||||
enableSoftDelete: true
|
|
||||||
enablePurgeProtection: true
|
|
||||||
accessPolicies: accessPolicies
|
|
||||||
networkAcls: {
|
|
||||||
defaultAction: 'Deny'
|
|
||||||
ipRules: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resource testSecret 'secrets' = {
|
|
||||||
name: 'testSecret'
|
|
||||||
properties: {
|
|
||||||
value: 'testSecretValue'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resource filterTestSecret 'secrets' = {
|
|
||||||
name: 'filterTestSecret'
|
|
||||||
properties: {
|
|
||||||
value: 'filterTestSecretValue'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Diagnostic Settings for Key Vaults
|
|
||||||
*/
|
|
||||||
|
|
||||||
resource keyVaultDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
|
|
||||||
name: 'keyVaultLogging'
|
|
||||||
scope: keyVault
|
|
||||||
properties: {
|
|
||||||
workspaceId: _logAnalyticsWorkspace.id
|
|
||||||
logs: [
|
|
||||||
{
|
|
||||||
category: 'AuditEvent'
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource keyVaultWithFirewallDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
|
|
||||||
name: 'keyVaultLogging'
|
|
||||||
scope: keyVaultWithFirewall
|
|
||||||
properties: {
|
|
||||||
workspaceId: _logAnalyticsWorkspace.id
|
|
||||||
logs: [
|
|
||||||
{
|
|
||||||
category: 'AuditEvent'
|
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
param nameFormat string
|
|
||||||
param location string
|
|
||||||
param tags object
|
|
||||||
|
|
||||||
/*
|
|
||||||
Log Analytics Workspace
|
|
||||||
*/
|
|
||||||
|
|
||||||
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
|
|
||||||
name: format(nameFormat, 'LAW', 1)
|
|
||||||
location: location
|
|
||||||
tags: tags
|
|
||||||
properties: {
|
|
||||||
sku: {
|
|
||||||
name: 'PerGB2018'
|
|
||||||
}
|
|
||||||
features: {
|
|
||||||
enableLogAccessUsingOnlyResourcePermissions: true
|
|
||||||
}
|
|
||||||
workspaceCapping: {
|
|
||||||
dailyQuotaGb: json('0.025')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
6
renovate.json
Normal file
6
renovate.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
]
|
||||||
|
}
|
302
src/main.rs
302
src/main.rs
|
@ -1,273 +1,121 @@
|
||||||
use anyhow::Result;
|
use azure_identity::DefaultAzureCredential;
|
||||||
use azure_identity::{DefaultAzureCredential, TokenCredentialOptions};
|
|
||||||
use azure_security_keyvault::prelude::KeyVaultGetSecretsResponse;
|
|
||||||
use azure_security_keyvault::KeyvaultClient;
|
use azure_security_keyvault::KeyvaultClient;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use paris::{error, Logger};
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fmt;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::Semaphore;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Parser)]
|
||||||
struct CustomError {
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for CustomError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for CustomError {}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[clap(author, version, about, long_about = None)]
|
#[clap(author, version, about, long_about = None)]
|
||||||
struct Opts {
|
struct Opts {
|
||||||
/// Sets the name of the Azure Key Vault
|
#[clap(
|
||||||
#[clap(short, long, value_name = "VAULT_NAME")]
|
short,
|
||||||
|
long,
|
||||||
|
value_name = "VAULT_NAME",
|
||||||
|
help = "Sets the name of the Azure Key Vault"
|
||||||
|
)]
|
||||||
vault_name: String,
|
vault_name: String,
|
||||||
|
|
||||||
/// Sets the name of the output file
|
#[clap(
|
||||||
#[clap(short, long, value_name = "FILE", default_value = ".env")]
|
short,
|
||||||
|
long,
|
||||||
|
value_name = "FILE",
|
||||||
|
default_value = ".env",
|
||||||
|
help = "Sets the name of the output file"
|
||||||
|
)]
|
||||||
output: String,
|
output: String,
|
||||||
|
|
||||||
/// Filters the secrets to be retrieved by name
|
#[clap(
|
||||||
#[clap(short, long, value_name = "FILTER")]
|
short,
|
||||||
|
long,
|
||||||
|
value_name = "FILTER",
|
||||||
|
help = "Filters the secrets to be retrieved by name"
|
||||||
|
)]
|
||||||
filter: Option<String>,
|
filter: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_vault_dns(vault_name: &str) -> Result<()> {
|
|
||||||
let vault_host = format!("{}.vault.azure.net", vault_name);
|
|
||||||
|
|
||||||
let lookup_result = { tokio::net::lookup_host((vault_host.as_str(), 443)).await };
|
|
||||||
|
|
||||||
match lookup_result {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(_err) => {
|
|
||||||
error!("DNS lookup failed for Key Vault: {}", vault_name);
|
|
||||||
error!(
|
|
||||||
"Please check that the Key Vault exists or that you have no connectivity issues."
|
|
||||||
);
|
|
||||||
Err(CustomError {
|
|
||||||
message: "An error occurred while fetching secrets".to_string(),
|
|
||||||
}
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_secrets_from_key_vault(
|
async fn fetch_secrets_from_key_vault(
|
||||||
client: &KeyvaultClient,
|
vault_url: &str,
|
||||||
filter: Option<&str>,
|
filter: Option<&str>,
|
||||||
) -> Result<Vec<(String, String)>> {
|
) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> {
|
||||||
|
let credential = DefaultAzureCredential::default();
|
||||||
|
let client = KeyvaultClient::new(vault_url, std::sync::Arc::new(credential))?.secret_client();
|
||||||
|
|
||||||
let mut secret_values = Vec::new();
|
let mut secret_values = Vec::new();
|
||||||
let mut secret_pages = client.secret_client().list_secrets().into_stream();
|
let mut secret_pages = client.list_secrets().into_stream();
|
||||||
|
|
||||||
while let Some(page) = secret_pages.next().await {
|
while let Some(page) = secret_pages.next().await {
|
||||||
let page = match page {
|
let page = page?;
|
||||||
Ok(p) => p,
|
let (tx, mut rx) = mpsc::channel(32); // Channel for concurrent secret retrieval
|
||||||
Err(err) => {
|
|
||||||
Logger::new().newline(1);
|
for secret in &page.value {
|
||||||
match err.as_http_error() {
|
if let Some(filter) = filter {
|
||||||
Some(err) => {
|
if !secret.id.contains(filter) {
|
||||||
if err
|
continue;
|
||||||
.error_message()
|
|
||||||
.unwrap()
|
|
||||||
.contains("does not have secrets list permission on key vault")
|
|
||||||
{
|
|
||||||
error!("Make sure you have List permissions on the Key Vault.")
|
|
||||||
} else if err
|
|
||||||
.error_message()
|
|
||||||
.unwrap()
|
|
||||||
.contains("is not authorized and caller is not a trusted service")
|
|
||||||
{
|
|
||||||
error!("Make sure you're on the Key Vaults Firewall allowlist.")
|
|
||||||
} else {
|
|
||||||
error!("HTTP Error: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
error!("Error: {}", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return Err(CustomError {
|
|
||||||
message: "An error occurred while fetching secrets".to_string(),
|
|
||||||
}
|
}
|
||||||
.into());
|
|
||||||
}
|
}
|
||||||
};
|
let tx = tx.clone();
|
||||||
secret_values
|
|
||||||
.extend(fetch_secrets_from_page(&client.secret_client(), &page, filter).await?);
|
// Clone necessary data before moving into the spawned task
|
||||||
|
let secret_id = secret.id.clone();
|
||||||
|
let client_clone = client.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let secret_name = secret_id.split('/').last().unwrap_or_default();
|
||||||
|
let secret_bundle = client_clone.get(secret_name).await;
|
||||||
|
|
||||||
|
// Handle the result and send it through the channel
|
||||||
|
match secret_bundle {
|
||||||
|
Ok(bundle) => {
|
||||||
|
tx.send((secret_id, bundle.value))
|
||||||
|
.await
|
||||||
|
.expect("Send error");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Error fetching secret: {}", err);
|
||||||
|
// You can decide to continue or not in case of an error.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(tx); // Drop the sender to signal the end of sending tasks
|
||||||
|
|
||||||
|
while let Some(result) = rx.recv().await {
|
||||||
|
let (key, value) = result;
|
||||||
|
secret_values.push((key, value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(secret_values)
|
Ok(secret_values)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_secrets_from_page(
|
fn create_env_file(secrets: Vec<(String, String)>, output_file: &str) -> std::io::Result<()> {
|
||||||
client: &azure_security_keyvault::SecretClient,
|
let mut file = File::create(output_file)?;
|
||||||
page: &KeyVaultGetSecretsResponse,
|
|
||||||
filter: Option<&str>,
|
|
||||||
) -> Result<Vec<(String, String)>> {
|
|
||||||
let (tx, mut rx) = mpsc::channel(32);
|
|
||||||
let semaphore = Arc::new(Semaphore::new(10));
|
|
||||||
let mut handles = Vec::new();
|
|
||||||
|
|
||||||
for secret in &page.value {
|
|
||||||
if let Some(filter) = filter {
|
|
||||||
if !secret.id.contains(filter) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let permit = semaphore.clone().acquire_owned().await.unwrap();
|
|
||||||
let tx = tx.clone();
|
|
||||||
let secret_id = secret.id.clone();
|
|
||||||
let client_clone = client.clone();
|
|
||||||
|
|
||||||
handles.push(tokio::spawn(async move {
|
|
||||||
let _permit = permit;
|
|
||||||
fetch_and_send_secret(client_clone, secret_id, tx).await
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(tx);
|
|
||||||
|
|
||||||
let mut secrets = Vec::new();
|
|
||||||
for handle in handles {
|
|
||||||
if let Ok(result) = handle.await {
|
|
||||||
secrets.push(result);
|
|
||||||
} else {
|
|
||||||
error!("Error occurred while fetching a secret.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(result) = rx.recv().await {
|
|
||||||
secrets.push(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(secrets)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_and_send_secret(
|
|
||||||
client: azure_security_keyvault::SecretClient,
|
|
||||||
secret_id: String,
|
|
||||||
tx: mpsc::Sender<(String, String)>,
|
|
||||||
) -> (String, String) {
|
|
||||||
let secret_name = secret_id.split('/').last().unwrap_or_default();
|
|
||||||
match client.get(secret_name).await {
|
|
||||||
Ok(bundle) => {
|
|
||||||
let _ = tx.send((secret_id.clone(), bundle.value.clone())).await;
|
|
||||||
(secret_id, bundle.value)
|
|
||||||
}
|
|
||||||
Err(_err) => {
|
|
||||||
error!("Error fetching secret. Make sure you have Get permissions on the Key Vault.");
|
|
||||||
(secret_id, String::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_env_file(secrets: Vec<(String, String)>, output_file: &str) -> Result<()> {
|
|
||||||
let mut file = match File::create(output_file) {
|
|
||||||
Ok(f) => f,
|
|
||||||
Err(err) => {
|
|
||||||
error!("Failed to create output file: {}", err);
|
|
||||||
return Err(CustomError {
|
|
||||||
message: "n Aerror occurred creating file".to_string(),
|
|
||||||
}
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (key, value) in secrets {
|
for (key, value) in secrets {
|
||||||
|
// Extract the secret name from the URL
|
||||||
if let Some(secret_name) = key.split('/').last() {
|
if let Some(secret_name) = key.split('/').last() {
|
||||||
if let Err(_err) = writeln!(file, "{}={}", secret_name, value) {
|
writeln!(file, "{}={}", secret_name, value)?;
|
||||||
error!("Failed to write to output file: {}", output_file);
|
|
||||||
return Err(CustomError {
|
|
||||||
message: "An error occurred while writing secrets to file".to_string(),
|
|
||||||
}
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::fs;
|
|
||||||
use std::io::{self, BufRead};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_create_env_file() -> Result<()> {
|
|
||||||
let test_secrets = vec![
|
|
||||||
("SECRET_KEY".to_string(), "secret_value1".to_string()),
|
|
||||||
("API_KEY".to_string(), "secret_value2".to_string()),
|
|
||||||
];
|
|
||||||
|
|
||||||
let test_file = "test_output.env";
|
|
||||||
create_env_file(test_secrets, test_file)?;
|
|
||||||
|
|
||||||
let file = fs::File::open(test_file)?;
|
|
||||||
let reader = io::BufReader::new(file);
|
|
||||||
let lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
lines,
|
|
||||||
vec!["SECRET_KEY=secret_value1", "API_KEY=secret_value2",]
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::remove_file(test_file)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let opts: Opts = Opts::parse();
|
let opts: Opts = Opts::parse();
|
||||||
let mut log: Logger<'_> = Logger::new();
|
|
||||||
|
|
||||||
let vault_url = format!("https://{}.vault.azure.net", opts.vault_name);
|
let vault_url = format!("https://{}.vault.azure.net", opts.vault_name);
|
||||||
|
|
||||||
log.loading("Detecting credentials.");
|
println!("Fetching secrets from Key Vault: {}", opts.vault_name);
|
||||||
let credential_options = TokenCredentialOptions::default();
|
|
||||||
let credential =
|
|
||||||
DefaultAzureCredential::create(credential_options).map_err(|e| CustomError {
|
|
||||||
message: format!("Failed to create DefaultAzureCredential: {}", e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let client = match KeyvaultClient::new(&vault_url, std::sync::Arc::new(credential)) {
|
let secrets = fetch_secrets_from_key_vault(&vault_url, opts.filter.as_deref()).await?;
|
||||||
Ok(c) => c,
|
|
||||||
Err(err) => {
|
|
||||||
error!("Failed to create KeyvaultClient: {}", err);
|
|
||||||
return Err(err.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
log.success("Detected credentials.");
|
|
||||||
|
|
||||||
check_vault_dns(&opts.vault_name).await?;
|
println!("Creating output file: {}", opts.output);
|
||||||
|
|
||||||
log.loading(format!(
|
|
||||||
"Fetching secrets from Key Vault: <blue>{}</>",
|
|
||||||
opts.vault_name
|
|
||||||
));
|
|
||||||
let secrets = fetch_secrets_from_key_vault(&client, opts.filter.as_deref()).await?;
|
|
||||||
log.success(format!(
|
|
||||||
"Fetched secrets from Key Vault: <blue>{}</>",
|
|
||||||
opts.vault_name
|
|
||||||
));
|
|
||||||
|
|
||||||
log.loading(format!("Creating output file: <blue>{}</>", opts.output));
|
|
||||||
create_env_file(secrets, &opts.output)?;
|
create_env_file(secrets, &opts.output)?;
|
||||||
log.success(format!("Created output file: <blue>{}</>", opts.output));
|
|
||||||
|
|
||||||
log.success("Done.");
|
println!("Process completed successfully!");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
151
tests/e2e.rs
151
tests/e2e.rs
|
@ -1,151 +0,0 @@
|
||||||
use assert_cmd::prelude::*;
|
|
||||||
use assert_fs::prelude::*;
|
|
||||||
use assert_fs::TempDir;
|
|
||||||
use predicates::prelude::*;
|
|
||||||
use serial_test::serial;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
static BINARY: &str = "keyweave";
|
|
||||||
static KEYVAULT: &str = "bvdbkeyweavetweukvt1";
|
|
||||||
static FIREWALL_KEYVAULT: &str = "bvdbkeyweavetweukvt2";
|
|
||||||
static NON_EXISTENT_KEYVAULT: &str = "bvdbkeyweavetweukvt3";
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_no_access_policies() {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let output_path = temp_dir.child(".env");
|
|
||||||
|
|
||||||
let mut cmd = Command::cargo_bin(BINARY).unwrap();
|
|
||||||
cmd.arg("--vault-name")
|
|
||||||
.arg(KEYVAULT)
|
|
||||||
.arg("--output")
|
|
||||||
.arg(output_path.path());
|
|
||||||
cmd.assert().failure().stderr(predicate::str::contains(
|
|
||||||
"Make sure you have List permissions on the Key Vault.",
|
|
||||||
));
|
|
||||||
|
|
||||||
temp_dir.close().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_only_get_access_policy() {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let output_path = temp_dir.child(".env");
|
|
||||||
|
|
||||||
let mut cmd = Command::cargo_bin(BINARY).unwrap();
|
|
||||||
cmd.arg("--vault-name")
|
|
||||||
.arg(KEYVAULT)
|
|
||||||
.arg("--output")
|
|
||||||
.arg(output_path.path());
|
|
||||||
cmd.assert().failure().stderr(predicate::str::contains(
|
|
||||||
"Make sure you have List permissions on the Key Vault.",
|
|
||||||
));
|
|
||||||
|
|
||||||
temp_dir.close().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test with only List access policy - expected to succeed with get errors.
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_only_list_access_policy() {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let output_path = temp_dir.child(".env");
|
|
||||||
|
|
||||||
let mut cmd = Command::cargo_bin(BINARY).unwrap();
|
|
||||||
cmd.arg("--vault-name")
|
|
||||||
.arg(KEYVAULT)
|
|
||||||
.arg("--output")
|
|
||||||
.arg(output_path.path());
|
|
||||||
cmd.assert().success().stderr(predicate::str::contains(
|
|
||||||
"Make sure you have Get permissions on the Key Vault.",
|
|
||||||
));
|
|
||||||
|
|
||||||
temp_dir.close().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test with both Get and List access policies - expected to pass.
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_get_and_list_access_policies() {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let output_path = temp_dir.child(".env");
|
|
||||||
|
|
||||||
let mut cmd = Command::cargo_bin(BINARY).unwrap();
|
|
||||||
cmd.arg("--vault-name")
|
|
||||||
.arg(KEYVAULT)
|
|
||||||
.arg("--output")
|
|
||||||
.arg(output_path.path());
|
|
||||||
cmd.assert().success();
|
|
||||||
|
|
||||||
output_path.assert(predicate::path::is_file());
|
|
||||||
output_path.assert(predicate::str::contains("testSecret=testSecretValue"));
|
|
||||||
output_path.assert(predicate::str::contains(
|
|
||||||
"filterTestSecret=filterTestSecretValue",
|
|
||||||
));
|
|
||||||
|
|
||||||
temp_dir.close().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test with both Get and List access policies and filter - expected to pass.
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_get_and_list_access_policies_filter() {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let output_path = temp_dir.child(".env");
|
|
||||||
|
|
||||||
let mut cmd = Command::cargo_bin(BINARY).unwrap();
|
|
||||||
cmd.arg("--vault-name")
|
|
||||||
.arg(KEYVAULT)
|
|
||||||
.arg("--output")
|
|
||||||
.arg(output_path.path())
|
|
||||||
.arg("--filter")
|
|
||||||
.arg("filter");
|
|
||||||
cmd.assert().success();
|
|
||||||
|
|
||||||
output_path.assert(predicate::path::is_file());
|
|
||||||
output_path.assert(predicate::str::contains(
|
|
||||||
"filterTestSecret=filterTestSecretValue",
|
|
||||||
));
|
|
||||||
|
|
||||||
temp_dir.close().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test with both Get and List access policies on a Key Vault with Firewall - expected to fail.
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_get_and_list_access_policies_firewall() {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let output_path = temp_dir.child(".env");
|
|
||||||
|
|
||||||
let mut cmd = Command::cargo_bin(BINARY).unwrap();
|
|
||||||
cmd.arg("--vault-name")
|
|
||||||
.arg(FIREWALL_KEYVAULT)
|
|
||||||
.arg("--output")
|
|
||||||
.arg(output_path.path());
|
|
||||||
cmd.assert().failure().stderr(predicate::str::contains(
|
|
||||||
"Make sure you're on the Key Vaults Firewall allowlist.",
|
|
||||||
));
|
|
||||||
|
|
||||||
temp_dir.close().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test with both Get and List access policies on a non-existent Key Vault - expected to fail.
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_get_and_list_access_policies_non_existent() {
|
|
||||||
let temp_dir = TempDir::new().unwrap();
|
|
||||||
let output_path = temp_dir.child(".env");
|
|
||||||
|
|
||||||
let mut cmd = Command::cargo_bin(BINARY).unwrap();
|
|
||||||
cmd.arg("--vault-name")
|
|
||||||
.arg(NON_EXISTENT_KEYVAULT)
|
|
||||||
.arg("--output")
|
|
||||||
.arg(output_path.path());
|
|
||||||
cmd.assert().failure().stderr(predicate::str::contains(
|
|
||||||
"Please check that the Key Vault exists or that you have no connectivity issues.",
|
|
||||||
));
|
|
||||||
|
|
||||||
temp_dir.close().unwrap();
|
|
||||||
}
|
|
Loading…
Reference in a new issue