Compare commits
592 Commits
v0.3.0-rc1
...
main
Author | SHA1 | Date |
---|---|---|
|
9f30cce152 | |
|
090d95418e | |
|
6a25fa9f5a | |
|
ffb6e688e8 | |
|
a7b6cc6b6b | |
|
fd12beb1cd | |
|
d3c99d5dfc | |
|
6d9b57671f | |
|
54c47d097f | |
|
7a94528218 | |
|
876dcf4653 | |
|
581cbbc316 | |
|
b26c309f4d | |
|
15dd8fde15 | |
|
abdf330e55 | |
|
2b06411214 | |
|
295c633684 | |
|
bccca00d90 | |
|
54ef1cbbf1 | |
|
464a8a3cf9 | |
|
e632903105 | |
|
48a247e521 | |
|
47ad8d0e29 | |
|
3bf89dd0c9 | |
|
bb64751b9f | |
|
1765e1d326 | |
|
a6dcad415c | |
|
cead711238 | |
|
33c01252f6 | |
|
537ebcf446 | |
|
918e88464d | |
|
4bfeb0b0b5 | |
|
dfa4c583b1 | |
|
7d5aee8bb2 | |
|
0bce031350 | |
|
84d7fc852e | |
|
a420868063 | |
|
dabadcec4f | |
|
68484cd32b | |
|
3319e217d0 | |
|
99e76506f0 | |
|
4da81510d1 | |
|
1a71698e3f | |
|
94b56ea805 | |
|
ec3025e878 | |
|
6c71d1551e | |
|
d715341096 | |
|
335e365a78 | |
|
946d8f7b05 | |
|
088e2b45ec | |
|
6bcfad674e | |
|
f9734bb752 | |
|
dc7634cc28 | |
|
e71898ce53 | |
|
8a4febabe5 | |
|
cd992ad99c | |
|
730f80f471 | |
|
18c4322102 | |
|
769a3e5683 | |
|
38f2b1d703 | |
|
ba2730f048 | |
|
b6e47e6ea5 | |
|
84edb08963 | |
|
6bcda9881d | |
|
186b281e71 | |
|
2b11bc95df | |
|
c1ac24469d | |
|
bbdf8ac2f2 | |
|
310a119170 | |
|
f222cf4174 | |
|
af1ebe264b | |
|
70dda77939 | |
|
40b9e11683 | |
|
be7d5c2a55 | |
|
6f64f37774 | |
|
9dafcec9af | |
|
6f241ae803 | |
|
5bf4186068 | |
|
98c4183dd5 | |
|
9f360e12dd | |
|
a40d97f766 | |
|
cdd984dbf2 | |
|
61d9c71d96 | |
|
5776706e3c | |
|
46a22251cd | |
|
28ccc66497 | |
|
4599fe26b8 | |
|
a06072bd76 | |
|
3967730442 | |
|
076aef28c1 | |
|
cb7fd150ba | |
|
afaa782db6 | |
|
cc97e5ebaa | |
|
c29cad76b0 | |
|
2604fe2228 | |
|
1ac62368af | |
|
063491c111 | |
|
5846155f97 | |
|
131abecc4b | |
|
d35acb1b98 | |
|
2e87813a52 | |
|
eb9e67af1f | |
|
029726ec5d | |
|
ad33f00b56 | |
|
23094128e8 | |
|
3d077a8cac | |
|
503961ae91 | |
|
b041a7c6df | |
|
c3cd34939b | |
|
fb0a65a113 | |
|
f737d5f5ac | |
|
59245fdd66 | |
|
d6b1cfbff3 | |
|
f5722b9482 | |
|
7c57d3ee69 | |
|
fac6f31c61 | |
|
133952e1a6 | |
|
1f19a08b6a | |
|
c3a8850de6 | |
|
ebafd49a81 | |
|
3662765ac3 | |
|
46a47e0ef0 | |
|
3b3b535632 | |
|
fad320efff | |
|
7a741b57a9 | |
|
11b95ab75f | |
|
b00c0bd462 | |
|
4f562a2884 | |
|
7e06ca9fe2 | |
|
0a0cd490c5 | |
|
4fce8b537b | |
|
9c510af202 | |
|
db496e591d | |
|
27d569627e | |
|
ce6e1eef36 | |
|
e0d71e7da4 | |
|
7e6b79e9bd | |
|
fcf4def293 | |
|
1632a2d4f7 | |
|
7e05ec1b73 | |
|
006301cd5a | |
|
d1a3559878 | |
|
1a5aee911f | |
|
45af0b205a | |
|
73bfdb5b56 | |
|
cfe101856a | |
|
2dcedc0511 | |
|
d2a010445f | |
|
36b951fff3 | |
|
228cb06d4f | |
|
f55a5e6a06 | |
|
fc648dcf0e | |
|
8c56a3c85c | |
|
1f8d186326 | |
|
913d8b44eb | |
|
ee92cbf49b | |
|
150ef391f5 | |
|
76be634364 | |
|
096b8eaac2 | |
|
d6dd5cdf2f | |
|
11182eb1b8 | |
|
1b6596b5f2 | |
|
dbf56c0a2c | |
|
f5ebba87f8 | |
|
cd5ac2c073 | |
|
d7d61f4a55 | |
|
eb8fbb741e | |
|
9d429de90d | |
|
f82978f817 | |
|
ab0bebd8b3 | |
|
0d663fc1c6 | |
|
219f3d7955 | |
|
6c5f43833f | |
|
72e7129727 | |
|
addd0cd577 | |
|
5cb609d1f9 | |
|
048e8a1375 | |
|
dd7b7169a4 | |
|
5af9b5290a | |
|
655f0ea1d4 | |
|
6b4ddd46eb | |
|
ae5a30be6b | |
|
da932e4bc8 | |
|
8ec7466b1c | |
|
d3f4b8c902 | |
|
6b870bc5ed | |
|
04abd653ef | |
|
8fb4c37130 | |
|
f660a9bca0 | |
|
9584f0e57a | |
|
ffbdac3d85 | |
|
352ceeb019 | |
|
ff7bc7817e | |
|
0469ee9353 | |
|
30429211f5 | |
|
7982692c6d | |
|
d0523cae9d | |
|
00d41694af | |
|
bf405ec0d1 | |
|
d772260f9b | |
|
67e2bb4e28 | |
|
2524b16440 | |
|
b98eff4e7a | |
|
cce5b80106 | |
|
64f00cd2dd | |
|
ea3fe222cc | |
|
058f959467 | |
|
913962c6cf | |
|
aaf3c4d778 | |
|
27c653f9e5 | |
|
da9865e209 | |
|
d2be4ab6b8 | |
|
c30089c970 | |
|
5bb003a24b | |
|
72bc9dd259 | |
|
d26a3eded2 | |
|
10cb99c4c9 | |
|
aefeac7221 | |
|
24c44967e8 | |
|
6de519341a | |
|
38ea1a3aea | |
|
390edc9e54 | |
|
f6b76de25c | |
|
da016311a5 | |
|
c36c065856 | |
|
f7bcb4af5c | |
|
ecf420f9f7 | |
|
5a9cc2909c | |
|
e5bc056ce6 | |
|
72decef12a | |
|
7c36297edf | |
|
363047407e | |
|
3e10f205ef | |
|
f0827ad844 | |
|
c73bac5113 | |
|
358abeb70a | |
|
75a2e12512 | |
|
5d5b16f3fd | |
|
7783ee535c | |
|
d94e19ec24 | |
|
388b8b02ba | |
|
0296002808 | |
|
4e3c510ed2 | |
|
1bda127c79 | |
|
c81c1b8c8b | |
|
1c8c697051 | |
|
a46a849389 | |
|
a1e10ab84a | |
|
92188837b5 | |
|
7a2d69e2e3 | |
|
064c201df3 | |
|
e596dea6bb | |
|
8f98b0db57 | |
|
d5539af528 | |
|
1c6ab282db | |
|
c2dead34f1 | |
|
72a5d38754 | |
|
a89aa89890 | |
|
cf958dc2d9 | |
|
7977184710 | |
|
18982d3434 | |
|
7667649a2e | |
|
8ec25c9287 | |
|
1d9d19d833 | |
|
0532e71ccd | |
|
580d49884a | |
|
46a3602fcf | |
|
1673558c55 | |
|
b6b799bc96 | |
|
477f2c3786 | |
|
d73a859e62 | |
|
fb82fec00c | |
|
82c2307d1a | |
|
d1f70386ea | |
|
e6665cd8fc | |
|
379832c738 | |
|
803c08e374 | |
|
816f79cf1e | |
|
4dd3f2a631 | |
|
c95c65af00 | |
|
be3ce60a75 | |
|
2d0a52a967 | |
|
3575a81966 | |
|
1b23e3f4f7 | |
|
9163a85547 | |
|
cf773bcfc0 | |
|
b9d5516f68 | |
|
6e39c4665f | |
|
ad38eaf8f3 | |
|
15b4ff9017 | |
|
f2a5a9060e | |
|
e005be04e7 | |
|
5d5632c5f0 | |
|
85f1edd708 | |
|
c62ffbd24f | |
|
e827ba1cee | |
|
ac15f50d35 | |
|
2ed594744c | |
|
0763d7d1bb | |
|
f5a8d65b88 | |
|
8859d1e625 | |
|
4e745c1813 | |
|
1fd04e0dbf | |
|
869fcbe642 | |
|
3337612782 | |
|
43f1d3c7f9 | |
|
5d2bfa7b49 | |
|
2b5982cd0b | |
|
6ee5a87614 | |
|
16dd7b093c | |
|
de9b39bad1 | |
|
18bae1d745 | |
|
d08fead3d3 | |
|
e03c73c33d | |
|
270f36234b | |
|
0ff99bd776 | |
|
bf23a65bcc | |
|
95104b15c6 | |
|
d3dbcbd6bb | |
|
215569116f | |
|
3eb92959c3 | |
|
0a29ccc7fd | |
|
16e2d1a45d | |
|
128916b4b4 | |
|
4f9b935aea | |
|
919a307bc2 | |
|
da51356ef5 | |
|
ef15eb2e4a | |
|
ff244b6d34 | |
|
e4ced876ba | |
|
3a8ac8e708 | |
|
bb3ee1d550 | |
|
4ec8660ed2 | |
|
139527dad6 | |
|
e87c302591 | |
|
1e299e6b48 | |
|
8717a9eeff | |
|
405e341449 | |
|
79a1b7f9f6 | |
|
478c7c6630 | |
|
d86ed68ad1 | |
|
889a6346ea | |
|
28dd5f6307 | |
|
a5f0ecd1cf | |
|
bf2aec883d | |
|
e6d75ca083 | |
|
1f079780d7 | |
|
d63deb6dcb | |
|
606672e3a3 | |
|
c14e50cc46 | |
|
c26ed79dd3 | |
|
dce407905a | |
|
72999a66dd | |
|
ca1be8ab1e | |
|
01986a74cf | |
|
58b66f5ec7 | |
|
c89a6a82eb | |
|
13434184bd | |
|
5cb7f759ed | |
|
2e95198423 | |
|
e6658f0e37 | |
|
d72536cb40 | |
|
cda38ca875 | |
|
36aa56c0a2 | |
|
ce7d0b231c | |
|
50654041ae | |
|
dc5eea97ae | |
|
f9062a7665 | |
|
c8d7485b3c | |
|
5adab46b99 | |
|
bf63125778 | |
|
0bdbd1b19d | |
|
2b21ae9f04 | |
|
913bae3010 | |
|
af100a3da8 | |
|
256e8bf4b9 | |
|
9783fd3690 | |
|
a50b7b321d | |
|
2b56ef422e | |
|
1a013c60ad | |
|
609a8a255d | |
|
d5893e1917 | |
|
7b8ef7abaa | |
|
c19ca32447 | |
|
2b217999ec | |
|
62d9da10bd | |
|
d594814c72 | |
|
313f047209 | |
|
15aa18090c | |
|
a39b5ac7c9 | |
|
610e851189 | |
|
077b671bc5 | |
|
0ae01ef387 | |
|
d762a9c6f7 | |
|
f80f3fd180 | |
|
0643ed3044 | |
|
42c069ef77 | |
|
cb049cb890 | |
|
4228e71a06 | |
|
1ad11dba9a | |
|
6d34d4045e | |
|
5cfe0575a3 | |
|
fbcb975f55 | |
|
f656760e78 | |
|
cdccbeb83e | |
|
ce1f9a8968 | |
|
f547c49766 | |
|
262029791e | |
|
b5363f6908 | |
|
bea50efc21 | |
|
ae7f565b03 | |
|
0490543b61 | |
|
05eb3df8eb | |
|
49e73bbec7 | |
|
da90565512 | |
|
b6678c2e18 | |
|
c8359f3348 | |
|
22eda6a284 | |
|
7a66862212 | |
|
9fa598a6bb | |
|
5507e3b6b2 | |
|
774927a48f | |
|
55ed9ef189 | |
|
7f0a8bf880 | |
|
1d66acc8ad | |
|
8f216097d8 | |
|
7227ec4afd | |
|
0d24ae0aeb | |
|
06cb770ec2 | |
|
ada1c20278 | |
|
95fdff8cba | |
|
6c2db677ed | |
|
39fcf05216 | |
|
b69f417163 | |
|
2f5494232a | |
|
460e28849c | |
|
7f1e8825a6 | |
|
3c017edef8 | |
|
730b8265f5 | |
|
22af4dd140 | |
|
0302b3c20b | |
|
3a3bdfbf4a | |
|
b9b4987ee0 | |
|
5e6ce83ded | |
|
8440ac99ae | |
|
8a3cb3cb78 | |
|
316b27bd77 | |
|
f2ec423544 | |
|
e1d594c2bd | |
|
7b3e14a01b | |
|
c748f32913 | |
|
80c6e4cf9f | |
|
472ef3bf0f | |
|
bccc34d211 | |
|
3686042ab8 | |
|
c574b6c5ad | |
|
363a4dca76 | |
|
7de8713952 | |
|
5209a11004 | |
|
dd318af558 | |
|
b67c0ee012 | |
|
07bcb837a4 | |
|
6a9fe36bf3 | |
|
4105621481 | |
|
cc49e6c6e3 | |
|
fa667771c9 | |
|
7a530956dc | |
|
63b6bf3f4a | |
|
a397401cb4 | |
|
90ba8812bf | |
|
59d2110df8 | |
|
afd6093764 | |
|
2eb13de0ac | |
|
084870dcd2 | |
|
97d94c9565 | |
|
c2e37c097d | |
|
ff362b2560 | |
|
16bc22303c | |
|
77d066d0a3 | |
|
013ba9772e | |
|
ad50f99f06 | |
|
56e5f4dc41 | |
|
791f5c5d7c | |
|
54c4c2d28d | |
|
7553c7bb93 | |
|
46005b471c | |
|
ffea6d493a | |
|
ef4ed0ddec | |
|
798aa4d2d1 | |
|
1c8f0bda16 | |
|
b29de40410 | |
|
58b684999b | |
|
bd54ed766f | |
|
534535cfb1 | |
|
46a189994d | |
|
0b87842102 | |
|
b0205e33ef | |
|
2adfff69b2 | |
|
b0f1f2705e | |
|
554c0dc55d | |
|
6cb916907c | |
|
a6e1e7f6ff | |
|
3b2e5c94f8 | |
|
502b68d4d0 | |
|
b67213f424 | |
|
23588f1d01 | |
|
e94e50ae32 | |
|
e9cb299ec6 | |
|
5b6c55ab8c | |
|
ddffd44393 | |
|
7ae72abd54 | |
|
ba0ab2832d | |
|
681ddd6f25 | |
|
0e0606f436 | |
|
e314beb057 | |
|
8e6cf0da0a | |
|
55e54ef8af | |
|
9aa559c974 | |
|
acfabc9b48 | |
|
49aeb3d75f | |
|
41f7909e17 | |
|
f539f7e9bb | |
|
f152dae87a | |
|
1048fb5709 | |
|
78fe87443c | |
|
b85748de7f | |
|
de696a4723 | |
|
5b9befd7c4 | |
|
accf7e3f6c | |
|
d685794f07 | |
|
bc12026e63 | |
|
b079c00e9c | |
|
6b4a1a5944 | |
|
2ad9cd3fba | |
|
d1bec56e62 | |
|
5af39d3758 | |
|
ffdbfca3e9 | |
|
b79d988145 | |
|
80f0cf6336 | |
|
7436ad229f | |
|
f0b8791de1 | |
|
229a607d86 | |
|
e11b6c9876 | |
|
c83fa607fd | |
|
bb7f7198b2 | |
|
5e0b8fad40 | |
|
3c60bf4940 | |
|
a5f62224aa | |
|
38439929f1 | |
|
04827bd40f | |
|
1a1abaa2db | |
|
e07f5209b3 | |
|
fc1e53bdc8 | |
|
2a5aebea7e | |
|
15a613490e | |
|
1b93b6710f | |
|
b3fb99b0d5 | |
|
71eb83f74d | |
|
630f9a3278 | |
|
6e376a27ca | |
|
8e5986948d | |
|
576ecd520e | |
|
f550f19fb3 | |
|
360faa0ae5 | |
|
16b9ebad78 | |
|
05f06caab9 | |
|
a0d839a345 | |
|
2c5cb2d5ee | |
|
3d61babd31 | |
|
cc92d41c81 | |
|
79b7a9a4f8 | |
|
811be9b700 | |
|
e4dbe68aa7 | |
|
6628ba6caa | |
|
8370fb79de | |
|
dbbd849708 | |
|
d3c8ac7ce9 | |
|
8560532f7d | |
|
dd096bdffb | |
|
d64e5024be | |
|
933edfdcee | |
|
71daaaa505 | |
|
d90712119e | |
|
5f72ed1867 | |
|
49e5a2a37c | |
|
e1104a6577 | |
|
cc94eec9a3 | |
|
35d74bf2f0 | |
|
df5dbd54f7 | |
|
0da5193736 | |
|
8f10092f08 | |
|
6e0d1db9c5 |
|
@ -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/falco/blob/dev/CONTRIBUTING.md) file in the Falco repository.
|
||||
1. If this is your first time, please read our contributor guidelines in the [CONTRIBUTING.md](https://github.com/falcosecurity/.github/blob/main/CONTRIBUTING.md) file in the Falco `.github` repository.
|
||||
2. Please label this pull request according to what type of issue you are addressing.
|
||||
3. Please add a release note!
|
||||
4. If the PR is unfinished while opening it specify a wip in the title before the actual title, for example, "wip: my awesome feature"
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
gomod:
|
||||
update-types:
|
||||
- "patch"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
actions:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
|
@ -1,22 +1,12 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: '28 11 * * 2'
|
||||
|
||||
|
@ -32,40 +22,16 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
language:
|
||||
- go
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@d23060145bc9131d50558d5d4185494a20208101 # v2.2.8
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
uses: github/codeql-action/autobuild@d23060145bc9131d50558d5d4185494a20208101 # v2.2.8
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@d23060145bc9131d50558d5d4185494a20208101 # v2.2.8
|
||||
|
|
|
@ -1,49 +1,103 @@
|
|||
name: docker-image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_call:
|
||||
inputs:
|
||||
release:
|
||||
required: true
|
||||
type: string
|
||||
commit:
|
||||
required: true
|
||||
type: string
|
||||
build_date:
|
||||
required: true
|
||||
type: string
|
||||
sign:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
outputs:
|
||||
digest:
|
||||
description: The digest of the pushed image.
|
||||
value: ${{ jobs.docker-image.outputs.digest }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
docker-image:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
image: ${{ steps.build-and-push.outputs.image }}
|
||||
digest: ${{ steps.build-and-push.outputs.digest }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v3.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 #v2.1.0
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: Buildx
|
||||
uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 #v2.2.1
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2.1.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_SECRET }}
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::292999226676:role/github_actions-falcoctl-ecr
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr-public
|
||||
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
|
||||
with:
|
||||
registry-type: public
|
||||
|
||||
- name: Docker Meta
|
||||
id: meta_falcoctl
|
||||
uses: docker/metadata-action@57396166ad8aefe6098280995947635806a0e6ea #v4.1.1
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
docker.io/falcosecurity/falcoctl
|
||||
public.ecr.aws/falcosecurity/falcoctl
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{ version }}
|
||||
type=semver,pattern={{ major }}
|
||||
type=semver,pattern={{ major }}.{{ minor }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@c56af957549030174b10d6867f20e78cfd7debc5 #v3.2.0
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta_falcoctl.outputs.tags }}
|
||||
file: ./build/Dockerfile
|
||||
build-args: |
|
||||
RELEASE=${{ inputs.release }}
|
||||
COMMIT=${{ inputs.commit }}
|
||||
BUILD_DATE=${{ inputs.build_date }}
|
||||
|
||||
- name: Install Cosign
|
||||
if: ${{ inputs.sign }}
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
|
||||
- name: Sign the images with GitHub OIDC Token
|
||||
if: ${{ inputs.sign }}
|
||||
env:
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
TAGS: ${{ steps.meta_falcoctl.outputs.tags }}
|
||||
COSIGN_YES: "true"
|
||||
run: echo "${TAGS}" | xargs -I {} cosign sign {}@${DIGEST}
|
||||
|
|
|
@ -2,7 +2,8 @@ name: Integration Pipeline
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
@ -10,21 +11,27 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, darwin, windows]
|
||||
goarch: [arm64, amd64]
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- arm64
|
||||
- amd64
|
||||
exclude:
|
||||
- goarch: arm64
|
||||
goos: windows
|
||||
steps:
|
||||
- name: Checkout commit
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v3.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@84cbf8094393cdc5fe1fe1671ff2647332956b1a #v3.2.1
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
|
||||
- name: Build Falcoctl
|
||||
run: >
|
||||
|
@ -40,30 +47,120 @@ jobs:
|
|||
tar -czvf falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz falcoctl LICENSE
|
||||
|
||||
- name: Upload falcoctl artifacts
|
||||
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 #v3.1.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: ./falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload falcoctl archives
|
||||
uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 #v3.1.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
|
||||
path: ./falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
docker-configure:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release: ${{ steps.vars.outputs.release }}
|
||||
commit: ${{ steps.vars.outputs.commit }}
|
||||
build_date: ${{ steps.vars.outputs.build_date }}
|
||||
steps:
|
||||
- name: Set version fields
|
||||
id: vars
|
||||
run: |
|
||||
echo "release=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
echo "commit=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
|
||||
docker-image:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
needs: docker-configure
|
||||
uses: ./.github/workflows/docker-image.yaml
|
||||
secrets: inherit
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
with:
|
||||
release: ${{ needs.docker-configure.outputs.release }}
|
||||
commit: ${{ needs.docker-configure.outputs.commit }}
|
||||
build_date: ${{ needs.docker-configure.outputs.build_date }}
|
||||
sign: true
|
||||
|
||||
provenance-for-images-docker:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
needs: [docker-configure, docker-image]
|
||||
permissions:
|
||||
actions: read # for detecting the Github Actions environment.
|
||||
id-token: write # for creating OIDC tokens for signing.
|
||||
packages: write # for uploading attestations.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: docker.io/falcosecurity/falcoctl
|
||||
# The image digest is used to prevent TOCTOU issues.
|
||||
# This is an output of the docker/build-push-action
|
||||
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
|
||||
digest: ${{ needs.docker-image.outputs.digest }}
|
||||
secrets:
|
||||
registry-username: ${{ secrets.DOCKERHUB_USER }}
|
||||
registry-password: ${{ secrets.DOCKERHUB_SECRET }}
|
||||
|
||||
login-to-amazon-ecr:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::292999226676:role/github_actions-falcoctl-ecr
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr-public
|
||||
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
|
||||
with:
|
||||
registry-type: public
|
||||
mask-password: 'false'
|
||||
outputs:
|
||||
registry: ${{ steps.login-ecr-public.outputs.registry }}
|
||||
docker_username: ${{ steps.login-ecr-public.outputs.docker_username_public_ecr_aws }}
|
||||
docker_password: ${{ steps.login-ecr-public.outputs.docker_password_public_ecr_aws }}
|
||||
|
||||
provenance-for-images-aws-ecr:
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
needs: [docker-configure, docker-image, login-to-amazon-ecr]
|
||||
permissions:
|
||||
actions: read # for detecting the Github Actions environment.
|
||||
id-token: write # for creating OIDC tokens for signing.
|
||||
packages: write # for uploading attestations.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: public.ecr.aws/falcosecurity/falcoctl
|
||||
# The image digest is used to prevent TOCTOU issues.
|
||||
# This is an output of the docker/build-push-action
|
||||
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
|
||||
digest: ${{ needs.docker-image.outputs.digest }}
|
||||
secrets:
|
||||
registry-username: ${{ needs.login-to-amazon-ecr.outputs.docker_username }}
|
||||
registry-password: ${{ needs.login-to-amazon-ecr.outputs.docker_password }}
|
||||
|
||||
test:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout commit
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v3.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@84cbf8094393cdc5fe1fe1671ff2647332956b1a #v3.2.1
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
|
||||
- name: Run tests
|
||||
run: go test -cover ./...
|
||||
|
|
|
@ -8,22 +8,25 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: "^1.24.3"
|
||||
go-version-file: "go.mod"
|
||||
check-latest: true
|
||||
cache: "false"
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3.2.0
|
||||
uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2
|
||||
with:
|
||||
only-new-issues: true
|
||||
version: v1.48.0
|
||||
version: v1.64.7
|
||||
args: --timeout=900s
|
||||
|
||||
gomodtidy:
|
||||
|
@ -32,16 +35,17 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: "${{ github.event.pull_request.head.sha }}"
|
||||
repository: ${{github.event.pull_request.head.repo.full_name}}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version-file: "go.mod"
|
||||
check-latest: true
|
||||
|
||||
- name: Execute go mod tidy and check the outcome
|
||||
working-directory: ./
|
||||
|
|
|
@ -5,16 +5,16 @@ on:
|
|||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
# needed to upload archives as Github Releases.
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write # To add assets to a release.
|
||||
outputs:
|
||||
hashes: ${{ steps.hash.outputs.hashes }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b #v3.0.2
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -22,18 +22,157 @@ jobs:
|
|||
run: git fetch --force --tags
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@84cbf8094393cdc5fe1fe1671ff2647332956b1a #v3.2.1
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@68acf3b1adf004ac9c2f0a4259e85c5f66e99bef #v3.0.0
|
||||
id: run-goreleaser
|
||||
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: v1.10.3
|
||||
args: release --rm-dist
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate subject
|
||||
id: hash
|
||||
env:
|
||||
ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path')
|
||||
echo "hashes=$(cat $checksum_file | base64 -w0)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
provenance-for-binaries:
|
||||
needs: [goreleaser]
|
||||
permissions:
|
||||
actions: read # To read the workflow path.
|
||||
id-token: write # To sign the provenance.
|
||||
contents: write # To add assets to a release.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
|
||||
with:
|
||||
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
|
||||
upload-assets: true # upload to a new release
|
||||
|
||||
verification:
|
||||
needs: [goreleaser, provenance-for-binaries]
|
||||
runs-on: ubuntu-latest
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Install the verifier
|
||||
uses: slsa-framework/slsa-verifier/actions/installer@v2.7.1
|
||||
|
||||
- name: Download assets
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PROVENANCE: "${{ needs.provenance-for-binaries.outputs.provenance-name }}"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.tar.gz"
|
||||
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.zip"
|
||||
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$PROVENANCE"
|
||||
|
||||
- name: Verify assets
|
||||
env:
|
||||
CHECKSUMS: ${{ needs.goreleaser.outputs.hashes }}
|
||||
PROVENANCE: "${{ needs.provenance-for-binaries.outputs.provenance-name }}"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
checksums=$(echo "$CHECKSUMS" | base64 -d)
|
||||
while read -r line; do
|
||||
fn=$(echo $line | cut -d ' ' -f2)
|
||||
echo "Verifying $fn"
|
||||
slsa-verifier verify-artifact --provenance-path "$PROVENANCE" \
|
||||
--source-uri "github.com/$GITHUB_REPOSITORY" \
|
||||
--source-tag "$GITHUB_REF_NAME" \
|
||||
"$fn"
|
||||
done <<<"$checksums"
|
||||
|
||||
docker-configure:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
release: ${{ steps.vars.outputs.release }}
|
||||
commit: ${{ steps.vars.outputs.commit }}
|
||||
build_date: ${{ steps.vars.outputs.build_date }}
|
||||
steps:
|
||||
- name: Set version fields
|
||||
id: vars
|
||||
run: |
|
||||
echo "release=$(echo $GITHUB_REF | cut -d / -f 3 | sed 's/^v//')" >> $GITHUB_OUTPUT
|
||||
echo "commit=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
|
||||
docker-image:
|
||||
needs: docker-configure
|
||||
uses: ./.github/workflows/docker-image.yaml
|
||||
secrets: inherit
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||
with:
|
||||
release: ${{ needs.docker-configure.outputs.release }}
|
||||
commit: ${{ needs.docker-configure.outputs.commit }}
|
||||
build_date: ${{ needs.docker-configure.outputs.build_date }}
|
||||
sign: true
|
||||
|
||||
provenance-for-images-docker:
|
||||
needs: [docker-configure, docker-image]
|
||||
permissions:
|
||||
actions: read # for detecting the Github Actions environment.
|
||||
id-token: write # for creating OIDC tokens for signing.
|
||||
packages: write # for uploading attestations.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: docker.io/falcosecurity/falcoctl
|
||||
# The image digest is used to prevent TOCTOU issues.
|
||||
# This is an output of the docker/build-push-action
|
||||
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
|
||||
digest: ${{ needs.docker-image.outputs.digest }}
|
||||
secrets:
|
||||
registry-username: ${{ secrets.DOCKERHUB_USER }}
|
||||
registry-password: ${{ secrets.DOCKERHUB_SECRET }}
|
||||
|
||||
login-to-amazon-ecr:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::292999226676:role/github_actions-falcoctl-ecr
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr-public
|
||||
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
|
||||
with:
|
||||
registry-type: public
|
||||
mask-password: 'false'
|
||||
outputs:
|
||||
registry: ${{ steps.login-ecr-public.outputs.registry }}
|
||||
docker_username: ${{ steps.login-ecr-public.outputs.docker_username_public_ecr_aws }}
|
||||
docker_password: ${{ steps.login-ecr-public.outputs.docker_password_public_ecr_aws }}
|
||||
|
||||
provenance-for-images-aws-ecr:
|
||||
needs: [docker-configure, docker-image, login-to-amazon-ecr]
|
||||
permissions:
|
||||
actions: read # for detecting the Github Actions environment.
|
||||
id-token: write # for creating OIDC tokens for signing.
|
||||
packages: write # for uploading attestations.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||
with:
|
||||
image: public.ecr.aws/falcosecurity/falcoctl
|
||||
# The image digest is used to prevent TOCTOU issues.
|
||||
# This is an output of the docker/build-push-action
|
||||
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
|
||||
digest: ${{ needs.docker-image.outputs.digest }}
|
||||
secrets:
|
||||
registry-username: ${{ needs.login-to-amazon-ecr.outputs.docker_username }}
|
||||
registry-password: ${{ needs.login-to-amazon-ecr.outputs.docker_password }}
|
||||
|
|
|
@ -11,7 +11,8 @@ linters-settings:
|
|||
const:
|
||||
AUTHORS: The Falco Authors
|
||||
template: |-
|
||||
Copyright {{ YEAR }} {{ AUTHORS }}
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
Copyright (C) {{ YEAR }} {{ AUTHORS }}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -43,17 +44,11 @@ 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
|
||||
|
@ -65,13 +60,12 @@ linters:
|
|||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- copyloopvar
|
||||
# - funlen
|
||||
# - gochecknoglobals
|
||||
# - gochecknoinits
|
||||
|
@ -142,4 +136,4 @@ issues:
|
|||
# Exclude the following linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- whitespace
|
||||
- gosec
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
version: 2
|
||||
|
||||
project_name: falcoctl
|
||||
before:
|
||||
hooks:
|
||||
|
@ -16,11 +18,13 @@ builds:
|
|||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
- goos: windows
|
||||
goarch: 386
|
||||
|
||||
ldflags: |
|
||||
-X github.com/falcosecurity/falcoctl/pkg/version.buildDate={{ .Date }}
|
||||
-X github.com/falcosecurity/falcoctl/pkg/version.gitCommit={{ .Commit }}
|
||||
-X github.com/falcosecurity/falcoctl/pkg/version.semVersion={{ if .IsSnapshot }}{{ .Commit }}{{ else }}{{ .Version }}{{ end }}
|
||||
-X github.com/falcosecurity/falcoctl/cmd/version.buildDate={{ .Date }}
|
||||
-X github.com/falcosecurity/falcoctl/cmd/version.gitCommit={{ .Commit }}
|
||||
-X github.com/falcosecurity/falcoctl/cmd/version.semVersion={{ if .IsSnapshot }}{{ .Commit }}{{ else }}{{ .Version }}{{ end }}
|
||||
-s
|
||||
-w
|
||||
main: .
|
||||
|
@ -38,9 +42,11 @@ snapshot:
|
|||
name_template: "{{ .ShortCommit }}"
|
||||
|
||||
release:
|
||||
github:
|
||||
prerelease: auto
|
||||
mode: replace
|
||||
|
||||
changelog:
|
||||
use: github
|
||||
use: github-native
|
||||
|
||||
git:
|
||||
tag_sort: -version:creatordate
|
||||
|
|
30
Makefile
30
Makefile
|
@ -10,9 +10,9 @@ GO ?= go
|
|||
DOCKER ?= docker
|
||||
|
||||
# version settings
|
||||
RELEASE?=$(shell git rev-parse --short HEAD)
|
||||
COMMIT?=$(shell git rev-parse --short HEAD)
|
||||
BUILD_DATE?=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
RELEASE?=$(shell git rev-parse HEAD)
|
||||
COMMIT?=$(shell git rev-parse HEAD)
|
||||
BUILD_DATE?=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
PROJECT?=github.com/falcosecurity/falcoctl
|
||||
|
||||
# todo(leogr): re-enable race when CLI tests can run with race enabled
|
||||
|
@ -21,9 +21,9 @@ TEST_FLAGS ?= -v -cover# -race
|
|||
.PHONY: falcoctl
|
||||
falcoctl:
|
||||
$(GO) build -ldflags \
|
||||
"-X ${PROJECT}/pkg/version.semVersion=${RELEASE} \
|
||||
-X ${PROJECT}/pkg/version.gitCommit=${COMMIT} \
|
||||
-X ${PROJECT}/pkg/version.buildDate=${BUILD_DATE}" \
|
||||
"-X '${PROJECT}/cmd/version.semVersion=${RELEASE}' \
|
||||
-X '${PROJECT}/cmd/version.gitCommit=${COMMIT}' \
|
||||
-X '${PROJECT}/cmd/version.buildDate=${BUILD_DATE}'" \
|
||||
-o falcoctl .
|
||||
|
||||
.PHONY: test
|
||||
|
@ -32,15 +32,17 @@ test:
|
|||
$(GO) test ${TEST_FLAGS} ./...
|
||||
|
||||
# Install gci if not available
|
||||
.PHONY: gci
|
||||
gci:
|
||||
ifeq (, $(shell which gci))
|
||||
@go install github.com/daixiang0/gci@v0.9.0
|
||||
@go install github.com/daixiang0/gci@v0.11.1
|
||||
GCI=$(GOBIN)/gci
|
||||
else
|
||||
GCI=$(shell which gci)
|
||||
endif
|
||||
|
||||
# Install addlicense if not available
|
||||
.PHONY: addlicense
|
||||
addlicense:
|
||||
ifeq (, $(shell which addlicense))
|
||||
@go install github.com/google/addlicense@v1.0.0
|
||||
|
@ -50,16 +52,18 @@ ADDLICENSE=$(shell which addlicense)
|
|||
endif
|
||||
|
||||
# Run go fmt against code and add the licence header
|
||||
.PHONY: fmt
|
||||
fmt: gci addlicense
|
||||
go mod tidy
|
||||
go fmt ./...
|
||||
find . -type f -name '*.go' -a -exec $(GCI) write -s standard -s default -s "prefix(github.com/falcosecurity/falcoctl)" {} \;
|
||||
find . -type f -name '*.go' -exec $(ADDLICENSE) -l apache -c "The Falco Authors" -y "$(shell date +%Y)" {} \;
|
||||
find . -type f -name '*.go' -exec $(ADDLICENSE) -l apache -s -c "The Falco Authors" -y "$(shell date +%Y)" {} \;
|
||||
|
||||
# Install golangci-lint if not available
|
||||
.PHONY: golangci-lint
|
||||
golangci-lint:
|
||||
ifeq (, $(shell which golangci-lint))
|
||||
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1
|
||||
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2
|
||||
GOLANGCILINT=$(GOBIN)/golangci-lint
|
||||
else
|
||||
GOLANGCILINT=$(shell which golangci-lint)
|
||||
|
@ -67,8 +71,14 @@ endif
|
|||
|
||||
# It works when called in a branch different than main.
|
||||
# "--new-from-rev REV Show only new issues created after git revision REV"
|
||||
.PHONY: lint
|
||||
lint: golangci-lint
|
||||
$(GOLANGCILINT) run --new-from-rev main
|
||||
|
||||
.PHONY: docker
|
||||
docker:
|
||||
$(DOCKER) build -f ./build/Dockerfile .
|
||||
$(DOCKER) build -f ./build/Dockerfile . --build-arg RELEASE=${RELEASE} --build-arg COMMIT=${COMMIT} --build-arg BUILD_DATE=${BUILD_DATE}
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@rm falcoctl || true
|
||||
|
|
1
OWNERS
1
OWNERS
|
@ -4,7 +4,6 @@ approvers:
|
|||
- maxgio92
|
||||
- fededp
|
||||
- cpanato
|
||||
reviewers:
|
||||
- alacuku
|
||||
- loresuso
|
||||
emeritus_approvers:
|
||||
|
|
263
README.md
263
README.md
|
@ -1,27 +1,8 @@
|
|||
<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
|
||||
|
||||
> A CLI tool to work with Falco, and perform useful tasks.
|
||||
[](https://github.com/falcosecurity/evolution/blob/main/REPOSITORIES.md#core-scope) [](https://github.com/falcosecurity/evolution/blob/main/REPOSITORIES.md#stable) [](./LICENSE)
|
||||
|
||||
## 📣 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.
|
||||
|
||||
## ⚠️ Current status
|
||||
|
||||
👷♀️ **Under active development** 👷♂️
|
||||
|
||||
So `falcoctl` was born out of a need to encapsulate common logic for the project.
|
||||
Right now there are a lot of scripts, in many languages, and even container images that perform ad-hoc tasks.
|
||||
We hope to make `falcoctl` the source of truth for these tasks or chores and give operators a first class experience.
|
||||
|
||||
Recently, we started an effort to revamp this project and make it a first-class citizen in the Falco ecosystem. As the first step, we are currently working on implementing a [proposal](proposals/20220916-rules-and-plugin-distribution.md) to allow our users to consume and install distributed plugins and rules files easily.
|
||||
The official CLI tool for working with [Falco](https://github.com/falcosecurity/falco) and its [ecosystem components](https://falco.org/docs/#what-are-the-ecosystem-projects-that-can-interact-with-falco).
|
||||
|
||||
## Installation
|
||||
### Install falcoctl manually
|
||||
|
@ -29,26 +10,37 @@ You can download and install *falcoctl* manually following the appropriate instr
|
|||
#### Linux
|
||||
##### AMD64
|
||||
```bash
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v0.2.0-rc1/falcoctl_0.2.0-rc1_linux_amd64.tar.gz" | tar -xz
|
||||
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_linux_amd64.tar.gz" | tar -xz
|
||||
sudo install -o root -g root -m 0755 falcoctl /usr/local/bin/falcoctl
|
||||
```
|
||||
##### ARM64
|
||||
```bash
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v0.2.0-rc1/falcoctl_0.2.0-rc1_linux_arm64.tar.gz" | tar -xz
|
||||
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_linux_arm64.tar.gz" | tar -xz
|
||||
sudo install -o root -g root -m 0755 falcoctl /usr/local/bin/falcoctl
|
||||
```
|
||||
> NOTE: Make sure */usr/local/bin* is in your PATH environment variable.
|
||||
|
||||
#### MacOS
|
||||
The easiest way to install on MacOS is via `Homebrew`:
|
||||
```bash
|
||||
brew install falcoctl
|
||||
```
|
||||
|
||||
Alternatively, you can download directly from the source:
|
||||
|
||||
##### Intel
|
||||
```bash
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v0.2.0-rc1/falcoctl_0.2.0-rc1_darwin_amd64.tar.gz" | tar -xz
|
||||
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_darwin_amd64.tar.gz" | tar -xz
|
||||
chmod +x falcoctl
|
||||
sudo mv falcoctl /usr/local/bin/falcoctl
|
||||
```
|
||||
##### Apple Silicon
|
||||
```bash
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v0.2.0-rc1/falcoctl_0.2.0-rc1_darwin_arm64.tar.gz" | tar -xz
|
||||
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
|
||||
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_darwin_arm64.tar.gz" | tar -xz
|
||||
chmod +x falcoctl
|
||||
sudo mv falcoctl /usr/local/bin/falcoctl
|
||||
```
|
||||
|
@ -72,12 +64,12 @@ This tutorial aims at presenting how to install a Falco artifact. The next few s
|
|||
|
||||
First thing, we need to add a new `index` to *falcoctl*:
|
||||
```bash
|
||||
falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
$ falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
```
|
||||
We just downloaded the metadata of the **artifacts** hosted and distributed by the **falcosecurity** organization and made them available to the *falcoctl* tool.
|
||||
Now let's check that the `index` file is in place by running:
|
||||
```
|
||||
falcoctl index list
|
||||
$ falcoctl index list
|
||||
```
|
||||
We should get an output similar to this one:
|
||||
```
|
||||
|
@ -86,14 +78,14 @@ falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml 2022-10-
|
|||
```
|
||||
Now let's search all the artifacts related to *cloudtrail*:
|
||||
```
|
||||
❯ falcoctl artifact search cloudtrail
|
||||
$ falcoctl artifact search cloudtrail
|
||||
INDEX ARTIFACT TYPE REGISTRY REPOSITORY
|
||||
falcosecurity cloudtrail plugin ghcr.io falcosecurity/plugins/plugin/cloudtrail
|
||||
falcosecurity cloudtrail-rules rulesfile ghcr.io falcosecurity/plugins/ruleset/cloudtrail
|
||||
```
|
||||
Lets install the *cloudtrail plugin*:
|
||||
```
|
||||
❯ falcoctl artifact install cloudtrail --plugins-dir=./
|
||||
$ falcoctl artifact install cloudtrail --plugins-dir=./
|
||||
INFO Reading all configured index files from "/home/aldo/.config/falcoctl/indexes.yaml"
|
||||
INFO Preparing to pull "ghcr.io/falcosecurity/plugins/plugin/cloudtrail:latest"
|
||||
INFO Remote registry "ghcr.io" implements docker registry API V2
|
||||
|
@ -104,7 +96,7 @@ Lets install the *cloudtrail plugin*:
|
|||
```
|
||||
Install the *cloudtrail-rules* rulesfile:
|
||||
```
|
||||
❯ ./falcoctl artifact install cloudtrail-rules --rulesfiles-dir=./
|
||||
$ ./falcoctl artifact install cloudtrail-rules --rulesfiles-dir=./
|
||||
INFO Reading all configured index files from "/home/aldo/.config/falcoctl/indexes.yaml"
|
||||
INFO Preparing to pull "ghcr.io/falcosecurity/plugins/ruleset/cloudtrail:latest"
|
||||
INFO Remote registry "ghcr.io" implements docker registry API V2
|
||||
|
@ -115,6 +107,64 @@ Install the *cloudtrail-rules* rulesfile:
|
|||
```
|
||||
|
||||
We should have now two new files in the current directory: `aws_cloudtrail_rules.yaml` and `libcloudtrail.so`.
|
||||
|
||||
# Falcoctl Configuration Files
|
||||
|
||||
## `/etc/falcoctl/falcoctl.yaml`
|
||||
|
||||
The `falco configuration file` is a yaml file that contains some metadata about the `falcoctl` behaviour.
|
||||
It contains the list of the indexes where the artifacts are listed, how often and which artifacts needed to be updated periodically.
|
||||
The default configuration is stored in `/etc/falcoctl/falcoctl.yaml`.
|
||||
This is an example of a falcoctl configuration file:
|
||||
|
||||
``` yaml
|
||||
artifact:
|
||||
follow:
|
||||
every: 6h0m0s
|
||||
falcoVersions: http://localhost:8765/versions
|
||||
refs:
|
||||
- falco-rules:0
|
||||
- my-rules:1
|
||||
install:
|
||||
refs:
|
||||
- cloudtrail-rules:latest
|
||||
- cloudtrail:latest
|
||||
rulesfilesdir: /tmp/rules
|
||||
pluginsdir: /tmp/plugins
|
||||
indexes:
|
||||
- name: falcosecurity
|
||||
url: https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
- name: my-index
|
||||
url: https://example.com/falcoctl/index.yaml
|
||||
registry:
|
||||
auth:
|
||||
basic:
|
||||
- password: password
|
||||
registry: myregistry.example.com:5000
|
||||
user: user
|
||||
oauth:
|
||||
- registry: myregistry.example.com:5001
|
||||
clientsecret: "999999"
|
||||
clientid: "000000"
|
||||
tokenurl: http://myregistry.example.com:9096/token
|
||||
gcp:
|
||||
- registry: europe-docker.pkg.dev
|
||||
```
|
||||
|
||||
## `~/.config/falcoctl/`
|
||||
|
||||
The `~/.config/falcoctl/` directory contains:
|
||||
- *cache objects*
|
||||
- *OAuth2 client credentials*
|
||||
|
||||
### `~/.config/falcoctl/indexes.yaml`
|
||||
|
||||
This file is used for cache purposes and contains the *index refs* added by the command `falcoctl index add [name] [ref]`. The *index ref* is enriched with two timestamps to track when it was added and the last time is was updated. Once the *index ref* is added, `falcoctl` will download the real index in the `~/.config/falcoctl/indexes/` directory. Moreover, every time the index is fetched, the `updated_timestamp` is updated.
|
||||
|
||||
### `~/.config/falcoctl/clientcredentials.json`
|
||||
|
||||
The command `falcoctl registry auth oauth` will add the `clientcredentials.json` file to the `~/.config/falcoctl/` directory. That file will contain all the needed information for the OAuth2 authetication.
|
||||
|
||||
# Falcoctl Commands
|
||||
|
||||
## Falcoctl index
|
||||
|
@ -156,28 +206,47 @@ 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. 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.
|
||||
New indexes are configured to be used by the *falcoctl* tool by adding them through the `index add` command. There are no limits to the number of indexes that can be added to the *falcoctl* tool. When adding a new index the tool adds a new entry in a file called **indexes.yaml** and downloads the *index* file in `~/.config/falcoctl`. The same folder is used to store the **indexes.yaml** file, too.
|
||||
The following command adds a new index named *falcosecurity*:
|
||||
```bash
|
||||
falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
$ falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
```
|
||||
|
||||
The following command adds the same index *falcosecurity*, but explicitly sets the storage backend to `https`:
|
||||
```bash
|
||||
$ falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml https
|
||||
```
|
||||
#### falcoctl index list
|
||||
Using the `index list` command you can check the configured `indexes` in your local system:
|
||||
```bash
|
||||
❯ falcoctl index list
|
||||
$ falcoctl index list
|
||||
NAME URL ADDED UPDATED
|
||||
falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml 2022-10-25 15:01:25 2022-10-25 15:01:25
|
||||
$ falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml 2022-10-25 15:01:25 2022-10-25 15:01:25
|
||||
```
|
||||
#### falcoctl index update
|
||||
The `index update` allows to update a previously configured `index` file by syncing the local one with the remote one:
|
||||
```bash
|
||||
falcoctl index update falcosecurity
|
||||
$ falcoctl index update falcosecurity
|
||||
```
|
||||
#### falcoctl index remove
|
||||
When we want to remove an `index` file that we configured previously, the `index remove` command is the one we need:
|
||||
```bash
|
||||
falcoctl index remove falcosecurity
|
||||
$ falcoctl index remove falcosecurity
|
||||
```
|
||||
The above command will remove the **falcosecurity** index from the local system.
|
||||
|
||||
|
@ -186,7 +255,7 @@ The *falcoctl* tool provides different commands to interact with Falco **artifac
|
|||
#### Falcoctl artifact search
|
||||
The `artifact search` command allows to search for **artifacts** provided by the `index` files configured in *falcoctl*. The command supports searches by name or by keywords and displays all the **artifacts** that match the search. Assuming that we have already configured the `index` provided by the `falcosecurity` organization, the following command shows all the **artifacts** that work with **Kubernetes**:
|
||||
```bash
|
||||
❯ falcoctl artifact search kubernetes
|
||||
$ falcoctl artifact search kubernetes
|
||||
INDEX ARTIFACT TYPE REGISTRY REPOSITORY
|
||||
falcosecurity k8saudit plugin ghcr.io falcosecurity/plugins/plugin/k8saudit
|
||||
falcosecurity k8saudit-rules rulesfile ghcr.io falcosecurity/plugins/ruleset/k8saudit
|
||||
|
@ -195,7 +264,7 @@ falcosecurity k8saudit-rules rulesfile ghcr.io falcosecurity/pl
|
|||
#### Falcoctl artifact info
|
||||
As per the name, `artifact info` prints some info for a given **artifact**:
|
||||
```bash
|
||||
❯ falcoctl artifact info k8saudit
|
||||
$ falcoctl artifact info k8saudit
|
||||
REF TAGS
|
||||
ghcr.io/falcosecurity/plugins/plugin/k8saudit 0.1.0 0.2.0 0.2.1 0.3.0 0.4.0-rc1 0.4.0 latest
|
||||
```
|
||||
|
@ -204,7 +273,7 @@ It shows the OCI **reference** and **tags** for the **artifact** of interest. Th
|
|||
#### Falcoctl artifact install
|
||||
The above commands help us to find all the necessary info for a given **artifact**. The `artifact install` command installs an **artifact**. It pulls the **artifact** from remote repository, and saves it in a given directory. The following command installs the *k8saudit* plugin in the default path:
|
||||
```bash
|
||||
❯ falcoctl artifact install k8saudit
|
||||
$ falcoctl artifact install k8saudit
|
||||
INFO Reading all configured index files from "/home/aldo/.config/falcoctl/indexes.yaml"
|
||||
INFO Preparing to pull "ghcr.io/falcosecurity/plugins/plugin/k8saudit:latest"
|
||||
INFO Remote registry "ghcr.io" implements docker registry API V2
|
||||
|
@ -213,37 +282,123 @@ The above commands help us to find all the necessary info for a given **artifact
|
|||
INFO Pulling 107d1230f3f0: ############################################# 100%
|
||||
INFO Artifact successfully installed in "/usr/share/falco/plugins"
|
||||
```
|
||||
|
||||
By default, if we give the name of an **artifact** it will search for the **artifact** in the configured `index` files and downlaod the `latest` version. The commands accepts also the OCI **reference** of an **artifact**. In this case, it will ignore the local `index` files.
|
||||
The command has two flags:
|
||||
* *--plugins-dir*: directory where to install plugins. Defaults to `/usr/share/falco/plugins`;
|
||||
* *--rulesfiles-dir*: directory where to install rules. Defaults to `/etc/falco`.
|
||||
* `--plugins-dir`: directory where to install plugins. Defaults to `/usr/share/falco/plugins`;
|
||||
* `--rulesfiles-dir`: directory where to install rules. Defaults to `/etc/falco`.
|
||||
|
||||
> If the repositories of the **artifacts** your are trying to install are not public then you need to authenticate to the remote registry.
|
||||
|
||||
#### Falcoctl artifact follow
|
||||
The above commands allow us to keep up-to-date one or more given **artifacts**. The `artifact follow` command checks for updates on a periodic basis and then downloads and installs the latest version, as specified by the passed tags.
|
||||
It pulls the **artifact** from remote repository, and saves it in a given directory. The following command installs the *github-rules* rulesfile in the default path:
|
||||
```bash
|
||||
$ falcoctl artifact follow github-rules
|
||||
WARN falcosecurity already exists with the same configuration, skipping
|
||||
INFO Reading all configured index files from "/root/.config/falcoctl/indexes.yaml"
|
||||
INFO: Creating follower for "github-rules", with check every 6h0m0s
|
||||
INFO Starting follower for "ghcr.io/falcosecurity/plugins/ruleset/github:latest"
|
||||
INFO (ghcr.io/falcosecurity/plugins/ruleset/github:latest) found new version under tag "latest"
|
||||
INFO (ghcr.io/falcosecurity/plugins/ruleset/github:latest) artifact with tag "latest" correctly installed
|
||||
|
||||
```
|
||||
|
||||
By default, if we give the name of an **artifact** it will search for the **artifact** in the configured `index` files and downlaod the `latest` version. The commands accepts also the OCI **reference** of an **artifact**. In this case, it will ignore the local `index` files.
|
||||
The command can specify the directory where to install the *rulesfile* artifacts through the `--rulesfiles-dir` flag (defaults to `/etc/falco`).
|
||||
|
||||
> If the repositories of the **artifacts** your are trying to install are not public then you need to authenticate to the remote registry.
|
||||
|
||||
> Please note that only **rulesfile** artifact can be followed.
|
||||
|
||||
## Falcoctl registry
|
||||
|
||||
The `registry` commands interact with OCI registries allowing the user to authenticate, pull and push artifacts. We have tested the *falcoctl* tool with the **ghcr.io** registry, but it should work with all the registries that support the OCI artifacts.
|
||||
|
||||
#### Falcoctl registry login
|
||||
The `registry login` authenticates a user to a given OCI registry. Run the command in advance for any private registries.
|
||||
### Falcoctl registry auth
|
||||
The `registry auth` command authenticates a user to a given OCI registry.
|
||||
|
||||
#### Falcoctl registry logout
|
||||
The `registry logout` removes the stored credentials by the `registry login` command.
|
||||
#### Falcoctl registry auth basic
|
||||
The `registry auth basic` command authenticates a user to a given OCI registry using HTTP Basic Authentication. Run the command in advance for any private registries.
|
||||
|
||||
#### Falcoctl registry push
|
||||
#### Falcoctl registry auth oauth
|
||||
The `registry auth oauth` command retrieves access and refresh tokens for OAuth2.0 client credentials flow authentication. Run the command in advance for any private registries.
|
||||
|
||||
#### Falcoctl registry auth gcp
|
||||
The `registry auth gcp` command retrieves access tokens using [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials). In particular, it supports access token retrieval using Google Compute Engine metadata server and Workload Identity, useful to authenticate your deployed Falco workloads. Run the command in advance for Artifact Registry authentication.
|
||||
|
||||
Two typical use cases:
|
||||
|
||||
1. You are manipulating some rules or plugins and use `falcoctl` to pull or push to an Artifact Registry:
|
||||
1. run `gcloud auth application-default login` to generate a JSON credential file that will be used by applications.
|
||||
2. run `falcoctl registry auth gcp europe-docker.pkg.dev` for instance to use Application Default Credentials to connect to any repository hosted at `europe-docker.pkg.dev`.
|
||||
2. You have a Falco instance with Falcoctl as a side car, running in a GKE cluster with Workload Identity enabled:
|
||||
1. Workload Identity is correctly set up for the Falco instance (see the [documentation](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity)).
|
||||
2. Add an environment variable like `FALCOCTL_REGISTRY_AUTH_GCP=europe-docker.pkg.dev` to enable GCP authentication for the `europe-docker.pkg.dev` registry.
|
||||
3. The Falcoctl instance will get access tokens from the metadata server and use them to authenticate to the registry and download your rules.
|
||||
|
||||
### Falcoctl registry push
|
||||
It pushes local files and references the artifact uniquely. The following command shows how to push a local file to a remote registry:
|
||||
```bash
|
||||
falcoctl registry push --type=plugin ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0 clouddrail-0.3.0-linux-x86_64.tar.gz --platform linux/amd64
|
||||
$ falcoctl registry push --type=plugin ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0 clouddrail-0.3.0-linux-x86_64.tar.gz --platform linux/amd64
|
||||
```
|
||||
The type denotes the **artifact** type in this case *plugins*. The `ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0` is the unique reference that points to the **artifact**.
|
||||
Currently, *falcoctl* supports only two types of artifacts: **plugin** and **rulefiles**. Based on **artifact type** the commands accepts different flags:
|
||||
* *--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"
|
||||
Currently, *falcoctl* supports only two types of artifacts: **plugin** and **rulesfile**. Based on **artifact type** the commands accepts different flags:
|
||||
* `--add-floating-tags`: add the floating tags for the major and minor versions
|
||||
* `--annotation-source`: set annotation source for the artifact;
|
||||
* `--depends-on`: set an artifact dependency (can be specified multiple times). Example: `--depends-on my-plugin:1.2.3`
|
||||
* `--tag`: additional artifact tag. Can be repeated multiple time
|
||||
* `--type`: type of artifact to be pushed. Allowed values: `rulesfile`, `plugin`, `asset`
|
||||
|
||||
#### Falcoctl registry pull
|
||||
### Falcoctl registry pull
|
||||
Pulling **artifacts** involves specifying the reference. The type of **artifact** is not required since the tool will implicitly extract it from the OCI **artifact**:
|
||||
```
|
||||
falcoctl registry pull ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0
|
||||
$ falcoctl registry pull ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0
|
||||
```
|
||||
|
||||
# Falcoctl Environment Variables
|
||||
|
||||
The arguments of `falcoctl` can passed as arguments through:
|
||||
- command line options
|
||||
- environment variables
|
||||
- configuration file
|
||||
|
||||
The `falcoctl` arguments can be passed through these different modalities are prioritized in the following order: command line options, environment variables, and finally the configuration file. This means that if an argument is passed through multiple modalities, the value set in the command line options will take precedence over the value set in environment variables, which will in turn take precedence over the value set in the configuration file.
|
||||
|
||||
This is the list of the environment variable that `falcoctl` will use:
|
||||
|
||||
| Name | Content |
|
||||
| ----------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `FALCOCTL_REGISTRY_AUTH_BASIC` | `registry,username,password;registry1,username1,password1` |
|
||||
| `FALCOCTL_REGISTRY_AUTH_OAUTH` | `registry,client-id,client-secret,token-url;registry1` |
|
||||
| `FALCOCTL_REGISTRY_AUTH_GCP` | `registry;registry1` |
|
||||
| `FALCOCTL_INDEXES` | `index-name,https://falcosecurity.github.io/falcoctl/index.yaml` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_EVERY` | `6h0m0s` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_CRON` | `cron-formatted-string` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_REFS` | `ref1;ref2` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_FALCOVERSIONS` | `falco-version-url` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_RULESFILEDIR` | `rules-directory-path` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_PLUGINSDIR` | `plugins-directory-path` |
|
||||
| `FALCOCTL_ARTIFACT_FOLLOW_TMPDIR` | `tmp-directory-path` |
|
||||
| `FALCOCTL_ARTIFACT_INSTALL_REFS` | `ref1;ref2` |
|
||||
| `FALCOCTL_ARTIFACT_INSTALL_RULESFILESDIR` | `rules-directory-path` |
|
||||
| `FALCOCTL_ARTIFACT_INSTALL_PLUGINSDIR` | `plugins-directory-path` |
|
||||
| `FALCOCTL_ARTIFACT_NOVERIFY` | |
|
||||
|
||||
Please note that when passing multiple arguments via an environment variable, they must be separated by a semicolon. Moreover, multiple fields of the same argument must be separated by a comma.
|
||||
|
||||
Here is an example of `falcoctl` usage with environment variables:
|
||||
|
||||
```bash
|
||||
$ export FALCOCTL_REGISTRY_AUTH_OAUTH="localhost:6000,000000,999999,http://localhost:9096/token"
|
||||
$ falcoctl registry oauth
|
||||
```
|
||||
|
||||
# Container image signature verification
|
||||
|
||||
Official container images for Falcoctl, starting from version 0.5.0, are signed with [cosign](https://github.com/sigstore/cosign) v2. To verify the signature run:
|
||||
|
||||
```bash
|
||||
$ FALCOCTL_VERSION=x.y.z # e.g. 0.5.0
|
||||
$ cosign verify docker.io/falcosecurity/falcoctl:$FALCOCTL_VERSION --certificate-oidc-issuer=https://token.actions.githubusercontent.com --certificate-identity-regexp=https://github.com/falcosecurity/falcoctl/ --certificate-github-workflow-ref=refs/tags/v$FALCOCTL_VERSION
|
||||
```
|
||||
|
|
|
@ -1,22 +1,36 @@
|
|||
FROM golang:1.19 as builder
|
||||
FROM cgr.dev/chainguard/go AS builder
|
||||
WORKDIR /tmp/builder
|
||||
|
||||
ARG RELEASE
|
||||
ARG COMMIT
|
||||
ARG BUILD_DATE
|
||||
ARG PROJECT=github.com/falcosecurity/falcoctl
|
||||
|
||||
RUN test -n "$RELEASE" || ( echo "The RELEASE argument is unset. Aborting" && false )
|
||||
RUN test -n "$COMMIT" || ( echo "The COMMIT argument is unset. Aborting" && false )
|
||||
RUN test -n "$BUILD_DATE" || ( echo "The BUILD_DATE argument is unset. Aborting" && false )
|
||||
|
||||
COPY go.mod ./go.mod
|
||||
COPY go.sum ./go.sum
|
||||
RUN go mod download
|
||||
|
||||
COPY . ./
|
||||
RUN CGO_ENABLED=0 GOOS=$(go env GOOS) GOARCH=$(go env GOARCH) go build -ldflags="-s -w" ./
|
||||
|
||||
RUN CGO_ENABLED=0 \
|
||||
GOOS=$(go env GOOS) \
|
||||
GOARCH=$(go env GOARCH) \
|
||||
go build -ldflags \
|
||||
"-s \
|
||||
-w \
|
||||
-X '${PROJECT}/cmd/version.semVersion=${RELEASE}' \
|
||||
-X '${PROJECT}/cmd/version.gitCommit=${COMMIT}' \
|
||||
-X '${PROJECT}/cmd/version.buildDate=${BUILD_DATE}'" \
|
||||
./
|
||||
|
||||
FROM alpine:3.16.3
|
||||
RUN echo ${RELEASE}
|
||||
|
||||
RUN rm -rf /var/cache/apk/*
|
||||
FROM cgr.dev/chainguard/static:latest
|
||||
|
||||
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
|
||||
COPY --from=builder /tmp/builder/falcoctl /usr/bin/falcoctl
|
||||
|
||||
ENTRYPOINT [ "/usr/bin/falcoctl-bin" ]
|
||||
ENTRYPOINT [ "/usr/bin/falcoctl" ]
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
// 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 (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/artifact/follow"
|
||||
"github.com/falcosecurity/falcoctl/internal/artifact/info"
|
||||
"github.com/falcosecurity/falcoctl/internal/artifact/install"
|
||||
"github.com/falcosecurity/falcoctl/internal/artifact/list"
|
||||
"github.com/falcosecurity/falcoctl/internal/artifact/search"
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/index/add"
|
||||
"github.com/falcosecurity/falcoctl/internal/registry/login"
|
||||
"github.com/falcosecurity/falcoctl/internal/registry/oauth"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewArtifactCmd return the artifact 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",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
opt.Initialize()
|
||||
opt.Printer.CheckErr(config.Load(opt.ConfigFile))
|
||||
|
||||
// add indexes if needed
|
||||
// Set up basic authentication
|
||||
indexes, err := config.Indexes()
|
||||
opt.Printer.CheckErr(err)
|
||||
|
||||
for _, ind := range indexes {
|
||||
indexMgr := add.IndexAddOptions{
|
||||
CommonOptions: opt,
|
||||
}
|
||||
opt.Printer.CheckErr(indexMgr.Validate([]string{ind.Name, ind.URL}))
|
||||
opt.Printer.CheckErr(indexMgr.RunIndexAdd(ctx, []string{ind.Name, ind.URL}))
|
||||
}
|
||||
|
||||
basicAuths, err := config.BasicAuths()
|
||||
opt.Printer.CheckErr(err)
|
||||
for _, basicAuth := range basicAuths {
|
||||
cred := &auth.Credential{
|
||||
Username: basicAuth.User,
|
||||
Password: basicAuth.Password,
|
||||
}
|
||||
|
||||
opt.Printer.CheckErr(login.DoLogin(ctx, basicAuth.Registry, cred))
|
||||
}
|
||||
|
||||
oauthAuths, err := config.OauthAuths()
|
||||
opt.Printer.CheckErr(err)
|
||||
for _, auth := range oauthAuths {
|
||||
oauthMgr := oauth.RegistryOauthOptions{
|
||||
CommonOptions: opt,
|
||||
Conf: clientcredentials.Config{
|
||||
ClientID: auth.ClientID,
|
||||
ClientSecret: auth.ClientSecret,
|
||||
TokenURL: auth.TokenURL,
|
||||
},
|
||||
}
|
||||
opt.Printer.CheckErr(oauthMgr.RunOauth(ctx, []string{auth.Registry}))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(search.NewArtifactSearchCmd(ctx, opt))
|
||||
cmd.AddCommand(install.NewArtifactInstallCmd(ctx, opt))
|
||||
cmd.AddCommand(list.NewArtifactListCmd(ctx, opt))
|
||||
cmd.AddCommand(info.NewArtifactInfoCmd(ctx, opt))
|
||||
cmd.AddCommand(follow.NewArtifactFollowCmd(ctx, opt))
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package artifact
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
artifactconfig "github.com/falcosecurity/falcoctl/cmd/artifact/config"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/follow"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/info"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/install"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/list"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/manifest"
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/search"
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/cache"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewArtifactCmd return the artifact command.
|
||||
func NewArtifactCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "artifact",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Interact with Falco artifacts",
|
||||
Long: "Interact with Falco artifacts",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
var indexes []config.Index
|
||||
var indexCache *cache.Cache
|
||||
var err error
|
||||
|
||||
opt.Initialize()
|
||||
if err = config.Load(opt.ConfigFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add indexes if needed
|
||||
// Set up basic authentication
|
||||
if indexes, err = config.Indexes(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the index cache.
|
||||
if indexCache, err = cache.NewFromConfig(ctx, config.IndexesFile, config.IndexesDir, indexes); err != nil {
|
||||
return err
|
||||
}
|
||||
// Save the index cache for later use by the sub commands.
|
||||
opt.Initialize(commonoptions.WithIndexCache(indexCache))
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(search.NewArtifactSearchCmd(ctx, opt))
|
||||
cmd.AddCommand(install.NewArtifactInstallCmd(ctx, opt))
|
||||
cmd.AddCommand(list.NewArtifactListCmd(ctx, opt))
|
||||
cmd.AddCommand(info.NewArtifactInfoCmd(ctx, opt))
|
||||
cmd.AddCommand(follow.NewArtifactFollowCmd(ctx, opt))
|
||||
cmd.AddCommand(artifactconfig.NewArtifactConfigCmd(ctx, opt))
|
||||
cmd.AddCommand(manifest.NewArtifactManifestCmd(ctx, opt))
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
ocipuller "github.com/falcosecurity/falcoctl/pkg/oci/puller"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type artifactConfigOptions struct {
|
||||
*options.Common
|
||||
*options.Registry
|
||||
platform string
|
||||
}
|
||||
|
||||
// NewArtifactConfigCmd returns the artifact config command.
|
||||
func NewArtifactConfigCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactConfigOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "config [ref] [flags]",
|
||||
Short: "Get the config layer of an artifact",
|
||||
Long: "Get the config layer of an artifact",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactConfig(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
cmd.Flags().StringVar(&o.platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
"os and architecture of the artifact in OS/ARCH format")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *artifactConfigOptions) RunArtifactConfig(ctx context.Context, args []string) error {
|
||||
var (
|
||||
puller *ocipuller.Puller
|
||||
ref string
|
||||
config []byte
|
||||
err error
|
||||
)
|
||||
|
||||
// Create puller with auto login enabled.
|
||||
if puller, err = ociutils.Puller(o.PlainHTTP, o.Printer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve the artifact reference.
|
||||
if ref, err = o.IndexCache.ResolveReference(args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: implement two new flags (platforms, platform) based on the oci platform struct.
|
||||
// Split the platform.
|
||||
tokens := strings.Split(o.platform, "/")
|
||||
if len(tokens) != 2 {
|
||||
return fmt.Errorf("invalid platform format: %s", o.platform)
|
||||
}
|
||||
|
||||
if config, err = puller.RawConfigLayer(ctx, ref, tokens[0], tokens[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Printer.DefaultText.Println(string(config))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
//SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
localRegistryHost string
|
||||
localRegistry *remote.Registry
|
||||
testRuleTarball = "../../../pkg/test/data/rules.tar.gz"
|
||||
testPluginTarball = "../../../pkg/test/data/plugin.tar.gz"
|
||||
testPluginPlatform1 = "linux/amd64"
|
||||
testPluginPlatform2 = "windows/amd64"
|
||||
testPluginPlatform3 = "linux/arm64"
|
||||
ctx = context.Background()
|
||||
pluginMultiPlatformRef string
|
||||
rulesRef string
|
||||
artifactWithoutConfigRef string
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Config Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
var err error
|
||||
config := &configuration.Configuration{}
|
||||
// Get a free port to be used by the registry.
|
||||
port, err := testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Create the registry address to which will bind.
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
localRegistryHost = config.HTTP.Addr
|
||||
|
||||
// Create the oras registry.
|
||||
localRegistry, err = testutils.NewOrasRegistry(localRegistryHost, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Initialize options for command.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Push the artifacts to the registry.
|
||||
// Same artifacts will be used to test the puller code.
|
||||
pusher := ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), true, nil)
|
||||
|
||||
// Push plugin artifact with multiple architectures.
|
||||
filePathsAndPlatforms := ocipusher.WithFilepathsAndPlatforms([]string{testPluginTarball, testPluginTarball, testPluginTarball},
|
||||
[]string{testPluginPlatform1, testPluginPlatform2, testPluginPlatform3})
|
||||
pluginMultiPlatformRef = localRegistryHost + "/plugins:multiplatform"
|
||||
artConfig := oci.ArtifactConfig{}
|
||||
Expect(artConfig.ParseDependencies("my-dep:1.2.3|my-alt-dep:1.4.5")).ToNot(HaveOccurred())
|
||||
Expect(artConfig.ParseRequirements("my-req:7.8.9")).ToNot(HaveOccurred())
|
||||
artifactConfig := ocipusher.WithArtifactConfig(artConfig)
|
||||
|
||||
// Build options slice.
|
||||
options := []ocipusher.Option{filePathsAndPlatforms, artifactConfig}
|
||||
|
||||
// Push the plugin artifact.
|
||||
_, err = pusher.Push(ctx, oci.Plugin, pluginMultiPlatformRef, options...)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
|
||||
// Prepare and push artifact without config layer.
|
||||
filePaths := ocipusher.WithFilepaths([]string{testRuleTarball})
|
||||
artConfig = oci.ArtifactConfig{}
|
||||
Expect(artConfig.ParseDependencies("dep1:1.2.3", "dep2:2.3.1")).ToNot(HaveOccurred())
|
||||
options = []ocipusher.Option{
|
||||
filePaths,
|
||||
ocipusher.WithTags("latest"),
|
||||
}
|
||||
|
||||
// Push artifact without config layer.
|
||||
// Push artifact without config layer.
|
||||
artifactWithoutConfigRef = localRegistryHost + "/artifact:noconfig"
|
||||
_, err = pusher.Push(ctx, oci.Rulesfile, artifactWithoutConfigRef, options...)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
|
||||
// Push a rulesfile artifact
|
||||
options = append(options, ocipusher.WithArtifactConfig(artConfig))
|
||||
rulesRef = localRegistryHost + "/rulesfiles:regular"
|
||||
_, err = pusher.Push(ctx, oci.Rulesfile, rulesRef, options...)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
var usage = `Usage:
|
||||
falcoctl artifact config [ref] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for config
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
var help = `Get the config layer of an artifact
|
||||
|
||||
Usage:
|
||||
falcoctl artifact config [ref] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for config
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
var _ = Describe("Config", func() {
|
||||
const (
|
||||
artifactCmd = "artifact"
|
||||
configCmd = "config"
|
||||
plaingHTTP = "--plain-http"
|
||||
configFlag = "--config"
|
||||
platformFlag = "--platform"
|
||||
)
|
||||
|
||||
var (
|
||||
err error
|
||||
args []string
|
||||
configDir string
|
||||
)
|
||||
|
||||
var assertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
configDir = GinkgoT().TempDir()
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
err = nil
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
args = nil
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(string(output.Contents())).Should(Equal(help))
|
||||
})
|
||||
})
|
||||
|
||||
Context("wrong number of arguments", func() {
|
||||
When("number of arguments equal to 0", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 0 ")
|
||||
})
|
||||
|
||||
When("number of arguments equal to 2", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, "arg1", "arg2", configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 2 ")
|
||||
})
|
||||
})
|
||||
|
||||
Context("failure", func() {
|
||||
When("unreachable/non existing registry", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, "noregistry/noartifact", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR unable to get manifest: unable to fetch reference")
|
||||
})
|
||||
|
||||
When("non existing repository", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, localRegistryHost + "/noartifact", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "noartifact:latest: not found")
|
||||
})
|
||||
|
||||
When("non parsable reference", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, " ", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR cannot find among the configured indexes, skipping ")
|
||||
})
|
||||
|
||||
When("no manifest for given platform", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, "linux/wrong"}
|
||||
})
|
||||
assertFailedBehavior(usage, "ERROR unable to get manifest: unable to find a manifest matching the given platform: linux/wrong")
|
||||
})
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
When("empty config layer", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, artifactWithoutConfigRef, plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta("{}")))
|
||||
})
|
||||
})
|
||||
|
||||
When("with valid config layer", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, rulesRef, plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(`{"dependencies":[{"name":"dep1","version":"1.2.3"},{"name":"dep2","version":"2.3.1"}]}`)))
|
||||
})
|
||||
})
|
||||
|
||||
When("no platform flag", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
It("should success getting the platform where tests are running", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`{"dependencies":[{"name":"my-dep","version":"1.2.3","alternatives":[{"name":"my-alt-dep","version":"`)))
|
||||
})
|
||||
})
|
||||
|
||||
When("with valid platform", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`{"dependencies":[{"name":"my-dep","version":"1.2.3","alternatives":[{"name":"my-alt-dep","version":"`)))
|
||||
})
|
||||
})
|
||||
|
||||
When("with non existing platform for artifacts without platforms", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, configCmd, rulesRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(`{"dependencies":[{"name":"dep1","version":"1.2.3"},{"name":"dep2","version":"2.3.1"}]}`)))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package config defines the business logic to fetch config layer for artifacts.
|
||||
package config
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package artifact implements the artifact commands.
|
||||
package artifact
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -0,0 +1,500 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package follow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd/artifact/install"
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/follower"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/index"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
timeout = time.Second * 5
|
||||
|
||||
longFollow = `This command allows you to keep up-to-date one or more given artifacts.
|
||||
It checks for updates on a periodic basis and then downloads and installs the latest version,
|
||||
as specified by the passed tags.
|
||||
|
||||
Artifact references and flags are passed as arguments through:
|
||||
- command line options
|
||||
- environment variables
|
||||
- configuration file
|
||||
The arguments passed through these different modalities are prioritized in the following order:
|
||||
command line options, environment variables, and finally the configuration file. This means that
|
||||
if an argument is passed through multiple modalities, the value set in the command line options
|
||||
will take precedence over the value set in environment variables, which will in turn take precedence
|
||||
over the value set in the configuration file.
|
||||
Please note that when passing multiple artifact references via an environment variable, they must be
|
||||
separated by a semicolon ';' and the environment variable used for references is called
|
||||
FALCOCT_ARTIFACT_FOLLOW_REFS. Other arguments, if passed through environment variables, should start
|
||||
with "FALCOCTL_" and be followed by the hierarchical keys used in the configuration file separated by
|
||||
an underscore "_".
|
||||
|
||||
A reference is either a simple name or a fully qualified reference ("<registry>/<repository>"),
|
||||
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
|
||||
|
||||
When providing just the name of the artifact, the command will search for the artifacts in
|
||||
the configured index files, and if found, it will use the registry and repository specified
|
||||
in the indexes.
|
||||
|
||||
Example - Install and follow "latest" tag of "k8saudit-rules" artifact by relying on index metadata:
|
||||
falcoctl artifact follow k8saudit-rules
|
||||
|
||||
Example - Install and follow all updates from "k8saudit-rules" 0.5.x release series:
|
||||
falcoctl artifact follow k8saudit-rules:0.5
|
||||
|
||||
Example - Install and follow "cloudtrail" plugins using a fully qualified reference:
|
||||
falcoctl artifact follow ghcr.io/falcosecurity/plugins/ruleset/k8saudit:latest
|
||||
`
|
||||
)
|
||||
|
||||
type artifactFollowOptions struct {
|
||||
*options.Common
|
||||
*options.Registry
|
||||
*options.Directory
|
||||
tmpDir string
|
||||
every time.Duration
|
||||
cron string
|
||||
falcoVersions string
|
||||
versions config.FalcoVersions
|
||||
timeout time.Duration
|
||||
closeChan chan bool
|
||||
allowedTypes oci.ArtifactTypeSlice
|
||||
noVerify bool
|
||||
}
|
||||
|
||||
// NewArtifactFollowCmd returns the artifact follow command.
|
||||
//
|
||||
//nolint:gocyclo // unknown reason for cyclomatic complexity
|
||||
func NewArtifactFollowCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactFollowOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
Directory: &options.Directory{},
|
||||
closeChan: make(chan bool),
|
||||
versions: config.FalcoVersions{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "follow [ref1 [ref2 ...]] [flags]",
|
||||
Short: "Install a list of artifacts and continuously checks if there are updates",
|
||||
Long: longFollow,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Override "every" flag with viper config if not set by user.
|
||||
f := cmd.Flags().Lookup("every")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag every")
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowEveryKey) {
|
||||
val := viper.Get(config.ArtifactFollowEveryKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"every\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "cron" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("cron")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag cron")
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowCronKey) {
|
||||
val := viper.Get(config.ArtifactFollowCronKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"cron\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "falco-versions" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("falco-versions")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag falco-versions")
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowFalcoVersionsKey) {
|
||||
val := viper.Get(config.ArtifactFollowFalcoVersionsKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"falco-versions\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "rulesfiles-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagRulesFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagRulesFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowRulesfilesDirKey) {
|
||||
val := viper.Get(config.ArtifactFollowRulesfilesDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagRulesFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "plugins-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagPluginsFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagPluginsFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowPluginsDirKey) {
|
||||
val := viper.Get(config.ArtifactFollowPluginsDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagPluginsFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "assets-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagAssetsFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagAssetsFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowAssetsDirKey) {
|
||||
val := viper.Get(config.ArtifactFollowAssetsDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagAssetsFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "tmp-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("tmp-dir")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag tmp-dir")
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowTmpDirKey) {
|
||||
val := viper.Get(config.ArtifactFollowTmpDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"tmp-dir\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "allowed-types" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(install.FlagAllowedTypes)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %s", install.FlagAllowedTypes)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactAllowedTypesKey) {
|
||||
val, err := config.ArtifactAllowedTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set(f.Name, val.String()); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", install.FlagAllowedTypes, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "no-verify" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(install.FlagNoVerify)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %s", install.FlagNoVerify)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactNoVerifyKey) {
|
||||
val := viper.Get(config.ArtifactNoVerifyKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", install.FlagNoVerify, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get Falco versions via HTTP endpoint
|
||||
if err := o.retrieveFalcoVersions(ctx); err != nil {
|
||||
return fmt.Errorf("unable to retrieve Falco versions, please check if it is running "+
|
||||
"and correctly exposing the version endpoint: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactFollow(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
o.Directory.AddFlags(cmd)
|
||||
cmd.Flags().DurationVarP(&o.every, "every", "e", config.FollowResync, "Time interval how often it checks for a new version of the "+
|
||||
"artifact. Cannot be used together with 'cron' option.")
|
||||
cmd.Flags().StringVar(&o.cron, "cron", "", "Cron-like string to specify interval how often it checks for a new version of the artifact."+
|
||||
" Cannot be used together with 'every' option.")
|
||||
cmd.Flags().StringVar(&o.tmpDir, "tmp-dir", "", "Directory where to save temporary files")
|
||||
cmd.Flags().StringVar(&o.falcoVersions, "falco-versions", "http://localhost:8765/versions",
|
||||
"Where to retrieve versions, it can be either an URL or a path to a file")
|
||||
cmd.Flags().DurationVar(&o.timeout, "timeout", defaultBackoffConfig.MaxDelay,
|
||||
"Timeout for initial connection to the Falco versions endpoint")
|
||||
cmd.Flags().Var(&o.allowedTypes, install.FlagAllowedTypes,
|
||||
fmt.Sprintf(`list of artifact types that can be followed. If not specified or configured, all types are allowed.
|
||||
It accepts comma separated values or it can be repeated multiple times.
|
||||
Examples:
|
||||
--%s="rulesfile,plugin"
|
||||
--%s=rulesfile --%s=plugin`, install.FlagAllowedTypes, install.FlagAllowedTypes, install.FlagAllowedTypes))
|
||||
cmd.Flags().BoolVar(&o.noVerify, install.FlagNoVerify, false,
|
||||
"whether this command should skip signature verification")
|
||||
cmd.MarkFlagsMutuallyExclusive("cron", "every")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunArtifactFollow executes the business logic for the artifact follow command.
|
||||
func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []string) error {
|
||||
logger := o.Printer.Logger
|
||||
// Retrieve configuration for follower
|
||||
configuredFollower, err := config.Follower()
|
||||
if err != nil {
|
||||
o.Printer.CheckErr(fmt.Errorf("unable to retrieved the configured follower: %w", err))
|
||||
}
|
||||
|
||||
// Set args as configured if no arg was passed
|
||||
if len(args) == 0 {
|
||||
if len(configuredFollower.Artifacts) == 0 {
|
||||
return fmt.Errorf("no artifacts to follow, please configure artifacts or pass them as arguments to this command")
|
||||
}
|
||||
args = configuredFollower.Artifacts
|
||||
}
|
||||
|
||||
var sched cron.Schedule
|
||||
if o.cron != "" {
|
||||
sched, err = cron.ParseStandard(o.cron)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse cron '%s': %w", o.cron, err)
|
||||
}
|
||||
} else {
|
||||
sched = scheduledDuration{o.every}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
// For each artifact create a follower.
|
||||
var followers = make(map[string]*follower.Follower, 0)
|
||||
for _, a := range args {
|
||||
if o.cron != "" {
|
||||
logger.Info("Creating follower", logger.Args("artifact", a, "cron", o.cron))
|
||||
} else {
|
||||
logger.Info("Creating follower", logger.Args("artifact", a, "check every", o.every.String()))
|
||||
}
|
||||
ref, err := o.IndexCache.ResolveReference(a)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse artifact reference for %q: %w", a, err)
|
||||
}
|
||||
|
||||
var sig *index.Signature
|
||||
if !o.noVerify {
|
||||
sig = o.IndexCache.SignatureForIndexRef(a)
|
||||
}
|
||||
|
||||
cfg := &follower.Config{
|
||||
WaitGroup: &wg,
|
||||
Resync: sched,
|
||||
RulesfilesDir: o.RulesfilesDir,
|
||||
PluginsDir: o.PluginsDir,
|
||||
AssetsDir: o.AssetsDir,
|
||||
ArtifactReference: ref,
|
||||
PlainHTTP: o.PlainHTTP,
|
||||
CloseChan: o.closeChan,
|
||||
TmpDir: o.tmpDir,
|
||||
FalcoVersions: o.versions,
|
||||
AllowedTypes: o.allowedTypes,
|
||||
Signature: sig,
|
||||
}
|
||||
fol, err := follower.New(ref, o.Printer, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the follower for ref %q: %w", ref, err)
|
||||
}
|
||||
wg.Add(1)
|
||||
followers[ref] = fol
|
||||
}
|
||||
|
||||
for k, f := range followers {
|
||||
logger.Info("Starting follower", logger.Args("artifact", k))
|
||||
go f.Follow(ctx)
|
||||
}
|
||||
|
||||
// Wait until we receive a signal to be terminated
|
||||
<-ctx.Done()
|
||||
|
||||
// We are done, shutdown the followers.
|
||||
logger.Info("Closing followers...")
|
||||
close(o.closeChan)
|
||||
|
||||
// Wait for the followers to shutdown or that the timer expires.
|
||||
doneChan := make(chan bool)
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(doneChan)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-doneChan:
|
||||
logger.Info("Followers correctly stopped.")
|
||||
case <-time.After(timeout):
|
||||
logger.Info("Timed out waiting for followers to exit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *artifactFollowOptions) retrieveFalcoVersions(ctx context.Context) error {
|
||||
_, err := url.ParseRequestURI(o.falcoVersions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse URI: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, o.falcoVersions, http.NoBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot fetch Falco version: %w", err)
|
||||
}
|
||||
|
||||
backoffConfig := defaultBackoffConfig
|
||||
backoffConfig.MaxDelay = o.timeout
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &backoffTransport{
|
||||
Base: http.DefaultTransport,
|
||||
Printer: o.Printer,
|
||||
Config: backoffConfig,
|
||||
},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get versions from URL %q: %w", o.falcoVersions, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read response body: %w", err)
|
||||
}
|
||||
|
||||
var dataUnmarshalled map[string]interface{}
|
||||
|
||||
err = json.Unmarshal(data, &dataUnmarshalled)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshalling: %w", err)
|
||||
}
|
||||
|
||||
for key, value := range dataUnmarshalled {
|
||||
// todo(alacuku): how to handle types other than strings? Silently ignoring for now...
|
||||
if strValue, ok := value.(string); ok {
|
||||
o.versions[key] = strValue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config defines the configuration options for backoff.
|
||||
type backoffConfig struct {
|
||||
// BaseDelay is the amount of time to backoff after the first failure.
|
||||
BaseDelay time.Duration
|
||||
// Multiplier is the factor with which to multiply backoffs after a
|
||||
// failed retry. Should ideally be greater than 1.
|
||||
Multiplier float64
|
||||
// Jitter is the factor with which backoffs are randomized.
|
||||
// todo: not yet implemented
|
||||
// Jitter float64
|
||||
// MaxDelay is the upper bound of backoff delay.
|
||||
MaxDelay time.Duration
|
||||
}
|
||||
|
||||
var defaultBackoffConfig = backoffConfig{
|
||||
BaseDelay: 1.0 * time.Second,
|
||||
Multiplier: 1.6,
|
||||
// Jitter: 0.2, todo: not yet implemented
|
||||
MaxDelay: 120 * time.Second,
|
||||
}
|
||||
|
||||
type backoffTransport struct {
|
||||
Base http.RoundTripper
|
||||
Printer *output.Printer
|
||||
Config backoffConfig
|
||||
attempts int
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func (bt *backoffTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
var err error
|
||||
var resp *http.Response
|
||||
logger := bt.Printer.Logger
|
||||
bt.startTime = time.Now()
|
||||
bt.attempts = 0
|
||||
|
||||
logger.Debug(fmt.Sprintf("Retrieving versions from Falco (timeout %s) ...", bt.Config.MaxDelay))
|
||||
|
||||
for {
|
||||
resp, err = bt.Base.RoundTrip(req)
|
||||
if err != nil {
|
||||
if req.Context().Err() != nil {
|
||||
return nil, req.Context().Err()
|
||||
}
|
||||
sleep := bt.Config.backoff(bt.attempts)
|
||||
|
||||
wakeTime := time.Now().Add(sleep)
|
||||
if wakeTime.Sub(bt.startTime) > bt.Config.MaxDelay {
|
||||
return resp, fmt.Errorf("timeout occurred while retrieving versions from Falco")
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf("error: %s. Trying again in %s", err.Error(), sleep.String()))
|
||||
time.Sleep(sleep)
|
||||
} else {
|
||||
logger.Debug("Successfully retrieved versions from Falco")
|
||||
return resp, err
|
||||
}
|
||||
|
||||
bt.attempts++
|
||||
}
|
||||
}
|
||||
|
||||
// Backoff returns the amount of time to wait before the next retry given the
|
||||
// number of retries.
|
||||
func (bc backoffConfig) backoff(retries int) time.Duration {
|
||||
if retries == 0 {
|
||||
return bc.BaseDelay
|
||||
}
|
||||
backoff, max := float64(bc.BaseDelay), float64(bc.MaxDelay)
|
||||
for backoff < max && retries > 0 {
|
||||
backoff *= bc.Multiplier
|
||||
retries--
|
||||
}
|
||||
if backoff > max {
|
||||
backoff = max
|
||||
}
|
||||
// Randomize backoff delays so that if a cluster of requests start at
|
||||
// the same time, they won't operate in lockstep.
|
||||
// todo: implement jitter
|
||||
// backoff *= 1 + bc.Jitter*(math.Float64()*2-1)
|
||||
if backoff < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return time.Duration(backoff)
|
||||
}
|
||||
|
||||
type scheduledDuration struct {
|
||||
time.Duration
|
||||
}
|
||||
|
||||
func (sd scheduledDuration) Next(tm time.Time) time.Time {
|
||||
return tm.Add(sd.Duration)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
@ -16,30 +17,29 @@ package info
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index"
|
||||
"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.CommonOptions
|
||||
*options.RegistryOptions
|
||||
*options.Common
|
||||
*options.Registry
|
||||
}
|
||||
|
||||
// NewArtifactInfoCmd returns the artifact info command.
|
||||
func NewArtifactInfoCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
func NewArtifactInfoCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactInfoOptions{
|
||||
CommonOptions: opt,
|
||||
RegistryOptions: &options.RegistryOptions{},
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -48,35 +48,33 @@ func NewArtifactInfoCmd(ctx context.Context, opt *options.CommonOptions) *cobra.
|
|||
Short: "Retrieve all available versions of a given artifact",
|
||||
Long: "Retrieve all available versions of a given artifact",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.Printer.CheckErr(o.RunArtifactInfo(ctx, args))
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactInfo(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.RegistryOptions.AddFlags(cmd)
|
||||
o.Registry.AddFlags(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string) error {
|
||||
indexConfig, err := index.NewConfig(config.IndexesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergedIndexes, err := utils.Indexes(indexConfig, config.IndexesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 := mergedIndexes.EntryByName(name)
|
||||
entry, ok := o.IndexCache.MergedIndexes.EntryByName(name)
|
||||
if !ok {
|
||||
o.Printer.Warning.Printfln("cannot find %q, skipping", name)
|
||||
logger.Warn("Cannot find artifact, skipping", logger.Args("name", name))
|
||||
continue
|
||||
}
|
||||
ref = fmt.Sprintf("%s/%s", entry.Registry, entry.Repository)
|
||||
|
@ -85,16 +83,6 @@ 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))
|
||||
|
@ -103,18 +91,33 @@ func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string
|
|||
}
|
||||
|
||||
tags, err := repo.Tags(ctx)
|
||||
if err != nil {
|
||||
o.Printer.Warning.Printfln("cannot retrieve tags from %q, %w", ref, err)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
logger.Warn("Cannot retrieve tags from", logger.Args("ref", ref, "reason", err.Error()))
|
||||
continue
|
||||
} else if errors.Is(err, context.Canceled) {
|
||||
// When the context is canceled we exit, since we receive a termination signal.
|
||||
return err
|
||||
}
|
||||
|
||||
joinedTags := strings.Join(tags, " ")
|
||||
joinedTags := strings.Join(filterOutSigTags(tags), ", ")
|
||||
data = append(data, []string{ref, joinedTags})
|
||||
}
|
||||
|
||||
if err = o.Printer.PrintTable(output.ArtifactInfo, data); err != nil {
|
||||
return err
|
||||
// Print the table header + data only if there is data.
|
||||
if len(data) > 0 {
|
||||
return o.Printer.PrintTable(output.ArtifactInfo, data)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterOutSigTags(tags []string) []string {
|
||||
// Iterate the slice in reverse to avoid index shifting when deleting
|
||||
for i := len(tags) - 1; i >= 0; i-- {
|
||||
if strings.HasSuffix(tags[i], ".sig") {
|
||||
// Remove the element at index i by slicing the slice
|
||||
tags = append(tags[:i], tags[i+1:]...)
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package install
|
||||
|
||||
const (
|
||||
// FlagAllowedTypes is the name of the flag to specify allowed artifact types.
|
||||
FlagAllowedTypes = "allowed-types"
|
||||
|
||||
// FlagPlatform is the name of the flag to override the platform.
|
||||
FlagPlatform = "platform"
|
||||
|
||||
// FlagResolveDeps is the name of the flag to enable artifact dependencies resolution.
|
||||
FlagResolveDeps = "resolve-deps"
|
||||
|
||||
// FlagNoVerify is the name of the flag to disable signature verification.
|
||||
FlagNoVerify = "no-verify"
|
||||
)
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2023 The Falco Authors
|
||||
// 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.
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2023 The Falco Authors
|
||||
// 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.
|
||||
|
@ -82,7 +83,6 @@ func TestResolveDeps(t *testing.T) {
|
|||
// no dependencies here
|
||||
},
|
||||
}, nil
|
||||
|
||||
}),
|
||||
expectedOutRef: []string{ref1, dep1},
|
||||
expectedErr: nil,
|
||||
|
@ -187,7 +187,6 @@ func TestResolveDeps(t *testing.T) {
|
|||
// no dependencies here
|
||||
},
|
||||
}, nil
|
||||
|
||||
}),
|
||||
expectedOutRef: []string{ref1, alt1},
|
||||
expectedErr: ErrCannotSatisfyDependencies,
|
||||
|
@ -220,7 +219,6 @@ func TestResolveDeps(t *testing.T) {
|
|||
// no dependencies here
|
||||
},
|
||||
}, nil
|
||||
|
||||
}),
|
||||
expectedOutRef: nil,
|
||||
expectedErr: ErrCannotSatisfyDependencies,
|
||||
|
@ -239,5 +237,4 @@ func TestResolveDeps(t *testing.T) {
|
|||
testCase.scenario, testCase.description, outRef, testCase.expectedOutRef)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -0,0 +1,374 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package install
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/signature"
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/index"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
const (
|
||||
longInstall = `This command allows you to install one or more given artifacts.
|
||||
|
||||
Artifact references and flags are passed as arguments through:
|
||||
- command line options
|
||||
- environment variables
|
||||
- configuration file
|
||||
The arguments passed through these different modalities are prioritized in the following order:
|
||||
command line options, environment variables, and finally the configuration file. This means that
|
||||
if an argument is passed through multiple modalities, the value set in the command line options
|
||||
will take precedence over the value set in environment variables, which will in turn take precedence
|
||||
over the value set in the configuration file.
|
||||
Please note that when passing multiple artifact references via an environment variable, they must be
|
||||
separated by a semicolon ';'. Other arguments, if passed through environment variables, should start
|
||||
with "FALCOCTL_" and be followed by the hierarchical keys used in the configuration file separated by
|
||||
an underscore "_".
|
||||
|
||||
A reference is either a simple name or a fully qualified reference ("<registry>/<repository>"),
|
||||
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
|
||||
|
||||
When providing just the name of the artifact, the command will search for the artifacts in
|
||||
the configured index files, and if found, it will use the registry and repository specified
|
||||
in the indexes.
|
||||
|
||||
Example - Install "latest" tag of "k8saudit-rules" artifact by relying on index metadata:
|
||||
falcoctl artifact install k8saudit-rules
|
||||
|
||||
Example - Install all updates from "k8saudit-rules" 0.5.x release series:
|
||||
falcoctl artifact install k8saudit-rules:0.5
|
||||
|
||||
Example - Install "cloudtrail" plugins using a fully qualified reference:
|
||||
falcoctl artifact install ghcr.io/falcosecurity/plugins/ruleset/k8saudit:latest
|
||||
`
|
||||
)
|
||||
|
||||
type artifactInstallOptions struct {
|
||||
*options.Common
|
||||
*options.Registry
|
||||
*options.Directory
|
||||
allowedTypes oci.ArtifactTypeSlice
|
||||
platform string // Raw string from command line
|
||||
platformArch string // Architecture portion of parsed platform string
|
||||
platformOS string // OS portion of parsed platform string
|
||||
resolveDeps bool
|
||||
noVerify bool
|
||||
}
|
||||
|
||||
// NewArtifactInstallCmd returns the artifact install command.
|
||||
func NewArtifactInstallCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactInstallOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
Directory: &options.Directory{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "install [ref1 [ref2 ...]] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Install a list of artifacts",
|
||||
Long: longInstall,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Override "rulesfiles-dir" flag with viper config if not set by user.
|
||||
f := cmd.Flags().Lookup(options.FlagRulesFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagRulesFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactInstallRulesfilesDirKey) {
|
||||
val := viper.Get(config.ArtifactInstallRulesfilesDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagRulesFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "plugins-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagPluginsFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagPluginsFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactInstallPluginsDirKey) {
|
||||
val := viper.Get(config.ArtifactInstallPluginsDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagPluginsFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "assets-dir" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(options.FlagAssetsFilesDir)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", options.FlagAssetsFilesDir)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactFollowAssetsDirKey) {
|
||||
val := viper.Get(config.ArtifactFollowAssetsDirKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagAssetsFilesDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "allowed-types" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup(FlagAllowedTypes)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", FlagAllowedTypes)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactAllowedTypesKey) {
|
||||
val, err := config.ArtifactAllowedTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set(f.Name, val.String()); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %s flag: %w", FlagAllowedTypes, err)
|
||||
}
|
||||
}
|
||||
|
||||
f = cmd.Flags().Lookup(FlagResolveDeps)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", FlagResolveDeps)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactInstallResolveDepsKey) {
|
||||
val := viper.Get(config.ArtifactInstallResolveDepsKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", FlagResolveDeps, err)
|
||||
}
|
||||
}
|
||||
|
||||
f = cmd.Flags().Lookup(FlagNoVerify)
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag %q", FlagNoVerify)
|
||||
} else if !f.Changed && viper.IsSet(config.ArtifactNoVerifyKey) {
|
||||
val := viper.Get(config.ArtifactNoVerifyKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite %q flag: %w", FlagNoVerify, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse "platform" into OS and Arch
|
||||
if len(o.platform) > 0 {
|
||||
parts := strings.Split(o.platform, "/")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid %q: must be in the format OS/Arch", FlagPlatform)
|
||||
}
|
||||
o.platformOS, o.platformArch = parts[0], parts[1]
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactInstall(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
o.Directory.AddFlags(cmd)
|
||||
cmd.Flags().Var(&o.allowedTypes, FlagAllowedTypes,
|
||||
fmt.Sprintf(`list of artifact types that can be installed. If not specified or configured, all types are allowed.
|
||||
It accepts comma separated values or it can be repeated multiple times.
|
||||
Examples:
|
||||
--%s="rulesfile,plugin"
|
||||
--%s=rulesfile --%s=plugin`, FlagAllowedTypes, FlagAllowedTypes, FlagAllowedTypes))
|
||||
cmd.Flags().StringVar(&o.platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
"os and architecture of the artifact in OS/ARCH format")
|
||||
cmd.Flags().BoolVar(&o.resolveDeps, FlagResolveDeps, true,
|
||||
"whether this command should resolve dependencies or not")
|
||||
cmd.Flags().BoolVar(&o.noVerify, FlagNoVerify, false,
|
||||
"whether this command should skip signature verification")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunArtifactInstall executes the business logic for the artifact install command.
|
||||
func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []string) error {
|
||||
logger := o.Printer.Logger
|
||||
// Retrieve configuration for installer
|
||||
configuredInstaller, err := config.Installer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve the configured installer: %w", err)
|
||||
}
|
||||
|
||||
// Set args as configured if no arg was passed
|
||||
if len(args) == 0 {
|
||||
if len(configuredInstaller.Artifacts) == 0 {
|
||||
return fmt.Errorf("no artifacts to install, please configure artifacts or pass them as arguments to this command")
|
||||
}
|
||||
args = configuredInstaller.Artifacts
|
||||
}
|
||||
|
||||
// Create temp dir where to put pulled artifacts
|
||||
tmpDir, err := os.MkdirTemp("", "falcoctl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create temporary directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create registry puller with auto login enabled
|
||||
puller, err := ociutils.Puller(o.PlainHTTP, o.Printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Specify how to pull config layer for each artifact requested by user.
|
||||
resolver := artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
|
||||
ref, err := o.IndexCache.ResolveReference(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
artifactConfig, err := puller.ArtifactConfig(ctx, ref, o.platformOS, o.platformArch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &oci.RegistryResult{
|
||||
Config: *artifactConfig,
|
||||
}, nil
|
||||
})
|
||||
|
||||
signatures := make(map[string]*index.Signature)
|
||||
|
||||
// Compute input to install dependencies
|
||||
for i, arg := range args {
|
||||
ref, err := o.IndexCache.ResolveReference(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sig := o.IndexCache.SignatureForIndexRef(arg); sig != nil {
|
||||
signatures[ref] = sig
|
||||
}
|
||||
args[i] = ref
|
||||
}
|
||||
|
||||
var refs []string
|
||||
if o.resolveDeps {
|
||||
// Solve dependencies
|
||||
logger.Info("Resolving dependencies ...")
|
||||
refs, err = ResolveDeps(resolver, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
refs = args
|
||||
}
|
||||
|
||||
logger.Info("Installing artifacts", logger.Args("refs", refs))
|
||||
|
||||
for _, ref := range refs {
|
||||
resolvedRef, err := o.IndexCache.ResolveReference(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if signatures[resolvedRef] == nil {
|
||||
if sig := o.IndexCache.SignatureForIndexRef(ref); sig != nil {
|
||||
signatures[resolvedRef] = sig
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Preparing to pull artifact", logger.Args("ref", resolvedRef))
|
||||
|
||||
if err := puller.CheckAllowedType(ctx, resolvedRef, o.platformOS, o.platformArch, o.allowedTypes.Types); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Install will always install artifact for the current OS and architecture
|
||||
result, err := puller.Pull(ctx, resolvedRef, tmpDir, o.platformOS, o.platformArch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sig := signatures[resolvedRef]
|
||||
|
||||
if sig != nil && !o.noVerify {
|
||||
repo, err := utils.RepositoryFromRef(resolvedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// In order to prevent TOCTOU issues we'll perform signature verification after we complete a pull
|
||||
// and obtained a digest but before files are written to disk. This way we ensure that we're verifying
|
||||
// the exact digest that we just pulled, even if the tag gets overwritten in the meantime.
|
||||
digestRef := fmt.Sprintf("%s@%s", repo, result.RootDigest)
|
||||
|
||||
logger.Info("Verifying signature for artifact", logger.Args("digest", digestRef))
|
||||
err = signature.Verify(ctx, digestRef, sig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while verifying signature for %s: %w", digestRef, err)
|
||||
}
|
||||
logger.Info("Signature successfully verified!")
|
||||
}
|
||||
|
||||
var destDir string
|
||||
switch result.Type {
|
||||
case oci.Plugin:
|
||||
destDir = o.PluginsDir
|
||||
case oci.Rulesfile:
|
||||
destDir = o.RulesfilesDir
|
||||
case oci.Asset:
|
||||
destDir = o.AssetsDir
|
||||
default:
|
||||
return fmt.Errorf("unrecognized result type %q while pulling artifact", result.Type)
|
||||
}
|
||||
|
||||
// Check if directory exists and is writable.
|
||||
err = utils.ExistsAndIsWritable(destDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot use directory %q as install destination: %w", destDir, err)
|
||||
}
|
||||
|
||||
logger.Info("Extracting and installing artifact", logger.Args("type", result.Type, "file", result.Filename))
|
||||
|
||||
if !o.Printer.DisableStyling {
|
||||
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Extracting and installing")
|
||||
}
|
||||
|
||||
result.Filename = filepath.Join(tmpDir, result.Filename)
|
||||
|
||||
f, err := os.Open(result.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Extract artifact and move it to its destination directory
|
||||
_, err = utils.ExtractTarGz(ctx, f, destDir, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot extract %q to %q: %w", result.Filename, destDir, err)
|
||||
}
|
||||
|
||||
err = os.Remove(result.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.Printer.Spinner != nil {
|
||||
_ = o.Printer.Spinner.Stop()
|
||||
}
|
||||
logger.Info("Artifact successfully installed", logger.Args("name", resolvedRef, "type", result.Type, "digest", result.Digest, "directory", destDir))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package install_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
const (
|
||||
rulesfiletgz = "../../../pkg/test/data/rules.tar.gz"
|
||||
rulesfileyaml = "../../../pkg/test/data/rules.yaml"
|
||||
plugintgz = "../../../pkg/test/data/plugin.tar.gz"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
var (
|
||||
registry string
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
port int
|
||||
orasRegistry *remote.Registry
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
var err error
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
RunSpecs(t, "root suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
config := &configuration.Configuration{}
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create the oras registry.
|
||||
orasRegistry, err = testutils.NewOrasRegistry(registry, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
//nolint:unused // false positive
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,465 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package install_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
|
||||
out "github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
//nolint:lll,unused // no need to check for line length.
|
||||
var artifactInstallUsage = `Usage:
|
||||
falcoctl artifact install [ref1 [ref2 ...]] [flags]
|
||||
|
||||
Flags:
|
||||
--allowed-types ArtifactTypeSlice list of artifact types that can be installed. If not specified or configured, all types are allowed.
|
||||
It accepts comma separated values or it can be repeated multiple times.
|
||||
Examples:
|
||||
--allowed-types="rulesfile,plugin"
|
||||
--allowed-types=rulesfile --allowed-types=plugin
|
||||
-h, --help help for install
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
|
||||
--plugins-dir string directory where to install plugins. (default "/usr/share/falco/plugins")
|
||||
--resolve-deps whether this command should resolve dependencies or not (default true)
|
||||
--rulesfiles-dir string directory where to install rules. (default "/etc/falco")
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var artifactInstallHelp = `This command allows you to install one or more given artifacts.
|
||||
|
||||
Artifact references and flags are passed as arguments through:
|
||||
- command line options
|
||||
- environment variables
|
||||
- configuration file
|
||||
The arguments passed through these different modalities are prioritized in the following order:
|
||||
command line options, environment variables, and finally the configuration file. This means that
|
||||
if an argument is passed through multiple modalities, the value set in the command line options
|
||||
will take precedence over the value set in environment variables, which will in turn take precedence
|
||||
over the value set in the configuration file.
|
||||
Please note that when passing multiple artifact references via an environment variable, they must be
|
||||
separated by a semicolon ';'. Other arguments, if passed through environment variables, should start
|
||||
with "FALCOCTL_" and be followed by the hierarchical keys used in the configuration file separated by
|
||||
an underscore "_".
|
||||
|
||||
A reference is either a simple name or a fully qualified reference ("<registry>/<repository>"),
|
||||
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
|
||||
|
||||
When providing just the name of the artifact, the command will search for the artifacts in
|
||||
the configured index files, and if found, it will use the registry and repository specified
|
||||
in the indexes.
|
||||
|
||||
Example - Install "latest" tag of "k8saudit-rules" artifact by relying on index metadata:
|
||||
falcoctl artifact install k8saudit-rules
|
||||
|
||||
Example - Install all updates from "k8saudit-rules" 0.5.x release series:
|
||||
falcoctl artifact install k8saudit-rules:0.5
|
||||
|
||||
Example - Install "cloudtrail" plugins using a fully qualified reference:
|
||||
falcoctl artifact install ghcr.io/falcosecurity/plugins/ruleset/k8saudit:latest
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var correctIndexConfig = `indexes:
|
||||
- name: falcosecurity
|
||||
url: https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var installAssertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:unused // false positive
|
||||
var artifactInstallTests = Describe("install", func() {
|
||||
var (
|
||||
pusher *ocipusher.Pusher
|
||||
ref string
|
||||
config ocipusher.Option
|
||||
)
|
||||
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
artifactCmd = "artifact"
|
||||
installCmd = "install"
|
||||
dep1 = "myplugin:1.2.3"
|
||||
dep2 = "myplugin1:1.2.3|otherplugin:3.2.1"
|
||||
req = "engine_version:15"
|
||||
anSource = "myrepo.com/rules.git"
|
||||
artifact = "generic-repo"
|
||||
repo = "/" + artifact
|
||||
tag = "tag"
|
||||
repoAndTag = repo + ":" + tag
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, installCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(artifactInstallHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
Context("failure", func() {
|
||||
var (
|
||||
tracker out.Tracker
|
||||
options []ocipusher.Option
|
||||
filePathsAndPlatforms ocipusher.Option
|
||||
filePaths ocipusher.Option
|
||||
destDir string
|
||||
)
|
||||
const (
|
||||
plainHTTP = true
|
||||
testPluginPlatform1 = "linux/amd64"
|
||||
)
|
||||
|
||||
When("without artifact", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, "--config", configFile}
|
||||
})
|
||||
installAssertFailedBehavior(artifactInstallUsage,
|
||||
"ERROR no artifacts to install, please configure artifacts or pass them as arguments to this command")
|
||||
})
|
||||
|
||||
When("unreachable registry", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, "noregistry/testrules", "--plain-http", "--config", configFile}
|
||||
})
|
||||
installAssertFailedBehavior(artifactInstallUsage, `ERROR unable to get manifest: unable to fetch reference`)
|
||||
})
|
||||
|
||||
When("invalid repository", func() {
|
||||
newReg := registry + "/wrong:latest"
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, newReg, "--plain-http", "--config", configFile}
|
||||
})
|
||||
installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR unable to get manifest: unable to fetch reference %q", newReg))
|
||||
})
|
||||
|
||||
When("with disallowed types (rulesfile)", func() {
|
||||
BeforeEach(func() {
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push plugin
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "plugin1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http", "--platform", testPluginPlatform1,
|
||||
"--config", configFilePath, "--allowed-types", "rulesfile"}
|
||||
})
|
||||
|
||||
installAssertFailedBehavior(artifactInstallUsage, "ERROR cannot download artifact of type \"plugin\": type not permitted")
|
||||
})
|
||||
|
||||
When("with disallowed types (plugin)", func() {
|
||||
BeforeEach(func() {
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push rulesfile
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "rules1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePaths = ocipusher.WithFilepaths([]string{rulesfiletgz})
|
||||
options = []ocipusher.Option{filePaths, config}
|
||||
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http",
|
||||
"--config", configFilePath, "--allowed-types", "plugin"}
|
||||
})
|
||||
|
||||
installAssertFailedBehavior(artifactInstallUsage, "ERROR cannot download artifact of type \"rulesfile\": type not permitted")
|
||||
})
|
||||
|
||||
When("an unknown type is used", func() {
|
||||
wrongType := "mywrongtype"
|
||||
BeforeEach(func() {
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push rulesfile
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "rules1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePaths = ocipusher.WithFilepaths([]string{rulesfiletgz})
|
||||
options = []ocipusher.Option{filePaths, config}
|
||||
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http",
|
||||
"--config", configFilePath, "--allowed-types", "plugin," + wrongType}
|
||||
})
|
||||
|
||||
installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR invalid argument \"plugin,%s\" for \"--allowed-types\" flag: "+
|
||||
"not valid token %q: must be one of \"rulesfile\", \"plugin\"", wrongType, wrongType))
|
||||
})
|
||||
|
||||
When("--plugins-dir is not writable", func() {
|
||||
BeforeEach(func() {
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Chmod(destDir, 0o555)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push plugin
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "plugin1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http", "--platform", testPluginPlatform1,
|
||||
"--config", configFilePath, "--plugins-dir", destDir}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
|
||||
"as install destination: %s is not writable", destDir, destDir)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("--plugins-dir is not present", func() {
|
||||
BeforeEach(func() {
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Remove(destDir)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push plugin
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "plugin1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http", "--platform", testPluginPlatform1,
|
||||
"--config", configFilePath, "--plugins-dir", destDir}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
|
||||
"as install destination: %s doesn't exists", destDir, destDir)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("--rulesfile-dir is not writable", func() {
|
||||
BeforeEach(func() {
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Chmod(destDir, 0o555)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push plugin
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "rules1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePaths = ocipusher.WithFilepaths([]string{rulesfiletgz})
|
||||
options = []ocipusher.Option{filePaths, config}
|
||||
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http",
|
||||
"--config", configFilePath, "--rulesfiles-dir", destDir}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
|
||||
"as install destination: %s is not writable", destDir, destDir)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("not existing --plugins-dir", func() {
|
||||
BeforeEach(func() {
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Remove(destDir)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// push plugin
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
|
||||
Name: "rules1",
|
||||
Version: "0.0.1",
|
||||
})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepaths([]string{rulesfiletgz})
|
||||
options = []ocipusher.Option{filePaths, config}
|
||||
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{artifactCmd, installCmd, ref, "--plain-http",
|
||||
"--config", configFilePath, "--rulesfiles-dir", destDir}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
|
||||
"as install destination: %s doesn't exists", destDir, destDir)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("not --platform is not of the correct format", func() {
|
||||
BeforeEach(func() {
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Remove(destDir)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
ref = registry + repoAndTag
|
||||
args = []string{artifactCmd, installCmd, ref, "--config", configFile, "--platform", "this/is/invalid"}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := `ERROR invalid "platform": must be in the format OS/Arch`
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
@ -16,64 +17,55 @@ package list
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"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"
|
||||
)
|
||||
|
||||
// CommandName name of the command. It has to be the first word in the use line.
|
||||
const CommandName = "list"
|
||||
|
||||
type artifactListOptions struct {
|
||||
*options.CommonOptions
|
||||
*options.Common
|
||||
artifactType oci.ArtifactType
|
||||
index string
|
||||
}
|
||||
|
||||
// NewArtifactListCmd returns the artifact search command.
|
||||
func NewArtifactListCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
func NewArtifactListCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactListOptions{
|
||||
CommonOptions: opt,
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list [flags]",
|
||||
Use: fmt.Sprintf("%s [flags]", CommandName),
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "List all artifacts",
|
||||
Long: "List all artifacts",
|
||||
Aliases: []string{"ls"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.Printer.CheckErr(o.RunArtifactList(ctx, args))
|
||||
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""`)
|
||||
cmd.Flags().Var(&o.artifactType, "type", `Only list artifacts with a specific type. Allowed values: "rulesfile", "plugin", "asset"`)
|
||||
cmd.Flags().StringVar(&o.index, "index", "", "Only display artifacts from a configured index")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *artifactListOptions) RunArtifactList(ctx context.Context, args []string) error {
|
||||
indexConfig, err := index.NewConfig(config.IndexesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergedIndexes, err := utils.Indexes(indexConfig, config.IndexesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *artifactListOptions) RunArtifactList(_ context.Context, _ []string) error {
|
||||
var data [][]string
|
||||
for _, entry := range mergedIndexes.Entries {
|
||||
for _, entry := range o.IndexCache.MergedIndexes.Entries {
|
||||
if o.artifactType != "" && o.artifactType != oci.ArtifactType(entry.Type) {
|
||||
continue
|
||||
}
|
||||
|
||||
indexName := mergedIndexes.IndexByEntry(entry).Name
|
||||
indexName := o.IndexCache.MergedIndexes.IndexByEntry(entry).Name
|
||||
if o.index != "" && o.index != indexName {
|
||||
continue
|
||||
}
|
||||
|
@ -82,9 +74,5 @@ func (o *artifactListOptions) RunArtifactList(ctx context.Context, args []string
|
|||
data = append(data, row)
|
||||
}
|
||||
|
||||
if err = o.Printer.PrintTable(output.ArtifactSearch, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return o.Printer.PrintTable(output.ArtifactSearch, data)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package manifest defines the business logic to fetch manifest layer for artifacts.
|
||||
package manifest
|
|
@ -0,0 +1,93 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
ocipuller "github.com/falcosecurity/falcoctl/pkg/oci/puller"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type artifactManifestOptions struct {
|
||||
*options.Common
|
||||
*options.Registry
|
||||
platform string
|
||||
}
|
||||
|
||||
// NewArtifactManifestCmd returns the artifact manifest command.
|
||||
func NewArtifactManifestCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactManifestOptions{
|
||||
Common: opt,
|
||||
Registry: &options.Registry{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "manifest [ref] [flags]",
|
||||
Short: "Get the manifest layer of an artifact",
|
||||
Long: "Get the manifest layer of an artifact",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactManifest(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
cmd.Flags().StringVar(&o.platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
"os and architecture of the artifact in OS/ARCH format")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *artifactManifestOptions) RunArtifactManifest(ctx context.Context, args []string) error {
|
||||
var (
|
||||
puller *ocipuller.Puller
|
||||
ref string
|
||||
manifest []byte
|
||||
err error
|
||||
)
|
||||
|
||||
// Create puller with auto login enabled.
|
||||
if puller, err = ociutils.Puller(o.PlainHTTP, o.Printer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Resolve the artifact reference.
|
||||
if ref, err = o.IndexCache.ResolveReference(args[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: implement two new flags (platforms, platform) based on the oci platform struct.
|
||||
// Split the platform.
|
||||
tokens := strings.Split(o.platform, "/")
|
||||
if len(tokens) != 2 {
|
||||
return fmt.Errorf("invalid platform format: %s", o.platform)
|
||||
}
|
||||
|
||||
if manifest, err = puller.RawManifest(ctx, ref, tokens[0], tokens[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Printer.DefaultText.Println(string(manifest))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package manifest_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
localRegistryHost string
|
||||
localRegistry *remote.Registry
|
||||
testRuleTarball = "../../../pkg/test/data/rules.tar.gz"
|
||||
testPluginTarball = "../../../pkg/test/data/plugin.tar.gz"
|
||||
testPluginPlatform1 = "linux/amd64"
|
||||
testPluginPlatform2 = "windows/amd64"
|
||||
testPluginPlatform3 = "linux/arm64"
|
||||
ctx = context.Background()
|
||||
pluginMultiPlatformRef string
|
||||
rulesRef string
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
)
|
||||
|
||||
func TestManifest(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Manifest Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
var err error
|
||||
config := &configuration.Configuration{}
|
||||
// Get a free port to be used by the registry.
|
||||
port, err := testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
// Create the registry address to which will bind.
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
localRegistryHost = config.HTTP.Addr
|
||||
|
||||
// Create the oras registry.
|
||||
localRegistry, err = testutils.NewOrasRegistry(localRegistryHost, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Initialize options for command.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Push the artifacts to the registry.
|
||||
// Same artifacts will be used to test the puller code.
|
||||
pusher := ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), true, nil)
|
||||
|
||||
// Push plugin artifact with multiple architectures.
|
||||
filePathsAndPlatforms := ocipusher.WithFilepathsAndPlatforms([]string{testPluginTarball, testPluginTarball, testPluginTarball},
|
||||
[]string{testPluginPlatform1, testPluginPlatform2, testPluginPlatform3})
|
||||
pluginMultiPlatformRef = localRegistryHost + "/plugins:multiplatform"
|
||||
artConfig := oci.ArtifactConfig{}
|
||||
Expect(artConfig.ParseDependencies("my-dep:1.2.3|my-alt-dep:1.4.5")).ToNot(HaveOccurred())
|
||||
Expect(artConfig.ParseRequirements("my-req:7.8.9")).ToNot(HaveOccurred())
|
||||
artifactConfig := ocipusher.WithArtifactConfig(artConfig)
|
||||
|
||||
// Build options slice.
|
||||
options := []ocipusher.Option{filePathsAndPlatforms, artifactConfig}
|
||||
|
||||
// Push the plugin artifact.
|
||||
_, err = pusher.Push(ctx, oci.Plugin, pluginMultiPlatformRef, options...)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
|
||||
// Prepare and push artifact without config layer.
|
||||
filePaths := ocipusher.WithFilepaths([]string{testRuleTarball})
|
||||
artConfig = oci.ArtifactConfig{}
|
||||
Expect(artConfig.ParseDependencies("dep1:1.2.3", "dep2:2.3.1")).ToNot(HaveOccurred())
|
||||
options = []ocipusher.Option{
|
||||
filePaths,
|
||||
ocipusher.WithTags("latest"),
|
||||
}
|
||||
|
||||
// Push a rulesfile artifact
|
||||
options = append(options, ocipusher.WithArtifactConfig(artConfig))
|
||||
rulesRef = localRegistryHost + "/rulesfiles:regular"
|
||||
_, err = pusher.Push(ctx, oci.Rulesfile, rulesRef, options...)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package manifest_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
var usage = `Usage:
|
||||
falcoctl artifact manifest [ref] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for manifest
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
var help = `Get the manifest layer of an artifact
|
||||
|
||||
Usage:
|
||||
falcoctl artifact manifest [ref] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for manifest
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
var _ = Describe("Manifest", func() {
|
||||
const (
|
||||
artifactCmd = "artifact"
|
||||
manifestCmd = "manifest"
|
||||
plaingHTTP = "--plain-http"
|
||||
configFlag = "--config"
|
||||
platformFlag = "--platform"
|
||||
)
|
||||
|
||||
var (
|
||||
err error
|
||||
args []string
|
||||
configDir string
|
||||
)
|
||||
|
||||
var assertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
JustBeforeEach(func() {
|
||||
configDir = GinkgoT().TempDir()
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
err = nil
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
args = nil
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(string(output.Contents())).Should(Equal(help))
|
||||
})
|
||||
})
|
||||
|
||||
Context("wrong number of arguments", func() {
|
||||
When("number of arguments equal to 0", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 0 ")
|
||||
})
|
||||
|
||||
When("number of arguments equal to 2", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, "arg1", "arg2", configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 2 ")
|
||||
})
|
||||
})
|
||||
|
||||
Context("failure", func() {
|
||||
When("unreachable/non existing registry", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, "noregistry/noartifact", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR unable to fetch reference \"noregistry/noartifact:latest\"")
|
||||
})
|
||||
|
||||
When("non existing repository", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, localRegistryHost + "/noartifact", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "noartifact:latest: not found")
|
||||
})
|
||||
|
||||
When("non parsable reference", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, " ", plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
assertFailedBehavior(usage, "ERROR cannot find among the configured indexes, skipping ")
|
||||
})
|
||||
|
||||
When("no manifest for given platform", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, "linux/wrong"}
|
||||
})
|
||||
assertFailedBehavior(usage, "ERROR unable to find a manifest matching the given platform: linux/wrong")
|
||||
})
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
When("without image index and no platform (rulesfiles)", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, rulesRef, plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.rulesfile.config.v1+json","digest":"sha256:c329db306d80e7f1e3a5df28bb7d75a0a1545ad1e8f717a4ab4534a3d558affa","size":86},"layers":[{"mediaType":"application/vnd.cncf.falco.rulesfile.layer.v1+tar.gz","digest":"sha256:8ed676f9801d987a26854827beb176eb9164dec3b09a714406348fe1096f7c6c","size":2560,"annotations":{"org.opencontainers.image.title":"rules.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
|
||||
})
|
||||
})
|
||||
|
||||
When("no platform flag", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir}
|
||||
})
|
||||
|
||||
It("should success getting the platform where tests are running", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.plugin.config.v1+json","digest":"sha256:39ae8c14fd9ef38d0f1836ba7be71627023ce615f165c3663586a325eee04724","size":164},"layers":[{"mediaType":"application/vnd.cncf.falco.plugin.layer.v1+tar.gz","digest":"sha256:45a192b10e9bbfc82f4216b071afefd7fba56e02e856e37186430d40160e5d64","size":6659921,"annotations":{"org.opencontainers.image.title":"plugin.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
|
||||
})
|
||||
})
|
||||
|
||||
When("with valid platform", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
|
||||
})
|
||||
|
||||
It("should success", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.plugin.config.v1+json","digest":"sha256:39ae8c14fd9ef38d0f1836ba7be71627023ce615f165c3663586a325eee04724","size":164},"layers":[{"mediaType":"application/vnd.cncf.falco.plugin.layer.v1+tar.gz","digest":"sha256:45a192b10e9bbfc82f4216b071afefd7fba56e02e856e37186430d40160e5d64","size":6659921,"annotations":{"org.opencontainers.image.title":"plugin.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
|
||||
})
|
||||
})
|
||||
|
||||
When("with non existing platform for artifacts without platforms", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{artifactCmd, manifestCmd, rulesRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
|
||||
})
|
||||
|
||||
It("should success and ignore the platform flag", func() {
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.rulesfile.config.v1+json","digest":"sha256:c329db306d80e7f1e3a5df28bb7d75a0a1545ad1e8f717a4ab4534a3d558affa","size":86},"layers":[{"mediaType":"application/vnd.cncf.falco.rulesfile.layer.v1+tar.gz","digest":"sha256:8ed676f9801d987a26854827beb176eb9164dec3b09a714406348fe1096f7c6c","size":2560,"annotations":{"org.opencontainers.image.title":"rules.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
@ -20,9 +21,6 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"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"
|
||||
|
@ -30,10 +28,12 @@ import (
|
|||
|
||||
const (
|
||||
defaultMinScore = 0.65
|
||||
// CommandName name of the command. It has to be the first word in the use line.
|
||||
CommandName = "search"
|
||||
)
|
||||
|
||||
type artifactSearchOptions struct {
|
||||
*options.CommonOptions
|
||||
*options.Common
|
||||
minScore float64
|
||||
artifactType oci.ArtifactType
|
||||
}
|
||||
|
@ -47,59 +47,45 @@ func (o *artifactSearchOptions) Validate() error {
|
|||
}
|
||||
|
||||
// NewArtifactSearchCmd returns the artifact search command.
|
||||
func NewArtifactSearchCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
func NewArtifactSearchCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := artifactSearchOptions{
|
||||
CommonOptions: opt,
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "search [keyword1 [keyword2 ...]] [flags]",
|
||||
Use: fmt.Sprintf("%s [keyword1 [keyword2 ...]] [flags]", CommandName),
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Search an artifact by keywords",
|
||||
Long: "Search an artifact by keywords",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
o.Printer.CheckErr(o.Validate())
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.Validate()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.Printer.CheckErr(o.RunArtifactSearch(ctx, args))
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunArtifactSearch(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Float64VarP(&o.minScore, "min-score", "", defaultMinScore,
|
||||
"the minimum score used to match artifact names with search keywords")
|
||||
|
||||
cmd.Flags().Var(&o.artifactType, "type", `Only search artifacts with a specific type. Allowed values: "rulesfile", "plugin""`)
|
||||
cmd.Flags().Var(&o.artifactType, "type", `Only search artifacts with a specific type. Allowed values: "rulesfile", "plugin", "asset"`)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *artifactSearchOptions) RunArtifactSearch(ctx context.Context, args []string) error {
|
||||
indexConfig, err := index.NewConfig(config.IndexesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergedIndexes, err := utils.Indexes(indexConfig, config.IndexesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resultEntries := mergedIndexes.SearchByKeywords(o.minScore, args...)
|
||||
func (o *artifactSearchOptions) RunArtifactSearch(_ context.Context, args []string) error {
|
||||
resultEntries := o.IndexCache.MergedIndexes.SearchByKeywords(o.minScore, args...)
|
||||
|
||||
var data [][]string
|
||||
for _, entry := range resultEntries {
|
||||
if o.artifactType != "" && o.artifactType != oci.ArtifactType(entry.Type) {
|
||||
continue
|
||||
}
|
||||
indexName := mergedIndexes.IndexByEntry(entry).Name
|
||||
indexName := o.IndexCache.MergedIndexes.IndexByEntry(entry).Name
|
||||
row := []string{indexName, entry.Name, entry.Type, entry.Registry, entry.Repository}
|
||||
data = append(data, row)
|
||||
}
|
||||
|
||||
if err = o.Printer.PrintTable(output.ArtifactSearch, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return o.Printer.PrintTable(output.ArtifactSearch, data)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
135
cmd/cli_test.go
135
cmd/cli_test.go
|
@ -1,135 +0,0 @@
|
|||
// 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/ioutil"
|
||||
"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)
|
||||
|
||||
// test.args = append(test.args, "--dryrun") // todo > add dry run flag
|
||||
|
||||
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 := ioutil.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 := ioutil.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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Cmd Suite")
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package drivercleanup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type driverCleanupOptions struct {
|
||||
*options.Common
|
||||
*options.Driver
|
||||
}
|
||||
|
||||
// NewDriverCleanupCmd cleans a driver up.
|
||||
func NewDriverCleanupCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
|
||||
o := driverCleanupOptions{
|
||||
Common: opt,
|
||||
Driver: driver,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "cleanup [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Cleanup a driver",
|
||||
Long: `Cleans a driver up, eg for kmod, by removing it from dkms.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunDriverCleanup(ctx)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *driverCleanupOptions) RunDriverCleanup(_ context.Context) error {
|
||||
o.Printer.Logger.Info("Running falcoctl driver cleanup", o.Printer.Logger.Args(
|
||||
"driver type", o.Driver.Type,
|
||||
"driver name", o.Driver.Name))
|
||||
var buf bytes.Buffer
|
||||
if !o.Printer.DisableStyling {
|
||||
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Cleaning up existing drivers")
|
||||
}
|
||||
err := o.Driver.Type.Cleanup(o.Printer.WithWriter(&buf), o.Driver.Name)
|
||||
if o.Printer.Spinner != nil {
|
||||
_ = o.Printer.Spinner.Stop()
|
||||
}
|
||||
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
|
||||
// Only print formatted text if we are formatting to json
|
||||
out := strings.ReplaceAll(buf.String(), "\n", ";")
|
||||
o.Printer.Logger.Info("Driver cleanup", o.Printer.Logger.Args("output", out))
|
||||
} else {
|
||||
// Print much more readable output as-is
|
||||
o.Printer.DefaultText.Print(buf.String())
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package drivercleanup_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestCleanup(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Cleanup Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package drivercleanup_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var driverCleanupHelp = `Cleans a driver up, eg for kmod, by removing it from dkms.
|
||||
|
||||
Usage:
|
||||
falcoctl driver cleanup [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for cleanup
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--host-root string Driver host root to be used. (default "/")
|
||||
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
|
||||
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
--name string Driver name to be used. (default "falco")
|
||||
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
|
||||
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
|
||||
--version string Driver version to be used.
|
||||
`
|
||||
|
||||
var addAssertFailedBehavior = func(specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
var _ = Describe("cleanup", func() {
|
||||
|
||||
var (
|
||||
driverCmd = "driver"
|
||||
cleanupCmd = "cleanup"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, cleanupCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverCleanupHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing failure cases for cleaning a driver.
|
||||
Context("failure", func() {
|
||||
When("with non absolute host-root", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, cleanupCmd, "--config", configFile, "--host-root", "foo/"}
|
||||
})
|
||||
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
|
||||
})
|
||||
|
||||
When("with invalid driver type", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, cleanupCmd, "--config", configFile, "--type", "foo"}
|
||||
})
|
||||
addAssertFailedBehavior(`ERROR unsupported driver type specified: foo`)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package drivercleanup defines the cleanup logic for the driver cmd.
|
||||
package drivercleanup
|
|
@ -0,0 +1,326 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package driverconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/falcosecurity/driverkit/pkg/kernelrelease"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
drivertype "github.com/falcosecurity/falcoctl/pkg/driver/type"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
const (
|
||||
falcoName = "falco"
|
||||
)
|
||||
|
||||
func newOptions() *driverConfigOptions {
|
||||
common := options.NewOptions()
|
||||
common.Initialize()
|
||||
|
||||
// Parse the driver type.
|
||||
dType, _ := drivertype.Parse("modern_ebpf")
|
||||
return &driverConfigOptions{
|
||||
Common: common,
|
||||
Driver: &options.Driver{
|
||||
Type: dType,
|
||||
Name: falcoName,
|
||||
Repos: []string{"https://download.falco.org/driver"},
|
||||
Version: "6.0.0+driver",
|
||||
HostRoot: "/",
|
||||
Distro: nil,
|
||||
Kr: kernelrelease.KernelRelease{},
|
||||
},
|
||||
update: false,
|
||||
namespace: "",
|
||||
kubeconfig: "",
|
||||
configmap: "",
|
||||
configDir: "",
|
||||
}
|
||||
}
|
||||
|
||||
func createFalcoConfigFile(cfg falcoCfg, configDir string) error {
|
||||
engineKind, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal falco config: %w", err)
|
||||
}
|
||||
|
||||
// Write the engine configuration to a specialized config file.
|
||||
if err := os.WriteFile(filepath.Join(configDir, "falco.yaml"), engineKind, 0o600); err != nil {
|
||||
return fmt.Errorf("unable to write falco.yaml file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createFalcoConfigMap(cfg falcoCfg, dataKey string) (*v1.ConfigMap, error) {
|
||||
engineKind, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to marshal falco config: %w", err)
|
||||
}
|
||||
|
||||
cm := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: falcoName,
|
||||
Namespace: falcoName,
|
||||
},
|
||||
Data: map[string]string{
|
||||
dataKey: string(engineKind),
|
||||
},
|
||||
}
|
||||
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
func TestDriverConfigOptions_Commit_Host(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args func(t *testing.T) *driverConfigOptions
|
||||
expected func(t *testing.T, opt *driverConfigOptions, err error)
|
||||
}{
|
||||
{
|
||||
"no falco config file",
|
||||
func(t *testing.T) *driverConfigOptions {
|
||||
opt := newOptions()
|
||||
opt.configDir = "no-file-at-all"
|
||||
opt.update = true
|
||||
return opt
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.Error(t, err, "should error since falco configuration file does not exist")
|
||||
require.ErrorContains(t, err, "open no-file-at-all/falco.yaml: no such file or directory")
|
||||
},
|
||||
},
|
||||
{
|
||||
"update-falco-config",
|
||||
func(t *testing.T) *driverConfigOptions {
|
||||
opt := newOptions()
|
||||
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write falco configuration file.
|
||||
cfg := falcoCfg{engineCfg{Kind: "modern_ebpf"}}
|
||||
err = createFalcoConfigFile(cfg, dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
opt.configDir = dir
|
||||
return opt
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.NoError(t, err, "should not error")
|
||||
|
||||
// Config file.
|
||||
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
|
||||
|
||||
// Check that config file has been created.
|
||||
_, err = os.Stat(specCfgFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(specCfgFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := falcoCfg{}
|
||||
err = yaml.Unmarshal(content, &cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, opt.Type.String(), cfg.Engine.Kind)
|
||||
},
|
||||
},
|
||||
{
|
||||
"falco-not-in-driver-mode",
|
||||
func(t *testing.T) *driverConfigOptions {
|
||||
opt := newOptions()
|
||||
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write falco configuration file.
|
||||
cfg := falcoCfg{engineCfg{Kind: "nodriver"}}
|
||||
err = createFalcoConfigFile(cfg, dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
opt.configDir = dir
|
||||
return opt
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.NoError(t, err, "should not error")
|
||||
|
||||
// Config file.
|
||||
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
|
||||
|
||||
// Check that config file has been created.
|
||||
_, err = os.Stat(specCfgFile)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
opt := testCase.args(t)
|
||||
err := opt.Commit(context.Background(), nil, opt.Type)
|
||||
testCase.expected(t, opt, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriverConfigOptions_Commit_K8S(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap)
|
||||
expected func(t *testing.T, opt *driverConfigOptions, err error)
|
||||
}{
|
||||
{
|
||||
"no falco configmap, wrong namespace",
|
||||
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
|
||||
opt := newOptions()
|
||||
opt.namespace = "wrong-namespace"
|
||||
opt.configmap = falcoName
|
||||
|
||||
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "falco.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
return opt, cm
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.Error(t, err, "should error since falco configmap does not exist")
|
||||
require.ErrorContains(t, err, "unable to get configmap falco in namespace wrong-namespace")
|
||||
},
|
||||
},
|
||||
{
|
||||
"no falco configmap, wrong name",
|
||||
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
|
||||
opt := newOptions()
|
||||
opt.namespace = falcoName
|
||||
opt.configmap = "wrong-name"
|
||||
|
||||
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "falco.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
return opt, cm
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.Error(t, err, "should error since falco configmap does not exist")
|
||||
require.ErrorContains(t, err, "unable to get configmap wrong-name in namespace falco")
|
||||
},
|
||||
},
|
||||
{
|
||||
"no falco config, wrong data key",
|
||||
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
|
||||
opt := newOptions()
|
||||
opt.namespace = falcoName
|
||||
opt.configmap = falcoName
|
||||
|
||||
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "wrong-data-key")
|
||||
require.NoError(t, err)
|
||||
|
||||
return opt, cm
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.Error(t, err, "should error since falco configmap does not exist")
|
||||
require.ErrorContains(t, err, "configMap falco does not contain key \"falco.yaml\"")
|
||||
},
|
||||
},
|
||||
{
|
||||
"update-falco-config",
|
||||
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
|
||||
opt := newOptions()
|
||||
opt.namespace = falcoName
|
||||
opt.configmap = falcoName
|
||||
|
||||
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
|
||||
require.NoError(t, err)
|
||||
opt.configDir = dir
|
||||
|
||||
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "falco.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
return opt, cm
|
||||
},
|
||||
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.NoError(t, err, "should not error")
|
||||
|
||||
// Config file.
|
||||
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
|
||||
|
||||
// Check that config file has been created.
|
||||
_, err = os.Stat(specCfgFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(specCfgFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := falcoCfg{}
|
||||
err = yaml.Unmarshal(content, &cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, opt.Type.String(), cfg.Engine.Kind)
|
||||
},
|
||||
},
|
||||
{
|
||||
"falco-not-in-driver-mode",
|
||||
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
|
||||
opt := newOptions()
|
||||
opt.namespace = falcoName
|
||||
opt.configmap = falcoName
|
||||
|
||||
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
|
||||
require.NoError(t, err)
|
||||
|
||||
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "nodriver"}}, "falco.yaml")
|
||||
require.NoError(t, err)
|
||||
|
||||
opt.configDir = dir
|
||||
return opt, cm
|
||||
},
|
||||
func(t *testing.T, opt *driverConfigOptions, err error) {
|
||||
require.NoError(t, err, "should not error")
|
||||
|
||||
// Config file.
|
||||
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
|
||||
|
||||
// Check that config file has been created.
|
||||
_, err = os.Stat(specCfgFile)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
opt, cm := testCase.args(t)
|
||||
// Create fake client.
|
||||
fakeClient := fake.NewSimpleClientset(cm)
|
||||
err := opt.Commit(context.Background(), fakeClient, opt.Type)
|
||||
testCase.expected(t, opt, err)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/net/context"
|
||||
"gopkg.in/yaml.v3"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
drivertype "github.com/falcosecurity/falcoctl/pkg/driver/type"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
const (
|
||||
longConfig = `Configure a driver for future usages with other driver subcommands.
|
||||
It will also update local Falco configuration or k8s configmap depending on the environment where it is running, to let Falco use chosen driver.
|
||||
Only supports deployments of Falco that use a driver engine, ie: one between kmod, ebpf and modern-ebpf.
|
||||
If engine.kind key is set to a non-driver driven engine, Falco configuration won't be touched.
|
||||
`
|
||||
falcoConfigFile = "falco.yaml"
|
||||
falcoDriverConfigFile = "engine-kind-falcoctl.yaml"
|
||||
)
|
||||
|
||||
type driverConfigOptions struct {
|
||||
*options.Common
|
||||
*options.Driver
|
||||
update bool
|
||||
namespace string
|
||||
kubeconfig string
|
||||
configmap string
|
||||
configDir string
|
||||
}
|
||||
|
||||
type engineCfg struct {
|
||||
Kind string `yaml:"kind"`
|
||||
}
|
||||
type falcoCfg struct {
|
||||
Engine engineCfg `yaml:"engine"`
|
||||
}
|
||||
|
||||
// NewDriverConfigCmd configures a driver and stores it in config.
|
||||
func NewDriverConfigCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
|
||||
o := driverConfigOptions{
|
||||
Common: opt,
|
||||
Driver: driver,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "config [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Configure a driver",
|
||||
Long: longConfig,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
viper.AutomaticEnv()
|
||||
|
||||
_ = viper.BindPFlag("driver.config.configmap", cmd.Flags().Lookup("configmap"))
|
||||
_ = viper.BindPFlag("driver.config.namespace", cmd.Flags().Lookup("namespace"))
|
||||
_ = viper.BindPFlag("driver.config.update_falco", cmd.Flags().Lookup("update-falco"))
|
||||
_ = viper.BindPFlag("driver.config.kubeconfig", cmd.Flags().Lookup("kubeconfig"))
|
||||
_ = viper.BindPFlag("driver.config.configdir", cmd.Flags().Lookup("falco-config-dir"))
|
||||
|
||||
o.configmap = viper.GetString("driver.config.configmap")
|
||||
o.namespace = viper.GetString("driver.config.namespace")
|
||||
o.kubeconfig = viper.GetString("driver.config.kubeconfig")
|
||||
o.update = viper.GetBool("driver.config.update_falco")
|
||||
o.configDir = viper.GetString("driver.config.configdir")
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunDriverConfig(ctx)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&o.update, "update-falco", true, "Whether to overwrite Falco configuration")
|
||||
cmd.Flags().StringVar(&o.namespace, "namespace", "", "Kubernetes namespace.")
|
||||
cmd.Flags().StringVar(&o.kubeconfig, "kubeconfig", "", "Kubernetes config.")
|
||||
cmd.Flags().StringVar(&o.configmap, "configmap", "", "Falco configmap name.")
|
||||
cmd.Flags().StringVar(&o.configDir, "falco-config-dir", "/etc/falco", "Falco configuration directory.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunDriverConfig implements the driver configuration command.
|
||||
func (o *driverConfigOptions) RunDriverConfig(ctx context.Context) error {
|
||||
o.Printer.Logger.Info("Running falcoctl driver config", o.Printer.Logger.Args(
|
||||
"name", o.Driver.Name,
|
||||
"version", o.Driver.Version,
|
||||
"type", o.Driver.Type.String(),
|
||||
"host-root", o.Driver.HostRoot,
|
||||
"repos", strings.Join(o.Driver.Repos, ",")))
|
||||
|
||||
if o.update {
|
||||
var cl kubernetes.Interface
|
||||
var err error
|
||||
|
||||
if o.namespace != "" {
|
||||
// Create a new clientset.
|
||||
if cl, err = setupClient(o.kubeconfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := o.Commit(ctx, cl, o.Driver.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
o.Printer.Logger.Info("Storing falcoctl driver config")
|
||||
return config.StoreDriver(o.Driver.ToDriverConfig(), o.ConfigFile)
|
||||
}
|
||||
|
||||
func checkFalcoRunsWithDrivers(engineKind string) bool {
|
||||
// Modify the data in the ConfigMap/Falco config file ONLY if engine.kind is set to a known driver type.
|
||||
// This ensures that we modify the config only for Falcos running with drivers, and not plugins/gvisor.
|
||||
// Scenario: user has multiple Falco pods deployed in its cluster, one running with driver,
|
||||
// other running with plugins. We must only touch the one running with driver.
|
||||
if _, err := drivertype.Parse(engineKind); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (o *driverConfigOptions) IsRunningInDriverModeHost() (bool, error) {
|
||||
o.Printer.Logger.Debug("Checking if Falco is running in driver mode on host system")
|
||||
|
||||
falcoCfgFile := filepath.Join(o.configDir, falcoConfigFile)
|
||||
yamlFile, err := os.ReadFile(filepath.Clean(falcoCfgFile))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
cfg := falcoCfg{}
|
||||
if err = yaml.Unmarshal(yamlFile, &cfg); err != nil {
|
||||
return false, fmt.Errorf("unable to unmarshal falco.yaml to falcoCfg struct: %w", err)
|
||||
}
|
||||
|
||||
return checkFalcoRunsWithDrivers(cfg.Engine.Kind), nil
|
||||
}
|
||||
|
||||
func (o *driverConfigOptions) IsRunningInDriverModeK8S(ctx context.Context, cl kubernetes.Interface) (bool, error) {
|
||||
o.Printer.Logger.Debug("Checking if Falco is running in driver mode in Kubernetes")
|
||||
|
||||
configMap, err := cl.CoreV1().ConfigMaps(o.namespace).Get(ctx, o.configmap, metav1.GetOptions{})
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to get configmap %s in namespace %s: %w", o.configmap, o.namespace, err)
|
||||
}
|
||||
|
||||
// Check that this is a Falco config map
|
||||
falcoYaml, present := configMap.Data["falco.yaml"]
|
||||
if !present {
|
||||
o.Printer.Logger.Debug("Skip non Falco-related config map",
|
||||
o.Printer.Logger.Args("configMap", configMap.Name))
|
||||
return false, fmt.Errorf("configMap %s does not contain key \"falco.yaml\"", o.configmap)
|
||||
}
|
||||
|
||||
// Check that Falco is configured to run with a driver
|
||||
var falcoConfig falcoCfg
|
||||
err = yaml.Unmarshal([]byte(falcoYaml), &falcoConfig)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to unmarshal falco.yaml to falcoCfg struct: %w", err)
|
||||
}
|
||||
|
||||
return checkFalcoRunsWithDrivers(falcoConfig.Engine.Kind), nil
|
||||
}
|
||||
|
||||
// Commit saves the updated driver type to Falco config,
|
||||
// in a specialized configuration file under /etc/falco/config.d.
|
||||
func (o *driverConfigOptions) Commit(ctx context.Context, cl kubernetes.Interface, driverType drivertype.DriverType) error {
|
||||
// If set to true, then we need to overwrite the driver type.
|
||||
var overwrite bool
|
||||
var err error
|
||||
if cl != nil {
|
||||
if overwrite, err = o.IsRunningInDriverModeK8S(ctx, cl); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if overwrite, err = o.IsRunningInDriverModeHost(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if overwrite {
|
||||
o.Printer.Logger.Info("Committing driver config to specialized configuration file under",
|
||||
o.Printer.Logger.Args("directory", filepath.Join(o.configDir, "config.d")))
|
||||
return overwriteDriverType(o.configDir, driverType)
|
||||
}
|
||||
|
||||
o.Printer.Logger.Info("Falco is not configured to run with a driver, no need to set driver type.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupClient(kubeconfig string) (kubernetes.Interface, error) {
|
||||
var cfg *rest.Config
|
||||
var err error
|
||||
|
||||
// Create the rest config.
|
||||
if kubeconfig != "" {
|
||||
cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
} else {
|
||||
cfg, err = rest.InClusterConfig()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the clientset.
|
||||
return kubernetes.NewForConfig(cfg)
|
||||
}
|
||||
|
||||
func overwriteDriverType(configDir string, driverType drivertype.DriverType) error {
|
||||
var falcoConfig falcoCfg
|
||||
|
||||
configDir = filepath.Join(configDir, "config.d")
|
||||
// First thing, check if config.d folder exists in the configuration directory.
|
||||
_, err := os.Stat(configDir)
|
||||
if os.IsNotExist(err) {
|
||||
// Create it.
|
||||
// #nosec G301 -- under /etc we want 755 permissions
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
return fmt.Errorf("unable to create directory %s: %w", configDir, err)
|
||||
}
|
||||
} else if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
falcoConfig.Engine.Kind = driverType.String()
|
||||
engineKind, err := yaml.Marshal(falcoConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal falco config: %w", err)
|
||||
}
|
||||
|
||||
// Write the engine configuration to a specialized config file.
|
||||
// #nosec G306 //under /etc we want 644 permissions
|
||||
if err := os.WriteFile(filepath.Join(configDir, falcoDriverConfigFile), engineKind, 0o644); err != nil {
|
||||
return fmt.Errorf("unable to persist engine kind to filesystem: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverconfig_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Config Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverconfig_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var driverConfigHelp = `Configure a driver for future usages with other driver subcommands.
|
||||
It will also update local Falco configuration or k8s configmap depending on the environment where it is running, to let Falco use chosen driver.
|
||||
Only supports deployments of Falco that use a driver engine, ie: one between kmod, ebpf and modern-ebpf.
|
||||
If engine.kind key is set to a non-driver driven engine, Falco configuration won't be touched.
|
||||
|
||||
Usage:
|
||||
falcoctl driver config [flags]
|
||||
|
||||
Flags:
|
||||
--configmap string Falco configmap name.
|
||||
--falco-config-dir string Falco configuration directory. (default "/etc/falco")
|
||||
-h, --help help for config
|
||||
--kubeconfig string Kubernetes config.
|
||||
--namespace string Kubernetes namespace.
|
||||
--update-falco Whether to overwrite Falco configuration (default true)
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--host-root string Driver host root to be used. (default "/")
|
||||
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
|
||||
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
--name string Driver name to be used. (default "falco")
|
||||
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
|
||||
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
|
||||
--version string Driver version to be used.
|
||||
`
|
||||
|
||||
var addAssertFailedBehavior = func(specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
var _ = Describe("config", func() {
|
||||
|
||||
var (
|
||||
driverCmd = "driver"
|
||||
configCmd = "config"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, configCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverConfigHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing failure cases for configuring a driver.
|
||||
Context("failure", func() {
|
||||
When("with non absolute host-root", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, configCmd, "--config", configFile, "--host-root", "foo/"}
|
||||
})
|
||||
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
|
||||
})
|
||||
|
||||
When("with invalid driver type", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, configCmd, "--config", configFile, "--type", "foo"}
|
||||
})
|
||||
addAssertFailedBehavior(`ERROR unsupported driver type specified: foo`)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package driverconfig defines the configure logic for the driver cmd.
|
||||
package driverconfig
|
|
@ -0,0 +1,241 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Package driver implements the driver related cmd line interface.
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
drivercleanup "github.com/falcosecurity/falcoctl/cmd/driver/cleanup"
|
||||
driverconfig "github.com/falcosecurity/falcoctl/cmd/driver/config"
|
||||
driverinstall "github.com/falcosecurity/falcoctl/cmd/driver/install"
|
||||
driverprintenv "github.com/falcosecurity/falcoctl/cmd/driver/printenv"
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
driverdistro "github.com/falcosecurity/falcoctl/pkg/driver/distro"
|
||||
driverkernel "github.com/falcosecurity/falcoctl/pkg/driver/kernel"
|
||||
drivertype "github.com/falcosecurity/falcoctl/pkg/driver/type"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewDriverCmd returns the driver command.
|
||||
func NewDriverCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
driver := &options.Driver{}
|
||||
driverTypesEnum := options.NewDriverTypes()
|
||||
var (
|
||||
driverTypesStr []string
|
||||
driverKernelRelease string
|
||||
driverKernelVersion string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "driver",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Interact with falcosecurity driver",
|
||||
Long: `Interact with falcosecurity driver.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
opt.Initialize()
|
||||
if err := config.Load(opt.ConfigFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Override "version" flag with viper config if not set by user.
|
||||
f := cmd.Flags().Lookup("version")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag version")
|
||||
} else if !f.Changed && viper.IsSet(config.DriverVersionKey) {
|
||||
val := viper.Get(config.DriverVersionKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"version\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "repo" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("repo")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag repo")
|
||||
} else if !f.Changed && viper.IsSet(config.DriverReposKey) {
|
||||
val, err := config.DriverRepos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set(f.Name, strings.Join(val, ",")); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"repo\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "name" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("name")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag name")
|
||||
} else if !f.Changed && viper.IsSet(config.DriverNameKey) {
|
||||
val := viper.Get(config.DriverNameKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"name\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "host-root" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("host-root")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag host-root")
|
||||
} else if !f.Changed && viper.IsSet(config.DriverHostRootKey) {
|
||||
val := viper.Get(config.DriverHostRootKey)
|
||||
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"host-root\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override "type" flag with viper config if not set by user.
|
||||
f = cmd.Flags().Lookup("type")
|
||||
if f == nil {
|
||||
// should never happen
|
||||
return fmt.Errorf("unable to retrieve flag type")
|
||||
} else if !f.Changed && viper.IsSet(config.DriverTypeKey) {
|
||||
val, err := config.DriverTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set(f.Name, strings.Join(val, ",")); err != nil {
|
||||
return fmt.Errorf("unable to overwrite \"type\" flag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Logic to discover correct driver to be used
|
||||
// Step 1: build up allowed driver types
|
||||
allowedDriverTypes := make([]drivertype.DriverType, 0)
|
||||
for _, dTypeStr := range driverTypesStr {
|
||||
// Ok driver type was enforced by the user
|
||||
drvType, err := drivertype.Parse(dTypeStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allowedDriverTypes = append(allowedDriverTypes, drvType)
|
||||
opt.Printer.Logger.Debug("Allowed driver",
|
||||
opt.Printer.Logger.Args("type", drvType))
|
||||
}
|
||||
|
||||
// Step 2: fetch system info (kernel release/version and distro)
|
||||
var err error
|
||||
driver.Kr, err = driverkernel.FetchInfo(driverKernelRelease, driverKernelVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt.Printer.Logger.Debug("Fetched kernel info", opt.Printer.Logger.Args(
|
||||
"arch", driver.Kr.Architecture.ToNonDeb(),
|
||||
"kernel release", driver.Kr.String(),
|
||||
"kernel version", driver.Kr.KernelVersion))
|
||||
|
||||
driver.Distro, err = driverdistro.Discover(driver.Kr, driver.HostRoot)
|
||||
if err != nil {
|
||||
if !errors.Is(err, driverdistro.ErrUnsupported) {
|
||||
return err
|
||||
}
|
||||
opt.Printer.Logger.Debug("Detected an unsupported target system; falling back at generic logic.")
|
||||
}
|
||||
opt.Printer.Logger.Debug("Discovered distro", opt.Printer.Logger.Args("target", driver.Distro))
|
||||
|
||||
driver.Type = driver.Distro.PreferredDriver(driver.Kr, allowedDriverTypes)
|
||||
if driver.Type == nil {
|
||||
return fmt.Errorf("no supported driver found for distro: %s, "+
|
||||
"kernelrelease %s, "+
|
||||
"kernelversion %s, "+
|
||||
"arch %s",
|
||||
driver.Distro.String(),
|
||||
driver.Kr.String(),
|
||||
driver.Kr.KernelVersion,
|
||||
driver.Kr.Architecture.ToNonDeb())
|
||||
}
|
||||
opt.Printer.Logger.Debug("Detected supported driver", opt.Printer.Logger.Args("type", driver.Type.String()))
|
||||
|
||||
// If empty, try to load it automatically from /usr/src sub folders,
|
||||
// using the most recent (ie: the one with greatest semver) driver version.
|
||||
if driver.Version == "" {
|
||||
driver.Version = loadDriverVersion()
|
||||
}
|
||||
return driver.Validate()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSliceVar(&driverTypesStr, "type", config.DefaultDriver.Type,
|
||||
"Driver types allowed in descending priority order "+driverTypesEnum.Allowed())
|
||||
cmd.PersistentFlags().StringVar(&driver.Version, "version", config.DefaultDriver.Version, "Driver version to be used.")
|
||||
cmd.PersistentFlags().StringSliceVar(&driver.Repos, "repo", config.DefaultDriver.Repos, "Driver repo to be used.")
|
||||
cmd.PersistentFlags().StringVar(&driver.Name, "name", config.DefaultDriver.Name, "Driver name to be used.")
|
||||
cmd.PersistentFlags().StringVar(&driver.HostRoot, "host-root", config.DefaultDriver.HostRoot, "Driver host root to be used.")
|
||||
cmd.PersistentFlags().StringVar(&driverKernelRelease,
|
||||
"kernelrelease",
|
||||
"",
|
||||
"Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' "+
|
||||
"(e.g. '6.1.0-10-cloud-amd64')")
|
||||
cmd.PersistentFlags().StringVar(&driverKernelVersion,
|
||||
"kernelversion",
|
||||
"",
|
||||
"Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' "+
|
||||
"(e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')")
|
||||
|
||||
cmd.AddCommand(driverinstall.NewDriverInstallCmd(ctx, opt, driver))
|
||||
cmd.AddCommand(driverconfig.NewDriverConfigCmd(ctx, opt, driver))
|
||||
cmd.AddCommand(drivercleanup.NewDriverCleanupCmd(ctx, opt, driver))
|
||||
cmd.AddCommand(driverprintenv.NewDriverPrintenvCmd(ctx, opt, driver))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func loadDriverVersion() string {
|
||||
isSet := false
|
||||
greatestVrs := semver.Version{}
|
||||
paths, _ := filepath.Glob("/usr/src/falco-*")
|
||||
for _, path := range paths {
|
||||
fileInfo, err := os.Stat(path)
|
||||
// We expect path to point to a folder,
|
||||
// otherwise skip it.
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !fileInfo.IsDir() {
|
||||
continue
|
||||
}
|
||||
drvVer := strings.TrimPrefix(filepath.Base(path), "falco-")
|
||||
sv, err := semver.Parse(drvVer)
|
||||
if err != nil {
|
||||
// Not a semver; return it because we
|
||||
// Won't be able to check it against semver driver versions.
|
||||
return drvVer
|
||||
}
|
||||
if sv.GT(greatestVrs) {
|
||||
greatestVrs = sv
|
||||
isSet = true
|
||||
}
|
||||
}
|
||||
if isSet {
|
||||
return greatestVrs.String()
|
||||
}
|
||||
return ""
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !linux
|
||||
|
||||
package driver
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewDriverCmd returns an empty driver command since it is not supported on non linuxes
|
||||
func NewDriverCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
return &cobra.Command{}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package driverinstall defines the installation logic for the driver cmd.
|
||||
package driverinstall
|
|
@ -0,0 +1,219 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverinstall
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
driverdistro "github.com/falcosecurity/falcoctl/pkg/driver/distro"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type driverDownloadOptions struct {
|
||||
InsecureDownload bool
|
||||
HTTPTimeout time.Duration
|
||||
HTTPHeaders string
|
||||
}
|
||||
|
||||
type driverInstallOptions struct {
|
||||
*options.Common
|
||||
*options.Driver
|
||||
Download bool
|
||||
Compile bool
|
||||
DownloadHeaders bool
|
||||
driverDownloadOptions
|
||||
}
|
||||
|
||||
// NewDriverInstallCmd returns the driver install command.
|
||||
func NewDriverInstallCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
|
||||
o := driverInstallOptions{
|
||||
Common: opt,
|
||||
Driver: driver,
|
||||
// Defaults to downloading or building if needed
|
||||
Download: true,
|
||||
Compile: true,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "install [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Install previously configured driver",
|
||||
Long: `Install previously configured driver, either downloading it or attempting a build.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
dest, err := o.RunDriverInstall(ctx)
|
||||
if dest != "" {
|
||||
// We don't care about errors at this stage
|
||||
// Fallback: try to load any available driver if leaving with an error.
|
||||
// It is only useful for kmod, as it will try to
|
||||
// modprobe a pre-existent version of the driver,
|
||||
// hoping it will be compatible.
|
||||
_ = driver.Type.Load(o.Printer, dest, o.Driver.Name, err != nil)
|
||||
}
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&o.Download, "download", true, "Whether to enable download of prebuilt drivers")
|
||||
cmd.Flags().BoolVar(&o.Compile, "compile", true, "Whether to enable local compilation of drivers")
|
||||
cmd.Flags().BoolVar(&o.DownloadHeaders, "download-headers", true, "Whether to enable automatic kernel headers download where supported")
|
||||
cmd.Flags().BoolVar(&o.InsecureDownload, "http-insecure", false, "Whether you want to allow insecure downloads or not")
|
||||
cmd.Flags().DurationVar(&o.HTTPTimeout, "http-timeout", 60*time.Second, "Timeout for each http try")
|
||||
cmd.Flags().StringVar(&o.HTTPHeaders, "http-headers",
|
||||
"",
|
||||
"Optional comma-separated list of headers for the http GET request "+
|
||||
"(e.g. --http-headers='x-emc-namespace: default,Proxy-Authenticate: Basic'). Not necessary if default repo is used")
|
||||
return cmd
|
||||
}
|
||||
|
||||
//nolint:gosec // this was an existent option in falco-driver-loader that we are porting.
|
||||
func setDefaultHTTPClientOpts(downloadOptions driverDownloadOptions) {
|
||||
// Skip insecure verify
|
||||
if downloadOptions.InsecureDownload {
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
http.DefaultClient.Timeout = downloadOptions.HTTPTimeout
|
||||
}
|
||||
|
||||
// RunDriverInstall implements the driver install command.
|
||||
func (o *driverInstallOptions) RunDriverInstall(ctx context.Context) (string, error) {
|
||||
o.Printer.Logger.Info("Running falcoctl driver install", o.Printer.Logger.Args(
|
||||
"driver version", o.Driver.Version,
|
||||
"driver type", o.Driver.Type,
|
||||
"driver name", o.Driver.Name,
|
||||
"compile", o.Compile,
|
||||
"download", o.Download,
|
||||
"target", o.Distro.String(),
|
||||
"arch", o.Kr.Architecture.ToNonDeb(),
|
||||
"kernel release", o.Kr.String(),
|
||||
"kernel version", o.Kr.KernelVersion))
|
||||
|
||||
if !o.Driver.Type.HasArtifacts() {
|
||||
o.Printer.Logger.Info("No artifacts needed for the selected driver.")
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !o.Download && !o.Compile {
|
||||
o.Printer.Logger.Info("Nothing to do: download and compile disabled.")
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if o.Distro.String() == driverdistro.UndeterminedDistro {
|
||||
if o.Compile {
|
||||
o.Download = false
|
||||
o.Printer.Logger.Info(
|
||||
"Detected an unsupported target system, please get in touch with the Falco community. Trying to compile anyway.")
|
||||
} else {
|
||||
return "", fmt.Errorf("detected an unsupported target system, please get in touch with the Falco community")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
dest string
|
||||
buf bytes.Buffer
|
||||
)
|
||||
|
||||
if !o.Printer.DisableStyling {
|
||||
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Cleaning up existing drivers")
|
||||
}
|
||||
err := o.Driver.Type.Cleanup(o.Printer.WithWriter(&buf), o.Driver.Name)
|
||||
if o.Printer.Spinner != nil {
|
||||
_ = o.Printer.Spinner.Stop()
|
||||
}
|
||||
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
|
||||
// Only print formatted text if we are formatting to json
|
||||
out := strings.ReplaceAll(buf.String(), "\n", ";")
|
||||
o.Printer.Logger.Info("Driver cleanup", o.Printer.Logger.Args("output", out))
|
||||
} else {
|
||||
// Print much more readable output as-is
|
||||
o.Printer.DefaultText.Print(buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if o.Download {
|
||||
setDefaultHTTPClientOpts(o.driverDownloadOptions)
|
||||
if !o.Printer.DisableStyling {
|
||||
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Trying to download the driver")
|
||||
}
|
||||
dest, err = driverdistro.Download(ctx, o.Distro, o.Printer.WithWriter(&buf), o.Kr, o.Driver.Name,
|
||||
o.Driver.Type, o.Driver.Version, o.Driver.Repos, o.HTTPHeaders)
|
||||
if o.Printer.Spinner != nil {
|
||||
_ = o.Printer.Spinner.Stop()
|
||||
}
|
||||
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
|
||||
// Only print formatted text if we are formatting to json
|
||||
out := strings.ReplaceAll(buf.String(), "\n", ";")
|
||||
o.Printer.Logger.Info("Driver download", o.Printer.Logger.Args("output", out))
|
||||
} else {
|
||||
// Print much more readable output as-is
|
||||
o.Printer.DefaultText.Print(buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
if err == nil {
|
||||
o.Printer.Logger.Info("Driver downloaded.", o.Printer.Logger.Args("path", dest))
|
||||
return dest, nil
|
||||
}
|
||||
if errors.Is(err, driverdistro.ErrAlreadyPresent) {
|
||||
o.Printer.Logger.Info("Skipping download, driver already present.", o.Printer.Logger.Args("path", dest))
|
||||
return dest, nil
|
||||
}
|
||||
// Print the error but go on
|
||||
// attempting a build if requested
|
||||
if o.Compile {
|
||||
o.Printer.Logger.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if o.Compile {
|
||||
if !o.Printer.DisableStyling {
|
||||
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Trying to build the driver")
|
||||
}
|
||||
dest, err = driverdistro.Build(ctx, o.Distro, o.Printer.WithWriter(&buf), o.Kr, o.Driver.Name, o.Driver.Type, o.Driver.Version, o.DownloadHeaders)
|
||||
if o.Printer.Spinner != nil {
|
||||
_ = o.Printer.Spinner.Stop()
|
||||
}
|
||||
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
|
||||
// Only print formatted text if we are formatting to json
|
||||
out := strings.ReplaceAll(buf.String(), "\n", ";")
|
||||
o.Printer.Logger.Info("Driver build", o.Printer.Logger.Args("output", out))
|
||||
} else {
|
||||
// Print much more readable output as-is
|
||||
o.Printer.DefaultText.Print(buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
if err == nil {
|
||||
return dest, nil
|
||||
}
|
||||
if errors.Is(err, driverdistro.ErrAlreadyPresent) {
|
||||
o.Printer.Logger.Info("Skipping build, driver already present.", o.Printer.Logger.Args("path", dest))
|
||||
return dest, nil
|
||||
}
|
||||
}
|
||||
|
||||
return o.Driver.Name, fmt.Errorf("failed: %w", err)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverinstall_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Install Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverinstall_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var driverInstallHelp = `Install previously configured driver, either downloading it or attempting a build.
|
||||
|
||||
Usage:
|
||||
falcoctl driver install [flags]
|
||||
|
||||
Flags:
|
||||
--compile Whether to enable local compilation of drivers (default true)
|
||||
--download Whether to enable download of prebuilt drivers (default true)
|
||||
--download-headers Whether to enable automatic kernel headers download where supported (default true)
|
||||
-h, --help help for install
|
||||
--http-headers string Optional comma-separated list of headers for the http GET request (e.g. --http-headers='x-emc-namespace: default,Proxy-Authenticate: Basic'). Not necessary if default repo is used
|
||||
--http-insecure Whether you want to allow insecure downloads or not
|
||||
--http-timeout duration Timeout for each http try (default 1m0s)
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--host-root string Driver host root to be used. (default "/")
|
||||
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
|
||||
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
--name string Driver name to be used. (default "falco")
|
||||
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
|
||||
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
|
||||
--version string Driver version to be used.
|
||||
`
|
||||
|
||||
var addAssertFailedBehavior = func(specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
var addAssertOkBehavior = func(specificOut string) {
|
||||
It("check that does not fail and the usage is not printed", func() {
|
||||
Succeed()
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificOut)))
|
||||
})
|
||||
}
|
||||
|
||||
var _ = Describe("install", func() {
|
||||
|
||||
var (
|
||||
driverCmd = "driver"
|
||||
installCmd = "install"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, installCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverInstallHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing failure cases for installing a driver.
|
||||
Context("failure", func() {
|
||||
When("with empty driver version", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, installCmd, "--config", configFile}
|
||||
})
|
||||
addAssertFailedBehavior(`ERROR version is mandatory and cannot be empty`)
|
||||
})
|
||||
|
||||
When("with non absolute host-root", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, installCmd, "--config", configFile, "--host-root", "foo/", "--version", "1.0.0+driver"}
|
||||
})
|
||||
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
|
||||
})
|
||||
|
||||
When("with invalid driver type", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, installCmd, "--config", configFile, "--type", "foo", "--version", "1.0.0+driver"}
|
||||
})
|
||||
addAssertFailedBehavior(`ERROR unsupported driver type specified: foo`)
|
||||
})
|
||||
})
|
||||
|
||||
Context("nothing-to-do", func() {
|
||||
When("with false download and compile", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, installCmd, "--config", configFile, "--download=false", "--compile=false", "--version", "1.0.0+driver"}
|
||||
})
|
||||
addAssertOkBehavior("INFO Nothing to do: download and compile disabled.")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package driverprintenv defines the logic to print driver-related variables as env vars.
|
||||
package driverprintenv
|
|
@ -0,0 +1,65 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverprintenv
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type driverPrintenvOptions struct {
|
||||
*options.Common
|
||||
*options.Driver
|
||||
}
|
||||
|
||||
// NewDriverPrintenvCmd print info about driver falcoctl config as env vars.
|
||||
func NewDriverPrintenvCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
|
||||
o := driverPrintenvOptions{
|
||||
Common: opt,
|
||||
Driver: driver,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "printenv [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Print env vars",
|
||||
Long: `Print variables used by driver as env vars.`,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return o.RunDriverPrintenv(ctx)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *driverPrintenvOptions) RunDriverPrintenv(_ context.Context) error {
|
||||
o.Printer.DefaultText.Printf("DRIVER=%q\n", o.Driver.Type.String())
|
||||
o.Printer.DefaultText.Printf("DRIVERS_REPO=%q\n", strings.Join(o.Driver.Repos, ", "))
|
||||
o.Printer.DefaultText.Printf("DRIVER_VERSION=%q\n", o.Driver.Version)
|
||||
o.Printer.DefaultText.Printf("DRIVER_NAME=%q\n", o.Driver.Name)
|
||||
o.Printer.DefaultText.Printf("HOST_ROOT=%q\n", o.Driver.HostRoot)
|
||||
o.Printer.DefaultText.Printf("TARGET_ID=%q\n", o.Distro.String())
|
||||
o.Printer.DefaultText.Printf("ARCH=%q\n", o.Kr.Architecture.ToNonDeb())
|
||||
o.Printer.DefaultText.Printf("KERNEL_RELEASE=%q\n", o.Kr.String())
|
||||
o.Printer.DefaultText.Printf("KERNEL_VERSION=%q\n", o.Kr.KernelVersion)
|
||||
fixedKr := o.Distro.FixupKernel(o.Kr)
|
||||
o.Printer.DefaultText.Printf("FIXED_KERNEL_RELEASE=%q\n", fixedKr.String())
|
||||
o.Printer.DefaultText.Printf("FIXED_KERNEL_VERSION=%q\n", fixedKr.KernelVersion)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverprintenv_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestPrintenv(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Printenv Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driverprintenv_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var driverPrintenvHelp = `Print variables used by driver as env vars.
|
||||
|
||||
Usage:
|
||||
falcoctl driver printenv [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for printenv
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--host-root string Driver host root to be used. (default "/")
|
||||
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
|
||||
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
--name string Driver name to be used. (default "falco")
|
||||
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
|
||||
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
|
||||
--version string Driver version to be used.
|
||||
`
|
||||
|
||||
var driverPrintenvDefaultConfig = `DRIVER=".*"
|
||||
DRIVERS_REPO="https:\/\/download\.falco\.org\/driver"
|
||||
DRIVER_VERSION="1.0.0\+driver"
|
||||
DRIVER_NAME="falco"
|
||||
HOST_ROOT="\/"
|
||||
TARGET_ID=".*"
|
||||
ARCH="x86_64|aarch64"
|
||||
KERNEL_RELEASE=".*"
|
||||
KERNEL_VERSION=".*"
|
||||
FIXED_KERNEL_RELEASE=".*"
|
||||
FIXED_KERNEL_VERSION=".*"
|
||||
`
|
||||
|
||||
var addAssertFailedBehavior = func(specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
var _ = Describe("printenv", func() {
|
||||
|
||||
var (
|
||||
driverCmd = "driver"
|
||||
printenvCmd = "printenv"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, printenvCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverPrintenvHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing failure cases for cleaning a driver.
|
||||
Context("failure", func() {
|
||||
When("with empty driver version", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, printenvCmd, "--config", configFile}
|
||||
})
|
||||
addAssertFailedBehavior(`ERROR version is mandatory and cannot be empty `)
|
||||
})
|
||||
|
||||
When("with non absolute host-root", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, printenvCmd, "--config", configFile, "--host-root", "foo/", "--version", "1.0.0+driver"}
|
||||
})
|
||||
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
|
||||
})
|
||||
|
||||
When("with invalid driver type", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, printenvCmd, "--config", configFile, "--type", "foo", "--version", "1.0.0+driver"}
|
||||
})
|
||||
addAssertFailedBehavior(`unsupported driver type specified: foo`)
|
||||
})
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
When("with default config values", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{driverCmd, printenvCmd, "--config", configFile, "--version", "1.0.0+driver"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Succeed()
|
||||
MatchRegexp(driverPrintenvDefaultConfig)
|
||||
Expect(string(output.Contents())).To(MatchRegexp(driverPrintenvDefaultConfig))
|
||||
// Expect that output is bash setenv compatible
|
||||
scanner := bufio.NewScanner(output)
|
||||
for scanner.Scan() {
|
||||
vals := strings.Split(scanner.Text(), "=")
|
||||
Expect(vals).Should(HaveLen(2))
|
||||
err := os.Setenv(vals[0], vals[1])
|
||||
Expect(err).Should(BeNil())
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,95 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package add
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/cache"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// IndexAddOptions contains the options for the index add command.
|
||||
type IndexAddOptions struct {
|
||||
*options.Common
|
||||
}
|
||||
|
||||
// NewIndexAddCmd returns the index add command.
|
||||
func NewIndexAddCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := IndexAddOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [NAME] [URL] [BACKEND] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Add an index to the local falcoctl configuration",
|
||||
Long: "Add an index to the local falcoctl configuration. Indexes are used to perform search operations for artifacts",
|
||||
Args: cobra.RangeArgs(2, 3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunIndexAdd(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunIndexAdd implements the index add command.
|
||||
func (o *IndexAddOptions) RunIndexAdd(ctx context.Context, args []string) error {
|
||||
var err error
|
||||
logger := o.Printer.Logger
|
||||
|
||||
name := args[0]
|
||||
url := args[1]
|
||||
backend := ""
|
||||
if len(args) > 2 {
|
||||
backend = args[2]
|
||||
}
|
||||
|
||||
logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
|
||||
indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create index cache: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Adding index", logger.Args("name", name, "path", url))
|
||||
|
||||
if err = indexCache.Add(ctx, name, backend, url); err != nil {
|
||||
return fmt.Errorf("unable to add index: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Writing cache to disk")
|
||||
if _, err = indexCache.Write(); err != nil {
|
||||
return fmt.Errorf("unable to write cache to disk: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Adding new index entry to configuration", logger.Args("file", o.ConfigFile))
|
||||
if err = config.AddIndexes([]config.Index{{
|
||||
Name: name,
|
||||
URL: url,
|
||||
Backend: backend,
|
||||
}}, o.ConfigFile); err != nil {
|
||||
return fmt.Errorf("index entry %q: %w", name, err)
|
||||
}
|
||||
|
||||
logger.Info("Index successfully added")
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package add_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
const (
|
||||
rulesfiletgz = "../../../pkg/test/data/rules.tar.gz"
|
||||
rulesfileyaml = "../../../pkg/test/data/rules.yaml"
|
||||
plugintgz = "../../../pkg/test/data/plugin.tar.gz"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
var (
|
||||
registry string
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
port int
|
||||
orasRegistry *remote.Registry
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
RunSpecs(t, "Add Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
//nolint:unused // false positive
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package add_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var indexAddUsage = `Usage:
|
||||
falcoctl index add [NAME] [URL] [BACKEND] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for add
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
//nolint:lll // no need to check for line length.
|
||||
var indexAddHelp = `Add an index to the local falcoctl configuration. Indexes are used to perform search operations for artifacts
|
||||
|
||||
Usage:
|
||||
falcoctl index add [NAME] [URL] [BACKEND] [flags]
|
||||
|
||||
Flags:
|
||||
-h, --help help for add
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
var addAssertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
var indexAddTests = Describe("add", func() {
|
||||
|
||||
var (
|
||||
indexCmd = "index"
|
||||
addCmd = "add"
|
||||
indexName = "testName"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{indexCmd, addCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(indexAddHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing failure cases for adding a new index.
|
||||
Context("failure", func() {
|
||||
When("without URL", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{indexCmd, addCmd, "--config", configFile, indexName}
|
||||
})
|
||||
addAssertFailedBehavior(indexAddUsage, "ERROR accepts between 2 and 3 arg(s), received 1")
|
||||
})
|
||||
|
||||
When("with invalid URL", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{indexCmd, addCmd, "--config", configFile, indexName, "NOTAPROTOCAL://something"}
|
||||
})
|
||||
addAssertFailedBehavior(indexAddUsage, "ERROR unable to add index: unable to fetch index \"testName\""+
|
||||
" with URL \"NOTAPROTOCAL://something\": unable to fetch index: cannot fetch index: Get "+
|
||||
"\"notaprotocal://something\": unsupported protocol scheme \"notaprotocal\"")
|
||||
})
|
||||
|
||||
When("with invalid backend", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{indexCmd, addCmd, "--config", configFile, indexName, "http://noindex", "notabackend"}
|
||||
})
|
||||
addAssertFailedBehavior(indexAddUsage, "ERROR unable to add index: unable to fetch index \"testName\" "+
|
||||
"with URL \"http://noindex\": unsupported index backend type: notabackend")
|
||||
})
|
||||
})
|
||||
|
||||
})
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
@ -12,8 +13,5 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package index implements the index commands.
|
||||
package index
|
||||
|
||||
const (
|
||||
writePermissions = 0o600
|
||||
)
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
@ -12,31 +13,31 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
package index
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd/index/add"
|
||||
"github.com/falcosecurity/falcoctl/cmd/index/list"
|
||||
"github.com/falcosecurity/falcoctl/cmd/index/remove"
|
||||
"github.com/falcosecurity/falcoctl/cmd/index/update"
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/index/add"
|
||||
"github.com/falcosecurity/falcoctl/internal/index/list"
|
||||
"github.com/falcosecurity/falcoctl/internal/index/remove"
|
||||
"github.com/falcosecurity/falcoctl/internal/index/update"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewIndexCmd returns the index command.
|
||||
func NewIndexCmd(ctx context.Context, opt *commonoptions.CommonOptions) *cobra.Command {
|
||||
func NewIndexCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "index",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Interact with index",
|
||||
Long: "Interact with index",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
opt.Initialize()
|
||||
opt.Printer.CheckErr(config.Load(opt.ConfigFile))
|
||||
return config.Load(opt.ConfigFile)
|
||||
},
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
@ -20,19 +21,19 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index"
|
||||
indexConf "github.com/falcosecurity/falcoctl/pkg/index/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
type indexListOptions struct {
|
||||
*options.CommonOptions
|
||||
*options.Common
|
||||
}
|
||||
|
||||
// NewIndexListCmd returns the index list command.
|
||||
func NewIndexListCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
func NewIndexListCmd(_ context.Context, opt *options.Common) *cobra.Command {
|
||||
o := indexListOptions{
|
||||
CommonOptions: opt,
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
@ -42,16 +43,16 @@ func NewIndexListCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Com
|
|||
Long: "List all the added indexes that were configured in falcoctl",
|
||||
Args: cobra.ExactArgs(0),
|
||||
Aliases: []string{"ls"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.Printer.CheckErr(o.RunIndexList(ctx, args))
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return o.RunIndexList()
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *indexListOptions) RunIndexList(ctx context.Context, args []string) error {
|
||||
indexConfig, err := index.NewConfig(config.IndexesFile)
|
||||
func (o *indexListOptions) RunIndexList() error {
|
||||
indexConfig, err := indexConf.New(config.IndexesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -62,9 +63,5 @@ func (o *indexListOptions) RunIndexList(ctx context.Context, args []string) erro
|
|||
data = append(data, newEntry)
|
||||
}
|
||||
|
||||
if err = o.Printer.PrintTable(output.IndexList, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return o.Printer.PrintTable(output.IndexList, data)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -0,0 +1,83 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package remove
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/cache"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type indexRemoveOptions struct {
|
||||
*options.Common
|
||||
}
|
||||
|
||||
// NewIndexRemoveCmd returns the index remove command.
|
||||
func NewIndexRemoveCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := indexRemoveOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove [INDEX1 [INDEX2 ...]] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Remove an index from the local falcoctl configuration",
|
||||
Long: "Remove an index from the local falcoctl configuration",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Aliases: []string{"rm"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunIndexRemove(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *indexRemoveOptions) RunIndexRemove(ctx context.Context, args []string) error {
|
||||
logger := o.Printer.Logger
|
||||
|
||||
logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
|
||||
indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create index cache: %w", err)
|
||||
}
|
||||
|
||||
for _, name := range args {
|
||||
logger.Info("Removing index", logger.Args("name", name))
|
||||
if err = indexCache.Remove(name); err != nil {
|
||||
return fmt.Errorf("unable to remove index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Writing cache to disk")
|
||||
if _, err = indexCache.Write(); err != nil {
|
||||
return fmt.Errorf("unable to write cache to disk: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Removing indexes entries from configuration", logger.Args("file", o.ConfigFile))
|
||||
if err = config.RemoveIndexes(args, o.ConfigFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Indexes successfully removed")
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -0,0 +1,77 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package update
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/pkg/index/cache"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
type indexUpdateOptions struct {
|
||||
*options.Common
|
||||
}
|
||||
|
||||
// NewIndexUpdateCmd returns the index update command.
|
||||
func NewIndexUpdateCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := indexUpdateOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update [INDEX1 [INDEX2 ...]] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Update an existing index",
|
||||
Long: "Update an existing index",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunIndexUpdate(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *indexUpdateOptions) RunIndexUpdate(ctx context.Context, args []string) error {
|
||||
logger := o.Printer.Logger
|
||||
|
||||
logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
|
||||
indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create index cache: %w", err)
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
logger.Info("Updating index file", logger.Args("name", arg))
|
||||
if err := indexCache.Update(ctx, arg); err != nil {
|
||||
return fmt.Errorf("an error occurred while updating index %q: %w", arg, err)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Writing cache to disk")
|
||||
if _, err = indexCache.Write(); err != nil {
|
||||
return fmt.Errorf("unable to write cache to disk: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Indexes successfully updated")
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd/registry/auth/basic"
|
||||
"github.com/falcosecurity/falcoctl/cmd/registry/auth/gcp"
|
||||
"github.com/falcosecurity/falcoctl/cmd/registry/auth/oauth"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewAuthCmd returns the registry command.
|
||||
func NewAuthCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Handle authentication towards OCI registries",
|
||||
Long: "Handle authentication towards OCI registries",
|
||||
}
|
||||
|
||||
cmd.AddCommand(basic.NewBasicCmd(ctx, opt))
|
||||
cmd.AddCommand(oauth.NewOauthCmd(ctx, opt))
|
||||
cmd.AddCommand(gcp.NewGcpCmd(ctx, opt))
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package basic
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/term"
|
||||
"oras.land/oras-go/v2/registry/remote/credentials"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/login/basic"
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
type loginOptions struct {
|
||||
*options.Common
|
||||
username string
|
||||
password string
|
||||
passwordFromStdin bool
|
||||
}
|
||||
|
||||
// NewBasicCmd returns the basic command.
|
||||
func NewBasicCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := loginOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "basic [hostname]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Login to an OCI registry",
|
||||
Long: `Login to an OCI registry
|
||||
|
||||
Example - Log in with username and password from command line flags:
|
||||
falcoctl registry auth basic -u username -p password localhost:5000
|
||||
|
||||
Example - Login with username and password from env variables:
|
||||
FALCOCTL_REGISTRY_AUTH_BASIC_USERNAME=username FALCOCTL_REGISTRY_AUTH_BASIC_PASSWORD=password falcoctl registry auth basic localhost:5000
|
||||
|
||||
Example - Login with username and password from stdin:
|
||||
falcoctl registry auth basic -u username --password-stdin localhost:5000
|
||||
|
||||
Example - Login with username and password in an interactive prompt:
|
||||
falcoctl registry auth basic localhost:5000
|
||||
`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
_ = viper.BindPFlag("registry.auth.basic.username", cmd.Flags().Lookup("username"))
|
||||
_ = viper.BindPFlag("registry.auth.basic.password", cmd.Flags().Lookup("password"))
|
||||
_ = viper.BindPFlag("registry.auth.basic.password_stdin", cmd.Flags().Lookup("password-stdin"))
|
||||
|
||||
o.username = viper.GetString("registry.auth.basic.username")
|
||||
o.password = viper.GetString("registry.auth.basic.password")
|
||||
o.passwordFromStdin = viper.GetBool("registry.auth.basic.password_stdin")
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunBasic(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&o.username, "username", "u", "", "registry username")
|
||||
cmd.Flags().StringVarP(&o.password, "password", "p", "", "registry password")
|
||||
cmd.Flags().BoolVar(&o.passwordFromStdin, "password-stdin", false, "read password from stdin")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunBasic executes the business logic for the basic command.
|
||||
func (o *loginOptions) RunBasic(ctx context.Context, args []string) error {
|
||||
var reg string
|
||||
logger := o.Printer.Logger
|
||||
|
||||
// Allow to have the registry expressed as a ref, but actually extract it.
|
||||
reg, err := utils.GetRegistryFromRef(args[0])
|
||||
if err != nil {
|
||||
reg = args[0]
|
||||
}
|
||||
|
||||
if err := getCredentials(o.Printer, o); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create empty client
|
||||
client := authn.NewClient()
|
||||
|
||||
// create credential store
|
||||
credentialStore, err := credentials.NewStore(config.RegistryCredentialConfPath(), credentials.StoreOptions{
|
||||
AllowPlaintextPut: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create new store: %w", err)
|
||||
}
|
||||
|
||||
if err := basic.Login(ctx, client, credentialStore, reg, o.username, o.password); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debug("Credentials added", logger.Args("credential store", config.RegistryCredentialConfPath()))
|
||||
logger.Info("Login succeeded", logger.Args("registry", reg, "user", o.username))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCredentials is used to retrieve username and password from standard input.
|
||||
func getCredentials(p *output.Printer, opt *loginOptions) error {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
if opt.username == "" {
|
||||
p.DefaultText.Print(p.FormatTitleAsLoggerInfo("Enter username:"))
|
||||
username, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt.username = strings.TrimSpace(username)
|
||||
}
|
||||
|
||||
if opt.password == "" {
|
||||
if opt.passwordFromStdin {
|
||||
password, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt.password = strings.TrimSuffix(string(password), "\n")
|
||||
opt.password = strings.TrimSuffix(opt.password, "\r")
|
||||
} else {
|
||||
p.DefaultText.Print(p.FormatTitleAsLoggerInfo("Enter password: "))
|
||||
bytePassword, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opt.password = string(bytePassword)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package basic_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
var (
|
||||
registry string
|
||||
registryBasic string
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
port int
|
||||
portBasic int
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
var err error
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
portBasic, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
registryBasic = fmt.Sprintf("localhost:%d", portBasic)
|
||||
RunSpecs(t, "Auth Basic Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
config := &configuration.Configuration{}
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
|
||||
testHtpasswdFileBasename := "authtest.htpasswd"
|
||||
testUsername, testPassword := "username", "password"
|
||||
|
||||
pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
htpasswdPath := filepath.Join(GinkgoT().TempDir(), testHtpasswdFileBasename)
|
||||
err = os.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
tlsConfig, err := testutils.BuildRegistryTLSConfig(GinkgoT().TempDir(), []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"})
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
configBasic := &configuration.Configuration{}
|
||||
configBasic.HTTP.Addr = fmt.Sprintf("localhost:%d", portBasic)
|
||||
configBasic.Auth = configuration.Auth{
|
||||
"htpasswd": configuration.Parameters{
|
||||
"realm": "localhost",
|
||||
"path": htpasswdPath,
|
||||
},
|
||||
}
|
||||
configBasic.HTTP.DrainTimeout = time.Duration(10) * time.Second
|
||||
configBasic.HTTP.TLS.CipherSuites = tlsConfig.CipherSuites
|
||||
configBasic.HTTP.TLS.Certificate = tlsConfig.CertificatePath
|
||||
configBasic.HTTP.TLS.Key = tlsConfig.PrivateKeyPath
|
||||
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Start the local registry with basic authentication.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), configBasic)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("https://%s", configBasic.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
//nolint:unused // false positive
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package basic_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Registry Registry `yaml:"registry"`
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
Auth Auth `yaml:"auth"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
OAuth []OAuth `yaml:"oauth"`
|
||||
}
|
||||
|
||||
type OAuth struct {
|
||||
Registry string `yaml:"registry"`
|
||||
ClientSecret string `yaml:"clientsecret"`
|
||||
ClientID string `yaml:"clientid"`
|
||||
TokerURL string `yaml:"tokenurl"`
|
||||
}
|
||||
|
||||
//nolint:lll,unused // no need to check for line length.
|
||||
var registryAuthBasicUsage = `Usage:
|
||||
falcoctl registry auth basic [hostname]
|
||||
|
||||
Flags:
|
||||
-h, --help help for basic
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
|
||||
-v, --verbose Enable verbose logs (default false)
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthBasicHelp = `Login to an OCI registry
|
||||
|
||||
Example - Log in with username and password from command line flags:
|
||||
falcoctl registry auth basic -u username -p password localhost:5000
|
||||
|
||||
Example - Login with username and password from env variables:
|
||||
FALCOCTL_REGISTRY_AUTH_BASIC_USERNAME=username FALCOCTL_REGISTRY_AUTH_BASIC_PASSWORD=password falcoctl registry auth basic localhost:5000
|
||||
|
||||
Example - Login with username and password from stdin:
|
||||
falcoctl registry auth basic -u username --password-stdin localhost:5000
|
||||
|
||||
Example - Login with username and password in an interactive prompt:
|
||||
falcoctl registry auth basic localhost:5000
|
||||
|
||||
Usage:
|
||||
falcoctl registry auth basic [hostname]
|
||||
|
||||
Flags:
|
||||
-h, --help help for basic
|
||||
-p, --password string registry password
|
||||
--password-stdin read password from stdin
|
||||
-u, --username string registry username
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthBasicAssertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthBasicTests = Describe("auth", func() {
|
||||
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
registryCmd = "registry"
|
||||
authCmd = "auth"
|
||||
basicCmd = "basic"
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, authCmd, basicCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryAuthBasicHelp)))
|
||||
})
|
||||
})
|
||||
Context("failure", func() {
|
||||
|
||||
When("without hostname", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, authCmd, basicCmd}
|
||||
})
|
||||
registryAuthBasicAssertFailedBehavior(registryAuthBasicUsage,
|
||||
"ERROR accepts 1 arg(s), received 0")
|
||||
})
|
||||
})
|
||||
|
||||
})
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
@ -12,5 +13,5 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package login defines the logic to authenticate against an OCI registry.
|
||||
package login
|
||||
// Package basic defines the logic to authenticate against an OCI registry.
|
||||
package basic
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package auth defines the logic to authenticate against an OCI registry.
|
||||
package auth
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package gcp defines the logic to authenticate against an Artifact registry using GCP credentials.
|
||||
package gcp
|
|
@ -0,0 +1,84 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/login/gcp"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
const (
|
||||
longGcp = `Register an Artifact Registry to use GCP Application Default credentials to connect to it.
|
||||
|
||||
In particular, it can use Workload Identity or GCE metadata server to authenticate.
|
||||
|
||||
Example
|
||||
falcoctl registry auth gcp europe-docker.pkg.dev
|
||||
`
|
||||
)
|
||||
|
||||
// RegistryGcpOptions contains the options for the registry gcp command.
|
||||
type RegistryGcpOptions struct {
|
||||
*options.Common
|
||||
}
|
||||
|
||||
// NewGcpCmd returns the gcp command.
|
||||
func NewGcpCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := RegistryGcpOptions{
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "gcp [REGISTRY]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Register an Artifact Registry to log in using GCP Application Default credentials",
|
||||
Long: longGcp,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunGcp(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunGcp executes the business logic for the gcp command.
|
||||
func (o *RegistryGcpOptions) RunGcp(ctx context.Context, args []string) error {
|
||||
var err error
|
||||
logger := o.Printer.Logger
|
||||
reg := args[0]
|
||||
if err = gcp.Login(ctx, reg); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("GCP authentication successful", logger.Args("registry", reg))
|
||||
|
||||
logger.Debug("Adding new gcp entry to configuration", logger.Args("file", o.ConfigFile))
|
||||
if err = config.AddGcp([]config.GcpAuth{{
|
||||
Registry: reg,
|
||||
}}, o.ConfigFile); err != nil {
|
||||
return fmt.Errorf("index entry %q: %w", reg, err)
|
||||
}
|
||||
|
||||
logger.Info("GCG authentication entry successfully added", logger.Args("registry", reg, "confgi file", o.ConfigFile))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
@ -22,8 +23,9 @@ import (
|
|||
"golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/internal/login/oauth"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -42,44 +44,38 @@ Example
|
|||
|
||||
// RegistryOauthOptions contains the options for the registry oauth command.
|
||||
type RegistryOauthOptions struct {
|
||||
*options.CommonOptions
|
||||
*options.Common
|
||||
Conf clientcredentials.Config
|
||||
}
|
||||
|
||||
// NewOauthCmd returns the oauth command.
|
||||
func NewOauthCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command {
|
||||
func NewOauthCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := RegistryOauthOptions{
|
||||
CommonOptions: opt,
|
||||
Common: opt,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "oauth",
|
||||
Use: "oauth [HOSTNAME]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Retrieve access and refresh tokens for OAuth2.0 client credentials flow authentication",
|
||||
Long: longOauth,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
opt.Initialize()
|
||||
opt.Printer.CheckErr(config.Load(opt.ConfigFile))
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.Printer.CheckErr(o.RunOauth(ctx, args))
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunOAuth(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&o.Conf.TokenURL, "token-url", "", "token URL used to get access and refresh tokens")
|
||||
if err := cmd.MarkFlagRequired("token-url"); err != nil {
|
||||
o.Printer.Error.Println("unable to mark flag \"token-url\" as required")
|
||||
return nil
|
||||
output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"token-url\" as required"))
|
||||
}
|
||||
cmd.Flags().StringVar(&o.Conf.ClientID, "client-id", "", "client ID of the OAuth2.0 app")
|
||||
if err := cmd.MarkFlagRequired("client-id"); err != nil {
|
||||
o.Printer.Error.Println("unable to mark flag \"client-id\" as required")
|
||||
return nil
|
||||
output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"client-id\" as required"))
|
||||
}
|
||||
cmd.Flags().StringVar(&o.Conf.ClientSecret, "client-secret", "", "client secret of the OAuth2.0 app")
|
||||
if err := cmd.MarkFlagRequired("client-secret"); err != nil {
|
||||
o.Printer.Error.Println("unable to mark flag \"client-secret\" as required")
|
||||
output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"client-secret\" as required"))
|
||||
return nil
|
||||
}
|
||||
cmd.Flags().StringSliceVar(&o.Conf.Scopes, "scopes", nil, "comma separeted list of scopes for which requesting access")
|
||||
|
@ -87,46 +83,12 @@ func NewOauthCmd(ctx context.Context, opt *options.CommonOptions) *cobra.Command
|
|||
return cmd
|
||||
}
|
||||
|
||||
// RunOauth implements the registry oauth command.
|
||||
func (o *RegistryOauthOptions) RunOauth(ctx context.Context, args []string) error {
|
||||
// RunOAuth executes the business logic for the oauth command.
|
||||
func (o *RegistryOauthOptions) RunOAuth(ctx context.Context, args []string) error {
|
||||
reg := args[0]
|
||||
|
||||
// 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)
|
||||
if err := oauth.Login(ctx, reg, &o.Conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.OauthAuthsKey, 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)
|
||||
|
||||
o.Printer.Logger.Info("Client credentials correctly saved", o.Printer.Logger.Args("file", config.ClientCredentialsFile))
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package oauth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
var (
|
||||
registry string
|
||||
oauthServer string
|
||||
oauthPort int
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
port int
|
||||
orasRegistry *remote.Registry
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestOAuth(t *testing.T) {
|
||||
var err error
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
oauthPort, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
RunSpecs(t, "OAuth Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
|
||||
// Get the current user's home directory
|
||||
usr, err := user.Current()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Construct the path for the .config directory
|
||||
configDir := filepath.Join(usr.HomeDir, ".config", "falcoctl")
|
||||
|
||||
// Check if the directory already exists
|
||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||
// Directory doesn't exist, create it
|
||||
err := os.MkdirAll(configDir, 0o755)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
config := &configuration.Configuration{}
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create the oras registry.
|
||||
orasRegistry, err = testutils.NewOrasRegistry(registry, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
go func() {
|
||||
err := testutils.StartOAuthServer(context.Background(), oauthPort)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
//nolint:unused // false positive
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package oauth_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Registry Registry `yaml:"registry"`
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
Auth Auth `yaml:"auth"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
OAuth []OAuth `yaml:"oauth"`
|
||||
}
|
||||
|
||||
type OAuth struct {
|
||||
Registry string `yaml:"registry"`
|
||||
ClientSecret string `yaml:"clientsecret"`
|
||||
ClientID string `yaml:"clientid"`
|
||||
TokerURL string `yaml:"tokenurl"`
|
||||
}
|
||||
|
||||
//nolint:unused // false positive
|
||||
var correctIndexConfig = `indexes:
|
||||
- name: falcosecurity
|
||||
url: https://falcosecurity.github.io/falcoctl/index.yaml
|
||||
`
|
||||
|
||||
//nolint:lll,unused // no need to check for line length.
|
||||
var registryAuthOAuthUsage = `Usage:
|
||||
falcoctl registry auth oauth [HOSTNAME]
|
||||
|
||||
Flags:
|
||||
--client-id string client ID of the OAuth2.0 app
|
||||
--client-secret string client secret of the OAuth2.0 app
|
||||
-h, --help help for oauth
|
||||
--scopes strings comma separeted list of scopes for which requesting access
|
||||
--token-url string token URL used to get access and refresh tokens
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--log-format string Set formatting for logs (color, text, json) (default "color")
|
||||
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
|
||||
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthOAuthHelp = `Store client credentials for later OAuth2.0 authentication
|
||||
|
||||
Client credentials will be saved in the ~/.config directory.
|
||||
|
||||
Example
|
||||
falcoctl registry oauth \
|
||||
--token-url="http://localhost:9096/token" \
|
||||
--client-id=000000 \
|
||||
--client-secret=999999 --scopes="my-scope" \
|
||||
hostname
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthOAuthAssertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryAuthOAuthTests = Describe("auth", func() {
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
registryCmd = "registry"
|
||||
authCmd = "auth"
|
||||
oauthCmd = "oauth"
|
||||
anSource = "myrepo.com/rules.git"
|
||||
artifact = "generic-repo"
|
||||
repo = "/" + artifact
|
||||
tag = "tag"
|
||||
repoAndTag = repo + ":" + tag
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, authCmd, oauthCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryAuthOAuthHelp)))
|
||||
})
|
||||
})
|
||||
Context("failure", func() {
|
||||
|
||||
When("without hostname", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, authCmd, oauthCmd}
|
||||
})
|
||||
registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
|
||||
"ERROR accepts 1 arg(s), received 0")
|
||||
})
|
||||
|
||||
When("wrong client id", func() {
|
||||
BeforeEach(func() {
|
||||
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err = os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
args = []string{registryCmd, authCmd, oauthCmd,
|
||||
"--client-id=000001", "--client-secret=999999",
|
||||
"--token-url", fmt.Sprintf("http://localhost:%d/token", oauthPort),
|
||||
"--config", configFilePath,
|
||||
"127.0.0.1:5000",
|
||||
}
|
||||
})
|
||||
registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
|
||||
`ERROR wrong client credentials, unable to retrieve token`)
|
||||
})
|
||||
|
||||
When("wrong client secret", func() {
|
||||
BeforeEach(func() {
|
||||
// start the OAuthServer
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath := baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err := os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
args = []string{registryCmd, authCmd, oauthCmd,
|
||||
"--client-id=000000", "--client-secret=999998",
|
||||
"--token-url", fmt.Sprintf("http://localhost:%d/token", oauthPort),
|
||||
"--config", configFilePath,
|
||||
"127.0.0.1:5000",
|
||||
}
|
||||
})
|
||||
registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
|
||||
`ERROR wrong client credentials, unable to retrieve token`)
|
||||
})
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
var (
|
||||
configFilePath string
|
||||
)
|
||||
|
||||
When("all good", func() {
|
||||
BeforeEach(func() {
|
||||
baseDir := GinkgoT().TempDir()
|
||||
configFilePath = baseDir + "/config.yaml"
|
||||
content := []byte(correctIndexConfig)
|
||||
err = os.WriteFile(configFilePath, content, 0o644)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
args = []string{registryCmd, authCmd, oauthCmd,
|
||||
"--client-id=000000", "--client-secret=999999",
|
||||
"--token-url", fmt.Sprintf("http://localhost:%d/token", oauthPort),
|
||||
"--config", configFilePath,
|
||||
registry,
|
||||
}
|
||||
})
|
||||
|
||||
It("should successed", func() {
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
|
||||
`INFO Client credentials correctly saved`)))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package registry implements the registry commands.
|
||||
package registry
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -0,0 +1,144 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
longPull = `Pull Falco "rulesfile" or "plugin" OCI artifacts from remote registry.
|
||||
|
||||
Artifact references are passed as arguments.
|
||||
|
||||
A reference is a fully qualified reference ("<registry>/<repository>"),
|
||||
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
|
||||
|
||||
Example - Pull artifact "myplugin" for the platform where falcoctl is running (default) in the current working directory (default):
|
||||
falcoctl registry pull localhost:5000/myplugin:latest
|
||||
|
||||
Example - Pull artifact "myplugin" for platform "linux/arm64" in the current working directory (default):
|
||||
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64
|
||||
|
||||
Example - Pull artifact "myplugin" for platform "linux/arm64" in "myDir" directory:
|
||||
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64 --dest-dir=./myDir
|
||||
|
||||
Example - Pull artifact "myrulesfile":
|
||||
falcoctl registry pull localhost:5000/myrulesfile:latest
|
||||
`
|
||||
)
|
||||
|
||||
type pullOptions struct {
|
||||
*options.Common
|
||||
*options.Artifact
|
||||
*options.Registry
|
||||
destDir string
|
||||
}
|
||||
|
||||
func (o *pullOptions) Validate() error {
|
||||
return o.Artifact.Validate()
|
||||
}
|
||||
|
||||
// NewPullCmd returns the pull command.
|
||||
func NewPullCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := pullOptions{
|
||||
Common: opt,
|
||||
Artifact: &options.Artifact{},
|
||||
Registry: &options.Registry{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "pull hostname/repo[:tag|@digest] [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Pull a Falco OCI artifact from remote registry",
|
||||
Long: longPull,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := o.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := args[0]
|
||||
|
||||
_, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Common.Initialize()
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.RunPull(ctx, args)
|
||||
},
|
||||
}
|
||||
|
||||
o.Registry.AddFlags(cmd)
|
||||
output.ExitOnErr(o.Printer, o.Artifact.AddFlags(cmd))
|
||||
cmd.Flags().StringVarP(&o.destDir, "dest-dir", "o", "", "destination dir where to save the artifacts(default: current directory)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunPull executes the business logic for the pull command.
|
||||
func (o *pullOptions) RunPull(ctx context.Context, args []string) error {
|
||||
logger := o.Printer.Logger
|
||||
ref := args[0]
|
||||
|
||||
registry, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
puller, err := ociutils.Puller(o.PlainHTTP, o.Printer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while creating the puller for registry %s: %w", registry, err)
|
||||
}
|
||||
|
||||
err = ociutils.CheckConnectionForRegistry(ctx, puller.Client, o.PlainHTTP, registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Preparing to pull artifact", logger.Args("name", args[0]))
|
||||
|
||||
if o.destDir == "" {
|
||||
logger.Info("Pulling artifact in the current directory")
|
||||
} else {
|
||||
logger.Info("Pulling artifact in", logger.Args("directory", o.destDir))
|
||||
}
|
||||
|
||||
os, arch := runtime.GOOS, runtime.GOARCH
|
||||
if len(o.Artifact.Platforms) > 0 {
|
||||
os, arch = o.OSArch(0)
|
||||
}
|
||||
|
||||
res, err := puller.Pull(ctx, ref, o.destDir, os, arch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Artifact pulled", logger.Args("name", args[0], "type", res.Type, "digest", res.Digest))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pull_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/spf13/cobra"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
const (
|
||||
rulesfiletgz = "../../../pkg/test/data/rules.tar.gz"
|
||||
rulesfileyaml = "../../../pkg/test/data/rules.yaml"
|
||||
plugintgz = "../../../pkg/test/data/plugin.tar.gz"
|
||||
)
|
||||
|
||||
//nolint:unused // false positive
|
||||
var (
|
||||
registry string
|
||||
ctx = context.Background()
|
||||
output = gbytes.NewBuffer()
|
||||
rootCmd *cobra.Command
|
||||
opt *commonoptions.Common
|
||||
port int
|
||||
orasRegistry *remote.Registry
|
||||
configFile string
|
||||
err error
|
||||
args []string
|
||||
)
|
||||
|
||||
func TestPull(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
port, err = testutils.FreePort()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
registry = fmt.Sprintf("localhost:%d", port)
|
||||
RunSpecs(t, "Pull Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
config := &configuration.Configuration{}
|
||||
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
|
||||
// Create and configure the common options.
|
||||
opt = commonoptions.NewOptions()
|
||||
opt.Initialize(commonoptions.WithWriter(output))
|
||||
|
||||
// Create the oras registry.
|
||||
orasRegistry, err = testutils.NewOrasRegistry(registry, true)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Start the local registry.
|
||||
go func() {
|
||||
err := testutils.StartRegistry(context.Background(), config)
|
||||
Expect(err).ToNot(BeNil())
|
||||
}()
|
||||
|
||||
// Check that the registry is up and accepting connections.
|
||||
Eventually(func(g Gomega) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
|
||||
g.Expect(err).ShouldNot(HaveOccurred())
|
||||
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
|
||||
return err
|
||||
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
|
||||
|
||||
// Create temporary directory used to save the configuration file.
|
||||
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
configDir := filepath.Dir(configFile)
|
||||
Expect(os.RemoveAll(configDir)).Should(Succeed())
|
||||
})
|
||||
|
||||
//nolint:unused // false positive
|
||||
func executeRoot(args []string) error {
|
||||
rootCmd.SetArgs(args)
|
||||
rootCmd.SetOut(output)
|
||||
return cmd.Execute(rootCmd, opt)
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pull_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
|
||||
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
|
||||
out "github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
//nolint:lll,unused // no need to check for line length.
|
||||
var registryPullUsage = `Usage:
|
||||
falcoctl registry pull hostname/repo[:tag|@digest] [flags]
|
||||
|
||||
Flags:
|
||||
-o, --dest-dir string destination dir where to save the artifacts(default: current directory)
|
||||
-h, --help help for pull
|
||||
--plain-http allows interacting with remote registry via plain http requests
|
||||
--platform stringArray os and architecture of the artifact in OS/ARCH format (only for plugins artifacts)
|
||||
|
||||
Global Flags:
|
||||
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
|
||||
--disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
|
||||
-v, --verbose Enable verbose logs (default false)
|
||||
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryPullHelp = `Pull Falco "rulesfile" or "plugin" OCI artifacts from remote registry.
|
||||
|
||||
Artifact references are passed as arguments.
|
||||
|
||||
A reference is a fully qualified reference ("<registry>/<repository>"),
|
||||
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
|
||||
|
||||
Example - Pull artifact "myplugin" for the platform where falcoctl is running (default) in the current working directory (default):
|
||||
falcoctl registry pull localhost:5000/myplugin:latest
|
||||
|
||||
Example - Pull artifact "myplugin" for platform "linux/arm64" in the current working directory (default):
|
||||
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64
|
||||
|
||||
Example - Pull artifact "myplugin" for platform "linux/arm64" in "myDir" directory:
|
||||
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64 --dest-dir=./myDir
|
||||
|
||||
Example - Pull artifact "myrulesfile":
|
||||
falcoctl registry pull localhost:5000/myrulesfile:latest
|
||||
`
|
||||
|
||||
//nolint:unused // false positive
|
||||
var pullAssertFailedBehavior = func(usage, specificError string) {
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:unused // false positive
|
||||
var registryPullTests = Describe("pull", func() {
|
||||
var (
|
||||
pusher *ocipusher.Pusher
|
||||
ref string
|
||||
config ocipusher.Option
|
||||
)
|
||||
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
registryCmd = "registry"
|
||||
pullCmd = "pull"
|
||||
dep1 = "myplugin:1.2.3"
|
||||
dep2 = "myplugin1:1.2.3|otherplugin:3.2.1"
|
||||
req = "engine_version:15"
|
||||
anSource = "myrepo.com/rules.git"
|
||||
artifact = "generic-repo"
|
||||
repo = "/" + artifact
|
||||
tag = "tag"
|
||||
repoAndTag = repo + ":" + tag
|
||||
)
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("help message", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pullCmd, "--help"}
|
||||
})
|
||||
|
||||
It("should match the saved one", func() {
|
||||
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryPullHelp)))
|
||||
})
|
||||
})
|
||||
|
||||
// Here we are testing all the failure cases using both the rulesfile and plugin artifact types.
|
||||
// The common logic for the artifacts is tested once using a rulesfile artifact, no need to repeat
|
||||
// the same test using a plugin artifact.
|
||||
Context("failure", func() {
|
||||
var (
|
||||
tracker out.Tracker
|
||||
options []ocipusher.Option
|
||||
filePathsAndPlatforms ocipusher.Option
|
||||
destDir string
|
||||
)
|
||||
const (
|
||||
plainHTTP = true
|
||||
testPluginPlatform1 = "linux/amd64"
|
||||
)
|
||||
|
||||
When("without artifact", func() {
|
||||
BeforeEach(func() {
|
||||
args = []string{registryCmd, pullCmd}
|
||||
})
|
||||
pullAssertFailedBehavior(registryPullUsage, "ERROR accepts 1 arg(s), received 0")
|
||||
})
|
||||
|
||||
When("unreachable registry", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{registryCmd, pullCmd, "noregistry/testrules", "--plain-http", "--config", configFile}
|
||||
})
|
||||
pullAssertFailedBehavior(registryPullUsage, "ERROR unable to connect to remote registry")
|
||||
})
|
||||
|
||||
When("invalid repository", func() {
|
||||
newReg := registry + "/wrong:latest"
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{registryCmd, pullCmd, newReg, "--plain-http", "--config", configFile}
|
||||
})
|
||||
pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERROR %s: not found", newReg))
|
||||
})
|
||||
|
||||
When("unwritable --dest-dir", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
destDir = GinkgoT().TempDir()
|
||||
err = os.Chmod(destDir, 0o555)
|
||||
Expect(err).To(BeNil())
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
args = []string{registryCmd, pullCmd, ref, "--plain-http",
|
||||
"--platform", testPluginPlatform1, "--dest-dir", destDir,
|
||||
"--config", configFile,
|
||||
}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
tmp := strings.Split(repoAndTag, "/")
|
||||
artNameAndTag := tmp[len(tmp)-1]
|
||||
tmp = strings.Split(artNameAndTag, ":")
|
||||
artName := tmp[0]
|
||||
tag := tmp[1]
|
||||
expectedError := fmt.Sprintf(
|
||||
"ERROR unable to pull artifact generic-repo with %s tag from repo %s: failed to create file",
|
||||
tag, artName)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("--dest-dir not present (and parent not writable)", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
baseDir := GinkgoT().TempDir()
|
||||
err = os.Chmod(baseDir, 0o555)
|
||||
Expect(err).To(BeNil())
|
||||
destDir = baseDir + "/dest"
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
args = []string{registryCmd, pullCmd, ref, "--plain-http",
|
||||
"--platform", testPluginPlatform1, "--dest-dir", destDir,
|
||||
"--config", configFile,
|
||||
}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR unable to pull artifact %s with tag %s from repo %s: failed to ensure directories of the target path: "+
|
||||
"mkdir %s: permission denied", artifact, tag, artifact, destDir)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("wrong digest format", func() {
|
||||
wrongDigest := "sha256:06f961b802bc46ee168555f066d28f4f0e9afdf3f88174c1ee6f9de004fc30a0"
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag + "@" + wrongDigest
|
||||
args = []string{registryCmd, pullCmd, ref, "--plain-http",
|
||||
"--platform", testPluginPlatform1, "--config", configFile}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR %s: not found", registry+repo+"@"+wrongDigest)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("missing repository", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
ref = repoAndTag
|
||||
args = []string{registryCmd, pullCmd, ref, "--plain-http", "--config", configFile}
|
||||
})
|
||||
|
||||
It("check that fails and the usage is not printed", func() {
|
||||
expectedError := fmt.Sprintf("ERROR cannot extract registry name from ref %q", ref)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
|
||||
})
|
||||
})
|
||||
|
||||
When("invalid repository", func() {
|
||||
newReg := registry + "/wrong@something"
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
args = []string{registryCmd, pullCmd, newReg, "--plain-http", "--config", configFile}
|
||||
})
|
||||
pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERROR unable to create new repository with ref %s: "+
|
||||
"invalid reference: invalid digest %q: invalid checksum digest format\n", newReg, "something"))
|
||||
})
|
||||
|
||||
When("invalid platform", func() {
|
||||
BeforeEach(func() {
|
||||
configDir := GinkgoT().TempDir()
|
||||
configFile := filepath.Join(configDir, ".config")
|
||||
_, err := os.Create(configFile)
|
||||
Expect(err).To(BeNil())
|
||||
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
|
||||
ref = registry + repoAndTag
|
||||
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
|
||||
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
|
||||
options = []ocipusher.Option{filePathsAndPlatforms, config}
|
||||
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).ToNot(BeNil())
|
||||
ref = registry + repoAndTag
|
||||
args = []string{registryCmd, pullCmd, ref, "--plain-http",
|
||||
"--platform", "linux/unknown", "--config", configFile}
|
||||
})
|
||||
|
||||
pullAssertFailedBehavior(registryPullUsage, "not found: no matching manifest was found in the manifest list")
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
|
@ -0,0 +1,343 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package push
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/pterm/pterm"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
|
||||
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/options"
|
||||
"github.com/falcosecurity/falcoctl/pkg/output"
|
||||
)
|
||||
|
||||
const (
|
||||
longPush = `Push Falco "rulesfile" or "plugin" OCI artifacts to remote registry
|
||||
|
||||
Example - Push artifact "myplugin.tar.gz" of type "plugin" for the platform where falcoctl is running (default):
|
||||
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest myplugin.tar.gz
|
||||
|
||||
Example - Push artifact "myplugin.tar.gz" of type "plugin" for platform "linux/arm64":
|
||||
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest myplugin.tar.gz --platform linux/arm64
|
||||
|
||||
Example - Push artifact "myplugin.tar.gz" of type "plugin" for multiple platforms:
|
||||
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest \
|
||||
myplugin-linux-x86_64.tar.gz --platform linux/x86_64 \
|
||||
myplugin-linux-arm64.tar.gz --platform linux/arm64
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile":
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with floating tags for the major and minor versions (0 and 0.1):
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
|
||||
--add-floating-tags
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" to an insecure registry:
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" --plain-http localhost:5000/myrulesfile:latest myrulesfile.tar.gz
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with a dependency "myplugin:1.2.3":
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
|
||||
--depends-on myplugin:1.2.3
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with a dependency "myplugin:1.2.3" and an alternative "otherplugin:3.2.1":
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
|
||||
--depends-on "myplugin:1.2.3|otherplugin:3.2.1"
|
||||
|
||||
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with multiple dependencies "myplugin:1.2.3", "otherplugin:3.2.1":
|
||||
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
|
||||
--depends-on myplugin:1.2.3 \
|
||||
--depends-on otherplugin:3.2.1
|
||||
`
|
||||
)
|
||||
|
||||
type pushOptions struct {
|
||||
*options.Common
|
||||
*options.Artifact
|
||||
*options.Registry
|
||||
}
|
||||
|
||||
func (o *pushOptions) validate() error {
|
||||
return o.Artifact.Validate()
|
||||
}
|
||||
|
||||
// NewPushCmd returns the push command.
|
||||
func NewPushCmd(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
o := pushOptions{
|
||||
Common: opt,
|
||||
Artifact: &options.Artifact{},
|
||||
Registry: &options.Registry{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "push hostname/repo[:tag|@digest] file [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Push a Falco OCI artifact to remote registry",
|
||||
Long: longPush,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := o.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref := args[0]
|
||||
|
||||
_, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.runPush(ctx, args)
|
||||
},
|
||||
}
|
||||
o.Registry.AddFlags(cmd)
|
||||
output.ExitOnErr(o.Printer, o.Artifact.AddFlags(cmd))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runPush executes the business logic for the push command.
|
||||
func (o *pushOptions) runPush(ctx context.Context, args []string) error {
|
||||
ref := args[0]
|
||||
paths := args[1:]
|
||||
// When creating the tar.gz archives we need to remove them after we are done.
|
||||
// Holds the path for each temporary dir.
|
||||
var toBeDeletedTmpDirs []string
|
||||
logger := o.Printer.Logger
|
||||
|
||||
registry, err := utils.GetRegistryFromRef(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pusher, err := ociutils.Pusher(o.PlainHTTP, o.Printer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while creating the pusher for registry %s: %w", registry, err)
|
||||
}
|
||||
|
||||
err = ociutils.CheckConnectionForRegistry(ctx, pusher.Client, o.PlainHTTP, registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Preparing to push artifact", o.Printer.Logger.Args("name", args[0], "type", o.ArtifactType))
|
||||
|
||||
// Make sure to remove temporary working dirs.
|
||||
defer func() {
|
||||
for _, dir := range toBeDeletedTmpDirs {
|
||||
logger.Debug("Removing temporary dir", logger.Args("name", dir))
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
logger.Warn("Unable to remove temporary dir", logger.Args("name", dir, "error", err.Error()))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
config := &oci.ArtifactConfig{
|
||||
Name: o.Name,
|
||||
Version: o.Version,
|
||||
}
|
||||
|
||||
for i, p := range paths {
|
||||
if err = utils.IsTarGz(filepath.Clean(p)); err != nil && !errors.Is(err, utils.ErrNotTarGz) {
|
||||
return err
|
||||
} else if err == nil {
|
||||
continue
|
||||
} else {
|
||||
if o.ArtifactType == oci.Rulesfile {
|
||||
if config, err = rulesConfigLayer(o.Printer.Logger, p, o.Artifact); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
path, err := utils.CreateTarGzArchive("", p, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paths[i] = path
|
||||
toBeDeletedTmpDirs = append(toBeDeletedTmpDirs, filepath.Dir(path))
|
||||
}
|
||||
}
|
||||
|
||||
if config.Name == "" {
|
||||
// extract artifact name from ref, if not provided by the user
|
||||
if config.Name, err = utils.NameFromRef(ref); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := config.ParseDependencies(o.Dependencies...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := config.ParseRequirements(o.Requirements...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.AutoFloatingTags {
|
||||
v, err := semver.Parse(o.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expected semver for the flag \"--version\": %w", err)
|
||||
}
|
||||
o.Tags = append(o.Tags, o.Version, fmt.Sprintf("%v", v.Major), fmt.Sprintf("%v.%v", v.Major, v.Minor))
|
||||
}
|
||||
|
||||
opts := ocipusher.Options{
|
||||
ocipusher.WithTags(o.Tags...),
|
||||
ocipusher.WithAnnotationSource(o.AnnotationSource),
|
||||
ocipusher.WithArtifactConfig(*config),
|
||||
}
|
||||
|
||||
switch o.ArtifactType {
|
||||
case oci.Plugin:
|
||||
opts = append(opts, ocipusher.WithFilepathsAndPlatforms(paths, o.Platforms))
|
||||
case oci.Rulesfile:
|
||||
opts = append(opts, ocipusher.WithFilepaths(paths))
|
||||
case oci.Asset:
|
||||
opts = append(opts, ocipusher.WithFilepaths(paths))
|
||||
}
|
||||
|
||||
res, err := pusher.Push(ctx, o.ArtifactType, ref, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Artifact pushed", logger.Args("name", args[0], "type", res.Type, "digest", res.RootDigest))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
// depsKey is the key for deps in the rulesfiles.
|
||||
depsKey = "required_plugin_versions"
|
||||
// engineKey is the key in the rulesfiles.
|
||||
engineKey = "required_engine_version"
|
||||
// engineRequirementKey is used as name for the engine requirement in the config layer for the rulesfile artifacts.
|
||||
engineRequirementKey = "engine_version_semver"
|
||||
)
|
||||
|
||||
func rulesConfigLayer(logger *pterm.Logger, filePath string, artifactOptions *options.Artifact) (*oci.ArtifactConfig, error) {
|
||||
var data []map[string]interface{}
|
||||
|
||||
// Setup OCI artifact configuration
|
||||
config := oci.ArtifactConfig{
|
||||
Name: artifactOptions.Name,
|
||||
Version: artifactOptions.Version,
|
||||
}
|
||||
|
||||
yamlFile, err := os.ReadFile(filepath.Clean(filePath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open rulesfile %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(yamlFile, &data); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal rulesfile %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
// Parse the artifact dependencies.
|
||||
// Check if the user has provided any.
|
||||
if len(artifactOptions.Dependencies) != 0 {
|
||||
logger.Info("Dependencies provided by user", logger.Args("rulesfile", filePath))
|
||||
if err = config.ParseDependencies(artifactOptions.Dependencies...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// If no user provided then try to parse them from the rulesfile.
|
||||
var found bool
|
||||
logger.Info("Parsing dependencies from: ", logger.Args("rulesfile", filePath))
|
||||
var requiredPluginVersionsEntry interface{}
|
||||
var ok bool
|
||||
for _, entry := range data {
|
||||
if requiredPluginVersionsEntry, ok = entry[depsKey]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var deps []oci.ArtifactDependency
|
||||
byteData, err := yaml.Marshal(requiredPluginVersionsEntry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse dependencies from rulesfile: %w", err)
|
||||
}
|
||||
err = yaml.Unmarshal(byteData, &deps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse dependencies from rulesfile: %w", err)
|
||||
}
|
||||
logger.Info("Dependencies correctly parsed from rulesfile")
|
||||
// Set the deps.
|
||||
config.Dependencies = deps
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
logger.Warn("No dependencies were provided by the user and none were found in the rulesfile.")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the requirements.
|
||||
// Check if the user has provided any.
|
||||
if len(artifactOptions.Requirements) != 0 {
|
||||
logger.Info("Requirements provided by user")
|
||||
if err = config.ParseRequirements(artifactOptions.Requirements...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
var found bool
|
||||
var engineVersion string
|
||||
logger.Info("Parsing requirements from: ", logger.Args("rulesfile", filePath))
|
||||
// If no user provided requirements then try to parse them from the rulesfile.
|
||||
for _, entry := range data {
|
||||
if requiredEngineVersionEntry, ok := entry[engineKey]; ok {
|
||||
// Check if the version is an int. This is for backward compatibility. The engine version used to be an
|
||||
// int but internally used by falco as a semver minor version.
|
||||
// 15 -> 0.15.0
|
||||
if engVersionInt, ok := requiredEngineVersionEntry.(int); ok {
|
||||
engineVersion = fmt.Sprintf("0.%d.0", engVersionInt)
|
||||
} else {
|
||||
engineVersion, ok = requiredEngineVersionEntry.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s must be an int or a string respecting the semver specification, got type %T", engineKey, requiredEngineVersionEntry)
|
||||
}
|
||||
|
||||
// Check if it is in semver format.
|
||||
if _, err := semver.Parse(engineVersion); err != nil {
|
||||
return nil, fmt.Errorf("%s must be in semver format: %w", engineVersion, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the requirements.
|
||||
config.Requirements = []oci.ArtifactRequirement{{
|
||||
Name: engineRequirementKey,
|
||||
Version: engineVersion,
|
||||
}}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
logger.Warn("No requirements were provided by the user and none were found in the rulesfile.")
|
||||
}
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2024 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package push_test
|
||||
|
||||
// revive:disable
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/cmd"
|
||||
"github.com/falcosecurity/falcoctl/internal/utils"
|
||||
"github.com/falcosecurity/falcoctl/pkg/oci"
|
||||
testutils "github.com/falcosecurity/falcoctl/pkg/test"
|
||||
)
|
||||
|
||||
// revive:enable
|
||||
var _ = Describe("pushing plugins", func() {
|
||||
var (
|
||||
registryCmd = "registry"
|
||||
pushCmd = "push"
|
||||
version = "1.1.1"
|
||||
// fullRepoName is set each time before each test.
|
||||
fullRepoName string
|
||||
// repoName same as fullRepoName.
|
||||
repoName string
|
||||
// It is set in the config layer.
|
||||
artifactNameInConfigLayer = "test-push-plugins"
|
||||
pushedTags = []string{"tag1", "tag2", "latest"}
|
||||
|
||||
// Plugin's platforms.
|
||||
platformARM64 = "linux/arm64"
|
||||
platformAMD64 = "linux/amd64"
|
||||
|
||||
// Paths pointing to plugins that will be pushed.
|
||||
// Some of the functions expect these two variable to be set to valid paths.
|
||||
// They are set in beforeEach blocks by tests that need them.
|
||||
pluginOne string
|
||||
pluginTwo string
|
||||
// Data fetched from registry and used for assertions.
|
||||
pluginData *testutils.PluginArtifact
|
||||
)
|
||||
|
||||
const (
|
||||
// Used as flags for all the test cases.
|
||||
requirement = "plugin_api_version:3.2.1"
|
||||
anSource = "myrepo.com/rules.git"
|
||||
pluginsRepoBaseName = "push-plugins-tests"
|
||||
)
|
||||
|
||||
var AssertSuccessBehaviour = func(deps []oci.ArtifactDependency, reqs []oci.ArtifactRequirement, annotations map[string]string, platforms []string) {
|
||||
It("should succeed", func() {
|
||||
// We do not check the error here since we are checking it after
|
||||
// pushing the artifact.
|
||||
By("checking no error in output")
|
||||
Expect(output).ShouldNot(gbytes.Say("ERROR"))
|
||||
Expect(output).ShouldNot(gbytes.Say("Unable to remove temporary dir"))
|
||||
|
||||
By("checking descriptor")
|
||||
Expect(pluginData.Descriptor.MediaType).Should(Equal(v1.MediaTypeImageIndex))
|
||||
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(pluginData.Descriptor.Digest.String())))
|
||||
|
||||
By("checking index")
|
||||
Expect(pluginData.Index.Manifests).Should(HaveLen(len(platforms)))
|
||||
|
||||
By("checking platforms")
|
||||
for _, p := range platforms {
|
||||
Expect(pluginData.Platforms).Should(HaveKey(p))
|
||||
}
|
||||
|
||||
By("checking config layers")
|
||||
for plat, p := range pluginData.Platforms {
|
||||
By(fmt.Sprintf("platform %s", plat))
|
||||
Expect(p.Config.Version).Should(Equal(version))
|
||||
Expect(p.Config.Name).Should(Equal(artifactNameInConfigLayer))
|
||||
|
||||
By("checking dependencies")
|
||||
Expect(p.Config.Dependencies).Should(HaveLen(len(deps)))
|
||||
for _, dep := range deps {
|
||||
Expect(p.Config.Dependencies).Should(ContainElement(dep))
|
||||
}
|
||||
|
||||
By("checking requirements")
|
||||
Expect(p.Config.Requirements).Should(HaveLen(len(reqs)))
|
||||
for _, req := range reqs {
|
||||
Expect(p.Config.Requirements).Should(ContainElement(req))
|
||||
}
|
||||
|
||||
By("checking annotations")
|
||||
// The creation timestamp is always present.
|
||||
Expect(p.Manifest.Annotations).Should(HaveLen(len(annotations) + 1))
|
||||
for key, val := range annotations {
|
||||
Expect(p.Manifest.Annotations).Should(HaveKeyWithValue(key, val))
|
||||
}
|
||||
}
|
||||
|
||||
By("checking tags")
|
||||
Expect(pluginData.Tags).Should(HaveLen(len(pushedTags)))
|
||||
Expect(pluginData.Tags).Should(ContainElements(pushedTags))
|
||||
|
||||
By("checking that temporary dirs have been removed")
|
||||
|
||||
Eventually(func() bool {
|
||||
entries, err := os.ReadDir("/tmp")
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
matched, err := filepath.Match(utils.TmpDirPrefix+"*", regexp.QuoteMeta(e.Name()))
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}).WithTimeout(5 * time.Second).Should(BeFalse())
|
||||
})
|
||||
}
|
||||
|
||||
// Each test gets its own root command and runs it.
|
||||
// The err variable is asserted by each test.
|
||||
JustBeforeEach(func() {
|
||||
rootCmd = cmd.New(ctx, opt)
|
||||
err = executeRoot(args)
|
||||
})
|
||||
|
||||
JustAfterEach(func() {
|
||||
// Reset the status after each test.
|
||||
// This variable could be changed by single tests.
|
||||
// Make sure to set them at their default values.
|
||||
Expect(output.Clear()).ShouldNot(HaveOccurred())
|
||||
artifactNameInConfigLayer = "test-plugin"
|
||||
pushedTags = []string{"tag1", "tag2", "latest"}
|
||||
pluginOne = ""
|
||||
pluginTwo = ""
|
||||
})
|
||||
|
||||
Context("success", func() {
|
||||
JustBeforeEach(func() {
|
||||
// Check the returned error before proceeding.
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
pluginData, err = testutils.FetchPluginFromRegistry(ctx, repoName, pushedTags[0], orasRegistry)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
})
|
||||
|
||||
When("two platforms, with reqs and deps", func() {
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, pluginsRepoBaseName)
|
||||
pluginOne = rulesfileyaml
|
||||
pluginTwo = plugintgz
|
||||
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, pluginOne, pluginTwo, "--type", "plugin", "--platform",
|
||||
platformAMD64, "--platform", platformARM64, "--version", version, "--config", configFile,
|
||||
"--plain-http", "--depends-on", "my-test:4.3.2", "--requires", requirement, "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
|
||||
AssertSuccessBehaviour([]oci.ArtifactDependency{{
|
||||
Name: "my-test",
|
||||
Version: "4.3.2",
|
||||
Alternatives: nil,
|
||||
}}, []oci.ArtifactRequirement{
|
||||
{
|
||||
Name: "plugin_api_version",
|
||||
Version: "3.2.1",
|
||||
},
|
||||
}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
}, []string{
|
||||
platformAMD64, platformARM64,
|
||||
})
|
||||
})
|
||||
|
||||
When("one platform, no reqs", func() {
|
||||
BeforeEach(func() {
|
||||
repoName, fullRepoName = randomRulesRepoName(registry, pluginsRepoBaseName)
|
||||
pluginOne = plugintgz
|
||||
args = []string{registryCmd, pushCmd, fullRepoName, pluginOne, "--type", "plugin", "--platform",
|
||||
platformAMD64, "--version", version, "--config", configFile,
|
||||
"--plain-http", "--depends-on", "my-test:4.3.2", "--annotation-source", anSource,
|
||||
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
|
||||
})
|
||||
// We expect to succeed and that the requirement is empty.
|
||||
AssertSuccessBehaviour([]oci.ArtifactDependency{{
|
||||
Name: "my-test",
|
||||
Version: "4.3.2",
|
||||
Alternatives: nil,
|
||||
}}, []oci.ArtifactRequirement{}, map[string]string{
|
||||
"org.opencontainers.image.source": anSource,
|
||||
}, []string{
|
||||
platformAMD64,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,655 @@
|
|||
// 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")))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,108 @@
|
|||
// 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)
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
// 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,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
@ -12,35 +13,38 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/registry/login"
|
||||
"github.com/falcosecurity/falcoctl/internal/registry/logout"
|
||||
"github.com/falcosecurity/falcoctl/internal/registry/oauth"
|
||||
"github.com/falcosecurity/falcoctl/internal/registry/pull"
|
||||
"github.com/falcosecurity/falcoctl/internal/registry/push"
|
||||
"github.com/falcosecurity/falcoctl/cmd/registry/auth"
|
||||
"github.com/falcosecurity/falcoctl/cmd/registry/pull"
|
||||
"github.com/falcosecurity/falcoctl/cmd/registry/push"
|
||||
"github.com/falcosecurity/falcoctl/internal/config"
|
||||
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
|
||||
)
|
||||
|
||||
// NewRegistryCmd returns the registry command.
|
||||
func NewRegistryCmd(ctx context.Context, opt *commonoptions.CommonOptions) *cobra.Command {
|
||||
func NewRegistryCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "registry",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Interact with OCI registries",
|
||||
Long: "Interact with OCI registries",
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(login.NewLoginCmd(ctx, opt))
|
||||
cmd.AddCommand(logout.NewLogoutCmd(opt))
|
||||
cmd.AddCommand(auth.NewAuthCmd(ctx, opt))
|
||||
cmd.AddCommand(push.NewPushCmd(ctx, opt))
|
||||
cmd.AddCommand(pull.NewPullCmd(ctx, opt))
|
||||
cmd.AddCommand(oauth.NewOauthCmd(ctx, opt))
|
||||
|
||||
return cmd
|
||||
}
|
69
cmd/root.go
69
cmd/root.go
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Falco Authors
|
||||
// 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.
|
||||
|
@ -16,52 +17,68 @@ package cmd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/falcosecurity/falcoctl/internal/version"
|
||||
"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 (
|
||||
longRootCmd = `
|
||||
__ _ _ _
|
||||
/ _| __ _| | ___ ___ ___| |_| |
|
||||
| |_ / _ | |/ __/ _ \ / __| __| |
|
||||
| _| (_| | | (_| (_) | (__| |_| |
|
||||
|_| \__,_|_|\___\___/ \___|\__|_|
|
||||
|
||||
|
||||
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.CommonOptions) *cobra.Command {
|
||||
func New(ctx context.Context, opt *options.Common) *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "falcoctl",
|
||||
Short: "The control tool for running Falco in Kubernetes",
|
||||
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(NewTLSCmd())
|
||||
rootCmd.AddCommand(tls.NewTLSCmd(opt))
|
||||
rootCmd.AddCommand(version.NewVersionCmd(opt))
|
||||
rootCmd.AddCommand(NewRegistryCmd(ctx, opt))
|
||||
rootCmd.AddCommand(NewIndexCmd(ctx, opt))
|
||||
rootCmd.AddCommand(NewArtifactCmd(ctx, 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 creates the root command and runs it.
|
||||
func Execute() {
|
||||
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()
|
||||
}()
|
||||
|
||||
opt := options.NewOptions()
|
||||
cmd := New(ctx, opt)
|
||||
// Execute configures the signal handlers and runs the command.
|
||||
func Execute(cmd *cobra.Command, opt *options.Common) error {
|
||||
// we do not log the error here since we expect that each subcommand
|
||||
// handles the errors by itself.
|
||||
output.ExitOnErr(cmd.Execute())
|
||||
err := cmd.Execute()
|
||||
opt.Printer.CheckErr(err)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright (C) 2023 The Falco Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd_test
|
||||
|
||||
import (
|
||||
"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
|
||||
|
||||
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"))
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,21 +0,0 @@
|
|||
The control tool for running Falco in Kubernetes
|
||||
|
||||
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/config.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