Compare commits
No commits in common. "v0.0.1" and "main" have entirely different histories.
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: Report a bug encountered while using the falcoctl
|
||||
labels: kind/bug
|
||||
|
||||
---
|
||||
|
||||
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!
|
||||
|
||||
If the matter is security related, please disclose it privately via https://falco.org/security/
|
||||
-->
|
||||
|
||||
**What happened**:
|
||||
|
||||
**What you expected to happen**:
|
||||
|
||||
**How to reproduce it (as minimally and precisely as possible)**:
|
||||
|
||||
**Anything else we need to know?**:
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
name: Enhancement Request
|
||||
about: Suggest an enhancement to the Falco project
|
||||
labels: kind/feature
|
||||
|
||||
---
|
||||
<!-- Please only use this template for submitting enhancement requests -->
|
||||
|
||||
**What would you like to be added**:
|
||||
|
||||
**Why is this needed**:
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Failing Test
|
||||
about: Report test failures in Falco CI jobs
|
||||
labels: kind/failing-test
|
||||
|
||||
---
|
||||
|
||||
<!-- Please only use this template for submitting reports about failing tests in Falco CI jobs -->
|
||||
|
||||
**Which jobs are failing**:
|
||||
|
||||
**Which test(s) are failing**:
|
||||
|
||||
**Since when has it been failing**:
|
||||
|
||||
**Test link**:
|
||||
|
||||
**Reason for failure**:
|
||||
|
||||
**Anything else we need to know**:
|
|
@ -0,0 +1,51 @@
|
|||
<!-- Thanks for sending a pull request! Here are some tips for you:
|
||||
|
||||
1. If this is your first time, please read our contributor guidelines in the [CONTRIBUTING.md](https://github.com/falcosecurity/.github/blob/main/CONTRIBUTING.md) file in the Falco `.github` repository.
|
||||
2. Please label this pull request according to what type of issue you are addressing.
|
||||
3. Please add a release note!
|
||||
4. If the PR is unfinished while opening it specify a wip in the title before the actual title, for example, "wip: my awesome feature"
|
||||
-->
|
||||
|
||||
**What type of PR is this?**
|
||||
|
||||
> Uncomment one (or more) `/kind <>` lines:
|
||||
|
||||
> /kind bug
|
||||
|
||||
> /kind cleanup
|
||||
|
||||
> /kind design
|
||||
|
||||
> /kind documentation
|
||||
|
||||
> /kind failing-test
|
||||
|
||||
> /kind feature
|
||||
|
||||
> /kind flaky-test
|
||||
|
||||
**Any specific area of the project related to this PR?**
|
||||
|
||||
> Uncomment one (or more) `/area <>` lines:
|
||||
|
||||
> /area library
|
||||
|
||||
> /area cli
|
||||
|
||||
> /area tests
|
||||
|
||||
> /area examples
|
||||
|
||||
**What this PR does / why we need it**:
|
||||
|
||||
**Which issue(s) this PR fixes**:
|
||||
|
||||
<!--
|
||||
Automatically closes linked issue when PR is merged.
|
||||
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
|
||||
If PR is `kind/failing-tests` or `kind/flaky-test`, please post the related issues/tests in a comment and do not use `Fixes`.
|
||||
-->
|
||||
|
||||
Fixes #
|
||||
|
||||
**Special notes for your reviewer**:
|
|
@ -0,0 +1,22 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
gomod:
|
||||
update-types:
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
actions:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
|
@ -0,0 +1,37 @@
|
|||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: '28 11 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language:
|
||||
- go
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@d23060145bc9131d50558d5d4185494a20208101 # v2.2.8
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@d23060145bc9131d50558d5d4185494a20208101 # v2.2.8
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@d23060145bc9131d50558d5d4185494a20208101 # v2.2.8
|
|
@ -0,0 +1,103 @@
|
|||
name: docker-image
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release:
|
||||
required: true
|
||||
type: string
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
build_date:
|
||||
required: true
|
||||
type: string
|
||||
sign:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
outputs:
|
||||
digest:
|
||||
description: The digest of the pushed image.
|
||||
value: ${{ jobs.docker-image.outputs.digest }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
docker-image:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
image: ${{ steps.build-and-push.outputs.image }}
|
||||
digest: ${{ steps.build-and-push.outputs.digest }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_SECRET }}
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::292999226676:role/github_actions-falcoctl-ecr
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr-public
|
||||
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
|
||||
with:
|
||||
registry-type: public
|
||||
|
||||
- name: Docker Meta
|
||||
id: meta_falcoctl
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
docker.io/falcosecurity/falcoctl
|
||||
public.ecr.aws/falcosecurity/falcoctl
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{ version }}
|
||||
type=semver,pattern={{ major }}
|
||||
type=semver,pattern={{ major }}.{{ minor }}
|
||||
|
||||
- name: Build and push
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta_falcoctl.outputs.tags }}
|
||||
file: ./build/Dockerfile
|
||||
build-args: |
|
||||
RELEASE=${{ inputs.release }}
|
||||
COMMIT=${{ inputs.commit }}
|
||||
BUILD_DATE=${{ inputs.build_date }}
|
||||
|
||||
- name: Install Cosign
|
||||
if: ${{ inputs.sign }}
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
|
||||
- name: Sign the images with GitHub OIDC Token
|
||||
if: ${{ inputs.sign }}
|
||||
env:
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
TAGS: ${{ steps.meta_falcoctl.outputs.tags }}
|
||||
COSIGN_YES: "true"
|
||||
run: echo "${TAGS}" | xargs -I {} cosign sign {}@${DIGEST}
|
|
@ -0,0 +1,166 @@
|
|||
name: Integration Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- arm64
|
||||
- amd64
|
||||
exclude:
|
||||
- goarch: arm64
|
||||
goos: windows
|
||||
steps:
|
||||
- name: Checkout commit
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
|
||||
- name: Build Falcoctl
|
||||
run: >
|
||||
go build -ldflags="-s -w" -o falcoctl-${{ matrix.goos }}-${{ matrix.goarch }} .
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
|
||||
- name: Create Archives
|
||||
run: |
|
||||
cp falcoctl-${{ matrix.goos }}-${{ matrix.goarch }} falcoctl
|
||||
tar -czvf falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz falcoctl LICENSE
|
||||
|
||||
- name: Upload falcoctl artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: ./falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload falcoctl archives
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
|
||||
path: ./falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
docker-configure:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release: ${{ steps.vars.outputs.release }}
|
||||
commit: ${{ steps.vars.outputs.commit }}
|
||||
build_date: ${{ steps.vars.outputs.build_date }}
|
||||
steps:
|
||||
- name: Set version fields
|
||||
id: vars
|
||||
run: |
|
||||
echo "release=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
echo "commit=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
|
||||
docker-image:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
needs: docker-configure
|
||||
uses: ./.github/workflows/docker-image.yaml
|
||||
secrets: inherit
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
with:
|
||||
release: ${{ needs.docker-configure.outputs.release }}
|
||||
commit: ${{ needs.docker-configure.outputs.commit }}
|
||||
build_date: ${{ needs.docker-configure.outputs.build_date }}
|
||||
sign: true
|
||||
|
||||
provenance-for-images-docker:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
needs: [docker-configure, docker-image]
|
||||
permissions:
|
||||
actions: read # for detecting the Github Actions environment.
|
||||
id-token: write # for creating OIDC tokens for signing.
|
||||
packages: write # for uploading attestations.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: docker.io/falcosecurity/falcoctl
|
||||
# The image digest is used to prevent TOCTOU issues.
|
||||
# This is an output of the docker/build-push-action
|
||||
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
|
||||
digest: ${{ needs.docker-image.outputs.digest }}
|
||||
secrets:
|
||||
registry-username: ${{ secrets.DOCKERHUB_USER }}
|
||||
registry-password: ${{ secrets.DOCKERHUB_SECRET }}
|
||||
|
||||
login-to-amazon-ecr:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::292999226676:role/github_actions-falcoctl-ecr
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr-public
|
||||
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
|
||||
with:
|
||||
registry-type: public
|
||||
mask-password: 'false'
|
||||
outputs:
|
||||
registry: ${{ steps.login-ecr-public.outputs.registry }}
|
||||
docker_username: ${{ steps.login-ecr-public.outputs.docker_username_public_ecr_aws }}
|
||||
docker_password: ${{ steps.login-ecr-public.outputs.docker_password_public_ecr_aws }}
|
||||
|
||||
provenance-for-images-aws-ecr:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
needs: [docker-configure, docker-image, login-to-amazon-ecr]
|
||||
permissions:
|
||||
actions: read # for detecting the Github Actions environment.
|
||||
id-token: write # for creating OIDC tokens for signing.
|
||||
packages: write # for uploading attestations.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: public.ecr.aws/falcosecurity/falcoctl
|
||||
# The image digest is used to prevent TOCTOU issues.
|
||||
# This is an output of the docker/build-push-action
|
||||
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
|
||||
digest: ${{ needs.docker-image.outputs.digest }}
|
||||
secrets:
|
||||
registry-username: ${{ needs.login-to-amazon-ecr.outputs.docker_username }}
|
||||
registry-password: ${{ needs.login-to-amazon-ecr.outputs.docker_password }}
|
||||
|
||||
test:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout commit
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
|
||||
- name: Run tests
|
||||
run: go test -cover ./...
|
|
@ -0,0 +1,64 @@
|
|||
name: Linting
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: Lint golang files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: "^1.24.3"
|
||||
go-version-file: "go.mod"
|
||||
check-latest: true
|
||||
cache: "false"
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2
|
||||
with:
|
||||
only-new-issues: true
|
||||
version: v1.64.7
|
||||
args: --timeout=900s
|
||||
|
||||
gomodtidy:
|
||||
name: Enforce go.mod tidiness
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: "${{ github.event.pull_request.head.sha }}"
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
check-latest: true
|
||||
|
||||
- name: Execute go mod tidy and check the outcome
|
||||
working-directory: ./
|
||||
run: |
|
||||
go mod tidy
|
||||
exit_code=$(git diff --exit-code)
|
||||
exit ${exit_code}
|
||||
|
||||
- name: Print a comment in case of failure
|
||||
run: |
|
||||
echo "The go.mod and/or go.sum files appear not to be correctly tidied.
|
||||
|
||||
Please, rerun go mod tidy to fix the issues."
|
||||
exit 1
|
||||
if: |
|
||||
failure() && github.event.pull_request.head.repo.full_name == github.repository
|
|
@ -0,0 +1,178 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write # To add assets to a release.
|
||||
outputs:
|
||||
hashes: ${{ steps.hash.outputs.hashes }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch all tags
|
||||
run: git fetch --force --tags
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
|
||||
- name: Run GoReleaser
|
||||
id: run-goreleaser
|
||||
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate subject
|
||||
id: hash
|
||||
env:
|
||||
ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path')
|
||||
echo "hashes=$(cat $checksum_file | base64 -w0)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
provenance-for-binaries:
|
||||
needs: [goreleaser]
|
||||
permissions:
|
||||
actions: read # To read the workflow path.
|
||||
id-token: write # To sign the provenance.
|
||||
contents: write # To add assets to a release.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
|
||||
with:
|
||||
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
|
||||
upload-assets: true # upload to a new release
|
||||
|
||||
verification:
|
||||
needs: [goreleaser, provenance-for-binaries]
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Install the verifier
|
||||
uses: slsa-framework/slsa-verifier/actions/installer@v2.7.1
|
||||
|
||||
- name: Download assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PROVENANCE: "${{ needs.provenance-for-binaries.outputs.provenance-name }}"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.tar.gz"
|
||||
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.zip"
|
||||
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$PROVENANCE"
|
||||
|
||||
- name: Verify assets
|
||||
env:
|
||||
CHECKSUMS: ${{ needs.goreleaser.outputs.hashes }}
|
||||
PROVENANCE: "${{ needs.provenance-for-binaries.outputs.provenance-name }}"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
checksums=$(echo "$CHECKSUMS" | base64 -d)
|
||||
while read -r line; do
|
||||
fn=$(echo $line | cut -d ' ' -f2)
|
||||
echo "Verifying $fn"
|
||||
slsa-verifier verify-artifact --provenance-path "$PROVENANCE" \
|
||||
--source-uri "github.com/$GITHUB_REPOSITORY" \
|
||||
--source-tag "$GITHUB_REF_NAME" \
|
||||
"$fn"
|
||||
done <<<"$checksums"
|
||||
|
||||
docker-configure:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release: ${{ steps.vars.outputs.release }}
|
||||
commit: ${{ steps.vars.outputs.commit }}
|
||||
build_date: ${{ steps.vars.outputs.build_date }}
|
||||
steps:
|
||||
- name: Set version fields
|
||||
id: vars
|
||||
run: |
|
||||
echo "release=$(echo $GITHUB_REF | cut -d / -f 3 | sed 's/^v//')" >> $GITHUB_OUTPUT
|
||||
echo "commit=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
|
||||
docker-image:
|
||||
needs: docker-configure
|
||||
uses: ./.github/workflows/docker-image.yaml
|
||||
secrets: inherit
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||
with:
|
||||
release: ${{ needs.docker-configure.outputs.release }}
|
||||
commit: ${{ needs.docker-configure.outputs.commit }}
|
||||
build_date: ${{ needs.docker-configure.outputs.build_date }}
|
||||
sign: true
|
||||
|
||||
provenance-for-images-docker:
|
||||
needs: [docker-configure, docker-image]
|
||||
permissions:
|
||||
actions: read # for detecting the Github Actions environment.
|
||||
id-token: write # for creating OIDC tokens for signing.
|
||||
packages: write # for uploading attestations.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: docker.io/falcosecurity/falcoctl
|
||||
# The image digest is used to prevent TOCTOU issues.
|
||||
# This is an output of the docker/build-push-action
|
||||
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
|
||||
digest: ${{ needs.docker-image.outputs.digest }}
|
||||
secrets:
|
||||
registry-username: ${{ secrets.DOCKERHUB_USER }}
|
||||
registry-password: ${{ secrets.DOCKERHUB_SECRET }}
|
||||
|
||||
login-to-amazon-ecr:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::292999226676:role/github_actions-falcoctl-ecr
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr-public
|
||||
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
|
||||
with:
|
||||
registry-type: public
|
||||
mask-password: 'false'
|
||||
outputs:
|
||||
registry: ${{ steps.login-ecr-public.outputs.registry }}
|
||||
docker_username: ${{ steps.login-ecr-public.outputs.docker_username_public_ecr_aws }}
|
||||
docker_password: ${{ steps.login-ecr-public.outputs.docker_password_public_ecr_aws }}
|
||||
|
||||
provenance-for-images-aws-ecr:
|
||||
needs: [docker-configure, docker-image, login-to-amazon-ecr]
|
||||
permissions:
|
||||
actions: read # for detecting the Github Actions environment.
|
||||
id-token: write # for creating OIDC tokens for signing.
|
||||
packages: write # for uploading attestations.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: public.ecr.aws/falcosecurity/falcoctl
|
||||
# The image digest is used to prevent TOCTOU issues.
|
||||
# This is an output of the docker/build-push-action
|
||||
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
|
||||
digest: ${{ needs.docker-image.outputs.digest }}
|
||||
secrets:
|
||||
registry-username: ${{ needs.login-to-amazon-ecr.outputs.docker_username }}
|
||||
registry-password: ${{ needs.login-to-amazon-ecr.outputs.docker_password }}
|
|
@ -1,3 +1,6 @@
|
|||
*.idea
|
||||
*.idea*
|
||||
.idea/*
|
||||
.idea/*
|
||||
.vscode/*
|
||||
falcoctl
|
||||
dist/
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
run:
|
||||
timeout: 10m
|
||||
|
||||
linters-settings:
|
||||
exhaustive:
|
||||
check-generated: false
|
||||
default-signifies-exhaustive: true
|
||||
|
||||
goheader:
|
||||
values:
|
||||
const:
|
||||
AUTHORS: The Falco Authors
|
||||
template: |-
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
Copyright (C) {{ YEAR }} {{ AUTHORS }}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
lll:
|
||||
line-length: 150
|
||||
|
||||
gci:
|
||||
sections:
|
||||
- standard # Captures all standard packages if they do not match another section.
|
||||
- default # Contains all imports that could not be matched to another section type.
|
||||
- prefix(github.com/falcosecurity/falcoctl) # Groups all imports with the specified Prefix.
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- experimental
|
||||
- opinionated
|
||||
- performance
|
||||
- style
|
||||
goimports:
|
||||
local-prefixes: github.com/falcosecurity/falcoctl
|
||||
misspell:
|
||||
locale: US
|
||||
nolintlint:
|
||||
allow-unused: false # report any unused nolint directives
|
||||
require-explanation: true # require an explanation for nolint directives
|
||||
require-specific: true # require nolint directives to be specific about which linter is being skipped
|
||||
dupl:
|
||||
threshold: 300
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- copyloopvar
|
||||
# - funlen
|
||||
# - gochecknoglobals
|
||||
# - gochecknoinits
|
||||
# - gocognit
|
||||
- gci
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
# - godox
|
||||
# - goerr113
|
||||
- gofmt
|
||||
- goheader
|
||||
- goimports
|
||||
- gomodguard
|
||||
# - gomnd
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- lll
|
||||
# - maligned
|
||||
- misspell
|
||||
- nakedret
|
||||
# - nestif
|
||||
- noctx
|
||||
- nolintlint
|
||||
# - prealloc
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- staticcheck
|
||||
- stylecheck
|
||||
# - testpackage
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- whitespace
|
||||
# - wsl
|
||||
|
||||
issues:
|
||||
#fix: true
|
||||
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
# Disable the default exclude patterns (as they disable the mandatory comments)
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
# errcheck: Almost all programs ignore errors on these functions and in most cases it's ok
|
||||
- Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked
|
||||
|
||||
exclude-rules:
|
||||
- linters:
|
||||
- govet
|
||||
text: 'declaration of "(err|ctx)" shadows declaration at'
|
||||
- linters:
|
||||
- errorlint
|
||||
# Disable the check to test errors type assertion on switches.
|
||||
text: type switch on error will fail on wrapped errors. Use errors.As to check for specific errors
|
||||
|
||||
# Disable goheader for multi authors files
|
||||
- linters:
|
||||
- goheader
|
||||
path: pkg/oci/authn/credentialstore.go
|
||||
|
||||
# Exclude the following linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gosec
|
|
@ -0,0 +1,52 @@
|
|||
version: 2
|
||||
|
||||
project_name: falcoctl
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- id: "falcoctl"
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: windows
|
||||
goarch: 386
|
||||
|
||||
ldflags: |
|
||||
-X github.com/falcosecurity/falcoctl/cmd/version.buildDate={{ .Date }}
|
||||
-X github.com/falcosecurity/falcoctl/cmd/version.gitCommit={{ .Commit }}
|
||||
-X github.com/falcosecurity/falcoctl/cmd/version.semVersion={{ if .IsSnapshot }}{{ .Commit }}{{ else }}{{ .Version }}{{ end }}
|
||||
-s
|
||||
-w
|
||||
main: .
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
- CGO_ENABLED=0
|
||||
|
||||
archives:
|
||||
- id: windows
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .ShortCommit }}"
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
mode: replace
|
||||
|
||||
changelog:
|
||||
use: github-native
|
||||
|
||||
git:
|
||||
tag_sort: -version:creatordate
|
|
@ -0,0 +1,191 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2019 The Falco Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,84 @@
|
|||
SHELL=/bin/bash -o pipefail
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
GOBIN=$(shell go env GOPATH)/bin
|
||||
else
|
||||
GOBIN=$(shell go env GOBIN)
|
||||
endif
|
||||
|
||||
GO ?= go
|
||||
DOCKER ?= docker
|
||||
|
||||
# version settings
|
||||
RELEASE?=$(shell git rev-parse HEAD)
|
||||
COMMIT?=$(shell git rev-parse HEAD)
|
||||
BUILD_DATE?=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
PROJECT?=github.com/falcosecurity/falcoctl
|
||||
|
||||
# todo(leogr): re-enable race when CLI tests can run with race enabled
|
||||
TEST_FLAGS ?= -v -cover# -race
|
||||
|
||||
.PHONY: falcoctl
|
||||
falcoctl:
|
||||
$(GO) build -ldflags \
|
||||
"-X '${PROJECT}/cmd/version.semVersion=${RELEASE}' \
|
||||
-X '${PROJECT}/cmd/version.gitCommit=${COMMIT}' \
|
||||
-X '${PROJECT}/cmd/version.buildDate=${BUILD_DATE}'" \
|
||||
-o falcoctl .
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(GO) vet ./...
|
||||
$(GO) test ${TEST_FLAGS} ./...
|
||||
|
||||
# Install gci if not available
|
||||
.PHONY: gci
|
||||
gci:
|
||||
ifeq (, $(shell which gci))
|
||||
@go install github.com/daixiang0/gci@v0.11.1
|
||||
GCI=$(GOBIN)/gci
|
||||
else
|
||||
GCI=$(shell which gci)
|
||||
endif
|
||||
|
||||
# Install addlicense if not available
|
||||
.PHONY: addlicense
|
||||
addlicense:
|
||||
ifeq (, $(shell which addlicense))
|
||||
@go install github.com/google/addlicense@v1.0.0
|
||||
ADDLICENSE=$(GOBIN)/addlicense
|
||||
else
|
||||
ADDLICENSE=$(shell which addlicense)
|
||||
endif
|
||||
|
||||
# Run go fmt against code and add the licence header
|
||||
.PHONY: fmt
|
||||
fmt: gci addlicense
|
||||
go mod tidy
|
||||
go fmt ./...
|
||||
find . -type f -name '*.go' -a -exec $(GCI) write -s standard -s default -s "prefix(github.com/falcosecurity/falcoctl)" {} \;
|
||||
find . -type f -name '*.go' -exec $(ADDLICENSE) -l apache -s -c "The Falco Authors" -y "$(shell date +%Y)" {} \;
|
||||
|
||||
# Install golangci-lint if not available
|
||||
.PHONY: golangci-lint
|
||||
golangci-lint:
|
||||
ifeq (, $(shell which golangci-lint))
|
||||
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2
|
||||
GOLANGCILINT=$(GOBIN)/golangci-lint
|
||||
else
|
||||
GOLANGCILINT=$(shell which golangci-lint)
|
||||
endif
|
||||
|
||||
# It works when called in a branch different than main.
|
||||
# "--new-from-rev REV Show only new issues created after git revision REV"
|
||||
.PHONY: lint
|
||||
lint: golangci-lint
|
||||
$(GOLANGCILINT) run --new-from-rev main
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
$(DOCKER) build -f ./build/Dockerfile . --build-arg RELEASE=${RELEASE} --build-arg COMMIT=${COMMIT} --build-arg BUILD_DATE=${BUILD_DATE}
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@rm falcoctl || true
|
|
@ -0,0 +1,14 @@
|
|||
approvers:
|
||||
- leogr
|
||||
- zuc
|
||||
- maxgio92
|
||||
- fededp
|
||||
- cpanato
|
||||
- alacuku
|
||||
- loresuso
|
||||
emeritus_approvers:
|
||||
- kris-nova
|
||||
- markyjackson-taulia
|
||||
- leodido
|
||||
- fntlnz
|
||||
- mstemm
|
|
@ -0,0 +1,404 @@
|
|||
# 🧰 falcoctl
|
||||
|
||||
[](https://github.com/falcosecurity/evolution/blob/main/REPOSITORIES.md#core-scope) [](https://github.com/falcosecurity/evolution/blob/main/REPOSITORIES.md#stable) [](./LICENSE)
|
||||
|
||||
The official CLI tool for working with [Falco](https://github.com/falcosecurity/falco) and its [ecosystem components](https://falco.org/docs/#what-are-the-ecosystem-projects-that-can-interact-with-falco).
|
||||
|
||||
## Installation
|
||||
### Install falcoctl manually
|
||||
You can download and install *falcoctl* manually following the appropriate instructions based on your operating system architecture.
|
||||
#### Linux
|
||||
##### AMD64
|
||||
```bash
|
||||
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_linux_amd64.tar.gz" | tar -xz
|
||||
sudo install -o root -g root -m 0755 falcoctl /usr/local/bin/falcoctl
|
||||
```
|
||||
##### ARM64
|
||||
```bash
|
||||
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_linux_arm64.tar.gz" | tar -xz
|
||||
sudo install -o root -g root -m 0755 falcoctl /usr/local/bin/falcoctl
|
||||
```
|
||||
> NOTE: Make sure */usr/local/bin* is in your PATH environment variable.
|
||||
|
||||
#### MacOS
|
||||
The easiest way to install on MacOS is via `Homebrew`:
|
||||
```bash
|
||||
brew install falcoctl
|
||||
```
|
||||
|
||||
Alternatively, you can download directly from the source:
|
||||
|
||||
##### Intel
|
||||
```bash
|
||||
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_darwin_amd64.tar.gz" | tar -xz
|
||||
chmod +x falcoctl
|
||||
sudo mv falcoctl /usr/local/bin/falcoctl
|
||||
```
|
||||
##### Apple Silicon
|
||||
```bash
|
||||
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_darwin_arm64.tar.gz" | tar -xz
|
||||
chmod +x falcoctl
|
||||
sudo mv falcoctl /usr/local/bin/falcoctl
|
||||
```
|
||||
|
||||
Alternatively, you can manually download *falcoctl* from the [falcoctl releases](https://github.com/falcosecurity/falcoctl/releases) page on GitHub.
|
||||
|
||||
### Install falcoctl from source
|
||||
You can install *falcoctl* from source. First thing clone the *falcoctl* repository, build the *falcoctl* binary, and move it to a file location in your system **PATH**.
|
||||
```bash
|
||||
git clone https://github.com/falcosecurity/falcoctl.git
|
||||
cd falcoctl
|
||||
make falcoctl
|
||||
sudo mv falcoctl /usr/local/bin/falcoctl
|
||||
```
|
||||
|
||||
# Getting Started
|
||||
|
||||
## Installing an artifact
|
||||
|
||||
This tutorial aims at presenting how to install a Falco artifact. The next few steps will present us with the fundamental commands of *falcoctl* and how to use them.
|
||||
|
||||
First thing, we need to add a new `index` to *falcoctl*:
|
||||
```bash
|
||||
$ falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
```
|
||||
We just downloaded the metadata of the **artifacts** hosted and distributed by the **falcosecurity** organization and made them available to the *falcoctl* tool.
|
||||
Now let's check that the `index` file is in place by running:
|
||||
```
|
||||
$ falcoctl index list
|
||||
```
|
||||
We should get an output similar to this one:
|
||||
```
|
||||
NAME URL ADDED UPDATED
|
||||
falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml 2022-10-25 15:01:25 2022-10-25 15:01:25
|
||||
```
|
||||
Now let's search all the artifacts related to *cloudtrail*:
|
||||
```
|
||||
$ falcoctl artifact search cloudtrail
|
||||
INDEX ARTIFACT TYPE REGISTRY REPOSITORY
|
||||
falcosecurity cloudtrail plugin ghcr.io falcosecurity/plugins/plugin/cloudtrail
|
||||
falcosecurity cloudtrail-rules rulesfile ghcr.io falcosecurity/plugins/ruleset/cloudtrail
|
||||
```
|
||||
Lets install the *cloudtrail plugin*:
|
||||
```
|
||||
$ falcoctl artifact install cloudtrail --plugins-dir=./
|
||||
INFO Reading all configured index files from "/home/aldo/.config/falcoctl/indexes.yaml"
|
||||
INFO Preparing to pull "ghcr.io/falcosecurity/plugins/plugin/cloudtrail:latest"
|
||||
INFO Remote registry "ghcr.io" implements docker registry API V2
|
||||
INFO Pulling 44136fa355b3: ############################################# 100%
|
||||
INFO Pulling 80e0c33f30c0: ############################################# 100%
|
||||
INFO Pulling b024dd7a2a63: ############################################# 100%
|
||||
INFO Artifact successfully installed in "./"
|
||||
```
|
||||
Install the *cloudtrail-rules* rulesfile:
|
||||
```
|
||||
$ ./falcoctl artifact install cloudtrail-rules --rulesfiles-dir=./
|
||||
INFO Reading all configured index files from "/home/aldo/.config/falcoctl/indexes.yaml"
|
||||
INFO Preparing to pull "ghcr.io/falcosecurity/plugins/ruleset/cloudtrail:latest"
|
||||
INFO Remote registry "ghcr.io" implements docker registry API V2
|
||||
INFO Pulling 44136fa355b3: ############################################# 100%
|
||||
INFO Pulling e0dccb7b0f1d: ############################################# 100%
|
||||
INFO Pulling 575bced78731: ############################################# 100%
|
||||
INFO Artifact successfully installed in "./"
|
||||
```
|
||||
|
||||
We should have now two new files in the current directory: `aws_cloudtrail_rules.yaml` and `libcloudtrail.so`.
|
||||
|
||||
# Falcoctl Configuration Files
|
||||
|
||||
## `/etc/falcoctl/falcoctl.yaml`
|
||||
|
||||
The `falco configuration file` is a yaml file that contains some metadata about the `falcoctl` behaviour.
|
||||
It contains the list of the indexes where the artifacts are listed, how often and which artifacts needed to be updated periodically.
|
||||
The default configuration is stored in `/etc/falcoctl/falcoctl.yaml`.
|
||||
This is an example of a falcoctl configuration file:
|
||||
|
||||
``` yaml
|
||||
artifact:
|
||||
follow:
|
||||
every: 6h0m0s
|
||||
falcoVersions: http://localhost:8765/versions
|
||||
refs:
|
||||
- falco-rules:0
|
||||
- my-rules:1
|
||||
install:
|
||||
refs:
|
||||
- cloudtrail-rules:latest
|
||||
- cloudtrail:latest
|
||||
rulesfilesdir: /tmp/rules
|
||||
pluginsdir: /tmp/plugins
|
||||
indexes:
|
||||
- name: falcosecurity
|
||||
url: https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
- name: my-index
|
||||
url: https://example.com/falcoctl/index.yaml
|
||||
registry:
|
||||
auth:
|
||||
basic:
|
||||
- password: password
|
||||
registry: myregistry.example.com:5000
|
||||
user: user
|
||||
oauth:
|
||||
- registry: myregistry.example.com:5001
|
||||
clientsecret: "999999"
|
||||
clientid: "000000"
|
||||
tokenurl: http://myregistry.example.com:9096/token
|
||||
gcp:
|
||||
- registry: europe-docker.pkg.dev
|
||||
```
|
||||
|
||||
## `~/.config/falcoctl/`
|
||||
|
||||
The `~/.config/falcoctl/` directory contains:
|
||||
- *cache objects*
|
||||
- *OAuth2 client credentials*
|
||||
|
||||
### `~/.config/falcoctl/indexes.yaml`
|
||||
|
||||
This file is used for cache purposes and contains the *index refs* added by the command `falcoctl index add [name] [ref]`. The *index ref* is enriched with two timestamps to track when it was added and the last time is was updated. Once the *index ref* is added, `falcoctl` will download the real index in the `~/.config/falcoctl/indexes/` directory. Moreover, every time the index is fetched, the `updated_timestamp` is updated.
|
||||
|
||||
### `~/.config/falcoctl/clientcredentials.json`
|
||||
|
||||
The command `falcoctl registry auth oauth` will add the `clientcredentials.json` file to the `~/.config/falcoctl/` directory. That file will contain all the needed information for the OAuth2 authetication.
|
||||
|
||||
# Falcoctl Commands
|
||||
|
||||
## Falcoctl index
|
||||
|
||||
The `index` file is a yaml file that contains some metadata about the Falco **artifacts**. Each entry carries information such as the name, type, registry, repository and other info for the given **artifact**. Different *falcoctl* commands rely on the metadata contained in the `index` file for their operation.
|
||||
This is an example of an index file:
|
||||
```yaml
|
||||
- name: okta
|
||||
type: plugin
|
||||
registry: ghcr.io
|
||||
repository: falcosecurity/plugins/plugin/okta
|
||||
description: Okta Log Events
|
||||
home: https://github.com/falcosecurity/plugins/tree/master/plugins/okta
|
||||
keywords:
|
||||
- audit
|
||||
- log-events
|
||||
- okta
|
||||
license: Apache-2.0
|
||||
maintainers:
|
||||
- email: cncf-falco-dev@lists.cncf.io
|
||||
name: The Falco Authors
|
||||
sources:
|
||||
- https://github.com/falcosecurity/plugins/tree/master/plugins/okta
|
||||
- name: okta-rules
|
||||
type: rulesfile
|
||||
registry: ghcr.io
|
||||
repository: falcosecurity/plugins/ruleset/okta
|
||||
description: Okta Log Events
|
||||
home: https://github.com/falcosecurity/plugins/tree/master/plugins/okta
|
||||
keywords:
|
||||
- audit
|
||||
- log-events
|
||||
- okta
|
||||
- okta-rules
|
||||
license: Apache-2.0
|
||||
maintainers:
|
||||
- email: cncf-falco-dev@lists.cncf.io
|
||||
name: The Falco Authors
|
||||
sources:
|
||||
- https://github.com/falcosecurity/plugins/tree/master/plugins/okta/rules
|
||||
```
|
||||
|
||||
### Index Storage Backends
|
||||
|
||||
Indices for *falcoctl* can be retrieved from various storage backends. The supported index storage backends are listed in the table below. Note if you do not specify a backend type when adding a new index *falcoctl* will try to guess based on the `URI Scheme`:
|
||||
|
||||
| Name | URI Scheme | Description |
|
||||
| ----- | ---------- | --------------------------------------------------------------------------------------------- |
|
||||
| http | http:// | Can be used to retrieve indices via simple HTTP GET requests. |
|
||||
| https | https:// | Convenience alias for the HTTP backend. |
|
||||
| gcs | gs:// | For indices stored as Google Cloud Storage objects. Supports application default credentials. |
|
||||
| file | file:// | For indices stored on the local file system. |
|
||||
| s3 | s3:// | For indices stored as AWS S3 objects. Supports default credentials, IRSA. |
|
||||
|
||||
|
||||
#### falcoctl index add
|
||||
New indexes are configured to be used by the *falcoctl* tool by adding them through the `index add` command. There are no limits to the number of indexes that can be added to the *falcoctl* tool. When adding a new index the tool adds a new entry in a file called **indexes.yaml** and downloads the *index* file in `~/.config/falcoctl`. The same folder is used to store the **indexes.yaml** file, too.
|
||||
The following command adds a new index named *falcosecurity*:
|
||||
```bash
|
||||
$ falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
```
|
||||
|
||||
The following command adds the same index *falcosecurity*, but explicitly sets the storage backend to `https`:
|
||||
```bash
|
||||
$ falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml https
|
||||
```
|
||||
#### falcoctl index list
|
||||
Using the `index list` command you can check the configured `indexes` in your local system:
|
||||
```bash
|
||||
$ falcoctl index list
|
||||
NAME URL ADDED UPDATED
|
||||
$ falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml 2022-10-25 15:01:25 2022-10-25 15:01:25
|
||||
```
|
||||
#### falcoctl index update
|
||||
The `index update` allows to update a previously configured `index` file by syncing the local one with the remote one:
|
||||
```bash
|
||||
$ falcoctl index update falcosecurity
|
||||
```
|
||||
#### falcoctl index remove
|
||||
When we want to remove an `index` file that we configured previously, the `index remove` command is the one we need:
|
||||
```bash
|
||||
$ falcoctl index remove falcosecurity
|
||||
```
|
||||
The above command will remove the **falcosecurity** index from the local system.
|
||||
|
||||
## Falcoctl artifact
|
||||
The *falcoctl* tool provides different commands to interact with Falco **artifacts**. It makes easy to *seach*, *install* and get *info* for the **artifacts** provided by a given `index` file. For these commands to properly work we need to configure at least an `index` file in our system as shown in the previus section.
|
||||
#### Falcoctl artifact search
|
||||
The `artifact search` command allows to search for **artifacts** provided by the `index` files configured in *falcoctl*. The command supports searches by name or by keywords and displays all the **artifacts** that match the search. Assuming that we have already configured the `index` provided by the `falcosecurity` organization, the following command shows all the **artifacts** that work with **Kubernetes**:
|
||||
```bash
|
||||
$ falcoctl artifact search kubernetes
|
||||
INDEX ARTIFACT TYPE REGISTRY REPOSITORY
|
||||
falcosecurity k8saudit plugin ghcr.io falcosecurity/plugins/plugin/k8saudit
|
||||
falcosecurity k8saudit-rules rulesfile ghcr.io falcosecurity/plugins/ruleset/k8saudit
|
||||
```
|
||||
|
||||
#### Falcoctl artifact info
|
||||
As per the name, `artifact info` prints some info for a given **artifact**:
|
||||
```bash
|
||||
$ falcoctl artifact info k8saudit
|
||||
REF TAGS
|
||||
ghcr.io/falcosecurity/plugins/plugin/k8saudit 0.1.0 0.2.0 0.2.1 0.3.0 0.4.0-rc1 0.4.0 latest
|
||||
```
|
||||
It shows the OCI **reference** and **tags** for the **artifact** of interest. Thot info is usually used with other commands.
|
||||
|
||||
#### Falcoctl artifact install
|
||||
The above commands help us to find all the necessary info for a given **artifact**. The `artifact install` command installs an **artifact**. It pulls the **artifact** from remote repository, and saves it in a given directory. The following command installs the *k8saudit* plugin in the default path:
|
||||
```bash
|
||||
$ falcoctl artifact install k8saudit
|
||||
INFO Reading all configured index files from "/home/aldo/.config/falcoctl/indexes.yaml"
|
||||
INFO Preparing to pull "ghcr.io/falcosecurity/plugins/plugin/k8saudit:latest"
|
||||
INFO Remote registry "ghcr.io" implements docker registry API V2
|
||||
INFO Pulling 44136fa355b3: ############################################# 100%
|
||||
INFO Pulling ded0b5419f40: ############################################# 100%
|
||||
INFO Pulling 107d1230f3f0: ############################################# 100%
|
||||
INFO Artifact successfully installed in "/usr/share/falco/plugins"
|
||||
```
|
||||
|
||||
By default, if we give the name of an **artifact** it will search for the **artifact** in the configured `index` files and downlaod the `latest` version. The commands accepts also the OCI **reference** of an **artifact**. In this case, it will ignore the local `index` files.
|
||||
The command has two flags:
|
||||
* `--plugins-dir`: directory where to install plugins. Defaults to `/usr/share/falco/plugins`;
|
||||
* `--rulesfiles-dir`: directory where to install rules. Defaults to `/etc/falco`.
|
||||
|
||||
> If the repositories of the **artifacts** your are trying to install are not public then you need to authenticate to the remote registry.
|
||||
|
||||
#### Falcoctl artifact follow
|
||||
The above commands allow us to keep up-to-date one or more given **artifacts**. The `artifact follow` command checks for updates on a periodic basis and then downloads and installs the latest version, as specified by the passed tags.
|
||||
It pulls the **artifact** from remote repository, and saves it in a given directory. The following command installs the *github-rules* rulesfile in the default path:
|
||||
```bash
|
||||
$ falcoctl artifact follow github-rules
|
||||
WARN falcosecurity already exists with the same configuration, skipping
|
||||
INFO Reading all configured index files from "/root/.config/falcoctl/indexes.yaml"
|
||||
INFO: Creating follower for "github-rules", with check every 6h0m0s
|
||||
INFO Starting follower for "ghcr.io/falcosecurity/plugins/ruleset/github:latest"
|
||||
INFO (ghcr.io/falcosecurity/plugins/ruleset/github:latest) found new version under tag "latest"
|
||||
INFO (ghcr.io/falcosecurity/plugins/ruleset/github:latest) artifact with tag "latest" correctly installed
|
||||
|
||||
```
|
||||
|
||||
By default, if we give the name of an **artifact** it will search for the **artifact** in the configured `index` files and downlaod the `latest` version. The commands accepts also the OCI **reference** of an **artifact**. In this case, it will ignore the local `index` files.
|
||||
The command can specify the directory where to install the *rulesfile* artifacts through the `--rulesfiles-dir` flag (defaults to `/etc/falco`).
|
||||
|
||||
> If the repositories of the **artifacts** your are trying to install are not public then you need to authenticate to the remote registry.
|
||||
|
||||
> Please note that only **rulesfile** artifact can be followed.
|
||||
|
||||
## Falcoctl registry
|
||||
|
||||
The `registry` commands interact with OCI registries allowing the user to authenticate, pull and push artifacts. We have tested the *falcoctl* tool with the **ghcr.io** registry, but it should work with all the registries that support the OCI artifacts.
|
||||
|
||||
### Falcoctl registry auth
|
||||
The `registry auth` command authenticates a user to a given OCI registry.
|
||||
|
||||
#### Falcoctl registry auth basic
|
||||
The `registry auth basic` command authenticates a user to a given OCI registry using HTTP Basic Authentication. Run the command in advance for any private registries.
|
||||
|
||||
#### Falcoctl registry auth oauth
|
||||
The `registry auth oauth` command retrieves access and refresh tokens for OAuth2.0 client credentials flow authentication. Run the command in advance for any private registries.
|
||||
|
||||
#### Falcoctl registry auth gcp
|
||||
The `registry auth gcp` command retrieves access tokens using [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials). In particular, it supports access token retrieval using Google Compute Engine metadata server and Workload Identity, useful to authenticate your deployed Falco workloads. Run the command in advance for Artifact Registry authentication.
|
||||
|
||||
Two typical use cases:
|
||||
|
||||
1. You are manipulating some rules or plugins and use `falcoctl` to pull or push to an Artifact Registry:
|
||||
1. run `gcloud auth application-default login` to generate a JSON credential file that will be used by applications.
|
||||
2. run `falcoctl registry auth gcp europe-docker.pkg.dev` for instance to use Application Default Credentials to connect to any repository hosted at `europe-docker.pkg.dev`.
|
||||
2. You have a Falco instance with Falcoctl as a side car, running in a GKE cluster with Workload Identity enabled:
|
||||
1. Workload Identity is correctly set up for the Falco instance (see the [documentation](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity)).
|
||||
2. Add an environment variable like `FALCOCTL_REGISTRY_AUTH_GCP=europe-docker.pkg.dev` to enable GCP authentication for the `europe-docker.pkg.dev` registry.
|
||||
3. The Falcoctl instance will get access tokens from the metadata server and use them to authenticate to the registry and download your rules.
|
||||
|
||||
### Falcoctl registry push
|
||||
It pushes local files and references the artifact uniquely. The following command shows how to push a local file to a remote registry:
|
||||
```bash
|
||||
$ falcoctl registry push --type=plugin ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0 clouddrail-0.3.0-linux-x86_64.tar.gz --platform linux/amd64
|
||||
```
|
||||
The type denotes the **artifact** type in this case *plugins*. The `ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0` is the unique reference that points to the **artifact**.
|
||||
Currently, *falcoctl* supports only two types of artifacts: **plugin** and **rulesfile**. Based on **artifact type** the commands accepts different flags:
|
||||
* `--add-floating-tags`: add the floating tags for the major and minor versions
|
||||
* `--annotation-source`: set annotation source for the artifact;
|
||||
* `--depends-on`: set an artifact dependency (can be specified multiple times). Example: `--depends-on my-plugin:1.2.3`
|
||||
* `--tag`: additional artifact tag. Can be repeated multiple time
|
||||
* `--type`: type of artifact to be pushed. Allowed values: `rulesfile`, `plugin`, `asset`
|
||||
|
||||
### Falcoctl registry pull
|
||||
Pulling **artifacts** involves specifying the reference. The type of **artifact** is not required since the tool will implicitly extract it from the OCI **artifact**:
|
||||
```
|
||||
$ falcoctl registry pull ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0
|
||||
```
|
||||
|
||||
# Falcoctl Environment Variables
|
||||
|
||||
The arguments of `falcoctl` can passed as arguments through:
|
||||
- command line options
|
||||
- environment variables
|
||||
- configuration file
|
||||
|
||||
The `falcoctl` arguments can be passed through these different modalities are prioritized in the following order: command line options, environment variables, and finally the configuration file. This means that if an argument is passed through multiple modalities, the value set in the command line options will take precedence over the value set in environment variables, which will in turn take precedence over the value set in the configuration file.
|
||||
|
||||
This is the list of the environment variable that `falcoctl` will use:
|
||||
|
||||
| Name | Content |
|
||||
| ----------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `FALCOCTL_REGISTRY_AUTH_BASIC` | `registry,username,password;registry1,username1,password1` |
|
||||
| `FALCOCTL_REGISTRY_AUTH_OAUTH` | `registry,client-id,client-secret,token-url;registry1` |
|
||||
| `FALCOCTL_REGISTRY_AUTH_GCP` | `registry;registry1` |
|
||||
| `FALCOCTL_INDEXES` | `index-name,https://falcosecurity.github.io/falcoctl/index.yaml` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_EVERY` | `6h0m0s` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_CRON` | `cron-formatted-string` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_REFS` | `ref1;ref2` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_FALCOVERSIONS` | `falco-version-url` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_RULESFILEDIR` | `rules-directory-path` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_PLUGINSDIR` | `plugins-directory-path` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_TMPDIR` | `tmp-directory-path` |
|
||||
| `FALCOCTL_ARTIFACT_INSTALL_REFS` | `ref1;ref2` |
|
||||
| `FALCOCTL_ARTIFACT_INSTALL_RULESFILESDIR` | `rules-directory-path` |
|
||||
| `FALCOCTL_ARTIFACT_INSTALL_PLUGINSDIR` | `plugins-directory-path` |
|
||||
| `FALCOCTL_ARTIFACT_NOVERIFY` | |
|
||||
|
||||
Please note that when passing multiple arguments via an environment variable, they must be separated by a semicolon. Moreover, multiple fields of the same argument must be separated by a comma.
|
||||
|
||||
Here is an example of `falcoctl` usage with environment variables:
|
||||
|
||||
```bash
|
||||
$ export FALCOCTL_REGISTRY_AUTH_OAUTH="localhost:6000,000000,999999,http://localhost:9096/token"
|
||||
$ falcoctl registry oauth
|
||||
```
|
||||
|
||||
# Container image signature verification
|
||||
|
||||
Official container images for Falcoctl, starting from version 0.5.0, are signed with [cosign](https://github.com/sigstore/cosign) v2. To verify the signature run:
|
||||
|
||||
```bash
|
||||
$ FALCOCTL_VERSION=x.y.z # e.g. 0.5.0
|
||||
$ cosign verify docker.io/falcosecurity/falcoctl:$FALCOCTL_VERSION --certificate-oidc-issuer=https://token.actions.githubusercontent.com --certificate-identity-regexp=https://github.com/falcosecurity/falcoctl/ --certificate-github-workflow-ref=refs/tags/v$FALCOCTL_VERSION
|
||||
```
|
|
@ -0,0 +1,36 @@
|
|||
FROM cgr.dev/chainguard/go AS builder
|
||||
WORKDIR /tmp/builder
|
||||
|
||||
ARG RELEASE
|
||||
ARG COMMIT
|
||||
ARG BUILD_DATE
|
||||
ARG PROJECT=github.com/falcosecurity/falcoctl
|
||||
|
||||
RUN test -n "$RELEASE" || ( echo "The RELEASE argument is unset. Aborting" && false )
|
||||
RUN test -n "$COMMIT" || ( echo "The COMMIT argument is unset. Aborting" && false )
|
||||
RUN test -n "$BUILD_DATE" || ( echo "The BUILD_DATE argument is unset. Aborting" && false )
|
||||
|
||||
COPY go.mod ./go.mod
|
||||
COPY go.sum ./go.sum
|
||||
RUN go mod download
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN CGO_ENABLED=0 \
|
||||
GOOS=$(go env GOOS) \
|
||||
GOARCH=$(go env GOARCH) \
|
||||
go build -ldflags \
|
||||
"-s \
|
||||
-w \
|
||||
-X '${PROJECT}/cmd/version.semVersion=${RELEASE}' \
|
||||
-X '${PROJECT}/cmd/version.gitCommit=${COMMIT}' \
|
||||
-X '${PROJECT}/cmd/version.buildDate=${BUILD_DATE}'" \
|
||||
./
|
||||
|
||||
RUN echo ${RELEASE}
|
||||
|
||||
FROM cgr.dev/chainguard/static:latest
|
||||
|
||||
COPY --from=builder /tmp/builder/falcoctl /usr/bin/falcoctl
|
||||
|
||||
ENTRYPOINT [ "/usr/bin/falcoctl" ]
|
|
@ -0,0 +1,78 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
artifactconfig "github.com/falcosecurity/falcoctl/cmd/artifact/config"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/follow"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/info"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/install"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/list"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/manifest"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/search"
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/cache"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewArtifactCmd return the artifact command.
|
||||
func NewArtifactCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "artifact",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Interact with Falco artifacts",
|
||||
Long: "Interact with Falco artifacts",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
var indexes []config.Index
|
||||
var indexCache *cache.Cache
|
||||
var err error
|
||||
|
||||
opt.Initialize()
|
||||
if err = config.Load(opt.ConfigFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add indexes if needed
|
||||
// Set up basic authentication
|
||||
if indexes, err = config.Indexes(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the index cache.
|
||||
if indexCache, err = cache.NewFromConfig(ctx, config.IndexesFile, config.IndexesDir, indexes); err != nil {
|
||||
return err
|
||||
}
|
||||
// Save the index cache for later use by the sub commands.
|
||||
opt.Initialize(commonoptions.WithIndexCache(indexCache))
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(search.NewArtifactSearchCmd(ctx, opt))
|
||||
cmd.AddCommand(install.NewArtifactInstallCmd(ctx, opt))
|
||||
cmd.AddCommand(list.NewArtifactListCmd(ctx, opt))
|
||||
cmd.AddCommand(info.NewArtifactInfoCmd(ctx, opt))
|
||||
cmd.AddCommand(follow.NewArtifactFollowCmd(ctx, opt))
|
||||
cmd.AddCommand(artifactconfig.NewArtifactConfigCmd(ctx, opt))
|
||||
cmd.AddCommand(manifest.NewArtifactManifestCmd(ctx, opt))
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
ocipuller "github.com/falcosecurity/falcoctl/pkg/oci/puller"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type artifactConfigOptions struct {
|
||||
*options.Common
|
||||
*options.Registry
|
||||
platform string
|
||||
}
|
||||
|
||||
// NewArtifactConfigCmd returns the artifact config command.
|
||||
func NewArtifactConfigCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactConfigOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "config [ref] [flags]",
|
||||
Short: "Get the config layer of an artifact",
|
||||
Long: "Get the config layer of an artifact",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactConfig(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
cmd.Flags().StringVar(&o.platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
"os and architecture of the artifact in OS/ARCH format")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *artifactConfigOptions) RunArtifactConfig(ctx context.Context, args []string) error {
|
||||
var (
|
||||
puller *ocipuller.Puller
|
||||
ref string
|
||||
config []byte
|
||||
err error
|
||||
)
|
||||
|
||||
// Create puller with auto login enabled.
|
||||
if puller, err = ociutils.Puller(o.PlainHTTP, o.Printer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve the artifact reference.
|
||||
if ref, err = o.IndexCache.ResolveReference(args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: implement two new flags (platforms, platform) based on the oci platform struct.
|
||||
// Split the platform.
|
||||
tokens := strings.Split(o.platform, "/")
|
||||
if len(tokens) != 2 {
|
||||
return fmt.Errorf("invalid platform format: %s", o.platform)
|
||||
}
|
||||
|
||||
if config, err = puller.RawConfigLayer(ctx, ref, tokens[0], tokens[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Printer.DefaultText.Println(string(config))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
//SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
localRegistryHost string
|
||||
localRegistry *remote.Registry
|
||||
testRuleTarball = "../../../pkg/test/data/rules.tar.gz"
|
||||
testPluginTarball = "../../../pkg/test/data/plugin.tar.gz"
|
||||
testPluginPlatform1 = "linux/amd64"
|
||||
testPluginPlatform2 = "windows/amd64"
|
||||
testPluginPlatform3 = "linux/arm64"
|
||||
ctx = context.Background()
|
||||
pluginMultiPlatformRef string
|
||||
rulesRef string
|
||||
artifactWithoutConfigRef string
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Config Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
var err error
|
||||
config := &configuration.Configuration{}
|
||||
// Get a free port to be used by the registry.
|
||||
port, err := testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Create the registry address to which will bind.
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
localRegistryHost = config.HTTP.Addr
|
||||
|
||||
// Create the oras registry.
|
||||
localRegistry, err = testutils.NewOrasRegistry(localRegistryHost, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Initialize options for command.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Push the artifacts to the registry.
|
||||
// Same artifacts will be used to test the puller code.
|
||||
pusher := ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), true, nil)
|
||||
|
||||
// Push plugin artifact with multiple architectures.
|
||||
filePathsAndPlatforms := ocipusher.WithFilepathsAndPlatforms([]string{testPluginTarball, testPluginTarball, testPluginTarball},
|
||||
[]string{testPluginPlatform1, testPluginPlatform2, testPluginPlatform3})
|
||||
pluginMultiPlatformRef = localRegistryHost + "/plugins:multiplatform"
|
||||
artConfig := oci.ArtifactConfig{}
|
||||
Expect(artConfig.ParseDependencies("my-dep:1.2.3|my-alt-dep:1.4.5")).ToNot(HaveOccurred())
|
||||
Expect(artConfig.ParseRequirements("my-req:7.8.9")).ToNot(HaveOccurred())
|
||||
artifactConfig := ocipusher.WithArtifactConfig(artConfig)
|
||||
|
||||
// Build options slice.
|
||||
options := []ocipusher.Option{filePathsAndPlatforms, artifactConfig}
|
||||
|
||||
// Push the plugin artifact.
|
||||
_, err = pusher.Push(ctx, oci.Plugin, pluginMultiPlatformRef, options...)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
|
||||
// Prepare and push artifact without config layer.
|
||||
filePaths := ocipusher.WithFilepaths([]string{testRuleTarball})
|
||||
artConfig = oci.ArtifactConfig{}
|
||||
Expect(artConfig.ParseDependencies("dep1:1.2.3", "dep2:2.3.1")).ToNot(HaveOccurred())
|
||||
options = []ocipusher.Option{
|
||||
filePaths,
|
||||
ocipusher.WithTags("latest"),
|
||||
}
|
||||
|
||||
// Push artifact without config layer.
|
||||
// Push artifact without config layer.
|
||||
artifactWithoutConfigRef = localRegistryHost + "/artifact:noconfig"
|
||||
_, err = pusher.Push(ctx, oci.Rulesfile, artifactWithoutConfigRef, options...)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
|
||||
// Push a rulesfile artifact
|
||||
options = append(options, ocipusher.WithArtifactConfig(artConfig))
|
||||
rulesRef = localRegistryHost + "/rulesfiles:regular"
|
||||
_, err = pusher.Push(ctx, oci.Rulesfile, rulesRef, options...)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
var usage = `Usage:
|
||||
falcoctl artifact config [ref] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for config
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
var help = `Get the config layer of an artifact
|
||||
|
||||
Usage:
|
||||
falcoctl artifact config [ref] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for config
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
var _ = Describe("Config", func() {
|
||||
const (
|
||||
artifactCmd = "artifact"
|
||||
configCmd = "config"
|
||||
plaingHTTP = "--plain-http"
|
||||
configFlag = "--config"
|
||||
platformFlag = "--platform"
|
||||
)
|
||||
|
||||
var (
|
||||
err error
|
||||
args []string
|
||||
configDir string
|
||||
)
|
||||
|
||||
var assertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
configDir = GinkgoT().TempDir()
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
err = nil
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
args = nil
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(string(output.Contents())).Should(Equal(help))
|
||||
})
|
||||
})
|
||||
|
||||
Context("wrong number of arguments", func() {
|
||||
When("number of arguments equal to 0", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 0 ")
|
||||
})
|
||||
|
||||
When("number of arguments equal to 2", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, "arg1", "arg2", configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 2 ")
|
||||
})
|
||||
})
|
||||
|
||||
Context("failure", func() {
|
||||
When("unreachable/non existing registry", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, "noregistry/noartifact", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR unable to get manifest: unable to fetch reference")
|
||||
})
|
||||
|
||||
When("non existing repository", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, localRegistryHost + "/noartifact", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "noartifact:latest: not found")
|
||||
})
|
||||
|
||||
When("non parsable reference", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, " ", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR cannot find among the configured indexes, skipping ")
|
||||
})
|
||||
|
||||
When("no manifest for given platform", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, "linux/wrong"}
|
||||
})
|
||||
assertFailedBehavior(usage, "ERROR unable to get manifest: unable to find a manifest matching the given platform: linux/wrong")
|
||||
})
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
When("empty config layer", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, artifactWithoutConfigRef, plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta("{}")))
|
||||
})
|
||||
})
|
||||
|
||||
When("with valid config layer", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, rulesRef, plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(`{"dependencies":[{"name":"dep1","version":"1.2.3"},{"name":"dep2","version":"2.3.1"}]}`)))
|
||||
})
|
||||
})
|
||||
|
||||
When("no platform flag", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
It("should success getting the platform where tests are running", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`{"dependencies":[{"name":"my-dep","version":"1.2.3","alternatives":[{"name":"my-alt-dep","version":"`)))
|
||||
})
|
||||
})
|
||||
|
||||
When("with valid platform", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`{"dependencies":[{"name":"my-dep","version":"1.2.3","alternatives":[{"name":"my-alt-dep","version":"`)))
|
||||
})
|
||||
})
|
||||
|
||||
When("with non existing platform for artifacts without platforms", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, rulesRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(`{"dependencies":[{"name":"dep1","version":"1.2.3"},{"name":"dep2","version":"2.3.1"}]}`)))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package config defines the business logic to fetch config layer for artifacts.
|
||||
package config
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package artifact implements the artifact commands.
|
||||
package artifact
|
|
@ -0,0 +1,18 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package follow defines the business logic to follow artifacts. Periodically checks if there are updates
|
||||
// and downlods them if any.
|
||||
package follow
|
|
@ -0,0 +1,500 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package follow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/install"
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/follower"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/index"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
timeout = time.Second * 5
|
||||
|
||||
longFollow = `This command allows you to keep up-to-date one or more given artifacts.
|
||||
It checks for updates on a periodic basis and then downloads and installs the latest version,
|
||||
as specified by the passed tags.
|
||||
|
||||
Artifact references and flags are passed as arguments through:
|
||||
- command line options
|
||||
- environment variables
|
||||
- configuration file
|
||||
The arguments passed through these different modalities are prioritized in the following order:
|
||||
command line options, environment variables, and finally the configuration file. This means that
|
||||
if an argument is passed through multiple modalities, the value set in the command line options
|
||||
will take precedence over the value set in environment variables, which will in turn take precedence
|
||||
over the value set in the configuration file.
|
||||
Please note that when passing multiple artifact references via an environment variable, they must be
|
||||
separated by a semicolon ';' and the environment variable used for references is called
|
||||
FALCOCT_ARTIFACT_FOLLOW_REFS. Other arguments, if passed through environment variables, should start
|
||||
with "FALCOCTL_" and be followed by the hierarchical keys used in the configuration file separated by
|
||||
an underscore "_".
|
||||
|
||||
A reference is either a simple name or a fully qualified reference ("<registry>/<repository>"),
|
||||
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
|
||||
|
||||
When providing just the name of the artifact, the command will search for the artifacts in
|
||||
the configured index files, and if found, it will use the registry and repository specified
|
||||
in the indexes.
|
||||
|
||||
Example - Install and follow "latest" tag of "k8saudit-rules" artifact by relying on index metadata:
|
||||
falcoctl artifact follow k8saudit-rules
|
||||
|
||||
Example - Install and follow all updates from "k8saudit-rules" 0.5.x release series:
|
||||
falcoctl artifact follow k8saudit-rules:0.5
|
||||
|
||||
Example - Install and follow "cloudtrail" plugins using a fully qualified reference:
|
||||
falcoctl artifact follow ghcr.io/falcosecurity/plugins/ruleset/k8saudit:latest
|
||||
`
|
||||
)
|
||||
|
||||
type artifactFollowOptions struct {
|
||||
*options.Common
|
||||
*options.Registry
|
||||
*options.Directory
|
||||
tmpDir string
|
||||
every time.Duration
|
||||
cron string
|
||||
falcoVersions string
|
||||
versions config.FalcoVersions
|
||||
timeout time.Duration
|
||||
closeChan chan bool
|
||||
allowedTypes oci.ArtifactTypeSlice
|
||||
noVerify bool
|
||||
}
|
||||
|
||||
// NewArtifactFollowCmd returns the artifact follow command.
|
||||
//
|
||||
//nolint:gocyclo // unknown reason for cyclomatic complexity
|
||||
func NewArtifactFollowCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactFollowOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
Directory: &options.Directory{},
|
||||
closeChan: make(chan bool),
|
||||
versions: config.FalcoVersions{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "follow [ref1 [ref2 ...]] [flags]",
|
||||
Short: "Install a list of artifacts and continuously checks if there are updates",
|
||||
Long: longFollow,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Override "every" flag with viper config if not set by user.
|
||||
f := cmd.Flags().Lookup("every")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag every")
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowEveryKey) {
|
||||
val := viper.Get(config.ArtifactFollowEveryKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"every\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "cron" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("cron")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag cron")
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowCronKey) {
|
||||
val := viper.Get(config.ArtifactFollowCronKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"cron\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "falco-versions" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("falco-versions")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag falco-versions")
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowFalcoVersionsKey) {
|
||||
val := viper.Get(config.ArtifactFollowFalcoVersionsKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"falco-versions\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "rulesfiles-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagRulesFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagRulesFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowRulesfilesDirKey) {
|
||||
val := viper.Get(config.ArtifactFollowRulesfilesDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagRulesFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "plugins-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagPluginsFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagPluginsFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowPluginsDirKey) {
|
||||
val := viper.Get(config.ArtifactFollowPluginsDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagPluginsFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "assets-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagAssetsFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagAssetsFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowAssetsDirKey) {
|
||||
val := viper.Get(config.ArtifactFollowAssetsDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagAssetsFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "tmp-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("tmp-dir")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag tmp-dir")
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowTmpDirKey) {
|
||||
val := viper.Get(config.ArtifactFollowTmpDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"tmp-dir\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "allowed-types" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(install.FlagAllowedTypes)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %s", install.FlagAllowedTypes)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactAllowedTypesKey) {
|
||||
val, err := config.ArtifactAllowedTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set(f.Name, val.String()); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", install.FlagAllowedTypes, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "no-verify" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(install.FlagNoVerify)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %s", install.FlagNoVerify)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactNoVerifyKey) {
|
||||
val := viper.Get(config.ArtifactNoVerifyKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", install.FlagNoVerify, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get Falco versions via HTTP endpoint
|
||||
if err := o.retrieveFalcoVersions(ctx); err != nil {
|
||||
return fmt.Errorf("unable to retrieve Falco versions, please check if it is running "+
|
||||
"and correctly exposing the version endpoint: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactFollow(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
o.Directory.AddFlags(cmd)
|
||||
cmd.Flags().DurationVarP(&o.every, "every", "e", config.FollowResync, "Time interval how often it checks for a new version of the "+
|
||||
"artifact. Cannot be used together with 'cron' option.")
|
||||
cmd.Flags().StringVar(&o.cron, "cron", "", "Cron-like string to specify interval how often it checks for a new version of the artifact."+
|
||||
" Cannot be used together with 'every' option.")
|
||||
cmd.Flags().StringVar(&o.tmpDir, "tmp-dir", "", "Directory where to save temporary files")
|
||||
cmd.Flags().StringVar(&o.falcoVersions, "falco-versions", "http://localhost:8765/versions",
|
||||
"Where to retrieve versions, it can be either an URL or a path to a file")
|
||||
cmd.Flags().DurationVar(&o.timeout, "timeout", defaultBackoffConfig.MaxDelay,
|
||||
"Timeout for initial connection to the Falco versions endpoint")
|
||||
cmd.Flags().Var(&o.allowedTypes, install.FlagAllowedTypes,
|
||||
fmt.Sprintf(`list of artifact types that can be followed. If not specified or configured, all types are allowed.
|
||||
It accepts comma separated values or it can be repeated multiple times.
|
||||
Examples:
|
||||
--%s="rulesfile,plugin"
|
||||
--%s=rulesfile --%s=plugin`, install.FlagAllowedTypes, install.FlagAllowedTypes, install.FlagAllowedTypes))
|
||||
cmd.Flags().BoolVar(&o.noVerify, install.FlagNoVerify, false,
|
||||
"whether this command should skip signature verification")
|
||||
cmd.MarkFlagsMutuallyExclusive("cron", "every")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunArtifactFollow executes the business logic for the artifact follow command.
|
||||
func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []string) error {
|
||||
logger := o.Printer.Logger
|
||||
// Retrieve configuration for follower
|
||||
configuredFollower, err := config.Follower()
|
||||
if err != nil {
|
||||
o.Printer.CheckErr(fmt.Errorf("unable to retrieved the configured follower: %w", err))
|
||||
}
|
||||
|
||||
// Set args as configured if no arg was passed
|
||||
if len(args) == 0 {
|
||||
if len(configuredFollower.Artifacts) == 0 {
|
||||
return fmt.Errorf("no artifacts to follow, please configure artifacts or pass them as arguments to this command")
|
||||
}
|
||||
args = configuredFollower.Artifacts
|
||||
}
|
||||
|
||||
var sched cron.Schedule
|
||||
if o.cron != "" {
|
||||
sched, err = cron.ParseStandard(o.cron)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse cron '%s': %w", o.cron, err)
|
||||
}
|
||||
} else {
|
||||
sched = scheduledDuration{o.every}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
// For each artifact create a follower.
|
||||
var followers = make(map[string]*follower.Follower, 0)
|
||||
for _, a := range args {
|
||||
if o.cron != "" {
|
||||
logger.Info("Creating follower", logger.Args("artifact", a, "cron", o.cron))
|
||||
} else {
|
||||
logger.Info("Creating follower", logger.Args("artifact", a, "check every", o.every.String()))
|
||||
}
|
||||
ref, err := o.IndexCache.ResolveReference(a)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse artifact reference for %q: %w", a, err)
|
||||
}
|
||||
|
||||
var sig *index.Signature
|
||||
if !o.noVerify {
|
||||
sig = o.IndexCache.SignatureForIndexRef(a)
|
||||
}
|
||||
|
||||
cfg := &follower.Config{
|
||||
WaitGroup: &wg,
|
||||
Resync: sched,
|
||||
RulesfilesDir: o.RulesfilesDir,
|
||||
PluginsDir: o.PluginsDir,
|
||||
AssetsDir: o.AssetsDir,
|
||||
ArtifactReference: ref,
|
||||
PlainHTTP: o.PlainHTTP,
|
||||
CloseChan: o.closeChan,
|
||||
TmpDir: o.tmpDir,
|
||||
FalcoVersions: o.versions,
|
||||
AllowedTypes: o.allowedTypes,
|
||||
Signature: sig,
|
||||
}
|
||||
fol, err := follower.New(ref, o.Printer, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the follower for ref %q: %w", ref, err)
|
||||
}
|
||||
wg.Add(1)
|
||||
followers[ref] = fol
|
||||
}
|
||||
|
||||
for k, f := range followers {
|
||||
logger.Info("Starting follower", logger.Args("artifact", k))
|
||||
go f.Follow(ctx)
|
||||
}
|
||||
|
||||
// Wait until we receive a signal to be terminated
|
||||
<-ctx.Done()
|
||||
|
||||
// We are done, shutdown the followers.
|
||||
logger.Info("Closing followers...")
|
||||
close(o.closeChan)
|
||||
|
||||
// Wait for the followers to shutdown or that the timer expires.
|
||||
doneChan := make(chan bool)
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(doneChan)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-doneChan:
|
||||
logger.Info("Followers correctly stopped.")
|
||||
case <-time.After(timeout):
|
||||
logger.Info("Timed out waiting for followers to exit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *artifactFollowOptions) retrieveFalcoVersions(ctx context.Context) error {
|
||||
_, err := url.ParseRequestURI(o.falcoVersions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse URI: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, o.falcoVersions, http.NoBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot fetch Falco version: %w", err)
|
||||
}
|
||||
|
||||
backoffConfig := defaultBackoffConfig
|
||||
backoffConfig.MaxDelay = o.timeout
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &backoffTransport{
|
||||
Base: http.DefaultTransport,
|
||||
Printer: o.Printer,
|
||||
Config: backoffConfig,
|
||||
},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get versions from URL %q: %w", o.falcoVersions, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read response body: %w", err)
|
||||
}
|
||||
|
||||
var dataUnmarshalled map[string]interface{}
|
||||
|
||||
err = json.Unmarshal(data, &dataUnmarshalled)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshalling: %w", err)
|
||||
}
|
||||
|
||||
for key, value := range dataUnmarshalled {
|
||||
// todo(alacuku): how to handle types other than strings? Silently ignoring for now...
|
||||
if strValue, ok := value.(string); ok {
|
||||
o.versions[key] = strValue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config defines the configuration options for backoff.
|
||||
type backoffConfig struct {
|
||||
// BaseDelay is the amount of time to backoff after the first failure.
|
||||
BaseDelay time.Duration
|
||||
// Multiplier is the factor with which to multiply backoffs after a
|
||||
// failed retry. Should ideally be greater than 1.
|
||||
Multiplier float64
|
||||
// Jitter is the factor with which backoffs are randomized.
|
||||
// todo: not yet implemented
|
||||
// Jitter float64
|
||||
// MaxDelay is the upper bound of backoff delay.
|
||||
MaxDelay time.Duration
|
||||
}
|
||||
|
||||
var defaultBackoffConfig = backoffConfig{
|
||||
BaseDelay: 1.0 * time.Second,
|
||||
Multiplier: 1.6,
|
||||
// Jitter: 0.2, todo: not yet implemented
|
||||
MaxDelay: 120 * time.Second,
|
||||
}
|
||||
|
||||
type backoffTransport struct {
|
||||
Base http.RoundTripper
|
||||
Printer *output.Printer
|
||||
Config backoffConfig
|
||||
attempts int
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func (bt *backoffTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
var err error
|
||||
var resp *http.Response
|
||||
logger := bt.Printer.Logger
|
||||
bt.startTime = time.Now()
|
||||
bt.attempts = 0
|
||||
|
||||
logger.Debug(fmt.Sprintf("Retrieving versions from Falco (timeout %s) ...", bt.Config.MaxDelay))
|
||||
|
||||
for {
|
||||
resp, err = bt.Base.RoundTrip(req)
|
||||
if err != nil {
|
||||
if req.Context().Err() != nil {
|
||||
return nil, req.Context().Err()
|
||||
}
|
||||
sleep := bt.Config.backoff(bt.attempts)
|
||||
|
||||
wakeTime := time.Now().Add(sleep)
|
||||
if wakeTime.Sub(bt.startTime) > bt.Config.MaxDelay {
|
||||
return resp, fmt.Errorf("timeout occurred while retrieving versions from Falco")
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("error: %s. Trying again in %s", err.Error(), sleep.String()))
|
||||
time.Sleep(sleep)
|
||||
} else {
|
||||
logger.Debug("Successfully retrieved versions from Falco")
|
||||
return resp, err
|
||||
}
|
||||
|
||||
bt.attempts++
|
||||
}
|
||||
}
|
||||
|
||||
// Backoff returns the amount of time to wait before the next retry given the
|
||||
// number of retries.
|
||||
func (bc backoffConfig) backoff(retries int) time.Duration {
|
||||
if retries == 0 {
|
||||
return bc.BaseDelay
|
||||
}
|
||||
backoff, max := float64(bc.BaseDelay), float64(bc.MaxDelay)
|
||||
for backoff < max && retries > 0 {
|
||||
backoff *= bc.Multiplier
|
||||
retries--
|
||||
}
|
||||
if backoff > max {
|
||||
backoff = max
|
||||
}
|
||||
// Randomize backoff delays so that if a cluster of requests start at
|
||||
// the same time, they won't operate in lockstep.
|
||||
// todo: implement jitter
|
||||
// backoff *= 1 + bc.Jitter*(math.Float64()*2-1)
|
||||
if backoff < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return time.Duration(backoff)
|
||||
}
|
||||
|
||||
type scheduledDuration struct {
|
||||
time.Duration
|
||||
}
|
||||
|
||||
func (sd scheduledDuration) Next(tm time.Time) time.Time {
|
||||
return tm.Add(sd.Duration)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package info defines the business logic to get information for a given artifact.
|
||||
package info
|
|
@ -0,0 +1,123 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package info
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/repository"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
type artifactInfoOptions struct {
|
||||
*options.Common
|
||||
*options.Registry
|
||||
}
|
||||
|
||||
// NewArtifactInfoCmd returns the artifact info command.
|
||||
func NewArtifactInfoCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactInfoOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "info [ref1 [ref2 ...]] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Retrieve all available versions of a given artifact",
|
||||
Long: "Retrieve all available versions of a given artifact",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactInfo(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string) error {
|
||||
var data [][]string
|
||||
logger := o.Printer.Logger
|
||||
|
||||
client, err := ociutils.Client(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// resolve references
|
||||
for _, name := range args {
|
||||
var ref string
|
||||
parsedRef, err := registry.ParseReference(name)
|
||||
if err != nil {
|
||||
entry, ok := o.IndexCache.MergedIndexes.EntryByName(name)
|
||||
if !ok {
|
||||
logger.Warn("Cannot find artifact, skipping", logger.Args("name", name))
|
||||
continue
|
||||
}
|
||||
ref = fmt.Sprintf("%s/%s", entry.Registry, entry.Repository)
|
||||
} else {
|
||||
parsedRef.Reference = ""
|
||||
ref = parsedRef.String()
|
||||
}
|
||||
|
||||
repo, err := repository.NewRepository(ref,
|
||||
repository.WithClient(client),
|
||||
repository.WithPlainHTTP(o.PlainHTTP))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags, err := repo.Tags(ctx)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
logger.Warn("Cannot retrieve tags from", logger.Args("ref", ref, "reason", err.Error()))
|
||||
continue
|
||||
} else if errors.Is(err, context.Canceled) {
|
||||
// When the context is canceled we exit, since we receive a termination signal.
|
||||
return err
|
||||
}
|
||||
|
||||
joinedTags := strings.Join(filterOutSigTags(tags), ", ")
|
||||
data = append(data, []string{ref, joinedTags})
|
||||
}
|
||||
|
||||
// Print the table header + data only if there is data.
|
||||
if len(data) > 0 {
|
||||
return o.Printer.PrintTable(output.ArtifactInfo, data)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterOutSigTags(tags []string) []string {
|
||||
// Iterate the slice in reverse to avoid index shifting when deleting
|
||||
for i := len(tags) - 1; i >= 0; i-- {
|
||||
if strings.HasSuffix(tags[i], ".sig") {
|
||||
// Remove the element at index i by slicing the slice
|
||||
tags = append(tags[:i], tags[i+1:]...)
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package install
|
||||
|
||||
const (
|
||||
// FlagAllowedTypes is the name of the flag to specify allowed artifact types.
|
||||
FlagAllowedTypes = "allowed-types"
|
||||
|
||||
// FlagPlatform is the name of the flag to override the platform.
|
||||
FlagPlatform = "platform"
|
||||
|
||||
// FlagResolveDeps is the name of the flag to enable artifact dependencies resolution.
|
||||
FlagResolveDeps = "resolve-deps"
|
||||
|
||||
// FlagNoVerify is the name of the flag to disable signature verification.
|
||||
FlagNoVerify = "no-verify"
|
||||
)
|
|
@ -0,0 +1,201 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package install
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/blang/semver"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
)
|
||||
|
||||
type artifactConfigResolver func(ref string) (*oci.RegistryResult, error)
|
||||
type depsMapType map[string]*depInfo
|
||||
|
||||
var (
|
||||
// ErrCannotSatisfyDependencies is the error returned when we cannot correctly resolve dependencies.
|
||||
ErrCannotSatisfyDependencies = errors.New("cannot satisfy dependencies")
|
||||
)
|
||||
|
||||
type depInfo struct {
|
||||
// ref is the remote reference to this artifact
|
||||
ref string
|
||||
// config contains the config layer for this artifact
|
||||
config *oci.ArtifactConfig
|
||||
// ver represents the semver version of this artifact
|
||||
ver *semver.Version
|
||||
// ok is used to mark this dependency as fully processed, with its own
|
||||
// dependencies and alternatives
|
||||
ok bool
|
||||
}
|
||||
|
||||
func copyDepsMap(in depsMapType) (out depsMapType) {
|
||||
out = make(depsMapType, len(in))
|
||||
for k, v := range in {
|
||||
out[k] = v
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ResolveDeps resolves dependencies to a list of references.
|
||||
func ResolveDeps(resolver artifactConfigResolver, inRefs ...string) (outRefs []string, err error) {
|
||||
depMap := make(depsMapType)
|
||||
// configMap is used to avoid getting a remote config layer more than once
|
||||
configMap := make(map[string]*oci.ArtifactConfig)
|
||||
|
||||
retrieveConfig := func(ref string) (*oci.ArtifactConfig, error) {
|
||||
config, ok := configMap[ref]
|
||||
if !ok {
|
||||
res, err := resolver(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configMap[ref] = &res.Config
|
||||
return &res.Config, nil
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
upsertMap := func(ref string) error {
|
||||
// fetch artifact config layer metadata
|
||||
config, err := retrieveConfig(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.Version == "" {
|
||||
return fmt.Errorf("empty version for ref %q: config may be corrupted", ref)
|
||||
}
|
||||
|
||||
ver, err := semver.Parse(config.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse version %q for ref %q, %w", config.Version, ref, err)
|
||||
}
|
||||
|
||||
depMap[config.Name] = &depInfo{
|
||||
ref: ref,
|
||||
config: config,
|
||||
ver: &ver,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepare initial map from user inputs
|
||||
for _, ref := range inRefs {
|
||||
config, err := retrieveConfig(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := config.Name
|
||||
|
||||
// todo: shall we shadow?
|
||||
if info, ok := depMap[name]; ok {
|
||||
return nil, fmt.Errorf(`cannot provide multiple references for %q: %q, %q`, name, info.ref, ref)
|
||||
}
|
||||
|
||||
if err := upsertMap(ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
allOk := true
|
||||
// Since we are updating depMap in this for loop, let's copy the map for iterating it
|
||||
// while we continue inserting new values in the real depMap map.
|
||||
for name, info := range copyDepsMap(depMap) {
|
||||
if info.ok {
|
||||
continue
|
||||
}
|
||||
for _, required := range info.config.Dependencies {
|
||||
// Does already exist in the map?
|
||||
if existing, ok := depMap[required.Name]; ok {
|
||||
requiredVer, err := semver.Parse(required.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`invalid artifact config: version %q is not semver compatible`, required.Version)
|
||||
}
|
||||
|
||||
// Is the existing dep compatible?
|
||||
if existing.ver.Major != requiredVer.Major {
|
||||
return nil, fmt.Errorf(
|
||||
`%w: %s depends on %s:%s but an incompatible version %s:%s is required by other artifacts`,
|
||||
ErrCannotSatisfyDependencies, name, required.Name, required.Version, required.Name, existing.ver.String(),
|
||||
)
|
||||
}
|
||||
|
||||
// Is required version greater than existing one?
|
||||
if requiredVer.Compare(*existing.ver) <= 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Are alternatives already in the map?
|
||||
var foundAlternative = false
|
||||
for _, alternative := range required.Alternatives {
|
||||
existing, ok := depMap[alternative.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
foundAlternative = true
|
||||
|
||||
alternativeVer, err := semver.Parse(alternative.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`invalid artifact config: version %q is not semver compatible`, required.Version)
|
||||
}
|
||||
|
||||
// Is the alternative specified by the user compatible?
|
||||
if existing.ver.Major != alternativeVer.Major {
|
||||
return nil, fmt.Errorf(
|
||||
`%w: %s depends on %s:%s but an incompatible version %s:%s is required by other artifacts`,
|
||||
ErrCannotSatisfyDependencies, name, required.Name, required.Version, required.Name, existing.ver.String(),
|
||||
)
|
||||
}
|
||||
|
||||
if alternativeVer.Compare(*existing.ver) > 0 {
|
||||
if err := upsertMap(alternative.Name + ":" + alternativeVer.String()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
if foundAlternative {
|
||||
continue
|
||||
}
|
||||
|
||||
// dep to be added or bumped
|
||||
if err := upsertMap(required.Name + ":" + required.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allOk = false
|
||||
}
|
||||
|
||||
// dep processed
|
||||
info.ok = true
|
||||
}
|
||||
|
||||
if allOk {
|
||||
for _, info := range depMap {
|
||||
outRefs = append(outRefs, info.ref)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package install
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
)
|
||||
|
||||
type testCase struct {
|
||||
scenario string
|
||||
description string
|
||||
inRef []string
|
||||
resolver artifactConfigResolver
|
||||
expectedOutRef []string
|
||||
expectedErr error
|
||||
}
|
||||
|
||||
func (t *testCase) checkOutRef(outRef []string) bool {
|
||||
if len(t.expectedOutRef) != len(outRef) {
|
||||
return false
|
||||
}
|
||||
|
||||
sort.Strings(outRef)
|
||||
sort.Strings(t.expectedOutRef)
|
||||
|
||||
for i, val := range t.expectedOutRef {
|
||||
if val != outRef[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TestResolveDeps(t *testing.T) {
|
||||
const (
|
||||
ref1 = "ref1:0.1.2"
|
||||
ref2 = "ref2:4.5.6"
|
||||
dep1 = "dep1:1.2.3"
|
||||
dep1Compatible = "dep1:1.3.0"
|
||||
alt1 = "alt1:2.5.0"
|
||||
)
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
scenario: "resolve one dependency",
|
||||
description: "ref:0.1.2 --> dep1:1.2.3",
|
||||
inRef: []string{ref1},
|
||||
resolver: artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
|
||||
if ref == ref1 {
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: "ref1",
|
||||
Version: "0.1.2",
|
||||
Dependencies: []oci.ArtifactDependency{{Name: "dep1", Version: "1.2.3"}},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
splittedRef := strings.Split(ref, ":")
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: splittedRef[0],
|
||||
Version: splittedRef[1],
|
||||
// no dependencies here
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
expectedOutRef: []string{ref1, dep1},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
scenario: "resolve common compatible dependency",
|
||||
description: "ref1:0.1.2 --> dep1:1.2.3, ref2:4.5.6 --> dep1:1.3.0",
|
||||
inRef: []string{ref1, ref2},
|
||||
resolver: artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
|
||||
switch ref {
|
||||
case ref1:
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: "ref1",
|
||||
Version: "0.1.2",
|
||||
Dependencies: []oci.ArtifactDependency{{Name: "dep1", Version: "1.2.3"}},
|
||||
},
|
||||
}, nil
|
||||
case ref2:
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: "ref2",
|
||||
Version: "4.5.6",
|
||||
Dependencies: []oci.ArtifactDependency{{Name: "dep1", Version: "1.3.0"}},
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
splittedRef := strings.Split(ref, ":")
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: splittedRef[0],
|
||||
Version: splittedRef[1],
|
||||
// no dependencies here
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}),
|
||||
expectedOutRef: []string{ref1, ref2, dep1Compatible},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
scenario: "resolve common but not compatible dependency",
|
||||
description: "ref1:0.1.2 --> dep1:1.2.3, ref2:4.5.6 --> dep1:2.3.0",
|
||||
inRef: []string{ref1, ref2},
|
||||
resolver: artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
|
||||
switch ref {
|
||||
case ref1:
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: "ref1",
|
||||
Version: "0.1.2",
|
||||
Dependencies: []oci.ArtifactDependency{{Name: "dep1", Version: "1.2.3"}},
|
||||
},
|
||||
}, nil
|
||||
case ref2:
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: "ref2",
|
||||
Version: "4.5.6",
|
||||
Dependencies: []oci.ArtifactDependency{{Name: "dep1", Version: "2.3.0"}},
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
splittedRef := strings.Split(ref, ":")
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: splittedRef[0],
|
||||
Version: splittedRef[1],
|
||||
// no dependencies here
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}),
|
||||
expectedOutRef: nil,
|
||||
expectedErr: ErrCannotSatisfyDependencies,
|
||||
},
|
||||
{
|
||||
scenario: "resolve compatible alternative",
|
||||
description: "ref1:0.1.2 --> dep1:1.2.3 | alt1:2.5.0",
|
||||
inRef: []string{ref1, alt1},
|
||||
resolver: artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
|
||||
if ref == ref1 {
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: "ref1",
|
||||
Version: "0.1.2",
|
||||
Dependencies: []oci.ArtifactDependency{
|
||||
{
|
||||
Name: "dep1",
|
||||
Version: "1.2.3",
|
||||
Alternatives: []oci.Dependency{{Name: "alt1", Version: "2.3.0"}},
|
||||
}},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
splittedRef := strings.Split(ref, ":")
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: splittedRef[0],
|
||||
Version: splittedRef[1],
|
||||
// no dependencies here
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
expectedOutRef: []string{ref1, alt1},
|
||||
expectedErr: ErrCannotSatisfyDependencies,
|
||||
},
|
||||
{
|
||||
scenario: "resolve not compatible alternative",
|
||||
description: "ref1:0.1.2 --> dep1:1.2.3 | alt1:3.0.0",
|
||||
inRef: []string{ref1, "alt1:3.0.0"},
|
||||
resolver: artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
|
||||
if ref == ref1 {
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: "ref1",
|
||||
Version: "0.1.2",
|
||||
Dependencies: []oci.ArtifactDependency{
|
||||
{
|
||||
Name: "dep1",
|
||||
Version: "1.2.3",
|
||||
Alternatives: []oci.Dependency{{Name: "alt1", Version: "2.3.0"}},
|
||||
}},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
splittedRef := strings.Split(ref, ":")
|
||||
return &oci.RegistryResult{
|
||||
Config: oci.ArtifactConfig{
|
||||
Name: splittedRef[0],
|
||||
Version: splittedRef[1],
|
||||
// no dependencies here
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
expectedOutRef: nil,
|
||||
expectedErr: ErrCannotSatisfyDependencies,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
outRef, err := ResolveDeps(testCase.resolver, testCase.inRef...)
|
||||
if err != nil && !errors.Is(err, testCase.expectedErr) {
|
||||
t.Fatalf("unexpected error in scenario %q, %q: %v",
|
||||
testCase.scenario, testCase.description, err)
|
||||
}
|
||||
|
||||
if !testCase.checkOutRef(outRef) {
|
||||
t.Fatalf("dependencies not correctly resolved in scenario %q, %q:\n got %v, expected %v",
|
||||
testCase.scenario, testCase.description, outRef, testCase.expectedOutRef)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package install defines options, and logic used to pull an artifact from a remote repository
|
||||
// and install it in the local system.
|
||||
package install
|
|
@ -0,0 +1,374 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package install
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/signature"
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/index"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
const (
|
||||
longInstall = `This command allows you to install one or more given artifacts.
|
||||
|
||||
Artifact references and flags are passed as arguments through:
|
||||
- command line options
|
||||
- environment variables
|
||||
- configuration file
|
||||
The arguments passed through these different modalities are prioritized in the following order:
|
||||
command line options, environment variables, and finally the configuration file. This means that
|
||||
if an argument is passed through multiple modalities, the value set in the command line options
|
||||
will take precedence over the value set in environment variables, which will in turn take precedence
|
||||
over the value set in the configuration file.
|
||||
Please note that when passing multiple artifact references via an environment variable, they must be
|
||||
separated by a semicolon ';'. Other arguments, if passed through environment variables, should start
|
||||
with "FALCOCTL_" and be followed by the hierarchical keys used in the configuration file separated by
|
||||
an underscore "_".
|
||||
|
||||
A reference is either a simple name or a fully qualified reference ("<registry>/<repository>"),
|
||||
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
|
||||
|
||||
When providing just the name of the artifact, the command will search for the artifacts in
|
||||
the configured index files, and if found, it will use the registry and repository specified
|
||||
in the indexes.
|
||||
|
||||
Example - Install "latest" tag of "k8saudit-rules" artifact by relying on index metadata:
|
||||
falcoctl artifact install k8saudit-rules
|
||||
|
||||
Example - Install all updates from "k8saudit-rules" 0.5.x release series:
|
||||
falcoctl artifact install k8saudit-rules:0.5
|
||||
|
||||
Example - Install "cloudtrail" plugins using a fully qualified reference:
|
||||
falcoctl artifact install ghcr.io/falcosecurity/plugins/ruleset/k8saudit:latest
|
||||
`
|
||||
)
|
||||
|
||||
type artifactInstallOptions struct {
|
||||
*options.Common
|
||||
*options.Registry
|
||||
*options.Directory
|
||||
allowedTypes oci.ArtifactTypeSlice
|
||||
platform string // Raw string from command line
|
||||
platformArch string // Architecture portion of parsed platform string
|
||||
platformOS string // OS portion of parsed platform string
|
||||
resolveDeps bool
|
||||
noVerify bool
|
||||
}
|
||||
|
||||
// NewArtifactInstallCmd returns the artifact install command.
|
||||
func NewArtifactInstallCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactInstallOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
Directory: &options.Directory{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "install [ref1 [ref2 ...]] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Install a list of artifacts",
|
||||
Long: longInstall,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Override "rulesfiles-dir" flag with viper config if not set by user.
|
||||
f := cmd.Flags().Lookup(options.FlagRulesFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagRulesFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactInstallRulesfilesDirKey) {
|
||||
val := viper.Get(config.ArtifactInstallRulesfilesDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagRulesFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "plugins-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagPluginsFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagPluginsFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactInstallPluginsDirKey) {
|
||||
val := viper.Get(config.ArtifactInstallPluginsDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagPluginsFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "assets-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagAssetsFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagAssetsFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowAssetsDirKey) {
|
||||
val := viper.Get(config.ArtifactFollowAssetsDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagAssetsFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "allowed-types" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(FlagAllowedTypes)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", FlagAllowedTypes)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactAllowedTypesKey) {
|
||||
val, err := config.ArtifactAllowedTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set(f.Name, val.String()); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %s flag: %w", FlagAllowedTypes, err)
|
||||
}
|
||||
}
|
||||
|
||||
f = cmd.Flags().Lookup(FlagResolveDeps)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", FlagResolveDeps)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactInstallResolveDepsKey) {
|
||||
val := viper.Get(config.ArtifactInstallResolveDepsKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", FlagResolveDeps, err)
|
||||
}
|
||||
}
|
||||
|
||||
f = cmd.Flags().Lookup(FlagNoVerify)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", FlagNoVerify)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactNoVerifyKey) {
|
||||
val := viper.Get(config.ArtifactNoVerifyKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", FlagNoVerify, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse "platform" into OS and Arch
|
||||
if len(o.platform) > 0 {
|
||||
parts := strings.Split(o.platform, "/")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid %q: must be in the format OS/Arch", FlagPlatform)
|
||||
}
|
||||
o.platformOS, o.platformArch = parts[0], parts[1]
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactInstall(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
o.Directory.AddFlags(cmd)
|
||||
cmd.Flags().Var(&o.allowedTypes, FlagAllowedTypes,
|
||||
fmt.Sprintf(`list of artifact types that can be installed. If not specified or configured, all types are allowed.
|
||||
It accepts comma separated values or it can be repeated multiple times.
|
||||
Examples:
|
||||
--%s="rulesfile,plugin"
|
||||
--%s=rulesfile --%s=plugin`, FlagAllowedTypes, FlagAllowedTypes, FlagAllowedTypes))
|
||||
cmd.Flags().StringVar(&o.platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
"os and architecture of the artifact in OS/ARCH format")
|
||||
cmd.Flags().BoolVar(&o.resolveDeps, FlagResolveDeps, true,
|
||||
"whether this command should resolve dependencies or not")
|
||||
cmd.Flags().BoolVar(&o.noVerify, FlagNoVerify, false,
|
||||
"whether this command should skip signature verification")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunArtifactInstall executes the business logic for the artifact install command.
|
||||
func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []string) error {
|
||||
logger := o.Printer.Logger
|
||||
// Retrieve configuration for installer
|
||||
configuredInstaller, err := config.Installer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve the configured installer: %w", err)
|
||||
}
|
||||
|
||||
// Set args as configured if no arg was passed
|
||||
if len(args) == 0 {
|
||||
if len(configuredInstaller.Artifacts) == 0 {
|
||||
return fmt.Errorf("no artifacts to install, please configure artifacts or pass them as arguments to this command")
|
||||
}
|
||||
args = configuredInstaller.Artifacts
|
||||
}
|
||||
|
||||
// Create temp dir where to put pulled artifacts
|
||||
tmpDir, err := os.MkdirTemp("", "falcoctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create temporary directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create registry puller with auto login enabled
|
||||
puller, err := ociutils.Puller(o.PlainHTTP, o.Printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Specify how to pull config layer for each artifact requested by user.
|
||||
resolver := artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
|
||||
ref, err := o.IndexCache.ResolveReference(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
artifactConfig, err := puller.ArtifactConfig(ctx, ref, o.platformOS, o.platformArch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &oci.RegistryResult{
|
||||
Config: *artifactConfig,
|
||||
}, nil
|
||||
})
|
||||
|
||||
signatures := make(map[string]*index.Signature)
|
||||
|
||||
// Compute input to install dependencies
|
||||
for i, arg := range args {
|
||||
ref, err := o.IndexCache.ResolveReference(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sig := o.IndexCache.SignatureForIndexRef(arg); sig != nil {
|
||||
signatures[ref] = sig
|
||||
}
|
||||
args[i] = ref
|
||||
}
|
||||
|
||||
var refs []string
|
||||
if o.resolveDeps {
|
||||
// Solve dependencies
|
||||
logger.Info("Resolving dependencies ...")
|
||||
refs, err = ResolveDeps(resolver, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
refs = args
|
||||
}
|
||||
|
||||
logger.Info("Installing artifacts", logger.Args("refs", refs))
|
||||
|
||||
for _, ref := range refs {
|
||||
resolvedRef, err := o.IndexCache.ResolveReference(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if signatures[resolvedRef] == nil {
|
||||
if sig := o.IndexCache.SignatureForIndexRef(ref); sig != nil {
|
||||
signatures[resolvedRef] = sig
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Preparing to pull artifact", logger.Args("ref", resolvedRef))
|
||||
|
||||
if err := puller.CheckAllowedType(ctx, resolvedRef, o.platformOS, o.platformArch, o.allowedTypes.Types); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Install will always install artifact for the current OS and architecture
|
||||
result, err := puller.Pull(ctx, resolvedRef, tmpDir, o.platformOS, o.platformArch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sig := signatures[resolvedRef]
|
||||
|
||||
if sig != nil && !o.noVerify {
|
||||
repo, err := utils.RepositoryFromRef(resolvedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// In order to prevent TOCTOU issues we'll perform signature verification after we complete a pull
|
||||
// and obtained a digest but before files are written to disk. This way we ensure that we're verifying
|
||||
// the exact digest that we just pulled, even if the tag gets overwritten in the meantime.
|
||||
digestRef := fmt.Sprintf("%s@%s", repo, result.RootDigest)
|
||||
|
||||
logger.Info("Verifying signature for artifact", logger.Args("digest", digestRef))
|
||||
err = signature.Verify(ctx, digestRef, sig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while verifying signature for %s: %w", digestRef, err)
|
||||
}
|
||||
logger.Info("Signature successfully verified!")
|
||||
}
|
||||
|
||||
var destDir string
|
||||
switch result.Type {
|
||||
case oci.Plugin:
|
||||
destDir = o.PluginsDir
|
||||
case oci.Rulesfile:
|
||||
destDir = o.RulesfilesDir
|
||||
case oci.Asset:
|
||||
destDir = o.AssetsDir
|
||||
default:
|
||||
return fmt.Errorf("unrecognized result type %q while pulling artifact", result.Type)
|
||||
}
|
||||
|
||||
// Check if directory exists and is writable.
|
||||
err = utils.ExistsAndIsWritable(destDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot use directory %q as install destination: %w", destDir, err)
|
||||
}
|
||||
|
||||
logger.Info("Extracting and installing artifact", logger.Args("type", result.Type, "file", result.Filename))
|
||||
|
||||
if !o.Printer.DisableStyling {
|
||||
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Extracting and installing")
|
||||
}
|
||||
|
||||
result.Filename = filepath.Join(tmpDir, result.Filename)
|
||||
|
||||
f, err := os.Open(result.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Extract artifact and move it to its destination directory
|
||||
_, err = utils.ExtractTarGz(ctx, f, destDir, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot extract %q to %q: %w", result.Filename, destDir, err)
|
||||
}
|
||||
|
||||
err = os.Remove(result.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Printer.Spinner != nil {
|
||||
_ = o.Printer.Spinner.Stop()
|
||||
}
|
||||
logger.Info("Artifact successfully installed", logger.Args("name", resolvedRef, "type", result.Type, "digest", result.Digest, "directory", destDir))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package install_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
const (
|
||||
rulesfiletgz = "../../../pkg/test/data/rules.tar.gz"
|
||||
rulesfileyaml = "../../../pkg/test/data/rules.yaml"
|
||||
plugintgz = "../../../pkg/test/data/plugin.tar.gz"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
var (
|
||||
registry string
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
port int
|
||||
orasRegistry *remote.Registry
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
var err error
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
RunSpecs(t, "root suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
config := &configuration.Configuration{}
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create the oras registry.
|
||||
orasRegistry, err = testutils.NewOrasRegistry(registry, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
//nolint:unused // false positive
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,465 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package install_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
|
||||
out "github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
//nolint:lll,unused // no need to check for line length.
|
||||
var artifactInstallUsage = `Usage:
|
||||
falcoctl artifact install [ref1 [ref2 ...]] [flags]
|
||||
|
||||
Flags:
|
||||
--allowed-types ArtifactTypeSlice list of artifact types that can be installed. If not specified or configured, all types are allowed.
|
||||
It accepts comma separated values or it can be repeated multiple times.
|
||||
Examples:
|
||||
--allowed-types="rulesfile,plugin"
|
||||
--allowed-types=rulesfile --allowed-types=plugin
|
||||
-h, --help help for install
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
|
||||
--plugins-dir string directory where to install plugins. (default "/usr/share/falco/plugins")
|
||||
--resolve-deps whether this command should resolve dependencies or not (default true)
|
||||
--rulesfiles-dir string directory where to install rules. (default "/etc/falco")
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var artifactInstallHelp = `This command allows you to install one or more given artifacts.
|
||||
|
||||
Artifact references and flags are passed as arguments through:
|
||||
- command line options
|
||||
- environment variables
|
||||
- configuration file
|
||||
The arguments passed through these different modalities are prioritized in the following order:
|
||||
command line options, environment variables, and finally the configuration file. This means that
|
||||
if an argument is passed through multiple modalities, the value set in the command line options
|
||||
will take precedence over the value set in environment variables, which will in turn take precedence
|
||||
over the value set in the configuration file.
|
||||
Please note that when passing multiple artifact references via an environment variable, they must be
|
||||
separated by a semicolon ';'. Other arguments, if passed through environment variables, should start
|
||||
with "FALCOCTL_" and be followed by the hierarchical keys used in the configuration file separated by
|
||||
an underscore "_".
|
||||
|
||||
A reference is either a simple name or a fully qualified reference ("<registry>/<repository>"),
|
||||
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
|
||||
|
||||
When providing just the name of the artifact, the command will search for the artifacts in
|
||||
the configured index files, and if found, it will use the registry and repository specified
|
||||
in the indexes.
|
||||
|
||||
Example - Install "latest" tag of "k8saudit-rules" artifact by relying on index metadata:
|
||||
falcoctl artifact install k8saudit-rules
|
||||
|
||||
Example - Install all updates from "k8saudit-rules" 0.5.x release series:
|
||||
falcoctl artifact install k8saudit-rules:0.5
|
||||
|
||||
Example - Install "cloudtrail" plugins using a fully qualified reference:
|
||||
falcoctl artifact install ghcr.io/falcosecurity/plugins/ruleset/k8saudit:latest
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var correctIndexConfig = `indexes:
|
||||
- name: falcosecurity
|
||||
url: https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var installAssertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:unused // false positive
|
||||
var artifactInstallTests = Describe("install", func() {
|
||||
var (
|
||||
pusher *ocipusher.Pusher
|
||||
ref string
|
||||
config ocipusher.Option
|
||||
)
|
||||
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
artifactCmd = "artifact"
|
||||
installCmd = "install"
|
||||
dep1 = "myplugin:1.2.3"
|
||||
dep2 = "myplugin1:1.2.3|otherplugin:3.2.1"
|
||||
req = "engine_version:15"
|
||||
anSource = "myrepo.com/rules.git"
|
||||
artifact = "generic-repo"
|
||||
repo = "/" + artifact
|
||||
tag = "tag"
|
||||
repoAndTag = repo + ":" + tag
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, installCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(artifactInstallHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
Context("failure", func() {
|
||||
var (
|
||||
tracker out.Tracker
|
||||
options []ocipusher.Option
|
||||
filePathsAndPlatforms ocipusher.Option
|
||||
filePaths ocipusher.Option
|
||||
destDir string
|
||||
)
|
||||
const (
|
||||
plainHTTP = true
|
||||
testPluginPlatform1 = "linux/amd64"
|
||||
)
|
||||
|
||||
When("without artifact", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, "--config", configFile}
|
||||
})
|
||||
installAssertFailedBehavior(artifactInstallUsage,
|
||||
"ERROR no artifacts to install, please configure artifacts or pass them as arguments to this command")
|
||||
})
|
||||
|
||||
When("unreachable registry", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, "noregistry/testrules", "--plain-http", "--config", configFile}
|
||||
})
|
||||
installAssertFailedBehavior(artifactInstallUsage, `ERROR unable to get manifest: unable to fetch reference`)
|
||||
})
|
||||
|
||||
When("invalid repository", func() {
|
||||
newReg := registry + "/wrong:latest"
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, newReg, "--plain-http", "--config", configFile}
|
||||
})
|
||||
installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR unable to get manifest: unable to fetch reference %q", newReg))
|
||||
})
|
||||
|
||||
When("with disallowed types (rulesfile)", func() {
|
||||
BeforeEach(func() {
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push plugin
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "plugin1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http", "--platform", testPluginPlatform1,
|
||||
"--config", configFilePath, "--allowed-types", "rulesfile"}
|
||||
})
|
||||
|
||||
installAssertFailedBehavior(artifactInstallUsage, "ERROR cannot download artifact of type \"plugin\": type not permitted")
|
||||
})
|
||||
|
||||
When("with disallowed types (plugin)", func() {
|
||||
BeforeEach(func() {
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push rulesfile
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "rules1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePaths = ocipusher.WithFilepaths([]string{rulesfiletgz})
|
||||
options = []ocipusher.Option{filePaths, config}
|
||||
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http",
|
||||
"--config", configFilePath, "--allowed-types", "plugin"}
|
||||
})
|
||||
|
||||
installAssertFailedBehavior(artifactInstallUsage, "ERROR cannot download artifact of type \"rulesfile\": type not permitted")
|
||||
})
|
||||
|
||||
When("an unknown type is used", func() {
|
||||
wrongType := "mywrongtype"
|
||||
BeforeEach(func() {
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push rulesfile
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "rules1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePaths = ocipusher.WithFilepaths([]string{rulesfiletgz})
|
||||
options = []ocipusher.Option{filePaths, config}
|
||||
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http",
|
||||
"--config", configFilePath, "--allowed-types", "plugin," + wrongType}
|
||||
})
|
||||
|
||||
installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR invalid argument \"plugin,%s\" for \"--allowed-types\" flag: "+
|
||||
"not valid token %q: must be one of \"rulesfile\", \"plugin\"", wrongType, wrongType))
|
||||
})
|
||||
|
||||
When("--plugins-dir is not writable", func() {
|
||||
BeforeEach(func() {
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Chmod(destDir, 0o555)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push plugin
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "plugin1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http", "--platform", testPluginPlatform1,
|
||||
"--config", configFilePath, "--plugins-dir", destDir}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
|
||||
"as install destination: %s is not writable", destDir, destDir)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("--plugins-dir is not present", func() {
|
||||
BeforeEach(func() {
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Remove(destDir)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push plugin
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "plugin1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http", "--platform", testPluginPlatform1,
|
||||
"--config", configFilePath, "--plugins-dir", destDir}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
|
||||
"as install destination: %s doesn't exists", destDir, destDir)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("--rulesfile-dir is not writable", func() {
|
||||
BeforeEach(func() {
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Chmod(destDir, 0o555)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push plugin
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "rules1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePaths = ocipusher.WithFilepaths([]string{rulesfiletgz})
|
||||
options = []ocipusher.Option{filePaths, config}
|
||||
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http",
|
||||
"--config", configFilePath, "--rulesfiles-dir", destDir}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
|
||||
"as install destination: %s is not writable", destDir, destDir)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("not existing --plugins-dir", func() {
|
||||
BeforeEach(func() {
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Remove(destDir)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push plugin
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "rules1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepaths([]string{rulesfiletgz})
|
||||
options = []ocipusher.Option{filePaths, config}
|
||||
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http",
|
||||
"--config", configFilePath, "--rulesfiles-dir", destDir}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
|
||||
"as install destination: %s doesn't exists", destDir, destDir)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("not --platform is not of the correct format", func() {
|
||||
BeforeEach(func() {
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Remove(destDir)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
ref = registry + repoAndTag
|
||||
args = []string{artifactCmd, installCmd, ref, "--config", configFile, "--platform", "this/is/invalid"}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := `ERROR invalid "platform": must be in the format OS/Arch`
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
|
@ -0,0 +1,78 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
// CommandName name of the command. It has to be the first word in the use line.
|
||||
const CommandName = "list"
|
||||
|
||||
type artifactListOptions struct {
|
||||
*options.Common
|
||||
artifactType oci.ArtifactType
|
||||
index string
|
||||
}
|
||||
|
||||
// NewArtifactListCmd returns the artifact search command.
|
||||
func NewArtifactListCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactListOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: fmt.Sprintf("%s [flags]", CommandName),
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "List all artifacts",
|
||||
Long: "List all artifacts",
|
||||
Aliases: []string{"ls"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactList(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Var(&o.artifactType, "type", `Only list artifacts with a specific type. Allowed values: "rulesfile", "plugin", "asset"`)
|
||||
cmd.Flags().StringVar(&o.index, "index", "", "Only display artifacts from a configured index")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *artifactListOptions) RunArtifactList(_ context.Context, _ []string) error {
|
||||
var data [][]string
|
||||
for _, entry := range o.IndexCache.MergedIndexes.Entries {
|
||||
if o.artifactType != "" && o.artifactType != oci.ArtifactType(entry.Type) {
|
||||
continue
|
||||
}
|
||||
|
||||
indexName := o.IndexCache.MergedIndexes.IndexByEntry(entry).Name
|
||||
if o.index != "" && o.index != indexName {
|
||||
continue
|
||||
}
|
||||
|
||||
row := []string{indexName, entry.Name, entry.Type, entry.Registry, entry.Repository}
|
||||
data = append(data, row)
|
||||
}
|
||||
|
||||
return o.Printer.PrintTable(output.ArtifactSearch, data)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package list defines the logic to list artifacts in the configured index files.
|
||||
package list
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package manifest defines the business logic to fetch manifest layer for artifacts.
|
||||
package manifest
|
|
@ -0,0 +1,93 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
ocipuller "github.com/falcosecurity/falcoctl/pkg/oci/puller"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type artifactManifestOptions struct {
|
||||
*options.Common
|
||||
*options.Registry
|
||||
platform string
|
||||
}
|
||||
|
||||
// NewArtifactManifestCmd returns the artifact manifest command.
|
||||
func NewArtifactManifestCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactManifestOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "manifest [ref] [flags]",
|
||||
Short: "Get the manifest layer of an artifact",
|
||||
Long: "Get the manifest layer of an artifact",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactManifest(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
cmd.Flags().StringVar(&o.platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
"os and architecture of the artifact in OS/ARCH format")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *artifactManifestOptions) RunArtifactManifest(ctx context.Context, args []string) error {
|
||||
var (
|
||||
puller *ocipuller.Puller
|
||||
ref string
|
||||
manifest []byte
|
||||
err error
|
||||
)
|
||||
|
||||
// Create puller with auto login enabled.
|
||||
if puller, err = ociutils.Puller(o.PlainHTTP, o.Printer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve the artifact reference.
|
||||
if ref, err = o.IndexCache.ResolveReference(args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: implement two new flags (platforms, platform) based on the oci platform struct.
|
||||
// Split the platform.
|
||||
tokens := strings.Split(o.platform, "/")
|
||||
if len(tokens) != 2 {
|
||||
return fmt.Errorf("invalid platform format: %s", o.platform)
|
||||
}
|
||||
|
||||
if manifest, err = puller.RawManifest(ctx, ref, tokens[0], tokens[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Printer.DefaultText.Println(string(manifest))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package manifest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
localRegistryHost string
|
||||
localRegistry *remote.Registry
|
||||
testRuleTarball = "../../../pkg/test/data/rules.tar.gz"
|
||||
testPluginTarball = "../../../pkg/test/data/plugin.tar.gz"
|
||||
testPluginPlatform1 = "linux/amd64"
|
||||
testPluginPlatform2 = "windows/amd64"
|
||||
testPluginPlatform3 = "linux/arm64"
|
||||
ctx = context.Background()
|
||||
pluginMultiPlatformRef string
|
||||
rulesRef string
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
)
|
||||
|
||||
func TestManifest(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Manifest Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
var err error
|
||||
config := &configuration.Configuration{}
|
||||
// Get a free port to be used by the registry.
|
||||
port, err := testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Create the registry address to which will bind.
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
localRegistryHost = config.HTTP.Addr
|
||||
|
||||
// Create the oras registry.
|
||||
localRegistry, err = testutils.NewOrasRegistry(localRegistryHost, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Initialize options for command.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Push the artifacts to the registry.
|
||||
// Same artifacts will be used to test the puller code.
|
||||
pusher := ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), true, nil)
|
||||
|
||||
// Push plugin artifact with multiple architectures.
|
||||
filePathsAndPlatforms := ocipusher.WithFilepathsAndPlatforms([]string{testPluginTarball, testPluginTarball, testPluginTarball},
|
||||
[]string{testPluginPlatform1, testPluginPlatform2, testPluginPlatform3})
|
||||
pluginMultiPlatformRef = localRegistryHost + "/plugins:multiplatform"
|
||||
artConfig := oci.ArtifactConfig{}
|
||||
Expect(artConfig.ParseDependencies("my-dep:1.2.3|my-alt-dep:1.4.5")).ToNot(HaveOccurred())
|
||||
Expect(artConfig.ParseRequirements("my-req:7.8.9")).ToNot(HaveOccurred())
|
||||
artifactConfig := ocipusher.WithArtifactConfig(artConfig)
|
||||
|
||||
// Build options slice.
|
||||
options := []ocipusher.Option{filePathsAndPlatforms, artifactConfig}
|
||||
|
||||
// Push the plugin artifact.
|
||||
_, err = pusher.Push(ctx, oci.Plugin, pluginMultiPlatformRef, options...)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
|
||||
// Prepare and push artifact without config layer.
|
||||
filePaths := ocipusher.WithFilepaths([]string{testRuleTarball})
|
||||
artConfig = oci.ArtifactConfig{}
|
||||
Expect(artConfig.ParseDependencies("dep1:1.2.3", "dep2:2.3.1")).ToNot(HaveOccurred())
|
||||
options = []ocipusher.Option{
|
||||
filePaths,
|
||||
ocipusher.WithTags("latest"),
|
||||
}
|
||||
|
||||
// Push a rulesfile artifact
|
||||
options = append(options, ocipusher.WithArtifactConfig(artConfig))
|
||||
rulesRef = localRegistryHost + "/rulesfiles:regular"
|
||||
_, err = pusher.Push(ctx, oci.Rulesfile, rulesRef, options...)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package manifest_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
var usage = `Usage:
|
||||
falcoctl artifact manifest [ref] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for manifest
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
var help = `Get the manifest layer of an artifact
|
||||
|
||||
Usage:
|
||||
falcoctl artifact manifest [ref] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for manifest
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
var _ = Describe("Manifest", func() {
|
||||
const (
|
||||
artifactCmd = "artifact"
|
||||
manifestCmd = "manifest"
|
||||
plaingHTTP = "--plain-http"
|
||||
configFlag = "--config"
|
||||
platformFlag = "--platform"
|
||||
)
|
||||
|
||||
var (
|
||||
err error
|
||||
args []string
|
||||
configDir string
|
||||
)
|
||||
|
||||
var assertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
configDir = GinkgoT().TempDir()
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
err = nil
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
args = nil
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(string(output.Contents())).Should(Equal(help))
|
||||
})
|
||||
})
|
||||
|
||||
Context("wrong number of arguments", func() {
|
||||
When("number of arguments equal to 0", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 0 ")
|
||||
})
|
||||
|
||||
When("number of arguments equal to 2", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, "arg1", "arg2", configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 2 ")
|
||||
})
|
||||
})
|
||||
|
||||
Context("failure", func() {
|
||||
When("unreachable/non existing registry", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, "noregistry/noartifact", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR unable to fetch reference \"noregistry/noartifact:latest\"")
|
||||
})
|
||||
|
||||
When("non existing repository", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, localRegistryHost + "/noartifact", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "noartifact:latest: not found")
|
||||
})
|
||||
|
||||
When("non parsable reference", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, " ", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR cannot find among the configured indexes, skipping ")
|
||||
})
|
||||
|
||||
When("no manifest for given platform", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, "linux/wrong"}
|
||||
})
|
||||
assertFailedBehavior(usage, "ERROR unable to find a manifest matching the given platform: linux/wrong")
|
||||
})
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
When("without image index and no platform (rulesfiles)", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, rulesRef, plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.rulesfile.config.v1+json","digest":"sha256:c329db306d80e7f1e3a5df28bb7d75a0a1545ad1e8f717a4ab4534a3d558affa","size":86},"layers":[{"mediaType":"application/vnd.cncf.falco.rulesfile.layer.v1+tar.gz","digest":"sha256:8ed676f9801d987a26854827beb176eb9164dec3b09a714406348fe1096f7c6c","size":2560,"annotations":{"org.opencontainers.image.title":"rules.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
|
||||
})
|
||||
})
|
||||
|
||||
When("no platform flag", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
It("should success getting the platform where tests are running", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.plugin.config.v1+json","digest":"sha256:39ae8c14fd9ef38d0f1836ba7be71627023ce615f165c3663586a325eee04724","size":164},"layers":[{"mediaType":"application/vnd.cncf.falco.plugin.layer.v1+tar.gz","digest":"sha256:45a192b10e9bbfc82f4216b071afefd7fba56e02e856e37186430d40160e5d64","size":6659921,"annotations":{"org.opencontainers.image.title":"plugin.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
|
||||
})
|
||||
})
|
||||
|
||||
When("with valid platform", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.plugin.config.v1+json","digest":"sha256:39ae8c14fd9ef38d0f1836ba7be71627023ce615f165c3663586a325eee04724","size":164},"layers":[{"mediaType":"application/vnd.cncf.falco.plugin.layer.v1+tar.gz","digest":"sha256:45a192b10e9bbfc82f4216b071afefd7fba56e02e856e37186430d40160e5d64","size":6659921,"annotations":{"org.opencontainers.image.title":"plugin.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
|
||||
})
|
||||
})
|
||||
|
||||
When("with non existing platform for artifacts without platforms", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, rulesRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
|
||||
})
|
||||
|
||||
It("should success and ignore the platform flag", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.rulesfile.config.v1+json","digest":"sha256:c329db306d80e7f1e3a5df28bb7d75a0a1545ad1e8f717a4ab4534a3d558affa","size":86},"layers":[{"mediaType":"application/vnd.cncf.falco.rulesfile.layer.v1+tar.gz","digest":"sha256:8ed676f9801d987a26854827beb176eb9164dec3b09a714406348fe1096f7c6c","size":2560,"annotations":{"org.opencontainers.image.title":"rules.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,91 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMinScore = 0.65
|
||||
// CommandName name of the command. It has to be the first word in the use line.
|
||||
CommandName = "search"
|
||||
)
|
||||
|
||||
type artifactSearchOptions struct {
|
||||
*options.Common
|
||||
minScore float64
|
||||
artifactType oci.ArtifactType
|
||||
}
|
||||
|
||||
func (o *artifactSearchOptions) Validate() error {
|
||||
if o.minScore <= 0 || o.minScore > 1 {
|
||||
return fmt.Errorf("minScore must be a number within (0,1]")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewArtifactSearchCmd returns the artifact search command.
|
||||
func NewArtifactSearchCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactSearchOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: fmt.Sprintf("%s [keyword1 [keyword2 ...]] [flags]", CommandName),
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Search an artifact by keywords",
|
||||
Long: "Search an artifact by keywords",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.Validate()
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactSearch(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Float64VarP(&o.minScore, "min-score", "", defaultMinScore,
|
||||
"the minimum score used to match artifact names with search keywords")
|
||||
|
||||
cmd.Flags().Var(&o.artifactType, "type", `Only search artifacts with a specific type. Allowed values: "rulesfile", "plugin", "asset"`)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *artifactSearchOptions) RunArtifactSearch(_ context.Context, args []string) error {
|
||||
resultEntries := o.IndexCache.MergedIndexes.SearchByKeywords(o.minScore, args...)
|
||||
|
||||
var data [][]string
|
||||
for _, entry := range resultEntries {
|
||||
if o.artifactType != "" && o.artifactType != oci.ArtifactType(entry.Type) {
|
||||
continue
|
||||
}
|
||||
indexName := o.IndexCache.MergedIndexes.IndexByEntry(entry).Name
|
||||
row := []string{indexName, entry.Name, entry.Type, entry.Registry, entry.Repository}
|
||||
data = append(data, row)
|
||||
}
|
||||
|
||||
return o.Printer.PrintTable(output.ArtifactSearch, data)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package search defines the logic to search for artifacts in the configured index files.
|
||||
package search
|
|
@ -0,0 +1,28 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Cmd Suite")
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
Copyright © 2019 Kris Nova <kris@nivenly.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// deleteCmd represents the delete command
|
||||
var deleteCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete a resource",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("delete called")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(deleteCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// deleteCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// deleteCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
Copyright © 2019 Kris Nova <kris@nivenly.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
kubernetesfalc "github.com/kris-nova/falcoctl/kubernetes"
|
||||
"github.com/kubicorn/kubicorn/pkg/cli"
|
||||
"github.com/kubicorn/kubicorn/pkg/local"
|
||||
"github.com/kubicorn/kubicorn/pkg/logger"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// deleteFalcoCmd represents the deleteFalco command
|
||||
var (
|
||||
deleteFalcoCmd = &cobra.Command{
|
||||
Use: "falco",
|
||||
Short: "Delete Falco from Kubernetes",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err, exitCode := DeleteFalcoEntry(i, kubeConfigPath)
|
||||
if err != nil {
|
||||
logger.Critical("Fatal error: %v", err)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
logger.Always("Success.")
|
||||
/// os.Exit(1) },
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// Global for all install methods
|
||||
i = &kubernetesfalc.FalcoInstaller{}
|
||||
kubeConfigPath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
deleteCmd.AddCommand(deleteFalcoCmd)
|
||||
installFalcoCmd.Flags().StringVarP(&kubeConfigPath, "kube-config-path", "k",
|
||||
cli.StrEnvDef("FALCOCTL_KUBE_CONFIG_PATH", fmt.Sprintf("%s/.kube/config", local.Home())),
|
||||
"Set the path to the Kube config")
|
||||
installFalcoCmd.Flags().StringVarP(&i.NamespaceName, "namespace", "n",
|
||||
cli.StrEnvDef("FALCOCTL_KUBE_NAMESPACE", "falco"), "Set the namespace to install Falco in")
|
||||
}
|
||||
|
||||
func DeleteFalcoEntry(installer *kubernetesfalc.FalcoInstaller, kubeConfigPath string) (error, int) {
|
||||
k8s, err := kubernetesfalc.NewK8sFromKubeConfigPath(kubeConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse kube config: %v", err), 98
|
||||
}
|
||||
err = installer.Delete(k8s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to delete falco in Kubernetes: %v", err), 99
|
||||
}
|
||||
return nil, 0
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package cmd implements all the falcoctl commands.
|
||||
package cmd
|
|
@ -0,0 +1,74 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package drivercleanup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type driverCleanupOptions struct {
|
||||
*options.Common
|
||||
*options.Driver
|
||||
}
|
||||
|
||||
// NewDriverCleanupCmd cleans a driver up.
|
||||
func NewDriverCleanupCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
|
||||
o := driverCleanupOptions{
|
||||
Common: opt,
|
||||
Driver: driver,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "cleanup [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Cleanup a driver",
|
||||
Long: `Cleans a driver up, eg for kmod, by removing it from dkms.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunDriverCleanup(ctx)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *driverCleanupOptions) RunDriverCleanup(_ context.Context) error {
|
||||
o.Printer.Logger.Info("Running falcoctl driver cleanup", o.Printer.Logger.Args(
|
||||
"driver type", o.Driver.Type,
|
||||
"driver name", o.Driver.Name))
|
||||
var buf bytes.Buffer
|
||||
if !o.Printer.DisableStyling {
|
||||
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Cleaning up existing drivers")
|
||||
}
|
||||
err := o.Driver.Type.Cleanup(o.Printer.WithWriter(&buf), o.Driver.Name)
|
||||
if o.Printer.Spinner != nil {
|
||||
_ = o.Printer.Spinner.Stop()
|
||||
}
|
||||
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
|
||||
// Only print formatted text if we are formatting to json
|
||||
out := strings.ReplaceAll(buf.String(), "\n", ";")
|
||||
o.Printer.Logger.Info("Driver cleanup", o.Printer.Logger.Args("output", out))
|
||||
} else {
|
||||
// Print much more readable output as-is
|
||||
o.Printer.DefaultText.Print(buf.String())
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package drivercleanup_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestCleanup(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Cleanup Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package drivercleanup_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var driverCleanupHelp = `Cleans a driver up, eg for kmod, by removing it from dkms.
|
||||
|
||||
Usage:
|
||||
falcoctl driver cleanup [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for cleanup
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--host-root string Driver host root to be used. (default "/")
|
||||
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
|
||||
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
--name string Driver name to be used. (default "falco")
|
||||
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
|
||||
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
|
||||
--version string Driver version to be used.
|
||||
`
|
||||
|
||||
var addAssertFailedBehavior = func(specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
var _ = Describe("cleanup", func() {
|
||||
|
||||
var (
|
||||
driverCmd = "driver"
|
||||
cleanupCmd = "cleanup"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, cleanupCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverCleanupHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing failure cases for cleaning a driver.
|
||||
Context("failure", func() {
|
||||
When("with non absolute host-root", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, cleanupCmd, "--config", configFile, "--host-root", "foo/"}
|
||||
})
|
||||
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
|
||||
})
|
||||
|
||||
When("with invalid driver type", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, cleanupCmd, "--config", configFile, "--type", "foo"}
|
||||
})
|
||||
addAssertFailedBehavior(`ERROR unsupported driver type specified: foo`)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package drivercleanup defines the cleanup logic for the driver cmd.
|
||||
package drivercleanup
|
|
@ -0,0 +1,326 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package driverconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/falcosecurity/driverkit/pkg/kernelrelease"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
drivertype "github.com/falcosecurity/falcoctl/pkg/driver/type"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
const (
|
||||
falcoName = "falco"
|
||||
)
|
||||
|
||||
func newOptions() *driverConfigOptions {
|
||||
common := options.NewOptions()
|
||||
common.Initialize()
|
||||
|
||||
// Parse the driver type.
|
||||
dType, _ := drivertype.Parse("modern_ebpf")
|
||||
return &driverConfigOptions{
|
||||
Common: common,
|
||||
Driver: &options.Driver{
|
||||
Type: dType,
|
||||
Name: falcoName,
|
||||
Repos: []string{"https://download.falco.org/driver"},
|
||||
Version: "6.0.0+driver",
|
||||
HostRoot: "/",
|
||||
Distro: nil,
|
||||
Kr: kernelrelease.KernelRelease{},
|
||||
},
|
||||
update: false,
|
||||
namespace: "",
|
||||
kubeconfig: "",
|
||||
configmap: "",
|
||||
configDir: "",
|
||||
}
|
||||
}
|
||||
|
||||
func createFalcoConfigFile(cfg falcoCfg, configDir string) error {
|
||||
engineKind, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal falco config: %w", err)
|
||||
}
|
||||
|
||||
// Write the engine configuration to a specialized config file.
|
||||
if err := os.WriteFile(filepath.Join(configDir, "falco.yaml"), engineKind, 0o600); err != nil {
|
||||
return fmt.Errorf("unable to write falco.yaml file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createFalcoConfigMap(cfg falcoCfg, dataKey string) (*v1.ConfigMap, error) {
|
||||
engineKind, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to marshal falco config: %w", err)
|
||||
}
|
||||
|
||||
cm := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: falcoName,
|
||||
Namespace: falcoName,
|
||||
},
|
||||
Data: map[string]string{
|
||||
dataKey: string(engineKind),
|
||||
},
|
||||
}
|
||||
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
func TestDriverConfigOptions_Commit_Host(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args func(t *testing.T) *driverConfigOptions
|
||||
expected func(t *testing.T, opt *driverConfigOptions, err error)
|
||||
}{
|
||||
{
|
||||
"no falco config file",
|
||||
func(t *testing.T) *driverConfigOptions {
|
||||
opt := newOptions()
|
||||
opt.configDir = "no-file-at-all"
|
||||
opt.update = true
|
||||
return opt
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.Error(t, err, "should error since falco configuration file does not exist")
|
||||
require.ErrorContains(t, err, "open no-file-at-all/falco.yaml: no such file or directory")
|
||||
},
|
||||
},
|
||||
{
|
||||
"update-falco-config",
|
||||
func(t *testing.T) *driverConfigOptions {
|
||||
opt := newOptions()
|
||||
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write falco configuration file.
|
||||
cfg := falcoCfg{engineCfg{Kind: "modern_ebpf"}}
|
||||
err = createFalcoConfigFile(cfg, dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
opt.configDir = dir
|
||||
return opt
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.NoError(t, err, "should not error")
|
||||
|
||||
// Config file.
|
||||
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
|
||||
|
||||
// Check that config file has been created.
|
||||
_, err = os.Stat(specCfgFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(specCfgFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := falcoCfg{}
|
||||
err = yaml.Unmarshal(content, &cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, opt.Type.String(), cfg.Engine.Kind)
|
||||
},
|
||||
},
|
||||
{
|
||||
"falco-not-in-driver-mode",
|
||||
func(t *testing.T) *driverConfigOptions {
|
||||
opt := newOptions()
|
||||
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write falco configuration file.
|
||||
cfg := falcoCfg{engineCfg{Kind: "nodriver"}}
|
||||
err = createFalcoConfigFile(cfg, dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
opt.configDir = dir
|
||||
return opt
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.NoError(t, err, "should not error")
|
||||
|
||||
// Config file.
|
||||
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
|
||||
|
||||
// Check that config file has been created.
|
||||
_, err = os.Stat(specCfgFile)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
opt := testCase.args(t)
|
||||
err := opt.Commit(context.Background(), nil, opt.Type)
|
||||
testCase.expected(t, opt, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriverConfigOptions_Commit_K8S(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap)
|
||||
expected func(t *testing.T, opt *driverConfigOptions, err error)
|
||||
}{
|
||||
{
|
||||
"no falco configmap, wrong namespace",
|
||||
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
|
||||
opt := newOptions()
|
||||
opt.namespace = "wrong-namespace"
|
||||
opt.configmap = falcoName
|
||||
|
||||
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "falco.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
return opt, cm
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.Error(t, err, "should error since falco configmap does not exist")
|
||||
require.ErrorContains(t, err, "unable to get configmap falco in namespace wrong-namespace")
|
||||
},
|
||||
},
|
||||
{
|
||||
"no falco configmap, wrong name",
|
||||
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
|
||||
opt := newOptions()
|
||||
opt.namespace = falcoName
|
||||
opt.configmap = "wrong-name"
|
||||
|
||||
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "falco.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
return opt, cm
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.Error(t, err, "should error since falco configmap does not exist")
|
||||
require.ErrorContains(t, err, "unable to get configmap wrong-name in namespace falco")
|
||||
},
|
||||
},
|
||||
{
|
||||
"no falco config, wrong data key",
|
||||
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
|
||||
opt := newOptions()
|
||||
opt.namespace = falcoName
|
||||
opt.configmap = falcoName
|
||||
|
||||
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "wrong-data-key")
|
||||
require.NoError(t, err)
|
||||
|
||||
return opt, cm
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.Error(t, err, "should error since falco configmap does not exist")
|
||||
require.ErrorContains(t, err, "configMap falco does not contain key \"falco.yaml\"")
|
||||
},
|
||||
},
|
||||
{
|
||||
"update-falco-config",
|
||||
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
|
||||
opt := newOptions()
|
||||
opt.namespace = falcoName
|
||||
opt.configmap = falcoName
|
||||
|
||||
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
|
||||
require.NoError(t, err)
|
||||
opt.configDir = dir
|
||||
|
||||
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "falco.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
return opt, cm
|
||||
},
|
||||
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.NoError(t, err, "should not error")
|
||||
|
||||
// Config file.
|
||||
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
|
||||
|
||||
// Check that config file has been created.
|
||||
_, err = os.Stat(specCfgFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(specCfgFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := falcoCfg{}
|
||||
err = yaml.Unmarshal(content, &cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, opt.Type.String(), cfg.Engine.Kind)
|
||||
},
|
||||
},
|
||||
{
|
||||
"falco-not-in-driver-mode",
|
||||
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
|
||||
opt := newOptions()
|
||||
opt.namespace = falcoName
|
||||
opt.configmap = falcoName
|
||||
|
||||
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
|
||||
require.NoError(t, err)
|
||||
|
||||
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "nodriver"}}, "falco.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
opt.configDir = dir
|
||||
return opt, cm
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.NoError(t, err, "should not error")
|
||||
|
||||
// Config file.
|
||||
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
|
||||
|
||||
// Check that config file has been created.
|
||||
_, err = os.Stat(specCfgFile)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
opt, cm := testCase.args(t)
|
||||
// Create fake client.
|
||||
fakeClient := fake.NewSimpleClientset(cm)
|
||||
err := opt.Commit(context.Background(), fakeClient, opt.Type)
|
||||
testCase.expected(t, opt, err)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/net/context"
|
||||
"gopkg.in/yaml.v3"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
drivertype "github.com/falcosecurity/falcoctl/pkg/driver/type"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
const (
|
||||
longConfig = `Configure a driver for future usages with other driver subcommands.
|
||||
It will also update local Falco configuration or k8s configmap depending on the environment where it is running, to let Falco use chosen driver.
|
||||
Only supports deployments of Falco that use a driver engine, ie: one between kmod, ebpf and modern-ebpf.
|
||||
If engine.kind key is set to a non-driver driven engine, Falco configuration won't be touched.
|
||||
`
|
||||
falcoConfigFile = "falco.yaml"
|
||||
falcoDriverConfigFile = "engine-kind-falcoctl.yaml"
|
||||
)
|
||||
|
||||
type driverConfigOptions struct {
|
||||
*options.Common
|
||||
*options.Driver
|
||||
update bool
|
||||
namespace string
|
||||
kubeconfig string
|
||||
configmap string
|
||||
configDir string
|
||||
}
|
||||
|
||||
type engineCfg struct {
|
||||
Kind string `yaml:"kind"`
|
||||
}
|
||||
type falcoCfg struct {
|
||||
Engine engineCfg `yaml:"engine"`
|
||||
}
|
||||
|
||||
// NewDriverConfigCmd configures a driver and stores it in config.
|
||||
func NewDriverConfigCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
|
||||
o := driverConfigOptions{
|
||||
Common: opt,
|
||||
Driver: driver,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "config [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Configure a driver",
|
||||
Long: longConfig,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
viper.AutomaticEnv()
|
||||
|
||||
_ = viper.BindPFlag("driver.config.configmap", cmd.Flags().Lookup("configmap"))
|
||||
_ = viper.BindPFlag("driver.config.namespace", cmd.Flags().Lookup("namespace"))
|
||||
_ = viper.BindPFlag("driver.config.update_falco", cmd.Flags().Lookup("update-falco"))
|
||||
_ = viper.BindPFlag("driver.config.kubeconfig", cmd.Flags().Lookup("kubeconfig"))
|
||||
_ = viper.BindPFlag("driver.config.configdir", cmd.Flags().Lookup("falco-config-dir"))
|
||||
|
||||
o.configmap = viper.GetString("driver.config.configmap")
|
||||
o.namespace = viper.GetString("driver.config.namespace")
|
||||
o.kubeconfig = viper.GetString("driver.config.kubeconfig")
|
||||
o.update = viper.GetBool("driver.config.update_falco")
|
||||
o.configDir = viper.GetString("driver.config.configdir")
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunDriverConfig(ctx)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&o.update, "update-falco", true, "Whether to overwrite Falco configuration")
|
||||
cmd.Flags().StringVar(&o.namespace, "namespace", "", "Kubernetes namespace.")
|
||||
cmd.Flags().StringVar(&o.kubeconfig, "kubeconfig", "", "Kubernetes config.")
|
||||
cmd.Flags().StringVar(&o.configmap, "configmap", "", "Falco configmap name.")
|
||||
cmd.Flags().StringVar(&o.configDir, "falco-config-dir", "/etc/falco", "Falco configuration directory.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunDriverConfig implements the driver configuration command.
|
||||
func (o *driverConfigOptions) RunDriverConfig(ctx context.Context) error {
|
||||
o.Printer.Logger.Info("Running falcoctl driver config", o.Printer.Logger.Args(
|
||||
"name", o.Driver.Name,
|
||||
"version", o.Driver.Version,
|
||||
"type", o.Driver.Type.String(),
|
||||
"host-root", o.Driver.HostRoot,
|
||||
"repos", strings.Join(o.Driver.Repos, ",")))
|
||||
|
||||
if o.update {
|
||||
var cl kubernetes.Interface
|
||||
var err error
|
||||
|
||||
if o.namespace != "" {
|
||||
// Create a new clientset.
|
||||
if cl, err = setupClient(o.kubeconfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := o.Commit(ctx, cl, o.Driver.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
o.Printer.Logger.Info("Storing falcoctl driver config")
|
||||
return config.StoreDriver(o.Driver.ToDriverConfig(), o.ConfigFile)
|
||||
}
|
||||
|
||||
func checkFalcoRunsWithDrivers(engineKind string) bool {
|
||||
// Modify the data in the ConfigMap/Falco config file ONLY if engine.kind is set to a known driver type.
|
||||
// This ensures that we modify the config only for Falcos running with drivers, and not plugins/gvisor.
|
||||
// Scenario: user has multiple Falco pods deployed in its cluster, one running with driver,
|
||||
// other running with plugins. We must only touch the one running with driver.
|
||||
if _, err := drivertype.Parse(engineKind); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (o *driverConfigOptions) IsRunningInDriverModeHost() (bool, error) {
|
||||
o.Printer.Logger.Debug("Checking if Falco is running in driver mode on host system")
|
||||
|
||||
falcoCfgFile := filepath.Join(o.configDir, falcoConfigFile)
|
||||
yamlFile, err := os.ReadFile(filepath.Clean(falcoCfgFile))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
cfg := falcoCfg{}
|
||||
if err = yaml.Unmarshal(yamlFile, &cfg); err != nil {
|
||||
return false, fmt.Errorf("unable to unmarshal falco.yaml to falcoCfg struct: %w", err)
|
||||
}
|
||||
|
||||
return checkFalcoRunsWithDrivers(cfg.Engine.Kind), nil
|
||||
}
|
||||
|
||||
func (o *driverConfigOptions) IsRunningInDriverModeK8S(ctx context.Context, cl kubernetes.Interface) (bool, error) {
|
||||
o.Printer.Logger.Debug("Checking if Falco is running in driver mode in Kubernetes")
|
||||
|
||||
configMap, err := cl.CoreV1().ConfigMaps(o.namespace).Get(ctx, o.configmap, metav1.GetOptions{})
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to get configmap %s in namespace %s: %w", o.configmap, o.namespace, err)
|
||||
}
|
||||
|
||||
// Check that this is a Falco config map
|
||||
falcoYaml, present := configMap.Data["falco.yaml"]
|
||||
if !present {
|
||||
o.Printer.Logger.Debug("Skip non Falco-related config map",
|
||||
o.Printer.Logger.Args("configMap", configMap.Name))
|
||||
return false, fmt.Errorf("configMap %s does not contain key \"falco.yaml\"", o.configmap)
|
||||
}
|
||||
|
||||
// Check that Falco is configured to run with a driver
|
||||
var falcoConfig falcoCfg
|
||||
err = yaml.Unmarshal([]byte(falcoYaml), &falcoConfig)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to unmarshal falco.yaml to falcoCfg struct: %w", err)
|
||||
}
|
||||
|
||||
return checkFalcoRunsWithDrivers(falcoConfig.Engine.Kind), nil
|
||||
}
|
||||
|
||||
// Commit saves the updated driver type to Falco config,
|
||||
// in a specialized configuration file under /etc/falco/config.d.
|
||||
func (o *driverConfigOptions) Commit(ctx context.Context, cl kubernetes.Interface, driverType drivertype.DriverType) error {
|
||||
// If set to true, then we need to overwrite the driver type.
|
||||
var overwrite bool
|
||||
var err error
|
||||
if cl != nil {
|
||||
if overwrite, err = o.IsRunningInDriverModeK8S(ctx, cl); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if overwrite, err = o.IsRunningInDriverModeHost(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if overwrite {
|
||||
o.Printer.Logger.Info("Committing driver config to specialized configuration file under",
|
||||
o.Printer.Logger.Args("directory", filepath.Join(o.configDir, "config.d")))
|
||||
return overwriteDriverType(o.configDir, driverType)
|
||||
}
|
||||
|
||||
o.Printer.Logger.Info("Falco is not configured to run with a driver, no need to set driver type.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupClient(kubeconfig string) (kubernetes.Interface, error) {
|
||||
var cfg *rest.Config
|
||||
var err error
|
||||
|
||||
// Create the rest config.
|
||||
if kubeconfig != "" {
|
||||
cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
} else {
|
||||
cfg, err = rest.InClusterConfig()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the clientset.
|
||||
return kubernetes.NewForConfig(cfg)
|
||||
}
|
||||
|
||||
func overwriteDriverType(configDir string, driverType drivertype.DriverType) error {
|
||||
var falcoConfig falcoCfg
|
||||
|
||||
configDir = filepath.Join(configDir, "config.d")
|
||||
// First thing, check if config.d folder exists in the configuration directory.
|
||||
_, err := os.Stat(configDir)
|
||||
if os.IsNotExist(err) {
|
||||
// Create it.
|
||||
// #nosec G301 -- under /etc we want 755 permissions
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
return fmt.Errorf("unable to create directory %s: %w", configDir, err)
|
||||
}
|
||||
} else if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
falcoConfig.Engine.Kind = driverType.String()
|
||||
engineKind, err := yaml.Marshal(falcoConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal falco config: %w", err)
|
||||
}
|
||||
|
||||
// Write the engine configuration to a specialized config file.
|
||||
// #nosec G306 //under /etc we want 644 permissions
|
||||
if err := os.WriteFile(filepath.Join(configDir, falcoDriverConfigFile), engineKind, 0o644); err != nil {
|
||||
return fmt.Errorf("unable to persist engine kind to filesystem: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverconfig_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Config Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverconfig_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var driverConfigHelp = `Configure a driver for future usages with other driver subcommands.
|
||||
It will also update local Falco configuration or k8s configmap depending on the environment where it is running, to let Falco use chosen driver.
|
||||
Only supports deployments of Falco that use a driver engine, ie: one between kmod, ebpf and modern-ebpf.
|
||||
If engine.kind key is set to a non-driver driven engine, Falco configuration won't be touched.
|
||||
|
||||
Usage:
|
||||
falcoctl driver config [flags]
|
||||
|
||||
Flags:
|
||||
--configmap string Falco configmap name.
|
||||
--falco-config-dir string Falco configuration directory. (default "/etc/falco")
|
||||
-h, --help help for config
|
||||
--kubeconfig string Kubernetes config.
|
||||
--namespace string Kubernetes namespace.
|
||||
--update-falco Whether to overwrite Falco configuration (default true)
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--host-root string Driver host root to be used. (default "/")
|
||||
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
|
||||
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
--name string Driver name to be used. (default "falco")
|
||||
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
|
||||
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
|
||||
--version string Driver version to be used.
|
||||
`
|
||||
|
||||
var addAssertFailedBehavior = func(specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
var _ = Describe("config", func() {
|
||||
|
||||
var (
|
||||
driverCmd = "driver"
|
||||
configCmd = "config"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, configCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverConfigHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing failure cases for configuring a driver.
|
||||
Context("failure", func() {
|
||||
When("with non absolute host-root", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, configCmd, "--config", configFile, "--host-root", "foo/"}
|
||||
})
|
||||
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
|
||||
})
|
||||
|
||||
When("with invalid driver type", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, configCmd, "--config", configFile, "--type", "foo"}
|
||||
})
|
||||
addAssertFailedBehavior(`ERROR unsupported driver type specified: foo`)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package driverconfig defines the configure logic for the driver cmd.
|
||||
package driverconfig
|
|
@ -0,0 +1,241 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Package driver implements the driver related cmd line interface.
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
drivercleanup "github.com/falcosecurity/falcoctl/cmd/driver/cleanup"
|
||||
driverconfig "github.com/falcosecurity/falcoctl/cmd/driver/config"
|
||||
driverinstall "github.com/falcosecurity/falcoctl/cmd/driver/install"
|
||||
driverprintenv "github.com/falcosecurity/falcoctl/cmd/driver/printenv"
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
driverdistro "github.com/falcosecurity/falcoctl/pkg/driver/distro"
|
||||
driverkernel "github.com/falcosecurity/falcoctl/pkg/driver/kernel"
|
||||
drivertype "github.com/falcosecurity/falcoctl/pkg/driver/type"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewDriverCmd returns the driver command.
|
||||
func NewDriverCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
driver := &options.Driver{}
|
||||
driverTypesEnum := options.NewDriverTypes()
|
||||
var (
|
||||
driverTypesStr []string
|
||||
driverKernelRelease string
|
||||
driverKernelVersion string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "driver",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Interact with falcosecurity driver",
|
||||
Long: `Interact with falcosecurity driver.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
opt.Initialize()
|
||||
if err := config.Load(opt.ConfigFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Override "version" flag with viper config if not set by user.
|
||||
f := cmd.Flags().Lookup("version")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag version")
|
||||
} else if !f.Changed && viper.IsSet(config.DriverVersionKey) {
|
||||
val := viper.Get(config.DriverVersionKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"version\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "repo" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("repo")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag repo")
|
||||
} else if !f.Changed && viper.IsSet(config.DriverReposKey) {
|
||||
val, err := config.DriverRepos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set(f.Name, strings.Join(val, ",")); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"repo\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "name" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("name")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag name")
|
||||
} else if !f.Changed && viper.IsSet(config.DriverNameKey) {
|
||||
val := viper.Get(config.DriverNameKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"name\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "host-root" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("host-root")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag host-root")
|
||||
} else if !f.Changed && viper.IsSet(config.DriverHostRootKey) {
|
||||
val := viper.Get(config.DriverHostRootKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"host-root\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "type" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("type")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag type")
|
||||
} else if !f.Changed && viper.IsSet(config.DriverTypeKey) {
|
||||
val, err := config.DriverTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set(f.Name, strings.Join(val, ",")); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"type\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Logic to discover correct driver to be used
|
||||
// Step 1: build up allowed driver types
|
||||
allowedDriverTypes := make([]drivertype.DriverType, 0)
|
||||
for _, dTypeStr := range driverTypesStr {
|
||||
// Ok driver type was enforced by the user
|
||||
drvType, err := drivertype.Parse(dTypeStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allowedDriverTypes = append(allowedDriverTypes, drvType)
|
||||
opt.Printer.Logger.Debug("Allowed driver",
|
||||
opt.Printer.Logger.Args("type", drvType))
|
||||
}
|
||||
|
||||
// Step 2: fetch system info (kernel release/version and distro)
|
||||
var err error
|
||||
driver.Kr, err = driverkernel.FetchInfo(driverKernelRelease, driverKernelVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt.Printer.Logger.Debug("Fetched kernel info", opt.Printer.Logger.Args(
|
||||
"arch", driver.Kr.Architecture.ToNonDeb(),
|
||||
"kernel release", driver.Kr.String(),
|
||||
"kernel version", driver.Kr.KernelVersion))
|
||||
|
||||
driver.Distro, err = driverdistro.Discover(driver.Kr, driver.HostRoot)
|
||||
if err != nil {
|
||||
if !errors.Is(err, driverdistro.ErrUnsupported) {
|
||||
return err
|
||||
}
|
||||
opt.Printer.Logger.Debug("Detected an unsupported target system; falling back at generic logic.")
|
||||
}
|
||||
opt.Printer.Logger.Debug("Discovered distro", opt.Printer.Logger.Args("target", driver.Distro))
|
||||
|
||||
driver.Type = driver.Distro.PreferredDriver(driver.Kr, allowedDriverTypes)
|
||||
if driver.Type == nil {
|
||||
return fmt.Errorf("no supported driver found for distro: %s, "+
|
||||
"kernelrelease %s, "+
|
||||
"kernelversion %s, "+
|
||||
"arch %s",
|
||||
driver.Distro.String(),
|
||||
driver.Kr.String(),
|
||||
driver.Kr.KernelVersion,
|
||||
driver.Kr.Architecture.ToNonDeb())
|
||||
}
|
||||
opt.Printer.Logger.Debug("Detected supported driver", opt.Printer.Logger.Args("type", driver.Type.String()))
|
||||
|
||||
// If empty, try to load it automatically from /usr/src sub folders,
|
||||
// using the most recent (ie: the one with greatest semver) driver version.
|
||||
if driver.Version == "" {
|
||||
driver.Version = loadDriverVersion()
|
||||
}
|
||||
return driver.Validate()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSliceVar(&driverTypesStr, "type", config.DefaultDriver.Type,
|
||||
"Driver types allowed in descending priority order "+driverTypesEnum.Allowed())
|
||||
cmd.PersistentFlags().StringVar(&driver.Version, "version", config.DefaultDriver.Version, "Driver version to be used.")
|
||||
cmd.PersistentFlags().StringSliceVar(&driver.Repos, "repo", config.DefaultDriver.Repos, "Driver repo to be used.")
|
||||
cmd.PersistentFlags().StringVar(&driver.Name, "name", config.DefaultDriver.Name, "Driver name to be used.")
|
||||
cmd.PersistentFlags().StringVar(&driver.HostRoot, "host-root", config.DefaultDriver.HostRoot, "Driver host root to be used.")
|
||||
cmd.PersistentFlags().StringVar(&driverKernelRelease,
|
||||
"kernelrelease",
|
||||
"",
|
||||
"Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' "+
|
||||
"(e.g. '6.1.0-10-cloud-amd64')")
|
||||
cmd.PersistentFlags().StringVar(&driverKernelVersion,
|
||||
"kernelversion",
|
||||
"",
|
||||
"Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' "+
|
||||
"(e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')")
|
||||
|
||||
cmd.AddCommand(driverinstall.NewDriverInstallCmd(ctx, opt, driver))
|
||||
cmd.AddCommand(driverconfig.NewDriverConfigCmd(ctx, opt, driver))
|
||||
cmd.AddCommand(drivercleanup.NewDriverCleanupCmd(ctx, opt, driver))
|
||||
cmd.AddCommand(driverprintenv.NewDriverPrintenvCmd(ctx, opt, driver))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func loadDriverVersion() string {
|
||||
isSet := false
|
||||
greatestVrs := semver.Version{}
|
||||
paths, _ := filepath.Glob("/usr/src/falco-*")
|
||||
for _, path := range paths {
|
||||
fileInfo, err := os.Stat(path)
|
||||
// We expect path to point to a folder,
|
||||
// otherwise skip it.
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !fileInfo.IsDir() {
|
||||
continue
|
||||
}
|
||||
drvVer := strings.TrimPrefix(filepath.Base(path), "falco-")
|
||||
sv, err := semver.Parse(drvVer)
|
||||
if err != nil {
|
||||
// Not a semver; return it because we
|
||||
// Won't be able to check it against semver driver versions.
|
||||
return drvVer
|
||||
}
|
||||
if sv.GT(greatestVrs) {
|
||||
greatestVrs = sv
|
||||
isSet = true
|
||||
}
|
||||
}
|
||||
if isSet {
|
||||
return greatestVrs.String()
|
||||
}
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package driver
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewDriverCmd returns an empty driver command since it is not supported on non linuxes
|
||||
func NewDriverCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
return &cobra.Command{}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package driverinstall defines the installation logic for the driver cmd.
|
||||
package driverinstall
|
|
@ -0,0 +1,219 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverinstall
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
driverdistro "github.com/falcosecurity/falcoctl/pkg/driver/distro"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type driverDownloadOptions struct {
|
||||
InsecureDownload bool
|
||||
HTTPTimeout time.Duration
|
||||
HTTPHeaders string
|
||||
}
|
||||
|
||||
type driverInstallOptions struct {
|
||||
*options.Common
|
||||
*options.Driver
|
||||
Download bool
|
||||
Compile bool
|
||||
DownloadHeaders bool
|
||||
driverDownloadOptions
|
||||
}
|
||||
|
||||
// NewDriverInstallCmd returns the driver install command.
|
||||
func NewDriverInstallCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
|
||||
o := driverInstallOptions{
|
||||
Common: opt,
|
||||
Driver: driver,
|
||||
// Defaults to downloading or building if needed
|
||||
Download: true,
|
||||
Compile: true,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "install [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Install previously configured driver",
|
||||
Long: `Install previously configured driver, either downloading it or attempting a build.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dest, err := o.RunDriverInstall(ctx)
|
||||
if dest != "" {
|
||||
// We don't care about errors at this stage
|
||||
// Fallback: try to load any available driver if leaving with an error.
|
||||
// It is only useful for kmod, as it will try to
|
||||
// modprobe a pre-existent version of the driver,
|
||||
// hoping it will be compatible.
|
||||
_ = driver.Type.Load(o.Printer, dest, o.Driver.Name, err != nil)
|
||||
}
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&o.Download, "download", true, "Whether to enable download of prebuilt drivers")
|
||||
cmd.Flags().BoolVar(&o.Compile, "compile", true, "Whether to enable local compilation of drivers")
|
||||
cmd.Flags().BoolVar(&o.DownloadHeaders, "download-headers", true, "Whether to enable automatic kernel headers download where supported")
|
||||
cmd.Flags().BoolVar(&o.InsecureDownload, "http-insecure", false, "Whether you want to allow insecure downloads or not")
|
||||
cmd.Flags().DurationVar(&o.HTTPTimeout, "http-timeout", 60*time.Second, "Timeout for each http try")
|
||||
cmd.Flags().StringVar(&o.HTTPHeaders, "http-headers",
|
||||
"",
|
||||
"Optional comma-separated list of headers for the http GET request "+
|
||||
"(e.g. --http-headers='x-emc-namespace: default,Proxy-Authenticate: Basic'). Not necessary if default repo is used")
|
||||
return cmd
|
||||
}
|
||||
|
||||
//nolint:gosec // this was an existent option in falco-driver-loader that we are porting.
|
||||
func setDefaultHTTPClientOpts(downloadOptions driverDownloadOptions) {
|
||||
// Skip insecure verify
|
||||
if downloadOptions.InsecureDownload {
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
http.DefaultClient.Timeout = downloadOptions.HTTPTimeout
|
||||
}
|
||||
|
||||
// RunDriverInstall implements the driver install command.
|
||||
func (o *driverInstallOptions) RunDriverInstall(ctx context.Context) (string, error) {
|
||||
o.Printer.Logger.Info("Running falcoctl driver install", o.Printer.Logger.Args(
|
||||
"driver version", o.Driver.Version,
|
||||
"driver type", o.Driver.Type,
|
||||
"driver name", o.Driver.Name,
|
||||
"compile", o.Compile,
|
||||
"download", o.Download,
|
||||
"target", o.Distro.String(),
|
||||
"arch", o.Kr.Architecture.ToNonDeb(),
|
||||
"kernel release", o.Kr.String(),
|
||||
"kernel version", o.Kr.KernelVersion))
|
||||
|
||||
if !o.Driver.Type.HasArtifacts() {
|
||||
o.Printer.Logger.Info("No artifacts needed for the selected driver.")
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !o.Download && !o.Compile {
|
||||
o.Printer.Logger.Info("Nothing to do: download and compile disabled.")
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if o.Distro.String() == driverdistro.UndeterminedDistro {
|
||||
if o.Compile {
|
||||
o.Download = false
|
||||
o.Printer.Logger.Info(
|
||||
"Detected an unsupported target system, please get in touch with the Falco community. Trying to compile anyway.")
|
||||
} else {
|
||||
return "", fmt.Errorf("detected an unsupported target system, please get in touch with the Falco community")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
dest string
|
||||
buf bytes.Buffer
|
||||
)
|
||||
|
||||
if !o.Printer.DisableStyling {
|
||||
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Cleaning up existing drivers")
|
||||
}
|
||||
err := o.Driver.Type.Cleanup(o.Printer.WithWriter(&buf), o.Driver.Name)
|
||||
if o.Printer.Spinner != nil {
|
||||
_ = o.Printer.Spinner.Stop()
|
||||
}
|
||||
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
|
||||
// Only print formatted text if we are formatting to json
|
||||
out := strings.ReplaceAll(buf.String(), "\n", ";")
|
||||
o.Printer.Logger.Info("Driver cleanup", o.Printer.Logger.Args("output", out))
|
||||
} else {
|
||||
// Print much more readable output as-is
|
||||
o.Printer.DefaultText.Print(buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if o.Download {
|
||||
setDefaultHTTPClientOpts(o.driverDownloadOptions)
|
||||
if !o.Printer.DisableStyling {
|
||||
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Trying to download the driver")
|
||||
}
|
||||
dest, err = driverdistro.Download(ctx, o.Distro, o.Printer.WithWriter(&buf), o.Kr, o.Driver.Name,
|
||||
o.Driver.Type, o.Driver.Version, o.Driver.Repos, o.HTTPHeaders)
|
||||
if o.Printer.Spinner != nil {
|
||||
_ = o.Printer.Spinner.Stop()
|
||||
}
|
||||
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
|
||||
// Only print formatted text if we are formatting to json
|
||||
out := strings.ReplaceAll(buf.String(), "\n", ";")
|
||||
o.Printer.Logger.Info("Driver download", o.Printer.Logger.Args("output", out))
|
||||
} else {
|
||||
// Print much more readable output as-is
|
||||
o.Printer.DefaultText.Print(buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
if err == nil {
|
||||
o.Printer.Logger.Info("Driver downloaded.", o.Printer.Logger.Args("path", dest))
|
||||
return dest, nil
|
||||
}
|
||||
if errors.Is(err, driverdistro.ErrAlreadyPresent) {
|
||||
o.Printer.Logger.Info("Skipping download, driver already present.", o.Printer.Logger.Args("path", dest))
|
||||
return dest, nil
|
||||
}
|
||||
// Print the error but go on
|
||||
// attempting a build if requested
|
||||
if o.Compile {
|
||||
o.Printer.Logger.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if o.Compile {
|
||||
if !o.Printer.DisableStyling {
|
||||
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Trying to build the driver")
|
||||
}
|
||||
dest, err = driverdistro.Build(ctx, o.Distro, o.Printer.WithWriter(&buf), o.Kr, o.Driver.Name, o.Driver.Type, o.Driver.Version, o.DownloadHeaders)
|
||||
if o.Printer.Spinner != nil {
|
||||
_ = o.Printer.Spinner.Stop()
|
||||
}
|
||||
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
|
||||
// Only print formatted text if we are formatting to json
|
||||
out := strings.ReplaceAll(buf.String(), "\n", ";")
|
||||
o.Printer.Logger.Info("Driver build", o.Printer.Logger.Args("output", out))
|
||||
} else {
|
||||
// Print much more readable output as-is
|
||||
o.Printer.DefaultText.Print(buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
if err == nil {
|
||||
return dest, nil
|
||||
}
|
||||
if errors.Is(err, driverdistro.ErrAlreadyPresent) {
|
||||
o.Printer.Logger.Info("Skipping build, driver already present.", o.Printer.Logger.Args("path", dest))
|
||||
return dest, nil
|
||||
}
|
||||
}
|
||||
|
||||
return o.Driver.Name, fmt.Errorf("failed: %w", err)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverinstall_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Install Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverinstall_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var driverInstallHelp = `Install previously configured driver, either downloading it or attempting a build.
|
||||
|
||||
Usage:
|
||||
falcoctl driver install [flags]
|
||||
|
||||
Flags:
|
||||
--compile Whether to enable local compilation of drivers (default true)
|
||||
--download Whether to enable download of prebuilt drivers (default true)
|
||||
--download-headers Whether to enable automatic kernel headers download where supported (default true)
|
||||
-h, --help help for install
|
||||
--http-headers string Optional comma-separated list of headers for the http GET request (e.g. --http-headers='x-emc-namespace: default,Proxy-Authenticate: Basic'). Not necessary if default repo is used
|
||||
--http-insecure Whether you want to allow insecure downloads or not
|
||||
--http-timeout duration Timeout for each http try (default 1m0s)
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--host-root string Driver host root to be used. (default "/")
|
||||
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
|
||||
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
--name string Driver name to be used. (default "falco")
|
||||
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
|
||||
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
|
||||
--version string Driver version to be used.
|
||||
`
|
||||
|
||||
var addAssertFailedBehavior = func(specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
var addAssertOkBehavior = func(specificOut string) {
|
||||
It("check that does not fail and the usage is not printed", func() {
|
||||
Succeed()
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificOut)))
|
||||
})
|
||||
}
|
||||
|
||||
var _ = Describe("install", func() {
|
||||
|
||||
var (
|
||||
driverCmd = "driver"
|
||||
installCmd = "install"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, installCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverInstallHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing failure cases for installing a driver.
|
||||
Context("failure", func() {
|
||||
When("with empty driver version", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, installCmd, "--config", configFile}
|
||||
})
|
||||
addAssertFailedBehavior(`ERROR version is mandatory and cannot be empty`)
|
||||
})
|
||||
|
||||
When("with non absolute host-root", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, installCmd, "--config", configFile, "--host-root", "foo/", "--version", "1.0.0+driver"}
|
||||
})
|
||||
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
|
||||
})
|
||||
|
||||
When("with invalid driver type", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, installCmd, "--config", configFile, "--type", "foo", "--version", "1.0.0+driver"}
|
||||
})
|
||||
addAssertFailedBehavior(`ERROR unsupported driver type specified: foo`)
|
||||
})
|
||||
})
|
||||
|
||||
Context("nothing-to-do", func() {
|
||||
When("with false download and compile", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, installCmd, "--config", configFile, "--download=false", "--compile=false", "--version", "1.0.0+driver"}
|
||||
})
|
||||
addAssertOkBehavior("INFO Nothing to do: download and compile disabled.")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package driverprintenv defines the logic to print driver-related variables as env vars.
|
||||
package driverprintenv
|
|
@ -0,0 +1,65 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverprintenv
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type driverPrintenvOptions struct {
|
||||
*options.Common
|
||||
*options.Driver
|
||||
}
|
||||
|
||||
// NewDriverPrintenvCmd print info about driver falcoctl config as env vars.
|
||||
func NewDriverPrintenvCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
|
||||
o := driverPrintenvOptions{
|
||||
Common: opt,
|
||||
Driver: driver,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "printenv [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Print env vars",
|
||||
Long: `Print variables used by driver as env vars.`,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return o.RunDriverPrintenv(ctx)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *driverPrintenvOptions) RunDriverPrintenv(_ context.Context) error {
|
||||
o.Printer.DefaultText.Printf("DRIVER=%q\n", o.Driver.Type.String())
|
||||
o.Printer.DefaultText.Printf("DRIVERS_REPO=%q\n", strings.Join(o.Driver.Repos, ", "))
|
||||
o.Printer.DefaultText.Printf("DRIVER_VERSION=%q\n", o.Driver.Version)
|
||||
o.Printer.DefaultText.Printf("DRIVER_NAME=%q\n", o.Driver.Name)
|
||||
o.Printer.DefaultText.Printf("HOST_ROOT=%q\n", o.Driver.HostRoot)
|
||||
o.Printer.DefaultText.Printf("TARGET_ID=%q\n", o.Distro.String())
|
||||
o.Printer.DefaultText.Printf("ARCH=%q\n", o.Kr.Architecture.ToNonDeb())
|
||||
o.Printer.DefaultText.Printf("KERNEL_RELEASE=%q\n", o.Kr.String())
|
||||
o.Printer.DefaultText.Printf("KERNEL_VERSION=%q\n", o.Kr.KernelVersion)
|
||||
fixedKr := o.Distro.FixupKernel(o.Kr)
|
||||
o.Printer.DefaultText.Printf("FIXED_KERNEL_RELEASE=%q\n", fixedKr.String())
|
||||
o.Printer.DefaultText.Printf("FIXED_KERNEL_VERSION=%q\n", fixedKr.KernelVersion)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverprintenv_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestPrintenv(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Printenv Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverprintenv_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var driverPrintenvHelp = `Print variables used by driver as env vars.
|
||||
|
||||
Usage:
|
||||
falcoctl driver printenv [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for printenv
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--host-root string Driver host root to be used. (default "/")
|
||||
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
|
||||
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
--name string Driver name to be used. (default "falco")
|
||||
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
|
||||
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
|
||||
--version string Driver version to be used.
|
||||
`
|
||||
|
||||
var driverPrintenvDefaultConfig = `DRIVER=".*"
|
||||
DRIVERS_REPO="https:\/\/download\.falco\.org\/driver"
|
||||
DRIVER_VERSION="1.0.0\+driver"
|
||||
DRIVER_NAME="falco"
|
||||
HOST_ROOT="\/"
|
||||
TARGET_ID=".*"
|
||||
ARCH="x86_64|aarch64"
|
||||
KERNEL_RELEASE=".*"
|
||||
KERNEL_VERSION=".*"
|
||||
FIXED_KERNEL_RELEASE=".*"
|
||||
FIXED_KERNEL_VERSION=".*"
|
||||
`
|
||||
|
||||
var addAssertFailedBehavior = func(specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
var _ = Describe("printenv", func() {
|
||||
|
||||
var (
|
||||
driverCmd = "driver"
|
||||
printenvCmd = "printenv"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, printenvCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverPrintenvHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing failure cases for cleaning a driver.
|
||||
Context("failure", func() {
|
||||
When("with empty driver version", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, printenvCmd, "--config", configFile}
|
||||
})
|
||||
addAssertFailedBehavior(`ERROR version is mandatory and cannot be empty `)
|
||||
})
|
||||
|
||||
When("with non absolute host-root", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, printenvCmd, "--config", configFile, "--host-root", "foo/", "--version", "1.0.0+driver"}
|
||||
})
|
||||
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
|
||||
})
|
||||
|
||||
When("with invalid driver type", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, printenvCmd, "--config", configFile, "--type", "foo", "--version", "1.0.0+driver"}
|
||||
})
|
||||
addAssertFailedBehavior(`unsupported driver type specified: foo`)
|
||||
})
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
When("with default config values", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, printenvCmd, "--config", configFile, "--version", "1.0.0+driver"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Succeed()
|
||||
MatchRegexp(driverPrintenvDefaultConfig)
|
||||
Expect(string(output.Contents())).To(MatchRegexp(driverPrintenvDefaultConfig))
|
||||
// Expect that output is bash setenv compatible
|
||||
scanner := bufio.NewScanner(output)
|
||||
for scanner.Scan() {
|
||||
vals := strings.Split(scanner.Text(), "=")
|
||||
Expect(vals).Should(HaveLen(2))
|
||||
err := os.Setenv(vals[0], vals[1])
|
||||
Expect(err).Should(BeNil())
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,95 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package add
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/cache"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// IndexAddOptions contains the options for the index add command.
|
||||
type IndexAddOptions struct {
|
||||
*options.Common
|
||||
}
|
||||
|
||||
// NewIndexAddCmd returns the index add command.
|
||||
func NewIndexAddCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := IndexAddOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [NAME] [URL] [BACKEND] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Add an index to the local falcoctl configuration",
|
||||
Long: "Add an index to the local falcoctl configuration. Indexes are used to perform search operations for artifacts",
|
||||
Args: cobra.RangeArgs(2, 3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunIndexAdd(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunIndexAdd implements the index add command.
|
||||
func (o *IndexAddOptions) RunIndexAdd(ctx context.Context, args []string) error {
|
||||
var err error
|
||||
logger := o.Printer.Logger
|
||||
|
||||
name := args[0]
|
||||
url := args[1]
|
||||
backend := ""
|
||||
if len(args) > 2 {
|
||||
backend = args[2]
|
||||
}
|
||||
|
||||
logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
|
||||
indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create index cache: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Adding index", logger.Args("name", name, "path", url))
|
||||
|
||||
if err = indexCache.Add(ctx, name, backend, url); err != nil {
|
||||
return fmt.Errorf("unable to add index: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Writing cache to disk")
|
||||
if _, err = indexCache.Write(); err != nil {
|
||||
return fmt.Errorf("unable to write cache to disk: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Adding new index entry to configuration", logger.Args("file", o.ConfigFile))
|
||||
if err = config.AddIndexes([]config.Index{{
|
||||
Name: name,
|
||||
URL: url,
|
||||
Backend: backend,
|
||||
}}, o.ConfigFile); err != nil {
|
||||
return fmt.Errorf("index entry %q: %w", name, err)
|
||||
}
|
||||
|
||||
logger.Info("Index successfully added")
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package add_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
const (
|
||||
rulesfiletgz = "../../../pkg/test/data/rules.tar.gz"
|
||||
rulesfileyaml = "../../../pkg/test/data/rules.yaml"
|
||||
plugintgz = "../../../pkg/test/data/plugin.tar.gz"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
var (
|
||||
registry string
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
port int
|
||||
orasRegistry *remote.Registry
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
RunSpecs(t, "Add Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
//nolint:unused // false positive
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package add_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var indexAddUsage = `Usage:
|
||||
falcoctl index add [NAME] [URL] [BACKEND] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for add
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var indexAddHelp = `Add an index to the local falcoctl configuration. Indexes are used to perform search operations for artifacts
|
||||
|
||||
Usage:
|
||||
falcoctl index add [NAME] [URL] [BACKEND] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for add
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
var addAssertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
var indexAddTests = Describe("add", func() {
|
||||
|
||||
var (
|
||||
indexCmd = "index"
|
||||
addCmd = "add"
|
||||
indexName = "testName"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{indexCmd, addCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(indexAddHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing failure cases for adding a new index.
|
||||
Context("failure", func() {
|
||||
When("without URL", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{indexCmd, addCmd, "--config", configFile, indexName}
|
||||
})
|
||||
addAssertFailedBehavior(indexAddUsage, "ERROR accepts between 2 and 3 arg(s), received 1")
|
||||
})
|
||||
|
||||
When("with invalid URL", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{indexCmd, addCmd, "--config", configFile, indexName, "NOTAPROTOCAL://something"}
|
||||
})
|
||||
addAssertFailedBehavior(indexAddUsage, "ERROR unable to add index: unable to fetch index \"testName\""+
|
||||
" with URL \"NOTAPROTOCAL://something\": unable to fetch index: cannot fetch index: Get "+
|
||||
"\"notaprotocal://something\": unsupported protocol scheme \"notaprotocal\"")
|
||||
})
|
||||
|
||||
When("with invalid backend", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{indexCmd, addCmd, "--config", configFile, indexName, "http://noindex", "notabackend"}
|
||||
})
|
||||
addAssertFailedBehavior(indexAddUsage, "ERROR unable to add index: unable to fetch index \"testName\" "+
|
||||
"with URL \"http://noindex\": unsupported index backend type: notabackend")
|
||||
})
|
||||
})
|
||||
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package add defines the options and add logic for the index files.
|
||||
package add
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package index implements the index commands.
|
||||
package index
|
|
@ -0,0 +1,50 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package index
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd/index/add"
|
||||
"github.com/falcosecurity/falcoctl/cmd/index/list"
|
||||
"github.com/falcosecurity/falcoctl/cmd/index/remove"
|
||||
"github.com/falcosecurity/falcoctl/cmd/index/update"
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewIndexCmd returns the index command.
|
||||
func NewIndexCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "index",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Interact with index",
|
||||
Long: "Interact with index",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
opt.Initialize()
|
||||
return config.Load(opt.ConfigFile)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(add.NewIndexAddCmd(ctx, opt))
|
||||
cmd.AddCommand(remove.NewIndexRemoveCmd(ctx, opt))
|
||||
cmd.AddCommand(update.NewIndexUpdateCmd(ctx, opt))
|
||||
cmd.AddCommand(list.NewIndexListCmd(ctx, opt))
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package list defines the logic to list the already configured index files.
|
||||
package list
|
|
@ -0,0 +1,67 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
indexConf "github.com/falcosecurity/falcoctl/pkg/index/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
type indexListOptions struct {
|
||||
*options.Common
|
||||
}
|
||||
|
||||
// NewIndexListCmd returns the index list command.
|
||||
func NewIndexListCmd(_ context.Context, opt *options.Common) *cobra.Command {
|
||||
o := indexListOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "List all the added indexes",
|
||||
Long: "List all the added indexes that were configured in falcoctl",
|
||||
Args: cobra.ExactArgs(0),
|
||||
Aliases: []string{"ls"},
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return o.RunIndexList()
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *indexListOptions) RunIndexList() error {
|
||||
indexConfig, err := indexConf.New(config.IndexesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data [][]string
|
||||
for _, conf := range indexConfig.Configs {
|
||||
newEntry := []string{conf.Name, conf.URL, conf.AddedTimestamp, conf.UpdatedTimestamp}
|
||||
data = append(data, newEntry)
|
||||
}
|
||||
|
||||
return o.Printer.PrintTable(output.IndexList, data)
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package remove defines options and logic to remove a previously add index file.
|
||||
package remove
|
|
@ -0,0 +1,83 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package remove
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/cache"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type indexRemoveOptions struct {
|
||||
*options.Common
|
||||
}
|
||||
|
||||
// NewIndexRemoveCmd returns the index remove command.
|
||||
func NewIndexRemoveCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := indexRemoveOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove [INDEX1 [INDEX2 ...]] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Remove an index from the local falcoctl configuration",
|
||||
Long: "Remove an index from the local falcoctl configuration",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Aliases: []string{"rm"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunIndexRemove(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *indexRemoveOptions) RunIndexRemove(ctx context.Context, args []string) error {
|
||||
logger := o.Printer.Logger
|
||||
|
||||
logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
|
||||
indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create index cache: %w", err)
|
||||
}
|
||||
|
||||
for _, name := range args {
|
||||
logger.Info("Removing index", logger.Args("name", name))
|
||||
if err = indexCache.Remove(name); err != nil {
|
||||
return fmt.Errorf("unable to remove index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Writing cache to disk")
|
||||
if _, err = indexCache.Write(); err != nil {
|
||||
return fmt.Errorf("unable to write cache to disk: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Removing indexes entries from configuration", logger.Args("file", o.ConfigFile))
|
||||
if err = config.RemoveIndexes(args, o.ConfigFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Indexes successfully removed")
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package update defines options and logic to update the index files.
|
||||
package update
|
|
@ -0,0 +1,77 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package update
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/cache"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type indexUpdateOptions struct {
|
||||
*options.Common
|
||||
}
|
||||
|
||||
// NewIndexUpdateCmd returns the index update command.
|
||||
func NewIndexUpdateCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := indexUpdateOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update [INDEX1 [INDEX2 ...]] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Update an existing index",
|
||||
Long: "Update an existing index",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunIndexUpdate(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *indexUpdateOptions) RunIndexUpdate(ctx context.Context, args []string) error {
|
||||
logger := o.Printer.Logger
|
||||
|
||||
logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
|
||||
indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create index cache: %w", err)
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
logger.Info("Updating index file", logger.Args("name", arg))
|
||||
if err := indexCache.Update(ctx, arg); err != nil {
|
||||
return fmt.Errorf("an error occurred while updating index %q: %w", arg, err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Writing cache to disk")
|
||||
if _, err = indexCache.Write(); err != nil {
|
||||
return fmt.Errorf("unable to write cache to disk: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Indexes successfully updated")
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
Copyright © 2019 Kris Nova <kris@nivenly.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// installCmd represents the install command
|
||||
var installCmd = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install a component wih falcoctl",
|
||||
Long: ``,
|
||||
//Run: func(cmd *cobra.Command, args []string) {
|
||||
// fmt.Println("install called")
|
||||
//},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(installCmd)
|
||||
|
||||
installCmd.AddCommand(installFalcoCmd)
|
||||
//installCmd.AddCommand(installOutputCmd)
|
||||
//installCmd.AddCommand(installRuleCmd)
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
Copyright © 2019 Kris Nova <kris@nivenly.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
kubernetesfalc "github.com/kris-nova/falcoctl/kubernetes"
|
||||
"github.com/kubicorn/kubicorn/pkg/cli"
|
||||
"github.com/kubicorn/kubicorn/pkg/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
installFalcoCmd = &cobra.Command{
|
||||
Use: "falco",
|
||||
Short: "Install Falco in Kubernetes",
|
||||
Long: `Deploy Falco to Kubernetes`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err, exitCode := InstallFalcoEntry(i, kubeConfigPath)
|
||||
if err != nil {
|
||||
logger.Critical("Fatal error: %v", err)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
logger.Always("Success.")
|
||||
/// os.Exit(1)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// InstallFalcoEntry is used as the main entry point that someone who wants to test or vendor the code should use.
|
||||
// This is the same starting place the CLI tool uses.
|
||||
func InstallFalcoEntry(installer *kubernetesfalc.FalcoInstaller, kubeConfigPath string) (error, int) {
|
||||
k8s, err := kubernetesfalc.NewK8sFromKubeConfigPath(kubeConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse kube config: %v", err), 98
|
||||
}
|
||||
err = installer.Install(k8s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to install falco in Kubernetes: %v", err), 99
|
||||
}
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
func init() {
|
||||
installFalcoCmd.Flags().StringVarP(&i.DameonSetName, "ds-name", "N",
|
||||
cli.StrEnvDef("FALCOCTL_KUBE_DS_NAME", "falco"), "Set the name to use with the Falco DS")
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd/registry/auth/basic"
|
||||
"github.com/falcosecurity/falcoctl/cmd/registry/auth/gcp"
|
||||
"github.com/falcosecurity/falcoctl/cmd/registry/auth/oauth"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewAuthCmd returns the registry command.
|
||||
func NewAuthCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Handle authentication towards OCI registries",
|
||||
Long: "Handle authentication towards OCI registries",
|
||||
}
|
||||
|
||||
cmd.AddCommand(basic.NewBasicCmd(ctx, opt))
|
||||
cmd.AddCommand(oauth.NewOauthCmd(ctx, opt))
|
||||
cmd.AddCommand(gcp.NewGcpCmd(ctx, opt))
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package basic
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/term"
|
||||
"oras.land/oras-go/v2/registry/remote/credentials"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/login/basic"
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
type loginOptions struct {
|
||||
*options.Common
|
||||
username string
|
||||
password string
|
||||
passwordFromStdin bool
|
||||
}
|
||||
|
||||
// NewBasicCmd returns the basic command.
|
||||
func NewBasicCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := loginOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "basic [hostname]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Login to an OCI registry",
|
||||
Long: `Login to an OCI registry
|
||||
|
||||
Example - Log in with username and password from command line flags:
|
||||
falcoctl registry auth basic -u username -p password localhost:5000
|
||||
|
||||
Example - Login with username and password from env variables:
|
||||
FALCOCTL_REGISTRY_AUTH_BASIC_USERNAME=username FALCOCTL_REGISTRY_AUTH_BASIC_PASSWORD=password falcoctl registry auth basic localhost:5000
|
||||
|
||||
Example - Login with username and password from stdin:
|
||||
falcoctl registry auth basic -u username --password-stdin localhost:5000
|
||||
|
||||
Example - Login with username and password in an interactive prompt:
|
||||
falcoctl registry auth basic localhost:5000
|
||||
`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
_ = viper.BindPFlag("registry.auth.basic.username", cmd.Flags().Lookup("username"))
|
||||
_ = viper.BindPFlag("registry.auth.basic.password", cmd.Flags().Lookup("password"))
|
||||
_ = viper.BindPFlag("registry.auth.basic.password_stdin", cmd.Flags().Lookup("password-stdin"))
|
||||
|
||||
o.username = viper.GetString("registry.auth.basic.username")
|
||||
o.password = viper.GetString("registry.auth.basic.password")
|
||||
o.passwordFromStdin = viper.GetBool("registry.auth.basic.password_stdin")
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunBasic(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&o.username, "username", "u", "", "registry username")
|
||||
cmd.Flags().StringVarP(&o.password, "password", "p", "", "registry password")
|
||||
cmd.Flags().BoolVar(&o.passwordFromStdin, "password-stdin", false, "read password from stdin")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunBasic executes the business logic for the basic command.
|
||||
func (o *loginOptions) RunBasic(ctx context.Context, args []string) error {
|
||||
var reg string
|
||||
logger := o.Printer.Logger
|
||||
|
||||
// Allow to have the registry expressed as a ref, but actually extract it.
|
||||
reg, err := utils.GetRegistryFromRef(args[0])
|
||||
if err != nil {
|
||||
reg = args[0]
|
||||
}
|
||||
|
||||
if err := getCredentials(o.Printer, o); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create empty client
|
||||
client := authn.NewClient()
|
||||
|
||||
// create credential store
|
||||
credentialStore, err := credentials.NewStore(config.RegistryCredentialConfPath(), credentials.StoreOptions{
|
||||
AllowPlaintextPut: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create new store: %w", err)
|
||||
}
|
||||
|
||||
if err := basic.Login(ctx, client, credentialStore, reg, o.username, o.password); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debug("Credentials added", logger.Args("credential store", config.RegistryCredentialConfPath()))
|
||||
logger.Info("Login succeeded", logger.Args("registry", reg, "user", o.username))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCredentials is used to retrieve username and password from standard input.
|
||||
func getCredentials(p *output.Printer, opt *loginOptions) error {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
if opt.username == "" {
|
||||
p.DefaultText.Print(p.FormatTitleAsLoggerInfo("Enter username:"))
|
||||
username, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt.username = strings.TrimSpace(username)
|
||||
}
|
||||
|
||||
if opt.password == "" {
|
||||
if opt.passwordFromStdin {
|
||||
password, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt.password = strings.TrimSuffix(string(password), "\n")
|
||||
opt.password = strings.TrimSuffix(opt.password, "\r")
|
||||
} else {
|
||||
p.DefaultText.Print(p.FormatTitleAsLoggerInfo("Enter password: "))
|
||||
bytePassword, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opt.password = string(bytePassword)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package basic_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
var (
|
||||
registry string
|
||||
registryBasic string
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
port int
|
||||
portBasic int
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
var err error
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
portBasic, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
registryBasic = fmt.Sprintf("localhost:%d", portBasic)
|
||||
RunSpecs(t, "Auth Basic Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
config := &configuration.Configuration{}
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
|
||||
testHtpasswdFileBasename := "authtest.htpasswd"
|
||||
testUsername, testPassword := "username", "password"
|
||||
|
||||
pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
htpasswdPath := filepath.Join(GinkgoT().TempDir(), testHtpasswdFileBasename)
|
||||
err = os.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
tlsConfig, err := testutils.BuildRegistryTLSConfig(GinkgoT().TempDir(), []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
configBasic := &configuration.Configuration{}
|
||||
configBasic.HTTP.Addr = fmt.Sprintf("localhost:%d", portBasic)
|
||||
configBasic.Auth = configuration.Auth{
|
||||
"htpasswd": configuration.Parameters{
|
||||
"realm": "localhost",
|
||||
"path": htpasswdPath,
|
||||
},
|
||||
}
|
||||
configBasic.HTTP.DrainTimeout = time.Duration(10) * time.Second
|
||||
configBasic.HTTP.TLS.CipherSuites = tlsConfig.CipherSuites
|
||||
configBasic.HTTP.TLS.Certificate = tlsConfig.CertificatePath
|
||||
configBasic.HTTP.TLS.Key = tlsConfig.PrivateKeyPath
|
||||
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Start the local registry with basic authentication.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), configBasic)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("https://%s", configBasic.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
//nolint:unused // false positive
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package basic_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Registry Registry `yaml:"registry"`
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
Auth Auth `yaml:"auth"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
OAuth []OAuth `yaml:"oauth"`
|
||||
}
|
||||
|
||||
type OAuth struct {
|
||||
Registry string `yaml:"registry"`
|
||||
ClientSecret string `yaml:"clientsecret"`
|
||||
ClientID string `yaml:"clientid"`
|
||||
TokerURL string `yaml:"tokenurl"`
|
||||
}
|
||||
|
||||
//nolint:lll,unused // no need to check for line length.
|
||||
var registryAuthBasicUsage = `Usage:
|
||||
falcoctl registry auth basic [hostname]
|
||||
|
||||
Flags:
|
||||
-h, --help help for basic
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
|
||||
-v, --verbose Enable verbose logs (default false)
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthBasicHelp = `Login to an OCI registry
|
||||
|
||||
Example - Log in with username and password from command line flags:
|
||||
falcoctl registry auth basic -u username -p password localhost:5000
|
||||
|
||||
Example - Login with username and password from env variables:
|
||||
FALCOCTL_REGISTRY_AUTH_BASIC_USERNAME=username FALCOCTL_REGISTRY_AUTH_BASIC_PASSWORD=password falcoctl registry auth basic localhost:5000
|
||||
|
||||
Example - Login with username and password from stdin:
|
||||
falcoctl registry auth basic -u username --password-stdin localhost:5000
|
||||
|
||||
Example - Login with username and password in an interactive prompt:
|
||||
falcoctl registry auth basic localhost:5000
|
||||
|
||||
Usage:
|
||||
falcoctl registry auth basic [hostname]
|
||||
|
||||
Flags:
|
||||
-h, --help help for basic
|
||||
-p, --password string registry password
|
||||
--password-stdin read password from stdin
|
||||
-u, --username string registry username
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthBasicAssertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthBasicTests = Describe("auth", func() {
|
||||
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
registryCmd = "registry"
|
||||
authCmd = "auth"
|
||||
basicCmd = "basic"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, authCmd, basicCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryAuthBasicHelp)))
|
||||
})
|
||||
})
|
||||
Context("failure", func() {
|
||||
|
||||
When("without hostname", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, authCmd, basicCmd}
|
||||
})
|
||||
registryAuthBasicAssertFailedBehavior(registryAuthBasicUsage,
|
||||
"ERROR accepts 1 arg(s), received 0")
|
||||
})
|
||||
})
|
||||
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package basic defines the logic to authenticate against an OCI registry.
|
||||
package basic
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package auth defines the logic to authenticate against an OCI registry.
|
||||
package auth
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package gcp defines the logic to authenticate against an Artifact registry using GCP credentials.
|
||||
package gcp
|
|
@ -0,0 +1,84 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/login/gcp"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
const (
|
||||
longGcp = `Register an Artifact Registry to use GCP Application Default credentials to connect to it.
|
||||
|
||||
In particular, it can use Workload Identity or GCE metadata server to authenticate.
|
||||
|
||||
Example
|
||||
falcoctl registry auth gcp europe-docker.pkg.dev
|
||||
`
|
||||
)
|
||||
|
||||
// RegistryGcpOptions contains the options for the registry gcp command.
|
||||
type RegistryGcpOptions struct {
|
||||
*options.Common
|
||||
}
|
||||
|
||||
// NewGcpCmd returns the gcp command.
|
||||
func NewGcpCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := RegistryGcpOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "gcp [REGISTRY]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Register an Artifact Registry to log in using GCP Application Default credentials",
|
||||
Long: longGcp,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunGcp(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunGcp executes the business logic for the gcp command.
|
||||
func (o *RegistryGcpOptions) RunGcp(ctx context.Context, args []string) error {
|
||||
var err error
|
||||
logger := o.Printer.Logger
|
||||
reg := args[0]
|
||||
if err = gcp.Login(ctx, reg); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("GCP authentication successful", logger.Args("registry", reg))
|
||||
|
||||
logger.Debug("Adding new gcp entry to configuration", logger.Args("file", o.ConfigFile))
|
||||
if err = config.AddGcp([]config.GcpAuth{{
|
||||
Registry: reg,
|
||||
}}, o.ConfigFile); err != nil {
|
||||
return fmt.Errorf("index entry %q: %w", reg, err)
|
||||
}
|
||||
|
||||
logger.Info("GCG authentication entry successfully added", logger.Args("registry", reg, "confgi file", o.ConfigFile))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package oauth defines the logic to authenticate against an OCI registry via OAuth2.0.
|
||||
package oauth
|
|
@ -0,0 +1,94 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/login/oauth"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
longOauth = `Store client credentials for later OAuth2.0 authentication
|
||||
|
||||
Client credentials will be saved in the ~/.config directory.
|
||||
|
||||
Example
|
||||
falcoctl registry oauth \
|
||||
--token-url="http://localhost:9096/token" \
|
||||
--client-id=000000 \
|
||||
--client-secret=999999 --scopes="my-scope" \
|
||||
hostname
|
||||
`
|
||||
)
|
||||
|
||||
// RegistryOauthOptions contains the options for the registry oauth command.
|
||||
type RegistryOauthOptions struct {
|
||||
*options.Common
|
||||
Conf clientcredentials.Config
|
||||
}
|
||||
|
||||
// NewOauthCmd returns the oauth command.
|
||||
func NewOauthCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := RegistryOauthOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "oauth [HOSTNAME]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Retrieve access and refresh tokens for OAuth2.0 client credentials flow authentication",
|
||||
Long: longOauth,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunOAuth(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&o.Conf.TokenURL, "token-url", "", "token URL used to get access and refresh tokens")
|
||||
if err := cmd.MarkFlagRequired("token-url"); err != nil {
|
||||
output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"token-url\" as required"))
|
||||
}
|
||||
cmd.Flags().StringVar(&o.Conf.ClientID, "client-id", "", "client ID of the OAuth2.0 app")
|
||||
if err := cmd.MarkFlagRequired("client-id"); err != nil {
|
||||
output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"client-id\" as required"))
|
||||
}
|
||||
cmd.Flags().StringVar(&o.Conf.ClientSecret, "client-secret", "", "client secret of the OAuth2.0 app")
|
||||
if err := cmd.MarkFlagRequired("client-secret"); err != nil {
|
||||
output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"client-secret\" as required"))
|
||||
return nil
|
||||
}
|
||||
cmd.Flags().StringSliceVar(&o.Conf.Scopes, "scopes", nil, "comma separeted list of scopes for which requesting access")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunOAuth executes the business logic for the oauth command.
|
||||
func (o *RegistryOauthOptions) RunOAuth(ctx context.Context, args []string) error {
|
||||
reg := args[0]
|
||||
if err := oauth.Login(ctx, reg, &o.Conf); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Printer.Logger.Info("Client credentials correctly saved", o.Printer.Logger.Args("file", config.ClientCredentialsFile))
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package oauth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
var (
|
||||
registry string
|
||||
oauthServer string
|
||||
oauthPort int
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
port int
|
||||
orasRegistry *remote.Registry
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestOAuth(t *testing.T) {
|
||||
var err error
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
oauthPort, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
RunSpecs(t, "OAuth Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Get the current user's home directory
|
||||
usr, err := user.Current()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Construct the path for the .config directory
|
||||
configDir := filepath.Join(usr.HomeDir, ".config", "falcoctl")
|
||||
|
||||
// Check if the directory already exists
|
||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||
// Directory doesn't exist, create it
|
||||
err := os.MkdirAll(configDir, 0o755)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
config := &configuration.Configuration{}
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create the oras registry.
|
||||
orasRegistry, err = testutils.NewOrasRegistry(registry, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
go func() {
|
||||
err := testutils.StartOAuthServer(context.Background(), oauthPort)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
//nolint:unused // false positive
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package oauth_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Registry Registry `yaml:"registry"`
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
Auth Auth `yaml:"auth"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
OAuth []OAuth `yaml:"oauth"`
|
||||
}
|
||||
|
||||
type OAuth struct {
|
||||
Registry string `yaml:"registry"`
|
||||
ClientSecret string `yaml:"clientsecret"`
|
||||
ClientID string `yaml:"clientid"`
|
||||
TokerURL string `yaml:"tokenurl"`
|
||||
}
|
||||
|
||||
//nolint:unused // false positive
|
||||
var correctIndexConfig = `indexes:
|
||||
- name: falcosecurity
|
||||
url: https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
`
|
||||
|
||||
//nolint:lll,unused // no need to check for line length.
|
||||
var registryAuthOAuthUsage = `Usage:
|
||||
falcoctl registry auth oauth [HOSTNAME]
|
||||
|
||||
Flags:
|
||||
--client-id string client ID of the OAuth2.0 app
|
||||
--client-secret string client secret of the OAuth2.0 app
|
||||
-h, --help help for oauth
|
||||
--scopes strings comma separeted list of scopes for which requesting access
|
||||
--token-url string token URL used to get access and refresh tokens
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthOAuthHelp = `Store client credentials for later OAuth2.0 authentication
|
||||
|
||||
Client credentials will be saved in the ~/.config directory.
|
||||
|
||||
Example
|
||||
falcoctl registry oauth \
|
||||
--token-url="http://localhost:9096/token" \
|
||||
--client-id=000000 \
|
||||
--client-secret=999999 --scopes="my-scope" \
|
||||
hostname
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthOAuthAssertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthOAuthTests = Describe("auth", func() {
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
registryCmd = "registry"
|
||||
authCmd = "auth"
|
||||
oauthCmd = "oauth"
|
||||
anSource = "myrepo.com/rules.git"
|
||||
artifact = "generic-repo"
|
||||
repo = "/" + artifact
|
||||
tag = "tag"
|
||||
repoAndTag = repo + ":" + tag
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, authCmd, oauthCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryAuthOAuthHelp)))
|
||||
})
|
||||
})
|
||||
Context("failure", func() {
|
||||
|
||||
When("without hostname", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, authCmd, oauthCmd}
|
||||
})
|
||||
registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
|
||||
"ERROR accepts 1 arg(s), received 0")
|
||||
})
|
||||
|
||||
When("wrong client id", func() {
|
||||
BeforeEach(func() {
|
||||
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err = os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
args = []string{registryCmd, authCmd, oauthCmd,
|
||||
"--client-id=000001", "--client-secret=999999",
|
||||
"--token-url", fmt.Sprintf("http://localhost:%d/token", oauthPort),
|
||||
"--config", configFilePath,
|
||||
"127.0.0.1:5000",
|
||||
}
|
||||
})
|
||||
registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
|
||||
`ERROR wrong client credentials, unable to retrieve token`)
|
||||
})
|
||||
|
||||
When("wrong client secret", func() {
|
||||
BeforeEach(func() {
|
||||
// start the OAuthServer
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
args = []string{registryCmd, authCmd, oauthCmd,
|
||||
"--client-id=000000", "--client-secret=999998",
|
||||
"--token-url", fmt.Sprintf("http://localhost:%d/token", oauthPort),
|
||||
"--config", configFilePath,
|
||||
"127.0.0.1:5000",
|
||||
}
|
||||
})
|
||||
registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
|
||||
`ERROR wrong client credentials, unable to retrieve token`)
|
||||
})
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
var (
|
||||
configFilePath string
|
||||
)
|
||||
|
||||
When("all good", func() {
|
||||
BeforeEach(func() {
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath = baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err = os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
args = []string{registryCmd, authCmd, oauthCmd,
|
||||
"--client-id=000000", "--client-secret=999999",
|
||||
"--token-url", fmt.Sprintf("http://localhost:%d/token", oauthPort),
|
||||
"--config", configFilePath,
|
||||
registry,
|
||||
}
|
||||
})
|
||||
|
||||
It("should successed", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`INFO Client credentials correctly saved`)))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package registry implements the registry commands.
|
||||
package registry
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package pull defnines the logic to pull artifacts from remote repositories.
|
||||
package pull
|
|
@ -0,0 +1,144 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
longPull = `Pull Falco "rulesfile" or "plugin" OCI artifacts from remote registry.
|
||||
|
||||
Artifact references are passed as arguments.
|
||||
|
||||
A reference is a fully qualified reference ("<registry>/<repository>"),
|
||||
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
|
||||
|
||||
Example - Pull artifact "myplugin" for the platform where falcoctl is running (default) in the current working directory (default):
|
||||
falcoctl registry pull localhost:5000/myplugin:latest
|
||||
|
||||
Example - Pull artifact "myplugin" for platform "linux/arm64" in the current working directory (default):
|
||||
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64
|
||||
|
||||
Example - Pull artifact "myplugin" for platform "linux/arm64" in "myDir" directory:
|
||||
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64 --dest-dir=./myDir
|
||||
|
||||
Example - Pull artifact "myrulesfile":
|
||||
falcoctl registry pull localhost:5000/myrulesfile:latest
|
||||
`
|
||||
)
|
||||
|
||||
type pullOptions struct {
|
||||
*options.Common
|
||||
*options.Artifact
|
||||
*options.Registry
|
||||
destDir string
|
||||
}
|
||||
|
||||
func (o *pullOptions) Validate() error {
|
||||
return o.Artifact.Validate()
|
||||
}
|
||||
|
||||
// NewPullCmd returns the pull command.
|
||||
func NewPullCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := pullOptions{
|
||||
Common: opt,
|
||||
Artifact: &options.Artifact{},
|
||||
Registry: &options.Registry{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "pull hostname/repo[:tag|@digest] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Pull a Falco OCI artifact from remote registry",
|
||||
Long: longPull,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := o.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := args[0]
|
||||
|
||||
_, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Common.Initialize()
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunPull(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
output.ExitOnErr(o.Printer, o.Artifact.AddFlags(cmd))
|
||||
cmd.Flags().StringVarP(&o.destDir, "dest-dir", "o", "", "destination dir where to save the artifacts(default: current directory)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunPull executes the business logic for the pull command.
|
||||
func (o *pullOptions) RunPull(ctx context.Context, args []string) error {
|
||||
logger := o.Printer.Logger
|
||||
ref := args[0]
|
||||
|
||||
registry, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
puller, err := ociutils.Puller(o.PlainHTTP, o.Printer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while creating the puller for registry %s: %w", registry, err)
|
||||
}
|
||||
|
||||
err = ociutils.CheckConnectionForRegistry(ctx, puller.Client, o.PlainHTTP, registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Preparing to pull artifact", logger.Args("name", args[0]))
|
||||
|
||||
if o.destDir == "" {
|
||||
logger.Info("Pulling artifact in the current directory")
|
||||
} else {
|
||||
logger.Info("Pulling artifact in", logger.Args("directory", o.destDir))
|
||||
}
|
||||
|
||||
os, arch := runtime.GOOS, runtime.GOARCH
|
||||
if len(o.Artifact.Platforms) > 0 {
|
||||
os, arch = o.OSArch(0)
|
||||
}
|
||||
|
||||
res, err := puller.Pull(ctx, ref, o.destDir, os, arch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Artifact pulled", logger.Args("name", args[0], "type", res.Type, "digest", res.Digest))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pull_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
const (
|
||||
rulesfiletgz = "../../../pkg/test/data/rules.tar.gz"
|
||||
rulesfileyaml = "../../../pkg/test/data/rules.yaml"
|
||||
plugintgz = "../../../pkg/test/data/plugin.tar.gz"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
var (
|
||||
registry string
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
port int
|
||||
orasRegistry *remote.Registry
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestPull(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
RunSpecs(t, "Pull Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
config := &configuration.Configuration{}
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create the oras registry.
|
||||
orasRegistry, err = testutils.NewOrasRegistry(registry, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
//nolint:unused // false positive
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pull_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
|
||||
out "github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
//nolint:lll,unused // no need to check for line length.
|
||||
var registryPullUsage = `Usage:
|
||||
falcoctl registry pull hostname/repo[:tag|@digest] [flags]
|
||||
|
||||
Flags:
|
||||
-o, --dest-dir string destination dir where to save the artifacts(default: current directory)
|
||||
-h, --help help for pull
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
--platform stringArray os and architecture of the artifact in OS/ARCH format (only for plugins artifacts)
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
|
||||
-v, --verbose Enable verbose logs (default false)
|
||||
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryPullHelp = `Pull Falco "rulesfile" or "plugin" OCI artifacts from remote registry.
|
||||
|
||||
Artifact references are passed as arguments.
|
||||
|
||||
A reference is a fully qualified reference ("<registry>/<repository>"),
|
||||
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
|
||||
|
||||
Example - Pull artifact "myplugin" for the platform where falcoctl is running (default) in the current working directory (default):
|
||||
falcoctl registry pull localhost:5000/myplugin:latest
|
||||
|
||||
Example - Pull artifact "myplugin" for platform "linux/arm64" in the current working directory (default):
|
||||
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64
|
||||
|
||||
Example - Pull artifact "myplugin" for platform "linux/arm64" in "myDir" directory:
|
||||
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64 --dest-dir=./myDir
|
||||
|
||||
Example - Pull artifact "myrulesfile":
|
||||
falcoctl registry pull localhost:5000/myrulesfile:latest
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var pullAssertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryPullTests = Describe("pull", func() {
|
||||
var (
|
||||
pusher *ocipusher.Pusher
|
||||
ref string
|
||||
config ocipusher.Option
|
||||
)
|
||||
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
registryCmd = "registry"
|
||||
pullCmd = "pull"
|
||||
dep1 = "myplugin:1.2.3"
|
||||
dep2 = "myplugin1:1.2.3|otherplugin:3.2.1"
|
||||
req = "engine_version:15"
|
||||
anSource = "myrepo.com/rules.git"
|
||||
artifact = "generic-repo"
|
||||
repo = "/" + artifact
|
||||
tag = "tag"
|
||||
repoAndTag = repo + ":" + tag
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pullCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryPullHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing all the failure cases using both the rulesfile and plugin artifact types.
|
||||
// The common logic for the artifacts is tested once using a rulesfile artifact, no need to repeat
|
||||
// the same test using a plugin artifact.
|
||||
Context("failure", func() {
|
||||
var (
|
||||
tracker out.Tracker
|
||||
options []ocipusher.Option
|
||||
filePathsAndPlatforms ocipusher.Option
|
||||
destDir string
|
||||
)
|
||||
const (
|
||||
plainHTTP = true
|
||||
testPluginPlatform1 = "linux/amd64"
|
||||
)
|
||||
|
||||
When("without artifact", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pullCmd}
|
||||
})
|
||||
pullAssertFailedBehavior(registryPullUsage, "ERROR accepts 1 arg(s), received 0")
|
||||
})
|
||||
|
||||
When("unreachable registry", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{registryCmd, pullCmd, "noregistry/testrules", "--plain-http", "--config", configFile}
|
||||
})
|
||||
pullAssertFailedBehavior(registryPullUsage, "ERROR unable to connect to remote registry")
|
||||
})
|
||||
|
||||
When("invalid repository", func() {
|
||||
newReg := registry + "/wrong:latest"
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{registryCmd, pullCmd, newReg, "--plain-http", "--config", configFile}
|
||||
})
|
||||
pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERROR %s: not found", newReg))
|
||||
})
|
||||
|
||||
When("unwritable --dest-dir", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Chmod(destDir, 0o555)
|
||||
Expect(err).To(BeNil())
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
args = []string{registryCmd, pullCmd, ref, "--plain-http",
|
||||
"--platform", testPluginPlatform1, "--dest-dir", destDir,
|
||||
"--config", configFile,
|
||||
}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
tmp := strings.Split(repoAndTag, "/")
|
||||
artNameAndTag := tmp[len(tmp)-1]
|
||||
tmp = strings.Split(artNameAndTag, ":")
|
||||
artName := tmp[0]
|
||||
tag := tmp[1]
|
||||
expectedError := fmt.Sprintf(
|
||||
"ERROR unable to pull artifact generic-repo with %s tag from repo %s: failed to create file",
|
||||
tag, artName)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("--dest-dir not present (and parent not writable)", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
err = os.Chmod(baseDir, 0o555)
|
||||
Expect(err).To(BeNil())
|
||||
destDir = baseDir + "/dest"
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
args = []string{registryCmd, pullCmd, ref, "--plain-http",
|
||||
"--platform", testPluginPlatform1, "--dest-dir", destDir,
|
||||
"--config", configFile,
|
||||
}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR unable to pull artifact %s with tag %s from repo %s: failed to ensure directories of the target path: "+
|
||||
"mkdir %s: permission denied", artifact, tag, artifact, destDir)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("wrong digest format", func() {
|
||||
wrongDigest := "sha256:06f961b802bc46ee168555f066d28f4f0e9afdf3f88174c1ee6f9de004fc30a0"
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag + "@" + wrongDigest
|
||||
args = []string{registryCmd, pullCmd, ref, "--plain-http",
|
||||
"--platform", testPluginPlatform1, "--config", configFile}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR %s: not found", registry+repo+"@"+wrongDigest)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("missing repository", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
ref = repoAndTag
|
||||
args = []string{registryCmd, pullCmd, ref, "--plain-http", "--config", configFile}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR cannot extract registry name from ref %q", ref)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("invalid repository", func() {
|
||||
newReg := registry + "/wrong@something"
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{registryCmd, pullCmd, newReg, "--plain-http", "--config", configFile}
|
||||
})
|
||||
pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERROR unable to create new repository with ref %s: "+
|
||||
"invalid reference: invalid digest %q: invalid checksum digest format\n", newReg, "something"))
|
||||
})
|
||||
|
||||
When("invalid platform", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
args = []string{registryCmd, pullCmd, ref, "--plain-http",
|
||||
"--platform", "linux/unknown", "--config", configFile}
|
||||
})
|
||||
|
||||
pullAssertFailedBehavior(registryPullUsage, "not found: no matching manifest was found in the manifest list")
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package push defines the logic to push local artifacts to a remote repository.
|
||||
package push
|
|
@ -0,0 +1,343 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package push
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
longPush = `Push Falco "rulesfile" or "plugin" OCI artifacts to remote registry
|
||||
|
||||
Example - Push artifact "myplugin.tar.gz" of type "plugin" for the platform where falcoctl is running (default):
|
||||
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest myplugin.tar.gz
|
||||
|
||||
Example - Push artifact "myplugin.tar.gz" of type "plugin" for platform "linux/arm64":
|
||||
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest myplugin.tar.gz --platform linux/arm64
|
||||
|
||||
Example - Push artifact "myplugin.tar.gz" of type "plugin" for multiple platforms:
|
||||
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest \
|
||||
myplugin-linux-x86_64.tar.gz --platform linux/x86_64 \
|
||||
myplugin-linux-arm64.tar.gz --platform linux/arm64
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile":
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with floating tags for the major and minor versions (0 and 0.1):
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
|
||||
--add-floating-tags
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" to an insecure registry:
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" --plain-http localhost:5000/myrulesfile:latest myrulesfile.tar.gz
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with a dependency "myplugin:1.2.3":
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
|
||||
--depends-on myplugin:1.2.3
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with a dependency "myplugin:1.2.3" and an alternative "otherplugin:3.2.1":
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
|
||||
--depends-on "myplugin:1.2.3|otherplugin:3.2.1"
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with multiple dependencies "myplugin:1.2.3", "otherplugin:3.2.1":
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
|
||||
--depends-on myplugin:1.2.3 \
|
||||
--depends-on otherplugin:3.2.1
|
||||
`
|
||||
)
|
||||
|
||||
type pushOptions struct {
|
||||
*options.Common
|
||||
*options.Artifact
|
||||
*options.Registry
|
||||
}
|
||||
|
||||
func (o *pushOptions) validate() error {
|
||||
return o.Artifact.Validate()
|
||||
}
|
||||
|
||||
// NewPushCmd returns the push command.
|
||||
func NewPushCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := pushOptions{
|
||||
Common: opt,
|
||||
Artifact: &options.Artifact{},
|
||||
Registry: &options.Registry{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "push hostname/repo[:tag|@digest] file [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Push a Falco OCI artifact to remote registry",
|
||||
Long: longPush,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := o.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := args[0]
|
||||
|
||||
_, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.runPush(ctx, args)
|
||||
},
|
||||
}
|
||||
o.Registry.AddFlags(cmd)
|
||||
output.ExitOnErr(o.Printer, o.Artifact.AddFlags(cmd))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runPush executes the business logic for the push command.
|
||||
func (o *pushOptions) runPush(ctx context.Context, args []string) error {
|
||||
ref := args[0]
|
||||
paths := args[1:]
|
||||
// When creating the tar.gz archives we need to remove them after we are done.
|
||||
// Holds the path for each temporary dir.
|
||||
var toBeDeletedTmpDirs []string
|
||||
logger := o.Printer.Logger
|
||||
|
||||
registry, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pusher, err := ociutils.Pusher(o.PlainHTTP, o.Printer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while creating the pusher for registry %s: %w", registry, err)
|
||||
}
|
||||
|
||||
err = ociutils.CheckConnectionForRegistry(ctx, pusher.Client, o.PlainHTTP, registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Preparing to push artifact", o.Printer.Logger.Args("name", args[0], "type", o.ArtifactType))
|
||||
|
||||
// Make sure to remove temporary working dirs.
|
||||
defer func() {
|
||||
for _, dir := range toBeDeletedTmpDirs {
|
||||
logger.Debug("Removing temporary dir", logger.Args("name", dir))
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
logger.Warn("Unable to remove temporary dir", logger.Args("name", dir, "error", err.Error()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
config := &oci.ArtifactConfig{
|
||||
Name: o.Name,
|
||||
Version: o.Version,
|
||||
}
|
||||
|
||||
for i, p := range paths {
|
||||
if err = utils.IsTarGz(filepath.Clean(p)); err != nil && !errors.Is(err, utils.ErrNotTarGz) {
|
||||
return err
|
||||
} else if err == nil {
|
||||
continue
|
||||
} else {
|
||||
if o.ArtifactType == oci.Rulesfile {
|
||||
if config, err = rulesConfigLayer(o.Printer.Logger, p, o.Artifact); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
path, err := utils.CreateTarGzArchive("", p, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paths[i] = path
|
||||
toBeDeletedTmpDirs = append(toBeDeletedTmpDirs, filepath.Dir(path))
|
||||
}
|
||||
}
|
||||
|
||||
if config.Name == "" {
|
||||
// extract artifact name from ref, if not provided by the user
|
||||
if config.Name, err = utils.NameFromRef(ref); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := config.ParseDependencies(o.Dependencies...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := config.ParseRequirements(o.Requirements...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.AutoFloatingTags {
|
||||
v, err := semver.Parse(o.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expected semver for the flag \"--version\": %w", err)
|
||||
}
|
||||
o.Tags = append(o.Tags, o.Version, fmt.Sprintf("%v", v.Major), fmt.Sprintf("%v.%v", v.Major, v.Minor))
|
||||
}
|
||||
|
||||
opts := ocipusher.Options{
|
||||
ocipusher.WithTags(o.Tags...),
|
||||
ocipusher.WithAnnotationSource(o.AnnotationSource),
|
||||
ocipusher.WithArtifactConfig(*config),
|
||||
}
|
||||
|
||||
switch o.ArtifactType {
|
||||
case oci.Plugin:
|
||||
opts = append(opts, ocipusher.WithFilepathsAndPlatforms(paths, o.Platforms))
|
||||
case oci.Rulesfile:
|
||||
opts = append(opts, ocipusher.WithFilepaths(paths))
|
||||
case oci.Asset:
|
||||
opts = append(opts, ocipusher.WithFilepaths(paths))
|
||||
}
|
||||
|
||||
res, err := pusher.Push(ctx, o.ArtifactType, ref, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Artifact pushed", logger.Args("name", args[0], "type", res.Type, "digest", res.RootDigest))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
// depsKey is the key for deps in the rulesfiles.
|
||||
depsKey = "required_plugin_versions"
|
||||
// engineKey is the key in the rulesfiles.
|
||||
engineKey = "required_engine_version"
|
||||
// engineRequirementKey is used as name for the engine requirement in the config layer for the rulesfile artifacts.
|
||||
engineRequirementKey = "engine_version_semver"
|
||||
)
|
||||
|
||||
func rulesConfigLayer(logger *pterm.Logger, filePath string, artifactOptions *options.Artifact) (*oci.ArtifactConfig, error) {
|
||||
var data []map[string]interface{}
|
||||
|
||||
// Setup OCI artifact configuration
|
||||
config := oci.ArtifactConfig{
|
||||
Name: artifactOptions.Name,
|
||||
Version: artifactOptions.Version,
|
||||
}
|
||||
|
||||
yamlFile, err := os.ReadFile(filepath.Clean(filePath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open rulesfile %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(yamlFile, &data); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal rulesfile %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
// Parse the artifact dependencies.
|
||||
// Check if the user has provided any.
|
||||
if len(artifactOptions.Dependencies) != 0 {
|
||||
logger.Info("Dependencies provided by user", logger.Args("rulesfile", filePath))
|
||||
if err = config.ParseDependencies(artifactOptions.Dependencies...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// If no user provided then try to parse them from the rulesfile.
|
||||
var found bool
|
||||
logger.Info("Parsing dependencies from: ", logger.Args("rulesfile", filePath))
|
||||
var requiredPluginVersionsEntry interface{}
|
||||
var ok bool
|
||||
for _, entry := range data {
|
||||
if requiredPluginVersionsEntry, ok = entry[depsKey]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var deps []oci.ArtifactDependency
|
||||
byteData, err := yaml.Marshal(requiredPluginVersionsEntry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse dependencies from rulesfile: %w", err)
|
||||
}
|
||||
err = yaml.Unmarshal(byteData, &deps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse dependencies from rulesfile: %w", err)
|
||||
}
|
||||
logger.Info("Dependencies correctly parsed from rulesfile")
|
||||
// Set the deps.
|
||||
config.Dependencies = deps
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
logger.Warn("No dependencies were provided by the user and none were found in the rulesfile.")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the requirements.
|
||||
// Check if the user has provided any.
|
||||
if len(artifactOptions.Requirements) != 0 {
|
||||
logger.Info("Requirements provided by user")
|
||||
if err = config.ParseRequirements(artifactOptions.Requirements...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
var found bool
|
||||
var engineVersion string
|
||||
logger.Info("Parsing requirements from: ", logger.Args("rulesfile", filePath))
|
||||
// If no user provided requirements then try to parse them from the rulesfile.
|
||||
for _, entry := range data {
|
||||
if requiredEngineVersionEntry, ok := entry[engineKey]; ok {
|
||||
// Check if the version is an int. This is for backward compatibility. The engine version used to be an
|
||||
// int but internally used by falco as a semver minor version.
|
||||
// 15 -> 0.15.0
|
||||
if engVersionInt, ok := requiredEngineVersionEntry.(int); ok {
|
||||
engineVersion = fmt.Sprintf("0.%d.0", engVersionInt)
|
||||
} else {
|
||||
engineVersion, ok = requiredEngineVersionEntry.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s must be an int or a string respecting the semver specification, got type %T", engineKey, requiredEngineVersionEntry)
|
||||
}
|
||||
|
||||
// Check if it is in semver format.
|
||||
if _, err := semver.Parse(engineVersion); err != nil {
|
||||
return nil, fmt.Errorf("%s must be in semver format: %w", engineVersion, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the requirements.
|
||||
config.Requirements = []oci.ArtifactRequirement{{
|
||||
Name: engineRequirementKey,
|
||||
Version: engineVersion,
|
||||
}}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
logger.Warn("No requirements were provided by the user and none were found in the rulesfile.")
|
||||
}
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package push_test
|
||||
|
||||
// revive:disable
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
// revive:enable
|
||||
var _ = Describe("pushing plugins", func() {
|
||||
var (
|
||||
registryCmd = "registry"
|
||||
pushCmd = "push"
|
||||
version = "1.1.1"
|
||||
// fullRepoName is set each time before each test.
|
||||
fullRepoName string
|
||||
// repoName same as fullRepoName.
|
||||
repoName string
|
||||
// It is set in the config layer.
|
||||
artifactNameInConfigLayer = "test-push-plugins"
|
||||
pushedTags = []string{"tag1", "tag2", "latest"}
|
||||
|
||||
// Plugin's platforms.
|
||||
platformARM64 = "linux/arm64"
|
||||
platformAMD64 = "linux/amd64"
|
||||
|
||||
// Paths pointing to plugins that will be pushed.
|
||||
// Some of the functions expect these two variable to be set to valid paths.
|
||||
// They are set in beforeEach blocks by tests that need them.
|
||||
pluginOne string
|
||||
pluginTwo string
|
||||
// Data fetched from registry and used for assertions.
|
||||
pluginData *testutils.PluginArtifact
|
||||
)
|
||||
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
requirement = "plugin_api_version:3.2.1"
|
||||
anSource = "myrepo.com/rules.git"
|
||||
pluginsRepoBaseName = "push-plugins-tests"
|
||||
)
|
||||
|
||||
var AssertSuccessBehaviour = func(deps []oci.ArtifactDependency, reqs []oci.ArtifactRequirement, annotations map[string]string, platforms []string) {
|
||||
It("should succeed", func() {
|
||||
// We do not check the error here since we are checking it after
|
||||
// pushing the artifact.
|
||||
By("checking no error in output")
|
||||
Expect(output).ShouldNot(gbytes.Say("ERROR"))
|
||||
Expect(output).ShouldNot(gbytes.Say("Unable to remove temporary dir"))
|
||||
|
||||
By("checking descriptor")
|
||||
Expect(pluginData.Descriptor.MediaType).Should(Equal(v1.MediaTypeImageIndex))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(pluginData.Descriptor.Digest.String())))
|
||||
|
||||
By("checking index")
|
||||
Expect(pluginData.Index.Manifests).Should(HaveLen(len(platforms)))
|
||||
|
||||
By("checking platforms")
|
||||
for _, p := range platforms {
|
||||
Expect(pluginData.Platforms).Should(HaveKey(p))
|
||||
}
|
||||
|
||||
By("checking config layers")
|
||||
for plat, p := range pluginData.Platforms {
|
||||
By(fmt.Sprintf("platform %s", plat))
|
||||
Expect(p.Config.Version).Should(Equal(version))
|
||||
Expect(p.Config.Name).Should(Equal(artifactNameInConfigLayer))
|
||||
|
||||
By("checking dependencies")
|
||||
Expect(p.Config.Dependencies).Should(HaveLen(len(deps)))
|
||||
for _, dep := range deps {
|
||||
Expect(p.Config.Dependencies).Should(ContainElement(dep))
|
||||
}
|
||||
|
||||
By("checking requirements")
|
||||
Expect(p.Config.Requirements).Should(HaveLen(len(reqs)))
|
||||
for _, req := range reqs {
|
||||
Expect(p.Config.Requirements).Should(ContainElement(req))
|
||||
}
|
||||
|
||||
By("checking annotations")
|
||||
// The creation timestamp is always present.
|
||||
Expect(p.Manifest.Annotations).Should(HaveLen(len(annotations) + 1))
|
||||
for key, val := range annotations {
|
||||
Expect(p.Manifest.Annotations).Should(HaveKeyWithValue(key, val))
|
||||
}
|
||||
}
|
||||
|
||||
By("checking tags")
|
||||
Expect(pluginData.Tags).Should(HaveLen(len(pushedTags)))
|
||||
Expect(pluginData.Tags).Should(ContainElements(pushedTags))
|
||||
|
||||
By("checking that temporary dirs have been removed")
|
||||
|
||||
Eventually(func() bool {
|
||||
entries, err := os.ReadDir("/tmp")
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
matched, err := filepath.Match(utils.TmpDirPrefix+"*", regexp.QuoteMeta(e.Name()))
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}).WithTimeout(5 * time.Second).Should(BeFalse())
|
||||
})
|
||||
}
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
// Reset the status after each test.
|
||||
// This variable could be changed by single tests.
|
||||
// Make sure to set them at their default values.
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
artifactNameInConfigLayer = "test-plugin"
|
||||
pushedTags = []string{"tag1", "tag2", "latest"}
|
||||
pluginOne = ""
|
||||
pluginTwo = ""
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
JustBeforeEach(func() {
|
||||
// Check the returned error before proceeding.
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
pluginData, err = testutils.FetchPluginFromRegistry(ctx, repoName, pushedTags[0], orasRegistry)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
When("two platforms, with reqs and deps", func() {
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, pluginsRepoBaseName)
|
||||
pluginOne = rulesfileyaml
|
||||
pluginTwo = plugintgz
|
||||
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, pluginOne, pluginTwo, "--type", "plugin", "--platform",
|
||||
platformAMD64, "--platform", platformARM64, "--version", version, "--config", configFile,
|
||||
"--plain-http", "--depends-on", "my-test:4.3.2", "--requires", requirement, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
|
||||
AssertSuccessBehaviour([]oci.ArtifactDependency{{
|
||||
Name: "my-test",
|
||||
Version: "4.3.2",
|
||||
Alternatives: nil,
|
||||
}}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "plugin_api_version",
|
||||
Version: "3.2.1",
|
||||
},
|
||||
}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
}, []string{
|
||||
platformAMD64, platformARM64,
|
||||
})
|
||||
})
|
||||
|
||||
When("one platform, no reqs", func() {
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, pluginsRepoBaseName)
|
||||
pluginOne = plugintgz
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, pluginOne, "--type", "plugin", "--platform",
|
||||
platformAMD64, "--version", version, "--config", configFile,
|
||||
"--plain-http", "--depends-on", "my-test:4.3.2", "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
// We expect to succeed and that the requirement is empty.
|
||||
AssertSuccessBehaviour([]oci.ArtifactDependency{{
|
||||
Name: "my-test",
|
||||
Version: "4.3.2",
|
||||
Alternatives: nil,
|
||||
}}, []oci.ArtifactRequirement{}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
}, []string{
|
||||
platformAMD64,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue