Compare commits
No commits in common. "main" and "v0.5.0-rc1" have entirely different histories.
main
...
v0.5.0-rc1
|
@ -1,6 +1,6 @@
|
|||
<!-- 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.
|
||||
1. If this is your first time, please read our contributor guidelines in the [CONTRIBUTING.md](https://github.com/falcosecurity/falco/blob/dev/CONTRIBUTING.md) file in the Falco 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"
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
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"
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
- go
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@d23060145bc9131d50558d5d4185494a20208101 # v2.2.8
|
||||
with:
|
||||
|
|
|
@ -12,64 +12,48 @@ on:
|
|||
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 }}
|
||||
image: ${{ steps.build-and-push.outputs.image }}
|
||||
digest: ${{ steps.build-and-push.outputs.digest }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2.1.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
uses: docker/setup-buildx-action@4b4e9c3e2d4531116a6f8ba8e71fc6e2cb6e6c8c # v2.5.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_SECRET }}
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@03d0fecf172873164a163bbc64bed0f3bf114ed7 # v3.4.0
|
||||
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
|
||||
cosign-release: 'v2.0.2'
|
||||
|
||||
- name: Docker Meta
|
||||
id: meta_falcoctl
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@507c2f2dc502c992ad446e3d7a5dfbe311567a96 # v4.3.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 }}
|
||||
|
@ -78,7 +62,7 @@ jobs:
|
|||
|
||||
- name: Build and push
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 # v4.0.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
@ -89,15 +73,9 @@ jobs:
|
|||
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}
|
||||
run: echo "${TAGS}" | xargs -I {} cosign sign -y {}@${DIGEST}
|
||||
|
|
|
@ -23,14 +23,14 @@ jobs:
|
|||
goos: windows
|
||||
steps:
|
||||
- name: Checkout commit
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
go-version: '1.20'
|
||||
check-latest: true
|
||||
|
||||
- name: Build Falcoctl
|
||||
|
@ -47,14 +47,14 @@ jobs:
|
|||
tar -czvf falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz falcoctl LICENSE
|
||||
|
||||
- name: Upload falcoctl artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.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
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
|
||||
path: ./falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
|
||||
|
@ -80,86 +80,22 @@ jobs:
|
|||
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
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
go-version: '1.20'
|
||||
check-latest: true
|
||||
|
||||
- name: Run tests
|
||||
|
|
|
@ -8,25 +8,24 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||
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
|
||||
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
|
||||
with:
|
||||
go-version: "^1.24.3"
|
||||
go-version-file: "go.mod"
|
||||
go-version: '1.20'
|
||||
check-latest: true
|
||||
cache: "false"
|
||||
cache: 'false'
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2
|
||||
uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0
|
||||
with:
|
||||
only-new-issues: true
|
||||
version: v1.64.7
|
||||
version: v1.52
|
||||
args: --timeout=900s
|
||||
|
||||
gomodtidy:
|
||||
|
@ -35,16 +34,16 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||
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
|
||||
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
go-version: '1.20'
|
||||
check-latest: true
|
||||
|
||||
- name: Execute go mod tidy and check the outcome
|
||||
|
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
hashes: ${{ steps.hash.outputs.hashes }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -22,21 +22,21 @@ jobs:
|
|||
run: git fetch --force --tags
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
go-version: '1.20'
|
||||
check-latest: true
|
||||
|
||||
- name: Run GoReleaser
|
||||
id: run-goreleaser
|
||||
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b # v4.2.0
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
- name: Generate subject
|
||||
id: hash
|
||||
env:
|
||||
|
@ -46,25 +46,25 @@ jobs:
|
|||
|
||||
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
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.6.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
|
||||
uses: slsa-framework/slsa-verifier/actions/installer@v2.3.0
|
||||
|
||||
- name: Download assets
|
||||
env:
|
||||
|
@ -75,7 +75,7 @@ jobs:
|
|||
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 }}
|
||||
|
@ -105,7 +105,7 @@ jobs:
|
|||
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
|
||||
|
@ -118,15 +118,14 @@ jobs:
|
|||
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:
|
||||
provenance-for-images:
|
||||
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
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.6.0
|
||||
with:
|
||||
image: docker.io/falcosecurity/falcoctl
|
||||
# The image digest is used to prevent TOCTOU issues.
|
||||
|
@ -136,43 +135,3 @@ jobs:
|
|||
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 }}
|
||||
|
|
|
@ -11,8 +11,7 @@ linters-settings:
|
|||
const:
|
||||
AUTHORS: The Falco Authors
|
||||
template: |-
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
Copyright (C) {{ YEAR }} {{ AUTHORS }}
|
||||
Copyright {{ YEAR }} {{ AUTHORS }}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -44,11 +43,17 @@ linters-settings:
|
|||
- opinionated
|
||||
- performance
|
||||
- style
|
||||
disabled-checks:
|
||||
# Conflicts with govet check-shadowing
|
||||
- sloppyReassign
|
||||
goimports:
|
||||
local-prefixes: github.com/falcosecurity/falcoctl
|
||||
govet:
|
||||
check-shadowing: true
|
||||
misspell:
|
||||
locale: US
|
||||
nolintlint:
|
||||
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
|
||||
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
|
||||
|
@ -60,12 +65,13 @@ linters:
|
|||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- copyloopvar
|
||||
- exportloopref
|
||||
# - funlen
|
||||
# - gochecknoglobals
|
||||
# - gochecknoinits
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
version: 2
|
||||
|
||||
project_name: falcoctl
|
||||
before:
|
||||
hooks:
|
||||
|
@ -18,8 +16,6 @@ builds:
|
|||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: windows
|
||||
goarch: 386
|
||||
|
||||
ldflags: |
|
||||
-X github.com/falcosecurity/falcoctl/cmd/version.buildDate={{ .Date }}
|
||||
|
@ -47,6 +43,3 @@ release:
|
|||
|
||||
changelog:
|
||||
use: github-native
|
||||
|
||||
git:
|
||||
tag_sort: -version:creatordate
|
||||
|
|
9
Makefile
9
Makefile
|
@ -18,7 +18,6 @@ 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}' \
|
||||
|
@ -35,7 +34,7 @@ test:
|
|||
.PHONY: gci
|
||||
gci:
|
||||
ifeq (, $(shell which gci))
|
||||
@go install github.com/daixiang0/gci@v0.11.1
|
||||
@go install github.com/daixiang0/gci@v0.9.0
|
||||
GCI=$(GOBIN)/gci
|
||||
else
|
||||
GCI=$(shell which gci)
|
||||
|
@ -57,13 +56,13 @@ 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)" {} \;
|
||||
find . -type f -name '*.go' -exec $(ADDLICENSE) -l apache -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
|
||||
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2
|
||||
GOLANGCILINT=$(GOBIN)/golangci-lint
|
||||
else
|
||||
GOLANGCILINT=$(shell which golangci-lint)
|
||||
|
@ -81,4 +80,4 @@ docker:
|
|||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@rm falcoctl || true
|
||||
@rm falcoctl
|
||||
|
|
1
OWNERS
1
OWNERS
|
@ -4,6 +4,7 @@ approvers:
|
|||
- maxgio92
|
||||
- fededp
|
||||
- cpanato
|
||||
reviewers:
|
||||
- alacuku
|
||||
- loresuso
|
||||
emeritus_approvers:
|
||||
|
|
99
README.md
99
README.md
|
@ -1,8 +1,17 @@
|
|||
<p align="center"><img src="https://raw.githubusercontent.com/falcosecurity/community/master/logo/primary-logo.png" width="360"></p>
|
||||
<p align="center"><b>Cloud Native Runtime Security.</b></p>
|
||||
|
||||
<hr>
|
||||
|
||||
# 🧰 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 and its ecosystem components.
|
||||
|
||||
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).
|
||||
## 📣 Call for contributors/maintainers
|
||||
|
||||
This is a Go project that has a lot of potential in the Falco ecosystem, but needs contributions and even a maintainer or two.
|
||||
|
||||
If you would like to get involved with contributing to this specific project, please check out [the Falco community](https://github.com/falcosecurity/community) to get involved.
|
||||
|
||||
## Installation
|
||||
### Install falcoctl manually
|
||||
|
@ -23,13 +32,6 @@ 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)}')
|
||||
|
@ -147,8 +149,6 @@ registry:
|
|||
clientsecret: "999999"
|
||||
clientid: "000000"
|
||||
tokenurl: http://myregistry.example.com:9096/token
|
||||
gcp:
|
||||
- registry: europe-docker.pkg.dev
|
||||
```
|
||||
|
||||
## `~/.config/falcoctl/`
|
||||
|
@ -206,31 +206,12 @@ This is an example of an index file:
|
|||
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.
|
||||
New indexes are configured to be used by the *falcoctl* tool by adding them through the `index add` command. The current implementation requires a valid HTTP URL from where to download the `index` file. 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
|
||||
|
@ -324,19 +305,6 @@ The `registry auth basic` command authenticates a user to a given OCI registry u
|
|||
#### 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
|
||||
|
@ -344,11 +312,10 @@ $ falcoctl registry push --type=plugin ghcr.io/falcosecurity/plugins/plugin/clou
|
|||
```
|
||||
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`
|
||||
* `--type`: type of artifact to be pushed. Allowed values: `rulesfile`, `plugin`
|
||||
|
||||
### 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**:
|
||||
|
@ -367,23 +334,21 @@ The `falcoctl` arguments can be passed through these different modalities are pr
|
|||
|
||||
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` | |
|
||||
| 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_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` |
|
||||
|
||||
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.
|
||||
|
||||
|
@ -394,11 +359,3 @@ $ export FALCOCTL_REGISTRY_AUTH_OAUTH="localhost:6000,000000,999999,http://local
|
|||
$ 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
|
||||
```
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM cgr.dev/chainguard/go AS builder
|
||||
FROM golang:1.19 as builder
|
||||
WORKDIR /tmp/builder
|
||||
|
||||
ARG RELEASE
|
||||
|
@ -29,8 +29,12 @@ RUN CGO_ENABLED=0 \
|
|||
|
||||
RUN echo ${RELEASE}
|
||||
|
||||
FROM cgr.dev/chainguard/static:latest
|
||||
FROM alpine:3.16.3
|
||||
|
||||
COPY --from=builder /tmp/builder/falcoctl /usr/bin/falcoctl
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
|
||||
ENTRYPOINT [ "/usr/bin/falcoctl" ]
|
||||
ARG BIN_NAME="falcoctl"
|
||||
COPY --from=builder /tmp/builder/${BIN_NAME} /usr/bin/${BIN_NAME}
|
||||
RUN ln -s /usr/bin/${BIN_NAME} /usr/bin/falcoctl-bin
|
||||
|
||||
ENTRYPOINT [ "/usr/bin/falcoctl-bin" ]
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -20,28 +19,31 @@ import (
|
|||
|
||||
"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/internal/login"
|
||||
"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 {
|
||||
func NewArtifactCmd(ctx context.Context, opt *commonoptions.CommonOptions) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "artifact",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Interact with Falco artifacts",
|
||||
Long: "Interact with Falco artifacts",
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
var indexes []config.Index
|
||||
var indexCache *cache.Cache
|
||||
var basicAuths []config.BasicAuth
|
||||
var oauthAuths []config.OauthAuth
|
||||
var err error
|
||||
|
||||
opt.Initialize()
|
||||
|
@ -62,6 +64,26 @@ func NewArtifactCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Comma
|
|||
// Save the index cache for later use by the sub commands.
|
||||
opt.Initialize(commonoptions.WithIndexCache(indexCache))
|
||||
|
||||
// Authenticate for commands that requires it.
|
||||
if cmd.CalledAs() != list.CommandName && cmd.CalledAs() != search.CommandName {
|
||||
// Perform authentication using basic auth.
|
||||
if basicAuths, err = config.BasicAuths(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = login.PerformBasicAuthsLogin(ctx, basicAuths); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform authentications using oauth auth.
|
||||
if oauthAuths, err = config.OauthAuths(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = login.PerformOauthAuths(ctx, opt, oauthAuths); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
@ -71,8 +93,6 @@ func NewArtifactCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Comma
|
|||
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
|
||||
}
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
// 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
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
//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)
|
||||
}
|
|
@ -1,213 +0,0 @@
|
|||
// 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"}]}`)))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -1,17 +0,0 @@
|
|||
// 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
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
@ -32,7 +31,8 @@ import (
|
|||
"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/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
|
@ -79,9 +79,10 @@ Example - Install and follow "cloudtrail" plugins using a fully qualified refere
|
|||
)
|
||||
|
||||
type artifactFollowOptions struct {
|
||||
*options.Common
|
||||
*options.Registry
|
||||
*options.Directory
|
||||
*options.CommonOptions
|
||||
*options.RegistryOptions
|
||||
rulesfilesDir string
|
||||
pluginsDir string
|
||||
tmpDir string
|
||||
every time.Duration
|
||||
cron string
|
||||
|
@ -90,25 +91,23 @@ type artifactFollowOptions struct {
|
|||
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 {
|
||||
func NewArtifactFollowCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := artifactFollowOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
Directory: &options.Directory{},
|
||||
closeChan: make(chan bool),
|
||||
versions: config.FalcoVersions{},
|
||||
CommonOptions: opt,
|
||||
RegistryOptions: &options.RegistryOptions{},
|
||||
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,
|
||||
Use: "follow [ref1 [ref2 ...]] [flags]",
|
||||
Short: "Install a list of artifacts and continuously checks if there are updates",
|
||||
Long: longFollow,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Override "every" flag with viper config if not set by user.
|
||||
f := cmd.Flags().Lookup("every")
|
||||
|
@ -147,38 +146,26 @@ func NewArtifactFollowCmd(ctx context.Context, opt *options.Common) *cobra.Comma
|
|||
}
|
||||
|
||||
// Override "rulesfiles-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagRulesFilesDir)
|
||||
f = cmd.Flags().Lookup(install.FlagRulesFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagRulesFilesDir)
|
||||
return fmt.Errorf("unable to retrieve flag %q", install.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)
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", install.FlagRulesFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "plugins-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagPluginsFilesDir)
|
||||
f = cmd.Flags().Lookup(install.FlagPluginsFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagPluginsFilesDir)
|
||||
return fmt.Errorf("unable to retrieve flag %q", install.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)
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", install.FlagPluginsFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,18 +196,6 @@ func NewArtifactFollowCmd(ctx context.Context, opt *options.Common) *cobra.Comma
|
|||
}
|
||||
}
|
||||
|
||||
// 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 "+
|
||||
|
@ -233,12 +208,16 @@ func NewArtifactFollowCmd(ctx context.Context, opt *options.Common) *cobra.Comma
|
|||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
o.Directory.AddFlags(cmd)
|
||||
o.RegistryOptions.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.")
|
||||
// TODO (alacuku): move it in a dedicate data structure since they are in common with artifactInstall command.
|
||||
cmd.Flags().StringVarP(&o.rulesfilesDir, install.FlagRulesFilesDir, "", config.RulesfilesDir,
|
||||
"Directory where to install rules")
|
||||
cmd.Flags().StringVarP(&o.pluginsDir, install.FlagPluginsFilesDir, "", config.PluginsDir,
|
||||
"Directory where to install plugins")
|
||||
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")
|
||||
|
@ -250,8 +229,6 @@ 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
|
||||
|
@ -259,7 +236,6 @@ Examples:
|
|||
|
||||
// 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 {
|
||||
|
@ -274,6 +250,21 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st
|
|||
args = configuredFollower.Artifacts
|
||||
}
|
||||
|
||||
o.Printer.Info.Printfln("Reading all configured index files from %q", config.IndexesFile)
|
||||
indexConfig, err := index.NewConfig(config.IndexesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergedIndexes, err := utils.Indexes(indexConfig, config.IndexesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(mergedIndexes.Entries) < 1 {
|
||||
o.Printer.Warning.Println("No configured index. Consider to configure one using the 'index add' command.")
|
||||
}
|
||||
|
||||
var sched cron.Schedule
|
||||
if o.cron != "" {
|
||||
sched, err = cron.ParseStandard(o.cron)
|
||||
|
@ -285,48 +276,46 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st
|
|||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
// Disable styling
|
||||
o.Printer.DisableStylingf()
|
||||
// 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))
|
||||
o.Printer.Info.Printfln("Creating follower for %q, with check using cron %s", a, o.cron)
|
||||
} else {
|
||||
logger.Info("Creating follower", logger.Args("artifact", a, "check every", o.every.String()))
|
||||
o.Printer.Info.Printfln("Creating follower for %q, with check every %s", a, 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,
|
||||
RulesfilesDir: o.rulesfilesDir,
|
||||
PluginsDir: o.pluginsDir,
|
||||
ArtifactReference: ref,
|
||||
PlainHTTP: o.PlainHTTP,
|
||||
Verbose: o.IsVerbose(),
|
||||
CloseChan: o.closeChan,
|
||||
TmpDir: o.tmpDir,
|
||||
FalcoVersions: o.versions,
|
||||
AllowedTypes: o.allowedTypes,
|
||||
Signature: sig,
|
||||
}
|
||||
fol, err := follower.New(ref, o.Printer, cfg)
|
||||
fol, err := follower.New(ctx, 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
|
||||
}
|
||||
// Enable styling
|
||||
o.Printer.EnableStyling()
|
||||
|
||||
for k, f := range followers {
|
||||
logger.Info("Starting follower", logger.Args("artifact", k))
|
||||
o.Printer.Info.Printfln("Starting follower for %q", k)
|
||||
go f.Follow(ctx)
|
||||
}
|
||||
|
||||
|
@ -334,7 +323,7 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st
|
|||
<-ctx.Done()
|
||||
|
||||
// We are done, shutdown the followers.
|
||||
logger.Info("Closing followers...")
|
||||
o.Printer.DefaultText.Printfln("closing followers...")
|
||||
close(o.closeChan)
|
||||
|
||||
// Wait for the followers to shutdown or that the timer expires.
|
||||
|
@ -347,9 +336,9 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st
|
|||
|
||||
select {
|
||||
case <-doneChan:
|
||||
logger.Info("Followers correctly stopped.")
|
||||
o.Printer.DefaultText.Printfln("followers correctly stopped.")
|
||||
case <-time.After(timeout):
|
||||
logger.Info("Timed out waiting for followers to exit")
|
||||
o.Printer.DefaultText.Printfln("Timed out waiting for followers to exit")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -436,11 +425,11 @@ type backoffTransport struct {
|
|||
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))
|
||||
bt.Printer.Verbosef("Retrieving versions from Falco (timeout %s) ...", bt.Config.MaxDelay)
|
||||
|
||||
for {
|
||||
resp, err = bt.Base.RoundTrip(req)
|
||||
|
@ -455,10 +444,10 @@ func (bt *backoffTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
|||
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()))
|
||||
bt.Printer.Verbosef("error: %s. Trying again in %s", err.Error(), sleep.String())
|
||||
time.Sleep(sleep)
|
||||
} else {
|
||||
logger.Debug("Successfully retrieved versions from Falco")
|
||||
bt.Printer.Verbosef("Successfully retrieved versions from Falco ...")
|
||||
return resp, err
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -17,29 +16,28 @@ package info
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"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
|
||||
*options.CommonOptions
|
||||
*options.RegistryOptions
|
||||
}
|
||||
|
||||
// NewArtifactInfoCmd returns the artifact info command.
|
||||
func NewArtifactInfoCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewArtifactInfoCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := artifactInfoOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
CommonOptions: opt,
|
||||
RegistryOptions: &options.RegistryOptions{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -48,33 +46,27 @@ func NewArtifactInfoCmd(ctx context.Context, opt *options.Common) *cobra.Command
|
|||
Short: "Retrieve all available versions of a given artifact",
|
||||
Long: "Retrieve all available versions of a given artifact",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactInfo(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
o.RegistryOptions.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))
|
||||
o.Printer.Warning.Printfln("cannot find %q, skipping", name)
|
||||
continue
|
||||
}
|
||||
ref = fmt.Sprintf("%s/%s", entry.Registry, entry.Repository)
|
||||
|
@ -83,6 +75,16 @@ func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string
|
|||
ref = parsedRef.String()
|
||||
}
|
||||
|
||||
reg, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := utils.ClientForRegistry(ctx, reg, o.PlainHTTP, o.Printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo, err := repository.NewRepository(ref,
|
||||
repository.WithClient(client),
|
||||
repository.WithPlainHTTP(o.PlainHTTP))
|
||||
|
@ -91,33 +93,14 @@ func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string
|
|||
}
|
||||
|
||||
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()))
|
||||
if err != nil {
|
||||
o.Printer.Warning.Printfln("cannot retrieve tags from %q, %v", ref, err)
|
||||
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), ", ")
|
||||
joinedTags := strings.Join(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
|
||||
return o.Printer.PrintTable(output.ArtifactInfo, data)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
@ -16,15 +15,16 @@
|
|||
package install
|
||||
|
||||
const (
|
||||
|
||||
// FlagRulesFilesDir is the name of the flag to specify the directory path of the rules files.
|
||||
FlagRulesFilesDir = "rulesfiles-dir"
|
||||
|
||||
// FlagPluginsFilesDir is the name of the flag to specify the directory path of the plugins.
|
||||
FlagPluginsFilesDir = "plugins-dir"
|
||||
|
||||
// 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"
|
||||
)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -21,17 +20,14 @@ import (
|
|||
"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/index"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
|
@ -52,11 +48,11 @@ separated by a semicolon ';'. Other arguments, if passed through environment var
|
|||
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>"),
|
||||
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
|
||||
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:
|
||||
|
@ -71,23 +67,19 @@ Example - Install "cloudtrail" plugins using a fully qualified reference:
|
|||
)
|
||||
|
||||
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
|
||||
*options.CommonOptions
|
||||
*options.RegistryOptions
|
||||
rulesfilesDir string
|
||||
pluginsDir string
|
||||
allowedTypes oci.ArtifactTypeSlice
|
||||
resolveDeps bool
|
||||
}
|
||||
|
||||
// NewArtifactInstallCmd returns the artifact install command.
|
||||
func NewArtifactInstallCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewArtifactInstallCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := artifactInstallOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
Directory: &options.Directory{},
|
||||
CommonOptions: opt,
|
||||
RegistryOptions: &options.RegistryOptions{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -95,40 +87,30 @@ func NewArtifactInstallCmd(ctx context.Context, opt *options.Common) *cobra.Comm
|
|||
DisableFlagsInUseLine: true,
|
||||
Short: "Install a list of artifacts",
|
||||
Long: longInstall,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
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)
|
||||
f := cmd.Flags().Lookup(FlagRulesFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagRulesFilesDir)
|
||||
return fmt.Errorf("unable to retrieve flag %q", 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)
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", FlagRulesFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "plugins-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagPluginsFilesDir)
|
||||
f = cmd.Flags().Lookup(FlagPluginsFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagPluginsFilesDir)
|
||||
return fmt.Errorf("unable to retrieve flag %q", 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)
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", FlagPluginsFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,27 +139,6 @@ func NewArtifactInstallCmd(ctx context.Context, opt *options.Common) *cobra.Comm
|
|||
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 {
|
||||
|
@ -185,27 +146,25 @@ func NewArtifactInstallCmd(ctx context.Context, opt *options.Common) *cobra.Comm
|
|||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
o.Directory.AddFlags(cmd)
|
||||
o.RegistryOptions.AddFlags(cmd)
|
||||
cmd.Flags().StringVarP(&o.rulesfilesDir, FlagRulesFilesDir, "", config.RulesfilesDir,
|
||||
"directory where to install rules.")
|
||||
cmd.Flags().StringVarP(&o.pluginsDir, FlagPluginsFilesDir, "", config.PluginsDir,
|
||||
"directory where to install plugins.")
|
||||
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:
|
||||
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 {
|
||||
|
@ -220,6 +179,21 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
|
|||
args = configuredInstaller.Artifacts
|
||||
}
|
||||
|
||||
o.Printer.Info.Printfln("Reading all configured index files from %q", config.IndexesFile)
|
||||
indexConfig, err := index.NewConfig(config.IndexesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergedIndexes, err := utils.Indexes(indexConfig, config.IndexesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(mergedIndexes.Entries) < 1 {
|
||||
o.Printer.Warning.Println("No configured index. Consider to configure one using the 'index add' command.")
|
||||
}
|
||||
|
||||
// Create temp dir where to put pulled artifacts
|
||||
tmpDir, err := os.MkdirTemp("", "falcoctl")
|
||||
if err != nil {
|
||||
|
@ -227,12 +201,6 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
|
|||
}
|
||||
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)
|
||||
|
@ -240,7 +208,17 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
|
|||
return nil, err
|
||||
}
|
||||
|
||||
artifactConfig, err := puller.ArtifactConfig(ctx, ref, o.platformOS, o.platformArch)
|
||||
reg, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
puller, err := utils.PullerForRegistry(ctx, reg, o.PlainHTTP, o.Printer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
artifactConfig, err := puller.PullConfigLayer(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -250,24 +228,18 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
|
|||
}, nil
|
||||
})
|
||||
|
||||
signatures := make(map[string]*index.Signature)
|
||||
|
||||
// Compute input to install dependencies
|
||||
for i, arg := range args {
|
||||
ref, err := o.IndexCache.ResolveReference(arg)
|
||||
args[i], 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 ...")
|
||||
o.Printer.Info.Println("Resolving dependencies ...")
|
||||
refs, err = ResolveDeps(resolver, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -276,63 +248,43 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
|
|||
refs = args
|
||||
}
|
||||
|
||||
logger.Info("Installing artifacts", logger.Args("refs", refs))
|
||||
o.Printer.Info.Printfln("Installing the following artifacts: %v", refs)
|
||||
|
||||
// Install artifacts
|
||||
for _, ref := range refs {
|
||||
resolvedRef, err := o.IndexCache.ResolveReference(ref)
|
||||
ref, 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
|
||||
}
|
||||
o.Printer.Info.Printfln("Preparing to pull %q", ref)
|
||||
|
||||
reg, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Preparing to pull artifact", logger.Args("ref", resolvedRef))
|
||||
puller, err := utils.PullerForRegistry(ctx, reg, o.PlainHTTP, o.Printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := puller.CheckAllowedType(ctx, resolvedRef, o.platformOS, o.platformArch, o.allowedTypes.Types); err != nil {
|
||||
if err := puller.CheckAllowedType(ctx, ref, 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)
|
||||
result, err := puller.Pull(ctx, ref, tmpDir, runtime.GOOS, runtime.GOARCH)
|
||||
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
|
||||
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)
|
||||
destDir = o.rulesfilesDir
|
||||
}
|
||||
|
||||
// Check if directory exists and is writable.
|
||||
|
@ -341,20 +293,16 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
|
|||
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")
|
||||
}
|
||||
|
||||
sp, _ := o.Printer.Spinner.Start(fmt.Sprintf("INFO: Extracting and installing %q %q", result.Type, result.Filename))
|
||||
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)
|
||||
_, err = utils.ExtractTarGz(f, destDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot extract %q to %q: %w", result.Filename, destDir, err)
|
||||
}
|
||||
|
@ -364,10 +312,7 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
|
|||
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))
|
||||
sp.Success(fmt.Sprintf("Artifact successfully installed in %q", destDir))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,465 +0,0 @@
|
|||
// 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)))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -30,15 +29,15 @@ import (
|
|||
const CommandName = "list"
|
||||
|
||||
type artifactListOptions struct {
|
||||
*options.Common
|
||||
*options.CommonOptions
|
||||
artifactType oci.ArtifactType
|
||||
index string
|
||||
}
|
||||
|
||||
// NewArtifactListCmd returns the artifact search command.
|
||||
func NewArtifactListCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewArtifactListCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := artifactListOptions{
|
||||
Common: opt,
|
||||
CommonOptions: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -47,12 +46,14 @@ func NewArtifactListCmd(ctx context.Context, opt *options.Common) *cobra.Command
|
|||
Short: "List all artifacts",
|
||||
Long: "List all artifacts",
|
||||
Aliases: []string{"ls"},
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
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().Var(&o.artifactType, "type", `Only list artifacts with a specific type. Allowed values: "rulesfile", "plugin""`)
|
||||
cmd.Flags().StringVar(&o.index, "index", "", "Only display artifacts from a configured index")
|
||||
|
||||
return cmd
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
// 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
|
|
@ -1,93 +0,0 @@
|
|||
// 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
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
// 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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -33,7 +32,7 @@ const (
|
|||
)
|
||||
|
||||
type artifactSearchOptions struct {
|
||||
*options.Common
|
||||
*options.CommonOptions
|
||||
minScore float64
|
||||
artifactType oci.ArtifactType
|
||||
}
|
||||
|
@ -47,9 +46,9 @@ func (o *artifactSearchOptions) Validate() error {
|
|||
}
|
||||
|
||||
// NewArtifactSearchCmd returns the artifact search command.
|
||||
func NewArtifactSearchCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewArtifactSearchCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := artifactSearchOptions{
|
||||
Common: opt,
|
||||
CommonOptions: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -58,6 +57,8 @@ func NewArtifactSearchCmd(ctx context.Context, opt *options.Common) *cobra.Comma
|
|||
Short: "Search an artifact by keywords",
|
||||
Long: "Search an artifact by keywords",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.Validate()
|
||||
},
|
||||
|
@ -69,7 +70,7 @@ func NewArtifactSearchCmd(ctx context.Context, opt *options.Common) *cobra.Comma
|
|||
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"`)
|
||||
cmd.Flags().Var(&o.artifactType, "type", `Only search artifacts with a specific type. Allowed values: "rulesfile", "plugin""`)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright 2022 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/acarl005/stripansi"
|
||||
"gotest.tools/assert"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type expect struct {
|
||||
err string
|
||||
out string
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
descr string
|
||||
env map[string]string
|
||||
args []string
|
||||
expect expect
|
||||
}
|
||||
|
||||
var tests = []testCase{
|
||||
{
|
||||
descr: "no-args-no-flags",
|
||||
args: []string{},
|
||||
expect: expect{
|
||||
out: "testdata/noargsnoflags.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
descr: "wrong-flag",
|
||||
args: []string{"--wrong"},
|
||||
expect: expect{
|
||||
out: "testdata/wrongflag.txt",
|
||||
err: "unknown flag: --wrong",
|
||||
},
|
||||
},
|
||||
{
|
||||
args: []string{"help"},
|
||||
expect: expect{
|
||||
out: "testdata/help.txt",
|
||||
},
|
||||
},
|
||||
{
|
||||
descr: "help-flag",
|
||||
args: []string{"--help"},
|
||||
expect: expect{
|
||||
out: "testdata/help.txt",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func run(t *testing.T, test *testCase) {
|
||||
// Setup
|
||||
c := New(context.Background(), &options.CommonOptions{})
|
||||
o := bytes.NewBufferString("")
|
||||
c.SetOut(o)
|
||||
c.SetErr(o)
|
||||
c.SetArgs(test.args)
|
||||
for k, v := range test.env {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
t.Fatalf("error setting env variables: %v", err)
|
||||
}
|
||||
}
|
||||
// Test
|
||||
err := c.Execute()
|
||||
if err != nil {
|
||||
if test.expect.err == "" {
|
||||
t.Fatalf("error executing CLI: %v", err)
|
||||
} else {
|
||||
assert.Error(t, err, test.expect.err)
|
||||
}
|
||||
}
|
||||
|
||||
out, err := io.ReadAll(o)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading CLI output: %v", err)
|
||||
}
|
||||
res := stripansi.Strip(string(out))
|
||||
assert.Equal(t, test.expect.out, res)
|
||||
// Teardown
|
||||
for k := range test.env {
|
||||
if err := os.Unsetenv(k); err != nil {
|
||||
t.Fatalf("error tearing down: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLI(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
descr := test.descr
|
||||
if descr == "" {
|
||||
if test.expect.out == "" {
|
||||
t.Fatal("malformed test case: missing both descr and expect.out fields")
|
||||
}
|
||||
test.descr = strings.TrimSuffix(filepath.Base(test.expect.out), ".txt")
|
||||
}
|
||||
if test.expect.out != "" {
|
||||
out, err := os.ReadFile(test.expect.out)
|
||||
if err != nil {
|
||||
t.Fatalf("output fixture not found: %v", err)
|
||||
}
|
||||
test.expect.out = string(out)
|
||||
}
|
||||
|
||||
t.Run(test.descr, func(t *testing.T) {
|
||||
run(t, &test)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
@ -16,13 +15,82 @@
|
|||
package cmd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
const (
|
||||
rulesfiletgz = "../pkg/test/data/rules.tar.gz"
|
||||
rulesfileyaml = "../pkg/test/data/rules.yaml"
|
||||
plugintgz = "../pkg/test/data/plugin.tar.gz"
|
||||
)
|
||||
|
||||
var (
|
||||
registry string
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.CommonOptions
|
||||
port int
|
||||
orasRegistry *remote.Registry
|
||||
configFile string
|
||||
)
|
||||
|
||||
func TestPush(t *testing.T) {
|
||||
var err error
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Cmd Suite")
|
||||
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))
|
||||
opt.Printer.DisableStylingf()
|
||||
|
||||
// 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())
|
||||
}()
|
||||
|
||||
// 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.Printer)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
// 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
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
// 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`)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,17 +0,0 @@
|
|||
// 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
|
|
@ -1,326 +0,0 @@
|
|||
// 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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,262 +0,0 @@
|
|||
// 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
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
// 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`)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,17 +0,0 @@
|
|||
// 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
|
|
@ -1,241 +0,0 @@
|
|||
// 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 ""
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
// 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{}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
// 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
|
|
@ -1,219 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
// 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.")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,17 +0,0 @@
|
|||
// 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
|
|
@ -1,65 +0,0 @@
|
|||
// 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
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
// 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())
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -28,21 +27,23 @@ import (
|
|||
|
||||
// IndexAddOptions contains the options for the index add command.
|
||||
type IndexAddOptions struct {
|
||||
*options.Common
|
||||
*options.CommonOptions
|
||||
}
|
||||
|
||||
// NewIndexAddCmd returns the index add command.
|
||||
func NewIndexAddCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewIndexAddCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := IndexAddOptions{
|
||||
Common: opt,
|
||||
CommonOptions: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [NAME] [URL] [BACKEND] [flags]",
|
||||
Use: "add [NAME] [URL] [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),
|
||||
Args: cobra.ExactArgs(2),
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunIndexAdd(ctx, args)
|
||||
},
|
||||
|
@ -54,42 +55,36 @@ func NewIndexAddCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
|||
// 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))
|
||||
o.Printer.Verbosef("Creating in-memory cache using indexes file %q and indexes directory %q", config.IndexesFile, 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))
|
||||
o.Printer.Info.Printfln("Adding index")
|
||||
|
||||
if err = indexCache.Add(ctx, name, backend, url); err != nil {
|
||||
if err = indexCache.Add(ctx, name, url); err != nil {
|
||||
return fmt.Errorf("unable to add index: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Writing cache to disk")
|
||||
o.Printer.Verbosef("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))
|
||||
o.Printer.Verbosef("Adding new index entry to configuration file %q", o.ConfigFile)
|
||||
if err = config.AddIndexes([]config.Index{{
|
||||
Name: name,
|
||||
URL: url,
|
||||
Backend: backend,
|
||||
Name: name,
|
||||
URL: url,
|
||||
}}, o.ConfigFile); err != nil {
|
||||
return fmt.Errorf("index entry %q: %w", name, err)
|
||||
}
|
||||
|
||||
logger.Info("Index successfully added")
|
||||
o.Printer.Success.Printfln("Index %q successfully added", name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
// 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")
|
||||
})
|
||||
})
|
||||
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -29,12 +28,14 @@ import (
|
|||
)
|
||||
|
||||
// NewIndexCmd returns the index command.
|
||||
func NewIndexCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
func NewIndexCmd(ctx context.Context, opt *commonoptions.CommonOptions) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "index",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Interact with index",
|
||||
Long: "Interact with index",
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
opt.Initialize()
|
||||
return config.Load(opt.ConfigFile)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -21,19 +20,19 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
indexConf "github.com/falcosecurity/falcoctl/pkg/index/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
type indexListOptions struct {
|
||||
*options.Common
|
||||
*options.CommonOptions
|
||||
}
|
||||
|
||||
// NewIndexListCmd returns the index list command.
|
||||
func NewIndexListCmd(_ context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewIndexListCmd(_ context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := indexListOptions{
|
||||
Common: opt,
|
||||
CommonOptions: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -43,6 +42,8 @@ func NewIndexListCmd(_ context.Context, opt *options.Common) *cobra.Command {
|
|||
Long: "List all the added indexes that were configured in falcoctl",
|
||||
Args: cobra.ExactArgs(0),
|
||||
Aliases: []string{"ls"},
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return o.RunIndexList()
|
||||
},
|
||||
|
@ -52,7 +53,7 @@ func NewIndexListCmd(_ context.Context, opt *options.Common) *cobra.Command {
|
|||
}
|
||||
|
||||
func (o *indexListOptions) RunIndexList() error {
|
||||
indexConfig, err := indexConf.New(config.IndexesFile)
|
||||
indexConfig, err := index.NewConfig(config.IndexesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -27,13 +26,13 @@ import (
|
|||
)
|
||||
|
||||
type indexRemoveOptions struct {
|
||||
*options.Common
|
||||
*options.CommonOptions
|
||||
}
|
||||
|
||||
// NewIndexRemoveCmd returns the index remove command.
|
||||
func NewIndexRemoveCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewIndexRemoveCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := indexRemoveOptions{
|
||||
Common: opt,
|
||||
CommonOptions: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -43,6 +42,8 @@ func NewIndexRemoveCmd(ctx context.Context, opt *options.Common) *cobra.Command
|
|||
Long: "Remove an index from the local falcoctl configuration",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Aliases: []string{"rm"},
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunIndexRemove(ctx, args)
|
||||
},
|
||||
|
@ -52,32 +53,30 @@ func NewIndexRemoveCmd(ctx context.Context, opt *options.Common) *cobra.Command
|
|||
}
|
||||
|
||||
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))
|
||||
o.Printer.Verbosef("Creating in-memory cache using indexes file %q and indexes directory %q", config.IndexesFile, 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))
|
||||
o.Printer.Info.Printfln("Removing index %q", name)
|
||||
if err = indexCache.Remove(name); err != nil {
|
||||
return fmt.Errorf("unable to remove index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Writing cache to disk")
|
||||
o.Printer.Verbosef("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))
|
||||
o.Printer.Verbosef("Removing indexes entries from configuration file %q", o.ConfigFile)
|
||||
if err = config.RemoveIndexes(args, o.ConfigFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Indexes successfully removed")
|
||||
o.Printer.Success.Printfln("Indexes successfully removed")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -27,13 +26,13 @@ import (
|
|||
)
|
||||
|
||||
type indexUpdateOptions struct {
|
||||
*options.Common
|
||||
*options.CommonOptions
|
||||
}
|
||||
|
||||
// NewIndexUpdateCmd returns the index update command.
|
||||
func NewIndexUpdateCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewIndexUpdateCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := indexUpdateOptions{
|
||||
Common: opt,
|
||||
CommonOptions: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -42,6 +41,7 @@ func NewIndexUpdateCmd(ctx context.Context, opt *options.Common) *cobra.Command
|
|||
Short: "Update an existing index",
|
||||
Long: "Update an existing index",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunIndexUpdate(ctx, args)
|
||||
},
|
||||
|
@ -51,27 +51,25 @@ func NewIndexUpdateCmd(ctx context.Context, opt *options.Common) *cobra.Command
|
|||
}
|
||||
|
||||
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))
|
||||
o.Printer.Verbosef("Creating in-memory cache using indexes file %q and indexes directory %q", config.IndexesFile, 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))
|
||||
o.Printer.Info.Printfln("Updating index %q", 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")
|
||||
o.Printer.Verbosef("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")
|
||||
o.Printer.Success.Printfln("Indexes successfully updated")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,556 @@
|
|||
// Copyright 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 (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
. "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"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var registryPushUsage = `Usage:
|
||||
falcoctl registry push hostname/repo[:tag|@digest] file [flags]
|
||||
|
||||
Flags:
|
||||
--annotation-source string set annotation source for the artifact
|
||||
-d, --depends-on stringArray set an artifact dependency (can be specified multiple times). Example: "--depends-on my-plugin:1.2.3"
|
||||
-h, --help help for push
|
||||
--name string set the unique name of the artifact (if not set, the name is extracted from the reference)
|
||||
--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)
|
||||
-r, --requires stringArray set an artifact requirement (can be specified multiple times). Example: "--requires plugin_api_version:1.2.3"
|
||||
-t, --tag stringArray additional artifact tag. Can be repeated multiple times
|
||||
--type ArtifactType type of artifact to be pushed. Allowed values: "rulesfile", "plugin" (default )
|
||||
--version string set the version of the artifact
|
||||
|
||||
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:lll // no need to check for line length.
|
||||
var registryPushHelp = `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/aarch64":
|
||||
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest myplugin.tar.gz --platform linux/aarch64
|
||||
|
||||
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/aarch64
|
||||
|
||||
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" 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
|
||||
|
||||
Usage:
|
||||
falcoctl registry push hostname/repo[:tag|@digest] file [flags]
|
||||
|
||||
Flags:
|
||||
--annotation-source string set annotation source for the artifact
|
||||
-d, --depends-on stringArray set an artifact dependency (can be specified multiple times). Example: "--depends-on my-plugin:1.2.3"
|
||||
-h, --help help for push
|
||||
--name string set the unique name of the artifact (if not set, the name is extracted from the reference)
|
||||
--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)
|
||||
-r, --requires stringArray set an artifact requirement (can be specified multiple times). Example: "--requires plugin_api_version:1.2.3"
|
||||
-t, --tag stringArray additional artifact tag. Can be repeated multiple times
|
||||
--type ArtifactType type of artifact to be pushed. Allowed values: "rulesfile", "plugin"
|
||||
--version string set the version of the artifact
|
||||
|
||||
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)
|
||||
`
|
||||
|
||||
var pushAssertFailedBehavior = 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 randomRulesRepoName = func(registry, repo string) (string, string) {
|
||||
rName := fmt.Sprintf("%s-%d", repo, rand.Int())
|
||||
return rName, fmt.Sprintf("%s/%s", registry, rName)
|
||||
}
|
||||
|
||||
var registryPushTests = Describe("push", func() {
|
||||
var (
|
||||
registryCmd = "registry"
|
||||
pushCmd = "push"
|
||||
)
|
||||
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
dep1 = "myplugin:1.2.3"
|
||||
dep2 = "myplugin1:1.2.3|otherplugin:3.2.1"
|
||||
req = "engine_version:15"
|
||||
anSource = "myrepo.com/rules.git"
|
||||
)
|
||||
|
||||
// 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, pushCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryPushHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// 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 (
|
||||
// Not really used since all the tests fail but needed as argument.
|
||||
rulesRepo = registry + "/push-rulesfile"
|
||||
pluginsRepo = registry + "/push-plugin"
|
||||
)
|
||||
When("without --version flag", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, "--config", configFile, rulesRepo, rulesfiletgz, "--type", "rulesfile"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERRO: required flag(s) \"version\" not set\n")
|
||||
})
|
||||
|
||||
When("without rulesfile", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, "--config", configFile, rulesRepo, "--type", "rulesfile"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERRO: requires at least 2 arg(s), only received 1\n")
|
||||
})
|
||||
|
||||
When("without registry", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, "--config", configFile, rulesfiletgz, "--type", "rulesfile"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERRO: requires at least 2 arg(s), only received 1\n")
|
||||
})
|
||||
|
||||
When("multiple rulesfiles", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, rulesRepo, "--config", configFile, rulesfiletgz, rulesfiletgz,
|
||||
"--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERRO: expecting 1 rulesfile object received 2: invalid number of rulesfiles\n")
|
||||
})
|
||||
|
||||
When("unreachable registry", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, "noregistry/testrules", "--config", configFile, rulesfiletgz,
|
||||
"--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERRO: an error occurred while creating the pusher for registry "+
|
||||
"noregistry: unable to connect to remote "+
|
||||
"registry \"noregistry\": Get \"http://noregistry/v2/\": dial tcp: lookup noregistry")
|
||||
})
|
||||
|
||||
When("missing repository", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, registry, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERRO: cannot extract registry name from ref %q", registry))
|
||||
})
|
||||
|
||||
When("invalid repository", func() {
|
||||
newReg := registry + "/wrong@something"
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, newReg, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERRO: unable to create new repository with ref %s: "+
|
||||
"invalid reference: invalid digest; invalid checksum digest format\n", newReg))
|
||||
})
|
||||
|
||||
When("invalid requirement", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, rulesRepo, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1",
|
||||
"--plain-http", "--requires", "wrongreq"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERRO: cannot parse \"wrongreq\"\n")
|
||||
})
|
||||
|
||||
When("invalid dependency", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, rulesRepo, rulesfiletgz, "--config", configFile, "--type", "rulesfile",
|
||||
"--version", "1.1.1", "--plain-http", "--depends-on", "wrongdep"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERRO: cannot parse \"wrongdep\": invalid artifact reference "+
|
||||
"(must be in the format \"name:version\")\n")
|
||||
})
|
||||
|
||||
When("without platform", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, pluginsRepo, plugintgz, "--config", configFile, "--type", "plugin", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERRO: \"filepaths\" length (1) must match \"platforms\" "+
|
||||
"length (0): number of filepaths and platform should be the same\n")
|
||||
})
|
||||
|
||||
When("wrong plugin type", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, pluginsRepo, pluginsRepo, "--config", configFile,
|
||||
"--type", "wrongType", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERRO: invalid argument \"wrongType\" for \"--type\" flag: must be one of \"rulesfile\", \"plugin\"\n")
|
||||
})
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
const (
|
||||
rulesRepoBaseName = "push-rulesfile"
|
||||
pluginsRepoBaseName = "push-plugins"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "1.1.1"
|
||||
// registry/rulesRepoBaseName-randomInt
|
||||
fullRepoName string
|
||||
// rulesRepoBaseName-randomInt
|
||||
repoName string
|
||||
// It is set in the config layer.
|
||||
artifactNameInConfigLayer = "test-rulesfile"
|
||||
pushedTags = []string{"tag1", "tag2", "latest"}
|
||||
|
||||
// Variables passed as arguments to the push command. Each test case updates them
|
||||
// to point to the file on disk living in pkg/test/data.
|
||||
rulesfile string
|
||||
plugin string
|
||||
pluginRaw string
|
||||
|
||||
// Plugin's platforms.
|
||||
platformARM64 = "linux/arm64"
|
||||
platformAMD64 = "linux/amd64"
|
||||
|
||||
// Data fetched from registry and used for assertions.
|
||||
pluginData *testutils.PluginArtifact
|
||||
rulesfileData *testutils.RulesfileArtifact
|
||||
)
|
||||
|
||||
// We keep it inside the success context since need the variables of this context.
|
||||
var AssertSuccesBehaviour = func(dependencies, requirements, annotation bool) {
|
||||
It("should succeed", func() {
|
||||
// We do not check the error here since we are checking it before
|
||||
// pulling the artifact.
|
||||
By("checking no error in output")
|
||||
Expect(output).ShouldNot(gbytes.Say("ERRO:"))
|
||||
Expect(output).ShouldNot(gbytes.Say("Unable to remove temporary dir"))
|
||||
|
||||
By("checking descriptor")
|
||||
Expect(rulesfileData.Descriptor.MediaType).Should(Equal(v1.MediaTypeImageManifest))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(rulesfileData.Descriptor.Digest.String())))
|
||||
|
||||
By("checking manifest")
|
||||
Expect(rulesfileData.Layer.Manifest.Layers).Should(HaveLen(1))
|
||||
if annotation {
|
||||
Expect(rulesfileData.Layer.Manifest.Annotations).Should(HaveKeyWithValue("org.opencontainers.image.source", anSource))
|
||||
} else {
|
||||
Expect(rulesfileData.Layer.Manifest.Annotations).ShouldNot(HaveKeyWithValue("org.opencontainers.image.source", anSource))
|
||||
}
|
||||
|
||||
By("checking config layer")
|
||||
Expect(rulesfileData.Layer.Config.Version).Should(Equal(version))
|
||||
Expect(rulesfileData.Layer.Config.Name).Should(Equal(artifactNameInConfigLayer))
|
||||
if dependencies {
|
||||
Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Dependencies[0].Name,
|
||||
rulesfileData.Layer.Config.Dependencies[0].Version)).Should(Equal(dep1))
|
||||
Expect(fmt.Sprintf("%s:%s|%s:%s", rulesfileData.Layer.Config.Dependencies[1].Name,
|
||||
rulesfileData.Layer.Config.Dependencies[1].Version, rulesfileData.Layer.Config.Dependencies[1].Alternatives[0].Name,
|
||||
rulesfileData.Layer.Config.Dependencies[1].Alternatives[0].Version)).Should(Equal(dep2))
|
||||
} else {
|
||||
Expect(rulesfileData.Layer.Config.Dependencies).Should(HaveLen(0))
|
||||
}
|
||||
if requirements {
|
||||
Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Requirements[0].Name,
|
||||
rulesfileData.Layer.Config.Requirements[0].Version)).Should(Equal(req))
|
||||
} else {
|
||||
Expect(rulesfileData.Layer.Config.Requirements).Should(HaveLen(0))
|
||||
}
|
||||
|
||||
By("checking tags")
|
||||
Expect(rulesfileData.Tags).Should(HaveLen(len(pushedTags)))
|
||||
Expect(rulesfileData.Tags).Should(ContainElements(pushedTags))
|
||||
})
|
||||
}
|
||||
|
||||
// Here we are testing all the success cases for the push command. The artifact type used here is of type
|
||||
// rulesfile. Keep in mind that here we are testing also the common flags that could be used by the plugin
|
||||
// artifacts. So we are testing that common logic only once, and are doing it here.
|
||||
commonFlagsAndRulesfileSpecificFlags := Context("rulesfiles and common flags", func() {
|
||||
JustBeforeEach(func() {
|
||||
// This runs after the push command, so check the returned error before proceeding.
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
rulesfileData, err = testutils.FetchRulesfileFromRegistry(ctx, repoName, pushedTags[0], orasRegistry)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
// This variable could be changed by single tests.
|
||||
// Make sure to set them at their default values.
|
||||
artifactNameInConfigLayer = "test-rulesfile"
|
||||
pushedTags = []string{"tag1", "tag2", "latest"}
|
||||
})
|
||||
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
})
|
||||
When("with full flags and args", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
AssertSuccesBehaviour(true, true, true)
|
||||
})
|
||||
|
||||
When("no --name flag provided", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2]}
|
||||
// Set name to the expected one.
|
||||
artifactNameInConfigLayer = repoName
|
||||
})
|
||||
AssertSuccesBehaviour(true, true, true)
|
||||
})
|
||||
|
||||
When("no --annotation-source provided", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
AssertSuccesBehaviour(true, true, false)
|
||||
})
|
||||
|
||||
When("no --tags provided", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req, "--annotation-source", anSource,
|
||||
"--name", artifactNameInConfigLayer}
|
||||
// We expect that latest tag is pushed, so set it in the pushed tags.
|
||||
pushedTags = []string{"latest"}
|
||||
})
|
||||
AssertSuccesBehaviour(true, true, true)
|
||||
})
|
||||
|
||||
When("no --depends-on flag provided", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--requires", req, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
AssertSuccesBehaviour(false, true, true)
|
||||
})
|
||||
|
||||
When("no --requires flag provided", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
AssertSuccesBehaviour(true, false, true)
|
||||
})
|
||||
|
||||
When("only required flags", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http"}
|
||||
// Set name to the expected one.
|
||||
artifactNameInConfigLayer = repoName
|
||||
// We expect that latest tag is pushed, so set it in the pushed tags.
|
||||
pushedTags = []string{"latest"}
|
||||
})
|
||||
AssertSuccesBehaviour(false, false, false)
|
||||
})
|
||||
})
|
||||
|
||||
Context("rulesfile", func() {
|
||||
Context("tar.gz format", func() {
|
||||
rulesfile = rulesfiletgz
|
||||
var _ = commonFlagsAndRulesfileSpecificFlags
|
||||
})
|
||||
|
||||
Context("raw format", func() {
|
||||
rulesfile = rulesfileyaml
|
||||
|
||||
// Push a raw rulesfiles using all the flags combinations.
|
||||
var _ = commonFlagsAndRulesfileSpecificFlags
|
||||
|
||||
Context("filesystem cleanup", func() {
|
||||
// Push a raw rulesfile.
|
||||
BeforeEach(func() {
|
||||
// Some values such as fullRepoName is the last one set by the other tests or the default one.
|
||||
// Anyway we do not really care since the tar.gz is created before.
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http"}
|
||||
})
|
||||
|
||||
It("temp dir should not exist", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
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())
|
||||
Expect(matched).ShouldNot(BeTrue())
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
// We keep it inside the success context since need the variables of this context.
|
||||
var AssertSuccessBehaviourPlugins = func(dependencies, requirements, annotation bool) {
|
||||
It("should succeed", func() {
|
||||
// We do not check the error here since we are checking it before
|
||||
// pulling the artifact.
|
||||
By("checking no error in output")
|
||||
Expect(output).ShouldNot(gbytes.Say("ERRO:"))
|
||||
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(2))
|
||||
|
||||
if annotation {
|
||||
Expect(pluginData.Index.Annotations).Should(HaveKeyWithValue("org.opencontainers.image.source", anSource))
|
||||
} else {
|
||||
Expect(pluginData.Descriptor.Annotations).ShouldNot(HaveKeyWithValue("org.opencontainers.image.source", anSource))
|
||||
}
|
||||
|
||||
By("checking platforms")
|
||||
Expect(pluginData.Platforms).Should(HaveKey(platformARM64))
|
||||
Expect(pluginData.Platforms).Should(HaveKey(platformAMD64))
|
||||
|
||||
By("checking config layer")
|
||||
for _, p := range pluginData.Platforms {
|
||||
Expect(p.Config.Version).Should(Equal(version))
|
||||
Expect(p.Config.Name).Should(Equal(artifactNameInConfigLayer))
|
||||
if dependencies {
|
||||
Expect(fmt.Sprintf("%s:%s", p.Config.Dependencies[0].Name, p.Config.Dependencies[0].Version)).Should(Equal(dep1))
|
||||
Expect(fmt.Sprintf("%s:%s|%s:%s", p.Config.Dependencies[1].Name, p.Config.Dependencies[1].Version,
|
||||
p.Config.Dependencies[1].Alternatives[0].Name, p.Config.Dependencies[1].Alternatives[0].Version)).Should(Equal(dep2))
|
||||
} else {
|
||||
Expect(p.Config.Dependencies).Should(HaveLen(0))
|
||||
}
|
||||
if requirements {
|
||||
Expect(fmt.Sprintf("%s:%s", p.Config.Requirements[0].Name, p.Config.Requirements[0].Version)).Should(Equal(req))
|
||||
} else {
|
||||
Expect(p.Config.Requirements).Should(HaveLen(0))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
By("checking tags")
|
||||
Expect(pluginData.Tags).Should(HaveLen(len(pushedTags)))
|
||||
Expect(pluginData.Tags).Should(ContainElements(pushedTags))
|
||||
})
|
||||
}
|
||||
|
||||
// Here we are testing the success cases for the push command using a plugin artifact and its related flags.
|
||||
// Other flags related to the plugin artifacts are tested in the rulesfile artifact section.
|
||||
PluginsSpecificFlags := Context("plugins specific flags", func() {
|
||||
JustBeforeEach(func() {
|
||||
// This runs after the push command, so check the returned error before proceeding.
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
pluginData, err = testutils.FetchPluginFromRegistry(ctx, repoName, pushedTags[0], orasRegistry)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
// This variable could be changed by single tests.
|
||||
// Make sure to set them at their default values.
|
||||
artifactNameInConfigLayer = "test-plugin"
|
||||
pushedTags = []string{"tag1", "tag2", "latest"}
|
||||
})
|
||||
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, pluginsRepoBaseName)
|
||||
})
|
||||
When("with full flags and args", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, plugin, pluginRaw, "--type", "plugin", "--platform",
|
||||
platformAMD64, "--platform", platformARM64, "--version", version, "--config", configFile,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
AssertSuccessBehaviourPlugins(true, true, true)
|
||||
})
|
||||
})
|
||||
|
||||
Context("plugin", func() {
|
||||
Context("tar.gz + raw format format", func() {
|
||||
plugin = plugintgz
|
||||
// We do not really care what the file is.
|
||||
pluginRaw = rulesfileyaml
|
||||
var _ = PluginsSpecificFlags
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
@ -21,23 +20,22 @@ import (
|
|||
"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 {
|
||||
func NewAuthCmd(ctx context.Context, opt *commonoptions.CommonOptions) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Handle authentication towards OCI registries",
|
||||
Long: "Handle authentication towards OCI registries",
|
||||
SilenceErrors: true,
|
||||
}
|
||||
|
||||
cmd.AddCommand(basic.NewBasicCmd(ctx, opt))
|
||||
cmd.AddCommand(oauth.NewOauthCmd(ctx, opt))
|
||||
cmd.AddCommand(gcp.NewGcpCmd(ctx, opt))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -16,146 +15,126 @@
|
|||
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"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"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"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/registry"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
type loginOptions struct {
|
||||
*options.Common
|
||||
username string
|
||||
password string
|
||||
passwordFromStdin bool
|
||||
*options.CommonOptions
|
||||
hostname string
|
||||
}
|
||||
|
||||
func (o *loginOptions) Validate(args []string) error {
|
||||
if len(args) != 0 {
|
||||
o.hostname = args[0]
|
||||
} else {
|
||||
o.hostname = oci.DefaultRegistry
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewBasicCmd returns the basic command.
|
||||
func NewBasicCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewBasicCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := loginOptions{
|
||||
Common: opt,
|
||||
CommonOptions: 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),
|
||||
Long: "Login to an OCI registry to push and pull artifacts",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
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
|
||||
return o.Validate(args)
|
||||
},
|
||||
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 {
|
||||
if n := len(args); n == 1 {
|
||||
reg = args[0]
|
||||
} else {
|
||||
reg = oci.DefaultRegistry
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
user, token, err := utils.GetCredentials(o.Printer)
|
||||
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))
|
||||
|
||||
cred := &auth.Credential{
|
||||
Username: user,
|
||||
Password: token,
|
||||
}
|
||||
|
||||
if err = DoLogin(ctx, reg, cred); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentAuths, err := config.BasicAuths()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get basicAuths from viper: %w", err)
|
||||
}
|
||||
|
||||
for _, a := range currentAuths {
|
||||
if a.Registry == reg {
|
||||
o.Printer.Verbosef("credentials for registry %q already exists in the config file %q", reg, config.ConfigPath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
currentAuths = append(currentAuths, config.BasicAuth{
|
||||
Registry: reg,
|
||||
User: user,
|
||||
Password: token,
|
||||
})
|
||||
|
||||
if err := config.UpdateConfigFile(config.RegistryAuthBasicKey, currentAuths, o.ConfigFile); err != nil {
|
||||
return fmt.Errorf("unable to update basic auths credential list in the config file %q: %w", config.ConfigPath, err)
|
||||
}
|
||||
o.Printer.Verbosef("credentials added to config file %q", config.ConfigPath)
|
||||
|
||||
o.Printer.Success.Println("Login succeeded")
|
||||
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)
|
||||
// DoLogin checks if passed credentials are correct and stores them.
|
||||
func DoLogin(ctx context.Context, reg string, cred *auth.Credential) error {
|
||||
client := authn.NewClient(authn.WithCredentials(cred))
|
||||
r, err := registry.NewRegistry(reg, registry.WithClient(client))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if err := r.CheckConnection(ctx); err != nil {
|
||||
return fmt.Errorf("unable to connect to registry %q: %w", reg, err)
|
||||
}
|
||||
|
||||
opt.password = string(bytePassword)
|
||||
}
|
||||
// Store validated credentials
|
||||
err = authn.Login(reg, cred.Username, cred.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -1,150 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
// 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")
|
||||
})
|
||||
})
|
||||
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
// 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
|
|
@ -1,84 +0,0 @@
|
|||
// 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
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -23,9 +22,8 @@ import (
|
|||
"golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/login/oauth"
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -33,25 +31,25 @@ const (
|
|||
|
||||
Client credentials will be saved in the ~/.config directory.
|
||||
|
||||
Example
|
||||
Example
|
||||
falcoctl registry oauth \
|
||||
--token-url="http://localhost:9096/token" \
|
||||
--client-id=000000 \
|
||||
--client-secret=999999 --scopes="my-scope" \
|
||||
--client-secret=999999 --scopes="my-scope" \
|
||||
hostname
|
||||
`
|
||||
)
|
||||
|
||||
// RegistryOauthOptions contains the options for the registry oauth command.
|
||||
type RegistryOauthOptions struct {
|
||||
*options.Common
|
||||
*options.CommonOptions
|
||||
Conf clientcredentials.Config
|
||||
}
|
||||
|
||||
// NewOauthCmd returns the oauth command.
|
||||
func NewOauthCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewOauthCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := RegistryOauthOptions{
|
||||
Common: opt,
|
||||
CommonOptions: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -60,22 +58,26 @@ func NewOauthCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
|||
Short: "Retrieve access and refresh tokens for OAuth2.0 client credentials flow authentication",
|
||||
Long: longOauth,
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunOAuth(ctx, args)
|
||||
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"))
|
||||
o.Printer.Error.Println("unable to mark flag \"token-url\" as required")
|
||||
return nil
|
||||
}
|
||||
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"))
|
||||
o.Printer.Error.Println("unable to mark flag \"client-id\" as required")
|
||||
return nil
|
||||
}
|
||||
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"))
|
||||
o.Printer.Error.Println("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")
|
||||
|
@ -83,12 +85,46 @@ func NewOauthCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
// RunOAuth executes the business logic for the oauth command.
|
||||
func (o *RegistryOauthOptions) RunOAuth(ctx context.Context, args []string) error {
|
||||
// RunOauth implements the registry 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
|
||||
|
||||
// Check that we can retrieve token using the passed credentials.
|
||||
_, err := o.Conf.Token(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wrong client credentials, unable to retrieve token: %w", err)
|
||||
}
|
||||
o.Printer.Logger.Info("Client credentials correctly saved", o.Printer.Logger.Args("file", config.ClientCredentialsFile))
|
||||
|
||||
// Save client credentials to file.
|
||||
if err = utils.WriteClientCredentials(reg, &o.Conf); err != nil {
|
||||
return fmt.Errorf("unable to save token: %w", err)
|
||||
}
|
||||
|
||||
currentAuths, err := config.OauthAuths()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get oauthAuths from viper: %w", err)
|
||||
}
|
||||
|
||||
for _, a := range currentAuths {
|
||||
if a.Registry == reg {
|
||||
o.Printer.Verbosef("credentials for registry %q already exists in the config file %q", reg, config.ConfigPath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
currentAuths = append(currentAuths, config.OauthAuth{
|
||||
Registry: reg,
|
||||
ClientSecret: o.Conf.ClientSecret,
|
||||
ClientID: o.Conf.ClientID,
|
||||
TokenURL: o.Conf.TokenURL,
|
||||
})
|
||||
|
||||
if err := config.UpdateConfigFile(config.RegistryAuthOauthKey, currentAuths, o.ConfigFile); err != nil {
|
||||
return fmt.Errorf("unable to update oauth auths credential list in the config file %q: %w", config.ConfigPath, err)
|
||||
}
|
||||
o.Printer.Verbosef("credentials added to config file %q", config.ConfigPath)
|
||||
|
||||
o.Printer.Success.Printfln("client credentials correctly saved in %q", config.ClientCredentialsFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
// 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`)))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
@ -22,8 +21,9 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/login"
|
||||
"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"
|
||||
)
|
||||
|
@ -31,19 +31,23 @@ import (
|
|||
const (
|
||||
longPull = `Pull Falco "rulesfile" or "plugin" OCI artifacts from remote registry.
|
||||
|
||||
Artifact references are passed as arguments.
|
||||
Artifact references are passed as arguments.
|
||||
|
||||
A reference is a fully qualified reference ("<registry>/<repository>"),
|
||||
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 - 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/aarch64" in the current working directory (default):
|
||||
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/aarch64
|
||||
|
||||
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 "myplugin" for platform "linux/aarch64" in "myDir" directory:
|
||||
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/aarch64 --dest-dir=./myDir
|
||||
|
||||
Example - Pull artifact "myrulesfile":
|
||||
falcoctl registry pull localhost:5000/myrulesfile:latest
|
||||
|
@ -51,22 +55,22 @@ Example - Pull artifact "myrulesfile":
|
|||
)
|
||||
|
||||
type pullOptions struct {
|
||||
*options.Common
|
||||
*options.Artifact
|
||||
*options.Registry
|
||||
*options.CommonOptions
|
||||
*options.ArtifactOptions
|
||||
*options.RegistryOptions
|
||||
destDir string
|
||||
}
|
||||
|
||||
func (o *pullOptions) Validate() error {
|
||||
return o.Artifact.Validate()
|
||||
return o.ArtifactOptions.Validate()
|
||||
}
|
||||
|
||||
// NewPullCmd returns the pull command.
|
||||
func NewPullCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewPullCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := pullOptions{
|
||||
Common: opt,
|
||||
Artifact: &options.Artifact{},
|
||||
Registry: &options.Registry{},
|
||||
CommonOptions: opt,
|
||||
ArtifactOptions: &options.ArtifactOptions{},
|
||||
RegistryOptions: &options.RegistryOptions{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -75,61 +79,66 @@ func NewPullCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
|||
Short: "Pull a Falco OCI artifact from remote registry",
|
||||
Long: longPull,
|
||||
Args: cobra.ExactArgs(1),
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
var basicAuths []config.BasicAuth
|
||||
var oauthAuths []config.OauthAuth
|
||||
var err error
|
||||
|
||||
if err := o.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := args[0]
|
||||
|
||||
_, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
// Perform authentications using basic auth.
|
||||
if basicAuths, err = config.BasicAuths(); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Common.Initialize()
|
||||
return nil
|
||||
if err = login.PerformBasicAuthsLogin(ctx, basicAuths); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform authentications using oauth auth.
|
||||
if oauthAuths, err = config.OauthAuths(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return login.PerformOauthAuths(ctx, o.CommonOptions, oauthAuths)
|
||||
},
|
||||
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))
|
||||
o.RegistryOptions.AddFlags(cmd)
|
||||
output.ExitOnErr(o.Printer, o.ArtifactOptions.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]
|
||||
o.Printer.Info.Printfln("Preparing to pull artifact %q", args[0])
|
||||
|
||||
registry, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
puller, err := ociutils.Puller(o.PlainHTTP, o.Printer)
|
||||
puller, err := utils.PullerForRegistry(ctx, registry, 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")
|
||||
o.Printer.Info.Printfln("Pulling artifact in the current directory")
|
||||
} else {
|
||||
logger.Info("Pulling artifact in", logger.Args("directory", o.destDir))
|
||||
o.Printer.Info.Printfln("Pulling artifact in %q directory", o.destDir)
|
||||
}
|
||||
|
||||
os, arch := runtime.GOOS, runtime.GOARCH
|
||||
if len(o.Artifact.Platforms) > 0 {
|
||||
if len(o.ArtifactOptions.Platforms) > 0 {
|
||||
os, arch = o.OSArch(0)
|
||||
}
|
||||
|
||||
|
@ -138,7 +147,7 @@ func (o *pullOptions) RunPull(ctx context.Context, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
logger.Info("Artifact pulled", logger.Args("name", args[0], "type", res.Type, "digest", res.Digest))
|
||||
o.Printer.Success.Printfln("Artifact of type %q pulled. Digest: %q", res.Type, res.Digest)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,327 +0,0 @@
|
|||
// 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")
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -22,15 +21,13 @@ import (
|
|||
"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/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/login"
|
||||
"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"
|
||||
)
|
||||
|
@ -41,21 +38,17 @@ const (
|
|||
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 platform "linux/aarch64":
|
||||
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest myplugin.tar.gz --platform linux/aarch64
|
||||
|
||||
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
|
||||
myplugin-linux-arm64.tar.gz --platform linux/aarch64
|
||||
|
||||
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
|
||||
|
||||
|
@ -75,21 +68,21 @@ Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with multiple d
|
|||
)
|
||||
|
||||
type pushOptions struct {
|
||||
*options.Common
|
||||
*options.Artifact
|
||||
*options.Registry
|
||||
*options.CommonOptions
|
||||
*options.ArtifactOptions
|
||||
*options.RegistryOptions
|
||||
}
|
||||
|
||||
func (o *pushOptions) validate() error {
|
||||
return o.Artifact.Validate()
|
||||
func (o pushOptions) validate() error {
|
||||
return o.ArtifactOptions.Validate()
|
||||
}
|
||||
|
||||
// NewPushCmd returns the push command.
|
||||
func NewPushCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func NewPushCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
o := pushOptions{
|
||||
Common: opt,
|
||||
Artifact: &options.Artifact{},
|
||||
Registry: &options.Registry{},
|
||||
CommonOptions: opt,
|
||||
ArtifactOptions: &options.ArtifactOptions{},
|
||||
RegistryOptions: &options.RegistryOptions{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -98,26 +91,38 @@ func NewPushCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
|||
Short: "Push a Falco OCI artifact to remote registry",
|
||||
Long: longPush,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
var basicAuths []config.BasicAuth
|
||||
var oauthAuths []config.OauthAuth
|
||||
var err error
|
||||
|
||||
if err := o.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := args[0]
|
||||
|
||||
_, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
// Perform authentications using basic auth.
|
||||
if basicAuths, err = config.BasicAuths(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = login.PerformBasicAuthsLogin(ctx, basicAuths); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
// Perform authentications using oauth auth.
|
||||
if oauthAuths, err = config.OauthAuths(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return login.PerformOauthAuths(ctx, o.CommonOptions, oauthAuths)
|
||||
},
|
||||
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))
|
||||
o.RegistryOptions.AddFlags(cmd)
|
||||
output.ExitOnErr(o.Printer, o.ArtifactOptions.AddFlags(cmd))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -127,62 +132,50 @@ 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
|
||||
// We save the temporary dir where they live here.
|
||||
var toBeDeleted string
|
||||
|
||||
o.Printer.Info.Printfln("Preparing to push artifact %q of type %q", args[0], o.ArtifactType)
|
||||
|
||||
registry, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pusher, err := ociutils.Pusher(o.PlainHTTP, o.Printer)
|
||||
pusher, err := utils.PusherForRegistry(ctx, o.PlainHTTP, registry, 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.
|
||||
// Make sure to remove temporary working dir.
|
||||
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()))
|
||||
}
|
||||
if err := os.RemoveAll(toBeDeleted); err != nil {
|
||||
o.Printer.Warning.Printfln("Unable to remove temporary dir %q: %s", toBeDeleted, 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)
|
||||
path, err := utils.CreateTarGzArchive(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paths[i] = path
|
||||
toBeDeletedTmpDirs = append(toBeDeletedTmpDirs, filepath.Dir(path))
|
||||
if toBeDeleted == "" {
|
||||
toBeDeleted = filepath.Dir(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup OCI artifact configuration
|
||||
config := oci.ArtifactConfig{
|
||||
Name: o.Name,
|
||||
Version: o.Version,
|
||||
}
|
||||
if config.Name == "" {
|
||||
// extract artifact name from ref, if not provided by the user
|
||||
if config.Name, err = utils.NameFromRef(ref); err != nil {
|
||||
|
@ -196,18 +189,10 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error {
|
|||
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),
|
||||
ocipusher.WithArtifactConfig(config),
|
||||
}
|
||||
|
||||
switch o.ArtifactType {
|
||||
|
@ -215,8 +200,6 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error {
|
|||
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...)
|
||||
|
@ -224,120 +207,7 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
logger.Info("Artifact pushed", logger.Args("name", args[0], "type", res.Type, "digest", res.RootDigest))
|
||||
o.Printer.Success.Printfln("Artifact pushed. Digest: %q", res.Digest)
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,217 +0,0 @@
|
|||
// 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,655 +0,0 @@
|
|||
// 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 rulesfiles", func() {
|
||||
var (
|
||||
registryCmd = "registry"
|
||||
pushCmd = "push"
|
||||
version = "1.1.1"
|
||||
// registry/rulesRepoBaseName-randomInt
|
||||
fullRepoName string
|
||||
// rulesRepoBaseName-randomInt
|
||||
repoName string
|
||||
// It is set in the config layer.
|
||||
artifactNameInConfigLayer = "test-rulesfile"
|
||||
pushedTags = []string{"tag1", "tag2", "latest"}
|
||||
|
||||
// Variables passed as arguments to the push command. Each test case updates them
|
||||
// to point to the file on disk living in pkg/test/data.
|
||||
rulesfile string
|
||||
|
||||
// Data fetched from registry and used for assertions.
|
||||
rulesfileData *testutils.RulesfileArtifact
|
||||
)
|
||||
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
dep1 = "myplugin:1.2.3"
|
||||
dep2 = "myplugin1:1.2.3|otherplugin:3.2.1"
|
||||
req = "engine_version_semver:0.37.0"
|
||||
anSource = "myrepo.com/rules.git"
|
||||
rulesRepoBaseName = "push-rulesfile"
|
||||
)
|
||||
|
||||
// We keep it inside the success context since need the variables of this context.
|
||||
var AssertSuccesBehaviour = func(deps []oci.ArtifactDependency, reqs []oci.ArtifactRequirement, annotations map[string]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(rulesfileData.Descriptor.MediaType).Should(Equal(v1.MediaTypeImageManifest))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(rulesfileData.Descriptor.Digest.String())))
|
||||
|
||||
By("checking manifest")
|
||||
Expect(rulesfileData.Layer.Manifest.Layers).Should(HaveLen(1))
|
||||
|
||||
By("checking platforms")
|
||||
Expect(rulesfileData.Descriptor.Platform).Should(BeNil())
|
||||
|
||||
By("checking config layer")
|
||||
Expect(rulesfileData.Layer.Config.Version).Should(Equal(version))
|
||||
Expect(rulesfileData.Layer.Config.Name).Should(Equal(artifactNameInConfigLayer))
|
||||
|
||||
By("checking dependencies")
|
||||
Expect(rulesfileData.Layer.Config.Dependencies).Should(HaveLen(len(deps)))
|
||||
for _, dep := range deps {
|
||||
Expect(rulesfileData.Layer.Config.Dependencies).Should(ContainElement(dep))
|
||||
}
|
||||
|
||||
By("checking requirements")
|
||||
Expect(rulesfileData.Layer.Config.Requirements).Should(HaveLen(len(reqs)))
|
||||
for _, req := range reqs {
|
||||
Expect(rulesfileData.Layer.Config.Requirements).Should(ContainElement(req))
|
||||
}
|
||||
|
||||
By("checking annotations")
|
||||
// The creation timestamp is always present.
|
||||
Expect(rulesfileData.Layer.Manifest.Annotations).Should(HaveLen(len(annotations) + 1))
|
||||
for key, val := range annotations {
|
||||
Expect(rulesfileData.Layer.Manifest.Annotations).Should(HaveKeyWithValue(key, val))
|
||||
}
|
||||
|
||||
By("checking tags")
|
||||
Expect(rulesfileData.Tags).Should(HaveLen(len(pushedTags)))
|
||||
Expect(rulesfileData.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 {
|
||||
fmt.Println(e.Name())
|
||||
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() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
// This variable could be changed by single tests.
|
||||
// Make sure to set them at their default values.
|
||||
artifactNameInConfigLayer = "test-rulesfile"
|
||||
pushedTags = []string{"tag1", "tag2", "latest"}
|
||||
rulesfile = ""
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
// Here we are testing all the success cases for the push command. The artifact type used here is of type
|
||||
// rulesfile. Keep in mind that here we are testing also the common flags that could be used by the plugin
|
||||
// artifacts. So we are testing that common logic only once, and are doing it here.
|
||||
|
||||
JustBeforeEach(func() {
|
||||
// This runs after the push command, so check the returned error before proceeding.
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
rulesfileData, err = testutils.FetchRulesfileFromRegistry(ctx, repoName, pushedTags[0], orasRegistry)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
})
|
||||
|
||||
When("with full flags and args", func() {
|
||||
BeforeEach(func() {
|
||||
rulesfile = rulesfileyaml
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "myplugin",
|
||||
Version: "1.2.3",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "myplugin1",
|
||||
Version: "1.2.3",
|
||||
Alternatives: []oci.Dependency{{
|
||||
Name: "otherplugin",
|
||||
Version: "3.2.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.37.0",
|
||||
},
|
||||
}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
When("no --name flag provided", func() {
|
||||
BeforeEach(func() {
|
||||
rulesfile = rulesfileyaml
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2]}
|
||||
// Set name to the expected one.
|
||||
artifactNameInConfigLayer = repoName
|
||||
})
|
||||
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "myplugin",
|
||||
Version: "1.2.3",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "myplugin1",
|
||||
Version: "1.2.3",
|
||||
Alternatives: []oci.Dependency{{
|
||||
Name: "otherplugin",
|
||||
Version: "3.2.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.37.0",
|
||||
},
|
||||
}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
When("no --annotation-source provided", func() {
|
||||
BeforeEach(func() {
|
||||
rulesfile = rulesfileyaml
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "myplugin",
|
||||
Version: "1.2.3",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "myplugin1",
|
||||
Version: "1.2.3",
|
||||
Alternatives: []oci.Dependency{{
|
||||
Name: "otherplugin",
|
||||
Version: "3.2.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.37.0",
|
||||
},
|
||||
}, map[string]string{})
|
||||
})
|
||||
|
||||
When("no --tags provided", func() {
|
||||
BeforeEach(func() {
|
||||
rulesfile = rulesfileyaml
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req, "--annotation-source", anSource,
|
||||
"--name", artifactNameInConfigLayer}
|
||||
// We expect that latest tag is pushed, so set it in the pushed tags.
|
||||
pushedTags = []string{"latest"}
|
||||
})
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "myplugin",
|
||||
Version: "1.2.3",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "myplugin1",
|
||||
Version: "1.2.3",
|
||||
Alternatives: []oci.Dependency{{
|
||||
Name: "otherplugin",
|
||||
Version: "3.2.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.37.0",
|
||||
},
|
||||
}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
When("no --depends-on flag provided", func() {
|
||||
BeforeEach(func() {
|
||||
rulesfile = rulesfileyaml
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--requires", req, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{},
|
||||
[]oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.37.0",
|
||||
},
|
||||
}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
When("no --requires flag provided", func() {
|
||||
BeforeEach(func() {
|
||||
rulesfile = rulesfileyaml
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "myplugin",
|
||||
Version: "1.2.3",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "myplugin1",
|
||||
Version: "1.2.3",
|
||||
Alternatives: []oci.Dependency{{
|
||||
Name: "otherplugin",
|
||||
Version: "3.2.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []oci.ArtifactRequirement{}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
When("only required flags", func() {
|
||||
BeforeEach(func() {
|
||||
rulesfile = rulesfileyaml
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http"}
|
||||
// Set name to the expected one.
|
||||
artifactNameInConfigLayer = repoName
|
||||
// We expect that latest tag is pushed, so set it in the pushed tags.
|
||||
pushedTags = []string{"latest"}
|
||||
})
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{},
|
||||
[]oci.ArtifactRequirement{},
|
||||
map[string]string{})
|
||||
})
|
||||
|
||||
When("with add-floating-tags and the required flags", func() {
|
||||
BeforeEach(func() {
|
||||
rulesfile = rulesfileyaml
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--add-floating-tags", "--plain-http"}
|
||||
// Set name to the expected one.
|
||||
artifactNameInConfigLayer = repoName
|
||||
// The semver tags are expected to be set.
|
||||
pushedTags = []string{"1.1.1", "1.1", "1"}
|
||||
})
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{},
|
||||
[]oci.ArtifactRequirement{},
|
||||
map[string]string{})
|
||||
})
|
||||
|
||||
When("with full flags and args but in tar.gz format", func() {
|
||||
BeforeEach(func() {
|
||||
rulesfile = rulesfiletgz
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "myplugin",
|
||||
Version: "1.2.3",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "myplugin1",
|
||||
Version: "1.2.3",
|
||||
Alternatives: []oci.Dependency{{
|
||||
Name: "otherplugin",
|
||||
Version: "3.2.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.37.0",
|
||||
},
|
||||
}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
Context("rulesfile deps and requirements", func() {
|
||||
When("user provided deps", func() {
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
rulesfile = rulesFileWithDepsAndReq
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "myplugin",
|
||||
Version: "1.2.3",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "myplugin1",
|
||||
Version: "1.2.3",
|
||||
Alternatives: []oci.Dependency{{
|
||||
Name: "otherplugin",
|
||||
Version: "3.2.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.37.0",
|
||||
},
|
||||
}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
When("parsed from file deps", func() {
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
rulesfile = rulesFileWithDepsAndReq
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "cloudtrail",
|
||||
Version: "0.2.3",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "json",
|
||||
Version: "0.2.2",
|
||||
Alternatives: nil,
|
||||
},
|
||||
}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.10.0",
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
When("parsed from file deps with alternatives", func() {
|
||||
var data = `
|
||||
- required_plugin_versions:
|
||||
- name: k8saudit
|
||||
version: 0.7.0
|
||||
alternatives:
|
||||
- name: k8saudit-eks
|
||||
version: 0.4.0
|
||||
- name: json
|
||||
version: 0.7.0
|
||||
`
|
||||
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
rulesfile, err = testutils.WriteToTmpFile(data, tmpDir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "json",
|
||||
Version: "0.7.0",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "k8saudit",
|
||||
Version: "0.7.0",
|
||||
Alternatives: []oci.Dependency{{
|
||||
Name: "k8saudit-eks",
|
||||
Version: "0.4.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, []oci.ArtifactRequirement{},
|
||||
map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
When("no deps at all", func() {
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
rulesfile = rulesfileyaml
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{}, []oci.ArtifactRequirement{},
|
||||
map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
When("user provided requirement", func() {
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
rulesfile = rulesFileWithDepsAndReq
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--requires", req, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "json",
|
||||
Version: "0.2.2",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "cloudtrail",
|
||||
Version: "0.2.3",
|
||||
Alternatives: nil,
|
||||
},
|
||||
}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.37.0",
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
It("reqs should be the ones provided by the user", func() {
|
||||
Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Requirements[0].Name,
|
||||
rulesfileData.Layer.Config.Requirements[0].Version)).Should(Equal(req))
|
||||
})
|
||||
})
|
||||
|
||||
When("requirement parsed from file in semver format", func() {
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
rulesfile = rulesFileWithDepsAndReq
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{
|
||||
{
|
||||
Name: "json",
|
||||
Version: "0.2.2",
|
||||
Alternatives: nil,
|
||||
}, {
|
||||
Name: "cloudtrail",
|
||||
Version: "0.2.3",
|
||||
Alternatives: nil,
|
||||
},
|
||||
}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.10.0",
|
||||
},
|
||||
}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
|
||||
When("requirement parsed from file in int format", func() {
|
||||
var rulesfileContent = `
|
||||
- required_engine_version: 10
|
||||
`
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
rulesfile, err = testutils.WriteToTmpFile(rulesfileContent, tmpDir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
|
||||
AssertSuccesBehaviour([]oci.ArtifactDependency{}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "engine_version_semver",
|
||||
Version: "0.10.0",
|
||||
},
|
||||
}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("failure", func() {
|
||||
When("requirement parsed from file -- invalid format (float)", func() {
|
||||
var rulesFile = `
|
||||
- required_engine_version: 10.0
|
||||
`
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
rulesfile, err = testutils.WriteToTmpFile(rulesFile, tmpDir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
|
||||
It("should fail", func() {
|
||||
Expect(err).Should(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta("required_engine_version must be an int or a string respecting " +
|
||||
"the semver specification, got type float64")))
|
||||
})
|
||||
})
|
||||
|
||||
When("requirement parsed from file -- invalid format (not semver)", func() {
|
||||
var rulesFile = `
|
||||
- required_engine_version: 10.0notsemver
|
||||
`
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName)
|
||||
tmpDir := GinkgoT().TempDir()
|
||||
rulesfile, err = testutils.WriteToTmpFile(rulesFile, tmpDir)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version,
|
||||
"--plain-http", "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
// Set name to the expected one.
|
||||
artifactNameInConfigLayer = repoName
|
||||
// We expect that latest tag is pushed, so set it in the pushed tags.
|
||||
pushedTags = []string{"latest"}
|
||||
})
|
||||
|
||||
It("reqs should be the ones provided by the user", func() {
|
||||
Expect(err).Should(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta("10.0notsemver must be in semver format: No Major.Minor.Patch elements found")))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,108 +0,0 @@
|
|||
// 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_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"
|
||||
)
|
||||
|
||||
const (
|
||||
rulesfiletgz = "../../../pkg/test/data/rules.tar.gz"
|
||||
rulesfileyaml = "../../../pkg/test/data/rulesWithoutReqAndDeps.yaml"
|
||||
rulesFileWithDepsAndReq = "../../../pkg/test/data/rules.yaml"
|
||||
plugintgz = "../../../pkg/test/data/plugin.tar.gz"
|
||||
)
|
||||
|
||||
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 TestRoot(t *testing.T) {
|
||||
var err error
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
RunSpecs(t, "Push 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())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -1,265 +0,0 @@
|
|||
// 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_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
var registryPushUsage = `Usage:
|
||||
falcoctl registry push hostname/repo[:tag|@digest] file [flags]
|
||||
|
||||
Flags:
|
||||
--add-floating-tags add the floating tags for the major and minor versions
|
||||
--annotation-source string set annotation source for the artifact
|
||||
-d, --depends-on stringArray set an artifact dependency (can be specified multiple times). Example: "--depends-on my-plugin:1.2.3"
|
||||
-h, --help help for push
|
||||
--name string set the unique name of the artifact (if not set, the name is extracted from the reference)
|
||||
--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)
|
||||
-r, --requires stringArray set an artifact requirement (can be specified multiple times). Example: "--requires plugin_api_version:1.2.3"
|
||||
-t, --tag stringArray additional artifact tag. Can be repeated multiple times
|
||||
--type ArtifactType type of artifact to be pushed. Allowed values: "rulesfile", "plugin", "asset" (default )
|
||||
--version string set the version of the artifact
|
||||
|
||||
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,unused // no need to check for line length.
|
||||
var registryPushHelp = `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
|
||||
|
||||
Usage:
|
||||
falcoctl registry push hostname/repo[:tag|@digest] file [flags]
|
||||
|
||||
Flags:
|
||||
--add-floating-tags add the floating tags for the major and minor versions
|
||||
--annotation-source string set annotation source for the artifact
|
||||
-d, --depends-on stringArray set an artifact dependency (can be specified multiple times). Example: "--depends-on my-plugin:1.2.3"
|
||||
-h, --help help for push
|
||||
--name string set the unique name of the artifact (if not set, the name is extracted from the reference)
|
||||
--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)
|
||||
-r, --requires stringArray set an artifact requirement (can be specified multiple times). Example: "--requires plugin_api_version:1.2.3"
|
||||
-t, --tag stringArray additional artifact tag. Can be repeated multiple times
|
||||
--type ArtifactType type of artifact to be pushed. Allowed values: "rulesfile", "plugin", "asset"
|
||||
--version string set the version of the artifact
|
||||
|
||||
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 pushAssertFailedBehavior = 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 randomRulesRepoName = func(registry, repo string) (string, string) {
|
||||
rName := fmt.Sprintf("%s-%d", repo, rand.Int())
|
||||
return rName, fmt.Sprintf("%s/%s", registry, rName)
|
||||
}
|
||||
|
||||
var _ = Describe("push", func() {
|
||||
var (
|
||||
registryCmd = "registry"
|
||||
pushCmd = "push"
|
||||
)
|
||||
|
||||
// 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, pushCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
outputMsg := string(output.Contents())
|
||||
Expect(outputMsg).Should(Equal(registryPushHelp))
|
||||
})
|
||||
})
|
||||
|
||||
// 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 (
|
||||
// Not really used since all the tests fail but needed as argument.
|
||||
rulesRepo = registry + "/push-rulesfile"
|
||||
pluginsRepo = registry + "/push-plugin"
|
||||
)
|
||||
When("without --version flag", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, "--config", configFile, rulesRepo, rulesfiletgz, "--type", "rulesfile"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR required flag(s) \"version\" not set")
|
||||
})
|
||||
|
||||
When("without rulesfile", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, "--config", configFile, rulesRepo, "--type", "rulesfile"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR requires at least 2 arg(s), only received 1")
|
||||
})
|
||||
|
||||
When("without registry", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, "--config", configFile, rulesfiletgz, "--type", "rulesfile"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR requires at least 2 arg(s), only received 1")
|
||||
})
|
||||
|
||||
When("multiple rulesfiles", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, "--config", configFile,
|
||||
"--type", "rulesfile", "--version", "1.1.1", "--plain-http", rulesRepo, rulesfiletgz, rulesfiletgz}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR expecting 1 rulesfile object, received 2: invalid number of rulesfiles")
|
||||
})
|
||||
|
||||
When("unreachable registry", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, "noregistry/testrules", "--config", configFile, rulesfiletgz,
|
||||
"--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR unable to connect to remote "+
|
||||
"registry \"noregistry\": Get \"http://noregistry/v2/\": dial tcp: lookup noregistry")
|
||||
})
|
||||
|
||||
When("wrong semver for --version flag with --add-floating-tags", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, rulesRepo, rulesfiletgz, "--config", configFile, "--type", "rulesfile",
|
||||
"--version", "notSemVer", "--add-floating-tags", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR expected semver for the flag \"--version\": No Major.Minor.Patch elements found")
|
||||
})
|
||||
|
||||
When("invalid character in semver for --version flag with --add-floating-tags", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, rulesRepo, rulesfiletgz, "--config", configFile, "--type", "rulesfile",
|
||||
"--version", "1.1.a", "--add-floating-tags", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR expected semver for the flag \"--version\": Invalid character(s) found in patch number \"a\"")
|
||||
})
|
||||
|
||||
When("missing repository", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, registry, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERROR cannot extract registry name from ref %q", registry))
|
||||
})
|
||||
|
||||
When("invalid repository", func() {
|
||||
newReg := registry + "/wrong@something"
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, newReg, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, 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 requirement", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, rulesRepo, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1",
|
||||
"--plain-http", "--requires", "wrongreq"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR cannot parse \"wrongreq\"")
|
||||
})
|
||||
|
||||
When("invalid dependency", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, rulesRepo, rulesfiletgz, "--config", configFile, "--type", "rulesfile",
|
||||
"--version", "1.1.1", "--plain-http", "--depends-on", "wrongdep"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR cannot parse \"wrongdep\": invalid artifact reference "+
|
||||
"(must be in the format \"name:version\")\n")
|
||||
})
|
||||
|
||||
When("without platform", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, pluginsRepo, plugintgz, "--config", configFile, "--type", "plugin", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR \"filepaths\" length (1) must match \"platforms\" "+
|
||||
"length (0): number of filepaths and platform should be the same")
|
||||
})
|
||||
|
||||
When("wrong plugin type", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pushCmd, pluginsRepo, pluginsRepo, "--config", configFile,
|
||||
"--type", "wrongType", "--version", "1.1.1", "--plain-http"}
|
||||
})
|
||||
pushAssertFailedBehavior(registryPushUsage, "ERROR invalid argument \"wrongType\" for \"--type\" "+
|
||||
"flag: must be one of \"rulesfile\", \"plugin\", \"asset")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -28,15 +27,14 @@ import (
|
|||
)
|
||||
|
||||
// NewRegistryCmd returns the registry command.
|
||||
func NewRegistryCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
func NewRegistryCmd(ctx context.Context, opt *commonoptions.CommonOptions) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "registry",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Interact with OCI registries",
|
||||
Long: "Interact with OCI registries",
|
||||
SilenceErrors: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Initialize the options.
|
||||
opt.Initialize()
|
||||
// Load configuration from ENV variables and/or config file.
|
||||
return config.Load(opt.ConfigFile)
|
||||
},
|
||||
|
|
34
cmd/root.go
34
cmd/root.go
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 2022 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.
|
||||
|
@ -17,16 +16,19 @@ package cmd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact"
|
||||
"github.com/falcosecurity/falcoctl/cmd/driver"
|
||||
"github.com/falcosecurity/falcoctl/cmd/index"
|
||||
"github.com/falcosecurity/falcoctl/cmd/registry"
|
||||
"github.com/falcosecurity/falcoctl/cmd/tls"
|
||||
"github.com/falcosecurity/falcoctl/cmd/version"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -43,42 +45,40 @@ The official CLI tool for working with Falco and its ecosystem components
|
|||
)
|
||||
|
||||
// New instantiates the root command and initializes the tree of commands.
|
||||
func New(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
func New(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "falcoctl",
|
||||
Short: "The official CLI tool for working with Falco and its ecosystem components",
|
||||
Long: longRootCmd,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
TraverseChildren: true,
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// Initialize the common options for all subcommands.
|
||||
// Subcommands con overwrite the default settings by calling initialize with
|
||||
// different options.
|
||||
opt.Initialize()
|
||||
},
|
||||
}
|
||||
|
||||
// Global flags
|
||||
opt.AddFlags(rootCmd.PersistentFlags())
|
||||
|
||||
// Commands
|
||||
rootCmd.AddCommand(tls.NewTLSCmd(opt))
|
||||
rootCmd.AddCommand(tls.NewTLSCmd())
|
||||
rootCmd.AddCommand(version.NewVersionCmd(opt))
|
||||
rootCmd.AddCommand(registry.NewRegistryCmd(ctx, opt))
|
||||
rootCmd.AddCommand(index.NewIndexCmd(ctx, opt))
|
||||
rootCmd.AddCommand(artifact.NewArtifactCmd(ctx, opt))
|
||||
rootCmd.AddCommand(driver.NewDriverCmd(ctx, opt))
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
// Execute configures the signal handlers and runs the command.
|
||||
func Execute(cmd *cobra.Command, opt *options.Common) error {
|
||||
func Execute(cmd *cobra.Command, printer *output.Printer) error {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
|
||||
|
||||
// If the ctx is marked as done then we reset the signals.
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
fmt.Printf("\nreceived signal, terminating...\n")
|
||||
stop()
|
||||
}()
|
||||
// we do not log the error here since we expect that each subcommand
|
||||
// handles the errors by itself.
|
||||
err := cmd.Execute()
|
||||
opt.Printer.CheckErr(err)
|
||||
printer.CheckErr(err)
|
||||
return err
|
||||
}
|
||||
|
|
161
cmd/root_test.go
161
cmd/root_test.go
|
@ -1,5 +1,4 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
// Copyright 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.
|
||||
|
@ -16,160 +15,18 @@
|
|||
package cmd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
. "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"
|
||||
)
|
||||
|
||||
var usageLinux = `
|
||||
__ _ _ _
|
||||
/ _| __ _| | ___ ___ ___| |_| |
|
||||
| |_ / _ | |/ __/ _ \ / __| __| |
|
||||
| _| (_| | | (_| (_) | (__| |_| |
|
||||
|_| \__,_|_|\___\___/ \___|\__|_|
|
||||
|
||||
|
||||
The official CLI tool for working with Falco and its ecosystem components
|
||||
var (
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
Usage:
|
||||
falcoctl [command]
|
||||
|
||||
Available Commands:
|
||||
artifact Interact with Falco artifacts
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
driver Interact with falcosecurity driver
|
||||
help Help about any command
|
||||
index Interact with index
|
||||
registry Interact with OCI registries
|
||||
tls Generate and install TLS material for Falco
|
||||
version Print the falcoctl version information
|
||||
|
||||
Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
-h, --help help for falcoctl
|
||||
--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")
|
||||
|
||||
Use "falcoctl [command] --help" for more information about a command.
|
||||
`
|
||||
|
||||
var usageOthers = `
|
||||
__ _ _ _
|
||||
/ _| __ _| | ___ ___ ___| |_| |
|
||||
| |_ / _ | |/ __/ _ \ / __| __| |
|
||||
| _| (_| | | (_| (_) | (__| |_| |
|
||||
|_| \__,_|_|\___\___/ \___|\__|_|
|
||||
|
||||
|
||||
The official CLI tool for working with Falco and its ecosystem components
|
||||
|
||||
Usage:
|
||||
falcoctl [command]
|
||||
|
||||
Available Commands:
|
||||
artifact Interact with Falco artifacts
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
help Help about any command
|
||||
index Interact with index
|
||||
registry Interact with OCI registries
|
||||
tls Generate and install TLS material for Falco
|
||||
version Print the falcoctl version information
|
||||
|
||||
Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
-h, --help help for falcoctl
|
||||
--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")
|
||||
|
||||
Use "falcoctl [command] --help" for more information about a command.
|
||||
`
|
||||
|
||||
func getUsage() string {
|
||||
if runtime.GOOS == "linux" {
|
||||
return usageLinux
|
||||
}
|
||||
return usageOthers
|
||||
}
|
||||
|
||||
var _ = Describe("Root", func() {
|
||||
var (
|
||||
rootCmd *cobra.Command
|
||||
ctx = context.Background()
|
||||
opt = commonoptions.NewOptions()
|
||||
err error
|
||||
outputBuf = gbytes.NewBuffer()
|
||||
args []string
|
||||
)
|
||||
|
||||
JustBeforeEach(func() {
|
||||
// Each test creates a new root command, configures, and executes it.
|
||||
opt.Initialize(commonoptions.WithWriter(outputBuf))
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
rootCmd.SetOut(outputBuf)
|
||||
rootCmd.SetErr(outputBuf)
|
||||
rootCmd.SetArgs(args)
|
||||
err = cmd.Execute(rootCmd, opt)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
// Reset the output buffer.
|
||||
Expect(outputBuf.Clear()).ShouldNot(HaveOccurred())
|
||||
// Reset the arguments
|
||||
args = nil
|
||||
})
|
||||
|
||||
Describe("Without args and without flags", func() {
|
||||
BeforeEach(func() {
|
||||
// Set args to an empty slice.
|
||||
args = []string{}
|
||||
})
|
||||
|
||||
It("Should print the usage message", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(string(outputBuf.Contents())).Should(Equal(getUsage()))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("help argument", func() {
|
||||
BeforeEach(func() {
|
||||
// Set the help argument.
|
||||
args = []string{"help"}
|
||||
})
|
||||
|
||||
It("Should print the usage message", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(string(outputBuf.Contents())).Should(Equal(getUsage()))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("help flag", func() {
|
||||
BeforeEach(func() {
|
||||
// Set the help argument.
|
||||
args = []string{"--help"}
|
||||
})
|
||||
|
||||
It("Should print the usage message", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(string(outputBuf.Contents())).Should(Equal(getUsage()))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("wrong flag", func() {
|
||||
BeforeEach(func() {
|
||||
// Set the help argument.
|
||||
args = []string{"--wrong-flag"}
|
||||
})
|
||||
|
||||
It("Should error and print the error", func() {
|
||||
Expect(err).Should(HaveOccurred())
|
||||
Expect(outputBuf).Should(gbytes.Say("ERROR unknown flag: --wrong-flag"))
|
||||
var _ = Describe("root", func() {
|
||||
Describe("registry command", func() {
|
||||
Context("push subcommand", func() {
|
||||
var _ = registryPushTests
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
__ _ _ _
|
||||
/ _| __ _| | ___ ___ ___| |_| |
|
||||
| |_ / _ | |/ __/ _ \ / __| __| |
|
||||
| _| (_| | | (_| (_) | (__| |_| |
|
||||
|_| \__,_|_|\___\___/ \___|\__|_|
|
||||
|
||||
|
||||
The official CLI tool for working with Falco and its ecosystem components
|
||||
|
||||
Usage:
|
||||
falcoctl [command]
|
||||
|
||||
Available Commands:
|
||||
artifact Interact with Falco artifacts
|
||||
completion Generate the autocompletion script for the specified shell
|
||||
help Help about any command
|
||||
index Interact with index
|
||||
registry Interact with OCI registries
|
||||
tls Generate and install TLS material for Falco
|
||||
version Print the falcoctl version information
|
||||
|
||||
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)
|
||||
-h, --help help for falcoctl
|
||||
-v, --verbose Enable verbose logs (default false)
|
||||
|
||||
Use "falcoctl [command] --help" for more information about a command.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue