Compare commits
319 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5efe7a17b | ||
|
|
7075b8b973 | ||
|
|
3f5957d30e | ||
|
|
bc29957d1e | ||
|
|
289835039a | ||
|
|
b1e08ef231 | ||
|
|
8a463767bf | ||
|
|
c598e4d289 | ||
|
|
a7624a2bf9 | ||
|
|
d20fcbd845 | ||
|
|
5d664855de | ||
|
|
8366cf0873 | ||
|
|
a41789a746 | ||
|
|
cde3251dd8 | ||
|
|
7c394bf735 | ||
|
|
76e0345609 | ||
|
|
d8820fa185 | ||
|
|
b6502693e4 | ||
|
|
f7e5402966 | ||
|
|
1e6a825412 | ||
|
|
c23065aacd | ||
|
|
04f5ba67a2 | ||
|
|
b87fa6715b | ||
|
|
f6f3298e03 | ||
|
|
6bfd781947 | ||
|
|
aff6331211 | ||
|
|
d547c39a16 | ||
|
|
3cea422365 | ||
|
|
ac39606386 | ||
|
|
12ae44d563 | ||
|
|
57b37d9005 | ||
|
|
9d5cc07567 | ||
|
|
75d4d767c6 | ||
|
|
0be4b86230 | ||
|
|
784d15b012 | ||
|
|
00f6045b12 | ||
|
|
b26f842de1 | ||
|
|
0ab2406db9 | ||
|
|
bf7e45439b | ||
|
|
0652273fe1 | ||
|
|
5927a54208 | ||
|
|
b46db59948 | ||
|
|
23b5a4729f | ||
|
|
8ae47bd490 | ||
|
|
1796d20399 | ||
|
|
5ac2447b85 | ||
|
|
db445c3a8e | ||
|
|
de2914978d | ||
|
|
09812a7bfc | ||
|
|
2eb3b541f4 | ||
|
|
e9791ff92c | ||
|
|
88516546fa | ||
|
|
9c75c55fa4 | ||
|
|
b9741e87bd | ||
|
|
c555478b54 | ||
|
|
3718372288 | ||
|
|
390b41bc26 | ||
|
|
530c5a64fb | ||
|
|
d285aaedc9 | ||
|
|
453fe18d7f | ||
|
|
5fae5cd308 | ||
|
|
7d7e225823 | ||
|
|
19f404799d | ||
|
|
3e4652dca3 | ||
|
|
45b08de874 | ||
|
|
310e26dda9 | ||
|
|
f4eb54b835 | ||
|
|
3968c12947 | ||
|
|
21c97d255f | ||
|
|
eb1053607a | ||
|
|
de7198e9dc | ||
|
|
0f30f47249 | ||
|
|
6b2ad8ebc8 | ||
|
|
1f302144ef | ||
|
|
d04c7a6ae4 | ||
|
|
9ca2cda8c7 | ||
|
|
1fd06ecbf9 | ||
|
|
97baad4cb1 | ||
|
|
fbd93ecf0d | ||
|
|
e941ccea92 | ||
|
|
d692483bc3 | ||
|
|
95cfbd0fff | ||
|
|
b3d1ed9e65 | ||
|
|
fe2b8c9fee | ||
|
|
2d7deef4e2 | ||
|
|
b4a70b02e3 | ||
|
|
c5c2364ed4 | ||
|
|
efcc4291a3 | ||
|
|
6ea6ee264d | ||
|
|
2865ba7632 | ||
|
|
2bed668d31 | ||
|
|
9dc24860f3 | ||
|
|
f01377b3bc | ||
|
|
7443dfac4c | ||
|
|
e6408e187c | ||
|
|
a02d282d3e | ||
|
|
f778f53744 | ||
|
|
95ea93564e | ||
|
|
d51029e86c | ||
|
|
1016699c94 | ||
|
|
63f63955e7 | ||
|
|
37be9fda9f | ||
|
|
0756133a7e | ||
|
|
83c5ab318b | ||
|
|
0c28957016 | ||
|
|
959084040d | ||
|
|
8a428c6936 | ||
|
|
48da23226c | ||
|
|
5f0c6e5102 | ||
|
|
29f1c6f50e | ||
|
|
4d187419ac | ||
|
|
58998f9cab | ||
|
|
cdc5ca8854 | ||
|
|
44e1e41266 | ||
|
|
33fda8605a | ||
|
|
5f9ed69299 | ||
|
|
7f1baea3b0 | ||
|
|
f75026e4b2 | ||
|
|
ce7a1a9c67 | ||
|
|
a62056fb19 | ||
|
|
f3434a8155 | ||
|
|
4e023ef517 | ||
|
|
97b80cb930 | ||
|
|
525b4e6a53 | ||
|
|
054eaf65b8 | ||
|
|
48fa796ab1 | ||
|
|
1873e022cc | ||
|
|
35a8062b58 | ||
|
|
636248ad67 | ||
|
|
4511c54fad | ||
|
|
7f3970541b | ||
|
|
4040f4d151 | ||
|
|
887d374c53 | ||
|
|
be4b87155d | ||
|
|
b987a7da4c | ||
|
|
7153fe5ad2 | ||
|
|
ccd8ba44d9 | ||
|
|
e7ef0f7a6c | ||
|
|
400b58c0e9 | ||
|
|
5257496214 | ||
|
|
e1bfe4a3ce | ||
|
|
f31cce8ec2 | ||
|
|
169ebfa72c | ||
|
|
7cace52ab5 | ||
|
|
95b93c60d9 | ||
|
|
5af1dcb8b1 | ||
|
|
6a61774fb7 | ||
|
|
ccbaca24f1 | ||
|
|
07b6048dc5 | ||
|
|
60dd34d066 | ||
|
|
28451d1e14 | ||
|
|
db95b6381f | ||
|
|
6b14c9bea4 | ||
|
|
742adc00fe | ||
|
|
52897cc16c | ||
|
|
c950568f1b | ||
|
|
845d7ff188 | ||
|
|
3bd8658da6 | ||
|
|
336a38081a | ||
|
|
01c2131436 | ||
|
|
c274231544 | ||
|
|
4a2864701c | ||
|
|
76ede10e0a | ||
|
|
274e01bb75 | ||
|
|
d75f763c99 | ||
|
|
5bc985663c | ||
|
|
df9e2e853f | ||
|
|
b4828a6f0a | ||
|
|
e99dd749a0 | ||
|
|
10ce7178c0 | ||
|
|
5c6a66eaf5 | ||
|
|
36d30bc985 | ||
|
|
a5152b82e9 | ||
|
|
e9af8a2595 | ||
|
|
84b5b60d49 | ||
|
|
8f60f42be3 | ||
|
|
583344138a | ||
|
|
016d021d5a | ||
|
|
115dc4bfa4 | ||
|
|
5b83febb23 | ||
|
|
c9d5c50402 | ||
|
|
fc839d2983 | ||
|
|
3bce96bbd5 | ||
|
|
6279be073b | ||
|
|
ea37132ce4 | ||
|
|
70eecd5289 | ||
|
|
380d03257f | ||
|
|
006de6da14 | ||
|
|
10aa80e8f5 | ||
|
|
013439af6d | ||
|
|
3408961155 | ||
|
|
f3b4a8d055 | ||
|
|
104af7e86f | ||
|
|
be39fbeff6 | ||
|
|
4109045fa4 | ||
|
|
90fd8023dd | ||
|
|
f67ad9c061 | ||
|
|
525e2bafee | ||
|
|
b65a9abf8e | ||
|
|
fec94aa53a | ||
|
|
3d4b345728 | ||
|
|
579975f08d | ||
|
|
3707b39fef | ||
|
|
f07387225b | ||
|
|
2648fb1bb1 | ||
|
|
d34715b4ba | ||
|
|
63af50bf98 | ||
|
|
456550c1d4 | ||
|
|
8174b88ec3 | ||
|
|
3233973748 | ||
|
|
bdfb1cf33e | ||
|
|
1c5fcd59e7 | ||
|
|
5cc960527e | ||
|
|
762c53fb8d | ||
|
|
ff20e67d07 | ||
|
|
c0cea013d1 | ||
|
|
5526bbba64 | ||
|
|
f0aa96ea8c | ||
|
|
e73007c398 | ||
|
|
fdc459ec5b | ||
|
|
bdb523ece1 | ||
|
|
164a9479ad | ||
|
|
e18adc781f | ||
|
|
33d89c2739 | ||
|
|
7cc9ab9083 | ||
|
|
4b4b7dc169 | ||
|
|
71ad5c5f05 | ||
|
|
39368bb5cb | ||
|
|
7a587ee8d1 | ||
|
|
77346527f3 | ||
|
|
1eba5833d5 | ||
|
|
83a747794e | ||
|
|
3e16d1da46 | ||
|
|
ae1860e859 | ||
|
|
2ebc8fdf2a | ||
|
|
be4023be66 | ||
|
|
7f4ad76298 | ||
|
|
0cbfaf98f3 | ||
|
|
631124e658 | ||
|
|
1685ee1ecb | ||
|
|
9b4d11f220 | ||
|
|
46a71296a9 | ||
|
|
1285588b62 | ||
|
|
d96392f65e | ||
|
|
d1c5a736ae | ||
|
|
6b1e038c5c | ||
|
|
eaab1aae28 | ||
|
|
31030343a2 | ||
|
|
325ca03a13 | ||
|
|
dea8e63df2 | ||
|
|
58421fd31a | ||
|
|
b961c96862 | ||
|
|
2d23c1b0f3 | ||
|
|
06952c224b | ||
|
|
2ea492c965 | ||
|
|
dbf84f6879 | ||
|
|
0fa3d6c462 | ||
|
|
d57f7aa03f | ||
|
|
d64f9f5401 | ||
|
|
a3029afc41 | ||
|
|
6a7d904fae | ||
|
|
d4043d3f86 | ||
|
|
b4902a4f58 | ||
|
|
ffe402f201 | ||
|
|
09cc7da282 | ||
|
|
2d2dad41f4 | ||
|
|
5f7c0a86dd | ||
|
|
fc1c631c98 | ||
|
|
89bdafacb8 | ||
|
|
73b6b3f129 | ||
|
|
b2a495f593 | ||
|
|
65ee904377 | ||
|
|
13f59230b5 | ||
|
|
36d2a0de1e | ||
|
|
a4db9fc8e5 | ||
|
|
9dae5ef83b | ||
|
|
e8842a740c | ||
|
|
0d3807ad09 | ||
|
|
5c27a249b7 | ||
|
|
7e41860b28 | ||
|
|
43ff92bbe7 | ||
|
|
28adc7e563 | ||
|
|
9788411995 | ||
|
|
0c9e8cc50e | ||
|
|
34d572c523 | ||
|
|
011b496b3f | ||
|
|
12b906eac6 | ||
|
|
20937d05c3 | ||
|
|
4943d37ccf | ||
|
|
3a8fd215de | ||
|
|
87572e8922 | ||
|
|
f1eedc7a01 | ||
|
|
b79e48dd77 | ||
|
|
18872194af | ||
|
|
bafd7ba282 | ||
|
|
b186481181 | ||
|
|
09ca6d11ad | ||
|
|
e68e4e786d | ||
|
|
ee638254c3 | ||
|
|
1e678905c4 | ||
|
|
10804c4b25 | ||
|
|
4bf9b4d41b | ||
|
|
1161872324 | ||
|
|
98cb570896 | ||
|
|
ed4ee3b58e | ||
|
|
066048f4de | ||
|
|
4b6b91c08b | ||
|
|
2980523a5b | ||
|
|
f2f9c043bf | ||
|
|
5d59cfd2c9 | ||
|
|
f94474e24f | ||
|
|
a63fc6d9ba | ||
|
|
076adeef80 | ||
|
|
a0e756317c | ||
|
|
252cb5f2f3 | ||
|
|
64288b4914 | ||
|
|
9ca6c6a315 | ||
|
|
3651ab5c0c | ||
|
|
b3f15e1ddc |
2
.ackrc
2
.ackrc
@@ -1,3 +1,5 @@
|
|||||||
--ignore-dir=old/
|
--ignore-dir=old/
|
||||||
--ignore-dir=tmp/
|
--ignore-dir=tmp/
|
||||||
--ignore-dir=vendor/
|
--ignore-dir=vendor/
|
||||||
|
--ignore-dir=releases/
|
||||||
|
--ignore-dir=rpmbuild/
|
||||||
|
|||||||
5
.github/FUNDING.yml
vendored
5
.github/FUNDING.yml
vendored
@@ -1,2 +1,5 @@
|
|||||||
# You can add one username per supported platform and one custom link
|
# You can add one username per supported platform and one custom link.
|
||||||
|
custom: "https://paypal.me/purpleidea"
|
||||||
|
github: purpleidea
|
||||||
|
liberapay: purpleidea
|
||||||
patreon: purpleidea
|
patreon: purpleidea
|
||||||
|
|||||||
2
.github/settings.yml
vendored
2
.github/settings.yml
vendored
@@ -68,6 +68,8 @@ labels:
|
|||||||
color: e11d21
|
color: e11d21
|
||||||
- name: question
|
- name: question
|
||||||
color: cc317c
|
color: cc317c
|
||||||
|
- name: needinfo
|
||||||
|
color: fbca04
|
||||||
- name: wontfix
|
- name: wontfix
|
||||||
color: ffffff
|
color: ffffff
|
||||||
# - name: first-timers-only
|
# - name: first-timers-only
|
||||||
|
|||||||
68
.github/workflows/test.yaml
vendored
Normal file
68
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Docs: https://help.github.com/en/articles/workflow-syntax-for-github-actions
|
||||||
|
|
||||||
|
# If the name is omitted, it uses the filename instead.
|
||||||
|
#name: Test
|
||||||
|
on:
|
||||||
|
# Run on all pull requests.
|
||||||
|
pull_request:
|
||||||
|
#branches:
|
||||||
|
#- master
|
||||||
|
# Run on all pushes.
|
||||||
|
push:
|
||||||
|
# Run daily at 4am.
|
||||||
|
schedule:
|
||||||
|
- cron: 0 4 * * *
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
maketest:
|
||||||
|
name: Test (${{ matrix.test_block }}) on ${{ matrix.os }} with golang ${{ matrix.golang_version }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
GOPATH: /home/runner/work/mgmt/mgmt/go
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# TODO: Add tip when it's supported: https://github.com/actions/setup-go/issues/21
|
||||||
|
os:
|
||||||
|
- ubuntu-latest
|
||||||
|
# macos tests are currently failing in CI
|
||||||
|
#- macos-latest
|
||||||
|
golang_version:
|
||||||
|
# TODO: add 1.19.x and tip
|
||||||
|
# minimum required and latest published go_version
|
||||||
|
- 1.18
|
||||||
|
test_block:
|
||||||
|
- basic
|
||||||
|
- shell
|
||||||
|
- race
|
||||||
|
#fail-fast: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Do not shallow fetch. The path can't be absolute, so we need to move it
|
||||||
|
# to the expected location later.
|
||||||
|
- name: Clone mgmt
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
path: ./go/src/github.com/purpleidea/mgmt
|
||||||
|
|
||||||
|
- name: Install Go ${{ matrix.golang_version }}
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.golang_version }}
|
||||||
|
|
||||||
|
# Install & configure ruby, fixes gem permissions error
|
||||||
|
- name: Install Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: head
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ./go/src/github.com/purpleidea/mgmt
|
||||||
|
run: |
|
||||||
|
make deps
|
||||||
|
|
||||||
|
- name: Run test
|
||||||
|
working-directory: ./go/src/github.com/purpleidea/mgmt
|
||||||
|
run: |
|
||||||
|
TEST_BLOCK="${{ matrix.test_block }}" make test
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,7 +7,6 @@ old/
|
|||||||
tmp/
|
tmp/
|
||||||
*WIP
|
*WIP
|
||||||
*_stringer.go
|
*_stringer.go
|
||||||
bindata/*.go
|
|
||||||
mgmt
|
mgmt
|
||||||
mgmt.static
|
mgmt.static
|
||||||
# crossbuild artifacts
|
# crossbuild artifacts
|
||||||
@@ -17,3 +16,5 @@ rpmbuild/
|
|||||||
releases/
|
releases/
|
||||||
# vim swap files
|
# vim swap files
|
||||||
.*.sw[op]
|
.*.sw[op]
|
||||||
|
# prevent `echo foo 2>1` typo errors by making this file read-only
|
||||||
|
1
|
||||||
|
|||||||
33
.gitmodules
vendored
33
.gitmodules
vendored
@@ -1,33 +0,0 @@
|
|||||||
[submodule "vendor/github.com/coreos/etcd"]
|
|
||||||
path = vendor/github.com/coreos/etcd
|
|
||||||
url = https://github.com/coreos/etcd/
|
|
||||||
[submodule "vendor/google.golang.org/grpc"]
|
|
||||||
path = vendor/google.golang.org/grpc
|
|
||||||
url = https://github.com/grpc/grpc-go
|
|
||||||
[submodule "vendor/github.com/grpc-ecosystem/grpc-gateway"]
|
|
||||||
path = vendor/github.com/grpc-ecosystem/grpc-gateway
|
|
||||||
url = https://github.com/grpc-ecosystem/grpc-gateway
|
|
||||||
[submodule "vendor/gopkg.in/fsnotify.v1"]
|
|
||||||
path = vendor/gopkg.in/fsnotify.v1
|
|
||||||
url = https://gopkg.in/fsnotify.v1
|
|
||||||
[submodule "vendor/github.com/purpleidea/go-systemd"]
|
|
||||||
path = vendor/github.com/purpleidea/go-systemd
|
|
||||||
url = https://github.com/purpleidea/go-systemd
|
|
||||||
[submodule "vendor/honnef.co/go/augeas"]
|
|
||||||
path = vendor/honnef.co/go/augeas
|
|
||||||
url = https://github.com/dominikh/go-augeas/
|
|
||||||
[submodule "vendor/github.com/grpc-ecosystem/go-grpc-prometheus"]
|
|
||||||
path = vendor/github.com/grpc-ecosystem/go-grpc-prometheus
|
|
||||||
url = https://github.com/grpc-ecosystem/go-grpc-prometheus
|
|
||||||
[submodule "vendor/github.com/ugorji/go"]
|
|
||||||
path = vendor/github.com/ugorji/go
|
|
||||||
url = https://github.com/ugorji/go
|
|
||||||
[submodule "vendor/github.com/purpleidea/docker"]
|
|
||||||
path = vendor/github.com/docker/docker
|
|
||||||
url = https://github.com/purpleidea/docker
|
|
||||||
[submodule "vendor/github.com/purpleidea/distribution"]
|
|
||||||
path = vendor/github.com/docker/distribution
|
|
||||||
url = https://github.com/purpleidea/distribution
|
|
||||||
[submodule "vendor/github.com/purpleidea/go-connections"]
|
|
||||||
path = vendor/github.com/docker/go-connections
|
|
||||||
url = https://github.com/docker/go-connections
|
|
||||||
14
.travis.yml
14
.travis.yml
@@ -24,21 +24,21 @@ install: 'make deps'
|
|||||||
matrix:
|
matrix:
|
||||||
fast_finish: false
|
fast_finish: false
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- go: 1.12.x
|
- go: 1.19.x
|
||||||
- go: tip
|
- go: tip
|
||||||
- os: osx
|
- os: osx
|
||||||
# include only one build for osx for a quicker build as the nr. of these runners are sparse
|
# include only one build for osx for a quicker build as the nr. of these runners are sparse
|
||||||
include:
|
include:
|
||||||
- name: "basic tests"
|
- name: "basic tests"
|
||||||
go: 1.11.x
|
go: 1.18.x
|
||||||
env: TEST_BLOCK=basic
|
env: TEST_BLOCK=basic
|
||||||
- name: "shell tests"
|
- name: "shell tests"
|
||||||
go: 1.11.x
|
go: 1.18.x
|
||||||
env: TEST_BLOCK=shell
|
env: TEST_BLOCK=shell
|
||||||
- name: "race tests"
|
- name: "race tests"
|
||||||
go: 1.11.x
|
go: 1.18.x
|
||||||
env: TEST_BLOCK=race
|
env: TEST_BLOCK=race
|
||||||
- go: 1.12.x
|
- go: 1.19.x
|
||||||
- go: tip
|
- go: tip
|
||||||
- os: osx
|
- os: osx
|
||||||
script: 'TEST_BLOCK="$TEST_BLOCK" make test'
|
script: 'TEST_BLOCK="$TEST_BLOCK" make test'
|
||||||
@@ -47,8 +47,8 @@ script: 'TEST_BLOCK="$TEST_BLOCK" make test'
|
|||||||
# with a value of: irc.freenode.net#mgmtconfig to eliminate noise from forks...
|
# with a value of: irc.freenode.net#mgmtconfig to eliminate noise from forks...
|
||||||
notifications:
|
notifications:
|
||||||
irc:
|
irc:
|
||||||
channels:
|
#channels:
|
||||||
- secure: htcuWAczm3C1zKC9vUfdRzhIXM1vtF+q0cLlQFXK1IQQlk693/pM30Mmf2L/9V2DVDeps+GyLdip0ARXD1DZEJV0lK+Ca1qbHdFP1r4Xv6l5+jaDb5Y88YU5LI8K758QShiZJojuQ1aO2j8xmmt9V0/5y5QwlpPeHbKYBOFPBX3HvlT9DhvwZNKGhBb4qJOEaPVOwq9IkN3DyQ456MHcJ3q3vF9Lb440uTuLsJNof2AbYZH8ZIHCSG2N8tBj2qhJOpWQboYtQJzE2pRaGkGBL4kYcHZSZMXX8sl4cBM1vx/IRUkvBxJUpLJz2gn/eRI+/gr59juZE2K0+FOLlx9dLnX626Y9xSViopBI6JsIoHJDqNC7aGaF2qaYulGYN65VNKVqmghjgt6JLmmiKeH10hYrJMMvt2rms8l4+5iwmCwXvhH/WU9edzk2p5wqERMnostJFEJib0zI3yzLoF0sdJs+veKtagzfayY2d2l7hlmt951IpqqVWldVgWUcQKVvi8gmRarbwFlK+5D7BEnkUDcLNly/cqf7BgEeX6YfF+FiR4pgfOhYvGCD+2q91NgWQXHBCxbyN0be1TVdkXD94f0Lkn94VyEJJ+PkPlG+rPgFwGcjqN4oEGkJeJmES2If05q2Ms1dJLwYQDL3+Py4lNMSdSWj24TzlFVhtwHepuw=
|
# - secure: htcuWAczm3C1zKC9vUfdRzhIXM1vtF+q0cLlQFXK1IQQlk693/pM30Mmf2L/9V2DVDeps+GyLdip0ARXD1DZEJV0lK+Ca1qbHdFP1r4Xv6l5+jaDb5Y88YU5LI8K758QShiZJojuQ1aO2j8xmmt9V0/5y5QwlpPeHbKYBOFPBX3HvlT9DhvwZNKGhBb4qJOEaPVOwq9IkN3DyQ456MHcJ3q3vF9Lb440uTuLsJNof2AbYZH8ZIHCSG2N8tBj2qhJOpWQboYtQJzE2pRaGkGBL4kYcHZSZMXX8sl4cBM1vx/IRUkvBxJUpLJz2gn/eRI+/gr59juZE2K0+FOLlx9dLnX626Y9xSViopBI6JsIoHJDqNC7aGaF2qaYulGYN65VNKVqmghjgt6JLmmiKeH10hYrJMMvt2rms8l4+5iwmCwXvhH/WU9edzk2p5wqERMnostJFEJib0zI3yzLoF0sdJs+veKtagzfayY2d2l7hlmt951IpqqVWldVgWUcQKVvi8gmRarbwFlK+5D7BEnkUDcLNly/cqf7BgEeX6YfF+FiR4pgfOhYvGCD+2q91NgWQXHBCxbyN0be1TVdkXD94f0Lkn94VyEJJ+PkPlG+rPgFwGcjqN4oEGkJeJmES2If05q2Ms1dJLwYQDL3+Py4lNMSdSWj24TzlFVhtwHepuw=
|
||||||
template:
|
template:
|
||||||
- "%{repository} (%{commit}: %{author}): %{message}"
|
- "%{repository} (%{commit}: %{author}): %{message}"
|
||||||
- "More info : %{build_url}"
|
- "More info : %{build_url}"
|
||||||
|
|||||||
1
AUTHORS
1
AUTHORS
@@ -6,6 +6,7 @@ This list is sorted alphabetically by first name.
|
|||||||
|
|
||||||
Felix Frank
|
Felix Frank
|
||||||
James Shubin
|
James Shubin
|
||||||
|
Joe Groocock
|
||||||
Johan Bloemberg
|
Johan Bloemberg
|
||||||
Jonathan Gold
|
Jonathan Gold
|
||||||
Julien Pivotto
|
Julien Pivotto
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Mgmt
|
Mgmt
|
||||||
Copyright (C) 2013-2019+ James Shubin and the project contributors
|
Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
Written by James Shubin <james@shubin.ca> and the project contributors
|
Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
180
Makefile
180
Makefile
@@ -1,5 +1,5 @@
|
|||||||
# Mgmt
|
# Mgmt
|
||||||
# Copyright (C) 2013-2019+ James Shubin and the project contributors
|
# Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
# Written by James Shubin <james@shubin.ca> and the project contributors
|
# Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
@@ -16,8 +16,12 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
SHELL = /usr/bin/env bash
|
SHELL = /usr/bin/env bash
|
||||||
.PHONY: all art cleanart version program lang path deps run race bindata generate build build-debug crossbuild clean test gofmt yamlfmt format docs rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms upload-releases copr tag release funcgen
|
.PHONY: all art cleanart version program lang path deps run race generate build build-debug crossbuild clean test gofmt yamlfmt format docs
|
||||||
.SILENT: clean bindata
|
.PHONY: rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms upload-releases copr tag
|
||||||
|
.PHONY: mkosi mkosi_fedora-30 mkosi_fedora-29 mkosi_centos-7 mkosi_debian-10 mkosi_ubuntu-bionic mkosi_archlinux
|
||||||
|
.PHONY: release releases_path release_fedora-30 release_fedora-29 release_centos-7 release_debian-10 release_ubuntu-bionic release_archlinux
|
||||||
|
.PHONY: funcgen
|
||||||
|
.SILENT: clean
|
||||||
|
|
||||||
# a large amount of output from this `find`, can cause `make` to be much slower!
|
# a large amount of output from this `find`, can cause `make` to be much slower!
|
||||||
GO_FILES := $(shell find * -name '*.go' -not -path 'old/*' -not -path 'tmp/*')
|
GO_FILES := $(shell find * -name '*.go' -not -path 'old/*' -not -path 'tmp/*')
|
||||||
@@ -49,9 +53,26 @@ GOOSARCHES ?= linux/amd64 linux/ppc64 linux/ppc64le linux/arm64 darwin/amd64
|
|||||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||||
|
|
||||||
RPM_PKG = releases/$(VERSION)/rpm/mgmt-$(VERSION)-1.x86_64.rpm
|
TOKEN_FEDORA-30 = fedora-30
|
||||||
DEB_PKG = releases/$(VERSION)/deb/mgmt_$(VERSION)_amd64.deb
|
TOKEN_FEDORA-29 = fedora-29
|
||||||
PACMAN_PKG = releases/$(VERSION)/pacman/mgmt-$(VERSION)-1-x86_64.pkg.tar.xz
|
TOKEN_CENTOS-7 = centos-7
|
||||||
|
TOKEN_DEBIAN-10 = debian-10
|
||||||
|
TOKEN_UBUNTU-BIONIC = ubuntu-bionic
|
||||||
|
TOKEN_ARCHLINUX = archlinux
|
||||||
|
|
||||||
|
FILE_FEDORA-30 = mgmt-$(TOKEN_FEDORA-30)-$(VERSION)-1.x86_64.rpm
|
||||||
|
FILE_FEDORA-29 = mgmt-$(TOKEN_FEDORA-29)-$(VERSION)-1.x86_64.rpm
|
||||||
|
FILE_CENTOS-7 = mgmt-$(TOKEN_CENTOS-7)-$(VERSION)-1.x86_64.rpm
|
||||||
|
FILE_DEBIAN-10 = mgmt_$(TOKEN_DEBIAN-10)_$(VERSION)_amd64.deb
|
||||||
|
FILE_UBUNTU-BIONIC = mgmt_$(TOKEN_UBUNTU-BIONIC)_$(VERSION)_amd64.deb
|
||||||
|
FILE_ARCHLINUX = mgmt-$(TOKEN_ARCHLINUX)-$(VERSION)-1-x86_64.pkg.tar.xz
|
||||||
|
|
||||||
|
PKG_FEDORA-30 = releases/$(VERSION)/$(TOKEN_FEDORA-30)/$(FILE_FEDORA-30)
|
||||||
|
PKG_FEDORA-29 = releases/$(VERSION)/$(TOKEN_FEDORA-29)/$(FILE_FEDORA-29)
|
||||||
|
PKG_CENTOS-7 = releases/$(VERSION)/$(TOKEN_CENTOS-7)/$(FILE_CENTOS-7)
|
||||||
|
PKG_DEBIAN-10 = releases/$(VERSION)/$(TOKEN_DEBIAN-10)/$(FILE_DEBIAN-10)
|
||||||
|
PKG_UBUNTU-BIONIC = releases/$(VERSION)/$(TOKEN_UBUNTU-BIONIC)/$(FILE_UBUNTU-BIONIC)
|
||||||
|
PKG_ARCHLINUX = releases/$(VERSION)/$(TOKEN_ARCHLINUX)/$(FILE_ARCHLINUX)
|
||||||
|
|
||||||
SHA256SUMS = releases/$(VERSION)/SHA256SUMS
|
SHA256SUMS = releases/$(VERSION)/SHA256SUMS
|
||||||
SHA256SUMS_ASC = $(SHA256SUMS).asc
|
SHA256SUMS_ASC = $(SHA256SUMS).asc
|
||||||
@@ -116,11 +137,6 @@ run: ## run mgmt
|
|||||||
race:
|
race:
|
||||||
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
|
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
|
||||||
|
|
||||||
# generate go files from non-go source
|
|
||||||
bindata: ## generate go files from non-go sources
|
|
||||||
$(MAKE) --quiet -C bindata
|
|
||||||
$(MAKE) --quiet -C lang/funcs
|
|
||||||
|
|
||||||
generate:
|
generate:
|
||||||
go generate
|
go generate
|
||||||
|
|
||||||
@@ -132,7 +148,7 @@ lang: ## generates the lexer/parser for the language frontend
|
|||||||
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch
|
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch
|
||||||
cp -a $< $@
|
cp -a $< $@
|
||||||
|
|
||||||
$(PROGRAM).static: $(GO_FILES) $(MCL_FILES)
|
$(PROGRAM).static: $(GO_FILES) $(MCL_FILES) go.mod go.sum
|
||||||
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
|
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
|
||||||
go generate
|
go generate
|
||||||
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION) -s -w' -o $(PROGRAM).static $(BUILD_FLAGS);
|
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION) -s -w' -o $(PROGRAM).static $(BUILD_FLAGS);
|
||||||
@@ -147,24 +163,21 @@ build-debug: $(PROGRAM)
|
|||||||
# extract os and arch from target pattern
|
# extract os and arch from target pattern
|
||||||
GOOS=$(firstword $(subst -, ,$*))
|
GOOS=$(firstword $(subst -, ,$*))
|
||||||
GOARCH=$(lastword $(subst -, ,$*))
|
GOARCH=$(lastword $(subst -, ,$*))
|
||||||
build/mgmt-%: $(GO_FILES) $(MCL_FILES) | bindata lang funcgen
|
build/mgmt-%: $(GO_FILES) $(MCL_FILES) go.mod go.sum | lang funcgen
|
||||||
@echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..."
|
@echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..."
|
||||||
@# reassigning GOOS and GOARCH to make build command copy/pastable
|
@time env GOOS=${GOOS} GOARCH=${GOARCH} go build -ldflags=$(PKGNAME)="-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS)
|
||||||
@# go 1.10+ requires specifying the package for ldflags
|
|
||||||
@if go version | grep -qE 'go1.9'; then \
|
|
||||||
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS); \
|
|
||||||
else \
|
|
||||||
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags=$(PKGNAME)="-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS); \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# create a list of binary file names to use as make targets
|
# create a list of binary file names to use as make targets
|
||||||
|
# to use this you might want to run something like:
|
||||||
|
# GOOSARCHES='linux/arm64' GOTAGS='noaugeas novirt' make crossbuild
|
||||||
|
# and the output will end up in build/
|
||||||
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
|
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
|
||||||
crossbuild: ${crossbuild_targets}
|
crossbuild: ${crossbuild_targets}
|
||||||
|
|
||||||
clean: ## clean things up
|
clean: ## clean things up
|
||||||
$(MAKE) --quiet -C bindata clean
|
$(MAKE) --quiet -C test clean
|
||||||
$(MAKE) --quiet -C lang/funcs clean
|
|
||||||
$(MAKE) --quiet -C lang clean
|
$(MAKE) --quiet -C lang clean
|
||||||
|
$(MAKE) --quiet -C misc/mkosi clean
|
||||||
rm -f lang/funcs/core/generated_funcs.go || true
|
rm -f lang/funcs/core/generated_funcs.go || true
|
||||||
rm -f lang/funcs/core/generated_funcs_test.go || true
|
rm -f lang/funcs/core/generated_funcs_test.go || true
|
||||||
[ ! -e $(PROGRAM) ] || rm $(PROGRAM)
|
[ ! -e $(PROGRAM) ] || rm $(PROGRAM)
|
||||||
@@ -174,6 +187,8 @@ clean: ## clean things up
|
|||||||
rm -f build/mgmt-*
|
rm -f build/mgmt-*
|
||||||
|
|
||||||
test: build ## run tests
|
test: build ## run tests
|
||||||
|
@# recursively run make in child dir named test
|
||||||
|
@$(MAKE) --quiet -C test
|
||||||
./test.sh
|
./test.sh
|
||||||
|
|
||||||
# create all test targets for make tab completion (eg: make test-gofmt)
|
# create all test targets for make tab completion (eg: make test-gofmt)
|
||||||
@@ -343,18 +358,63 @@ copr: upload-srpms ## build in copr
|
|||||||
tag: ## tags a new release
|
tag: ## tags a new release
|
||||||
./misc/tag.sh
|
./misc/tag.sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# mkosi
|
||||||
|
#
|
||||||
|
mkosi: mkosi_fedora-30 mkosi_fedora-29 mkosi_centos-7 mkosi_debian-10 mkosi_ubuntu-bionic mkosi_archlinux ## builds distro packages via mkosi
|
||||||
|
|
||||||
|
mkosi_fedora-30: releases/$(VERSION)/.mkdir
|
||||||
|
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||||
|
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||||
|
|
||||||
|
mkosi_fedora-29: releases/$(VERSION)/.mkdir
|
||||||
|
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||||
|
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||||
|
|
||||||
|
mkosi_centos-7: releases/$(VERSION)/.mkdir
|
||||||
|
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||||
|
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||||
|
|
||||||
|
mkosi_debian-10: releases/$(VERSION)/.mkdir
|
||||||
|
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||||
|
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||||
|
|
||||||
|
mkosi_ubuntu-bionic: releases/$(VERSION)/.mkdir
|
||||||
|
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||||
|
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||||
|
|
||||||
|
mkosi_archlinux: releases/$(VERSION)/.mkdir
|
||||||
|
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||||
|
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||||
|
|
||||||
#
|
#
|
||||||
# release
|
# release
|
||||||
#
|
#
|
||||||
release: releases/$(VERSION)/mgmt-release.url ## generates and uploads a release
|
release: releases/$(VERSION)/mgmt-release.url ## generates and uploads a release
|
||||||
|
|
||||||
releases/$(VERSION)/mgmt-release.url: $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG) $(SHA256SUMS_ASC)
|
releases_path:
|
||||||
|
@#Don't put any other output or dependencies in here or they'll show!
|
||||||
|
@echo "releases/$(VERSION)/"
|
||||||
|
|
||||||
|
release_fedora-30: $(PKG_FEDORA-30)
|
||||||
|
release_fedora-29: $(PKG_FEDORA-29)
|
||||||
|
release_centos-7: $(PKG_CENTOS-7)
|
||||||
|
release_debian-10: $(PKG_DEBIAN-10)
|
||||||
|
release_ubuntu-bionic: $(PKG_UBUNTU-BIONIC)
|
||||||
|
release_archlinux: $(PKG_ARCHLINUX)
|
||||||
|
|
||||||
|
releases/$(VERSION)/mgmt-release.url: $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_CENTOS-7) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX) $(SHA256SUMS_ASC)
|
||||||
|
@echo "Pushing git tag $(VERSION) to origin..."
|
||||||
|
git push origin $(VERSION)
|
||||||
@echo "Creating github release..."
|
@echo "Creating github release..."
|
||||||
hub release create \
|
hub release create \
|
||||||
-F <( echo -e "$(VERSION)\n";echo "Verify the signatures of all packages before you use them. The signing key can be downloaded from https://purpleidea.com/contact/#pgp-key to verify the release." ) \
|
-F <( echo -e "$(VERSION)\n";echo "Verify the signatures of all packages before you use them. The signing key can be downloaded from https://purpleidea.com/contact/#pgp-key to verify the release." ) \
|
||||||
-a $(RPM_PKG) \
|
-a $(PKG_FEDORA-30) \
|
||||||
-a $(DEB_PKG) \
|
-a $(PKG_FEDORA-29) \
|
||||||
-a $(PACMAN_PKG) \
|
-a $(PKG_CENTOS-7) \
|
||||||
|
-a $(PKG_DEBIAN-10) \
|
||||||
|
-a $(PKG_UBUNTU-BIONIC) \
|
||||||
|
-a $(PKG_ARCHLINUX) \
|
||||||
-a $(SHA256SUMS_ASC) \
|
-a $(SHA256SUMS_ASC) \
|
||||||
$(VERSION) \
|
$(VERSION) \
|
||||||
> releases/$(VERSION)/mgmt-release.url \
|
> releases/$(VERSION)/mgmt-release.url \
|
||||||
@@ -362,32 +422,56 @@ releases/$(VERSION)/mgmt-release.url: $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG) $(SHA2
|
|||||||
|| rm -f releases/$(VERSION)/mgmt-release.url
|
|| rm -f releases/$(VERSION)/mgmt-release.url
|
||||||
|
|
||||||
releases/$(VERSION)/.mkdir:
|
releases/$(VERSION)/.mkdir:
|
||||||
mkdir -p releases/$(VERSION)/{deb,rpm,pacman}/ && touch releases/$(VERSION)/.mkdir
|
mkdir -p releases/$(VERSION)/{$(TOKEN_FEDORA-30),$(TOKEN_FEDORA-29),$(TOKEN_CENTOS-7),$(TOKEN_DEBIAN-10),$(TOKEN_UBUNTU-BIONIC),$(TOKEN_ARCHLINUX)}/ && touch releases/$(VERSION)/.mkdir
|
||||||
|
|
||||||
releases/$(VERSION)/rpm/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
releases/$(VERSION)/$(TOKEN_FEDORA-30)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||||
@echo "Generating: rpm changelog..."
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
|
||||||
./misc/make-rpm-changelog.sh $(VERSION)
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-rpm-changelog.sh "$${distro}" $(VERSION)
|
||||||
|
|
||||||
$(RPM_PKG): releases/$(VERSION)/rpm/changelog
|
$(PKG_FEDORA-30): releases/$(VERSION)/$(TOKEN_FEDORA-30)/changelog
|
||||||
@echo "Building: rpm package..."
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||||
./misc/fpm-pack.sh rpm $(VERSION) libvirt-devel augeas-devel
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_FEDORA-30)" libvirt-devel augeas-devel
|
||||||
|
|
||||||
releases/$(VERSION)/deb/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
releases/$(VERSION)/$(TOKEN_FEDORA-29)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||||
@echo "Generating: deb changelog..."
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
|
||||||
./misc/make-deb-changelog.sh $(VERSION)
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-rpm-changelog.sh "$${distro}" $(VERSION)
|
||||||
|
|
||||||
$(DEB_PKG): releases/$(VERSION)/deb/changelog
|
$(PKG_FEDORA-29): releases/$(VERSION)/$(TOKEN_FEDORA-29)/changelog
|
||||||
@echo "Building: deb package..."
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||||
./misc/fpm-pack.sh deb $(VERSION) libvirt-dev libaugeas-dev
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_FEDORA-29)" libvirt-devel augeas-devel
|
||||||
|
|
||||||
$(PACMAN_PKG): $(PROGRAM) releases/$(VERSION)/.mkdir
|
releases/$(VERSION)/$(TOKEN_CENTOS-7)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||||
@echo "Building: pacman package..."
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
|
||||||
./misc/fpm-pack.sh pacman $(VERSION) libvirt augeas
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-rpm-changelog.sh "$${distro}" $(VERSION)
|
||||||
|
|
||||||
$(SHA256SUMS): $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG)
|
$(PKG_CENTOS-7): releases/$(VERSION)/$(TOKEN_CENTOS-7)/changelog
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_CENTOS-7)" libvirt-devel augeas-devel
|
||||||
|
|
||||||
|
releases/$(VERSION)/$(TOKEN_DEBIAN-10)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-deb-changelog.sh "$${distro}" $(VERSION)
|
||||||
|
|
||||||
|
$(PKG_DEBIAN-10): releases/$(VERSION)/$(TOKEN_DEBIAN-10)/changelog
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_DEBIAN-10)" libvirt-dev libaugeas-dev
|
||||||
|
|
||||||
|
releases/$(VERSION)/$(TOKEN_UBUNTU-BIONIC)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-deb-changelog.sh "$${distro}" $(VERSION)
|
||||||
|
|
||||||
|
$(PKG_UBUNTU-BIONIC): releases/$(VERSION)/$(TOKEN_UBUNTU-BIONIC)/changelog
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_UBUNTU-BIONIC)" libvirt-dev libaugeas-dev
|
||||||
|
|
||||||
|
$(PKG_ARCHLINUX): $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||||
|
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_ARCHLINUX)" libvirt augeas
|
||||||
|
|
||||||
|
$(SHA256SUMS): $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_CENTOS-7) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX)
|
||||||
@# remove the directory separator in the SHA256SUMS file
|
@# remove the directory separator in the SHA256SUMS file
|
||||||
@echo "Generating: sha256 sum..."
|
@echo "Generating: sha256 sum..."
|
||||||
sha256sum $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG) | awk -F '/| ' '{print $$1" "$$6}' > $(SHA256SUMS)
|
sha256sum $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_CENTOS-7) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX) | awk -F '/| ' '{print $$1" "$$6}' > $(SHA256SUMS)
|
||||||
|
|
||||||
$(SHA256SUMS_ASC): $(SHA256SUMS)
|
$(SHA256SUMS_ASC): $(SHA256SUMS)
|
||||||
@echo "Signing sha256 sum..."
|
@echo "Signing sha256 sum..."
|
||||||
@@ -413,14 +497,10 @@ help: ## show this help screen
|
|||||||
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
@echo ''
|
@echo ''
|
||||||
|
|
||||||
funcgen: lang/funcs/core/generated_funcs_test.go lang/funcs/core/generated_funcs.go
|
funcgen: lang/funcs/core/generated_funcs.go
|
||||||
|
|
||||||
lang/funcs/core/generated_funcs_test.go: lang/funcs/funcgen/*.go lang/funcs/core/funcgen.yaml lang/funcs/funcgen/templates/generated_funcs_test.go.tpl
|
|
||||||
@echo "Generating: funcs test..."
|
|
||||||
@go run lang/funcs/funcgen/*.go -templates lang/funcs/funcgen/templates/generated_funcs_test.go.tpl 2>/dev/null
|
|
||||||
|
|
||||||
lang/funcs/core/generated_funcs.go: lang/funcs/funcgen/*.go lang/funcs/core/funcgen.yaml lang/funcs/funcgen/templates/generated_funcs.go.tpl
|
lang/funcs/core/generated_funcs.go: lang/funcs/funcgen/*.go lang/funcs/core/funcgen.yaml lang/funcs/funcgen/templates/generated_funcs.go.tpl
|
||||||
@echo "Generating: funcs..."
|
@echo "Generating: funcs..."
|
||||||
@go run lang/funcs/funcgen/*.go -templates lang/funcs/funcgen/templates/generated_funcs.go.tpl 2>/dev/null
|
@go run `find lang/funcs/funcgen/ -maxdepth 1 -type f -name '*.go' -not -name '*_test.go'` -templates=lang/funcs/funcgen/templates/generated_funcs.go.tpl >/dev/null
|
||||||
|
|
||||||
# vim: ts=8
|
# vim: ts=8
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -4,8 +4,9 @@
|
|||||||
|
|
||||||
[](https://goreportcard.com/report/github.com/purpleidea/mgmt)
|
[](https://goreportcard.com/report/github.com/purpleidea/mgmt)
|
||||||
[](http://travis-ci.org/purpleidea/mgmt)
|
[](http://travis-ci.org/purpleidea/mgmt)
|
||||||
[](https://godoc.org/github.com/purpleidea/mgmt)
|
[](https://github.com/purpleidea/mgmt/actions/)
|
||||||
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
[](https://godocs.io/github.com/purpleidea/mgmt)
|
||||||
|
[](https://web.libera.chat/?channels=#mgmtconfig)
|
||||||
[](https://www.patreon.com/purpleidea)
|
[](https://www.patreon.com/purpleidea)
|
||||||
[](https://liberapay.com/purpleidea/donate)
|
[](https://liberapay.com/purpleidea/donate)
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ ensure that your file server is set to read-only when it's friday.
|
|||||||
import "datetime"
|
import "datetime"
|
||||||
$is_friday = datetime.weekday(datetime.now()) == "friday"
|
$is_friday = datetime.weekday(datetime.now()) == "friday"
|
||||||
file "/srv/files/" {
|
file "/srv/files/" {
|
||||||
state => "exists",
|
state => $const.res.file.state.exists,
|
||||||
mode => if $is_friday { # this updates the mode, the instant it changes!
|
mode => if $is_friday { # this updates the mode, the instant it changes!
|
||||||
"0550"
|
"0550"
|
||||||
} else {
|
} else {
|
||||||
@@ -65,7 +66,7 @@ Come join us in the `mgmt` community!
|
|||||||
|
|
||||||
| Medium | Link |
|
| Medium | Link |
|
||||||
|---|---|
|
|---|---|
|
||||||
| IRC | [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode |
|
| IRC | [#mgmtconfig](https://web.libera.chat/?channels=#mgmtconfig) on Libera.Chat |
|
||||||
| Twitter | [@mgmtconfig](https://twitter.com/mgmtconfig) & [#mgmtconfig](https://twitter.com/hashtag/mgmtconfig) |
|
| Twitter | [@mgmtconfig](https://twitter.com/mgmtconfig) & [#mgmtconfig](https://twitter.com/hashtag/mgmtconfig) |
|
||||||
| Mailing list | [mgmtconfig-list@redhat.com](https://www.redhat.com/mailman/listinfo/mgmtconfig-list) |
|
| Mailing list | [mgmtconfig-list@redhat.com](https://www.redhat.com/mailman/listinfo/mgmtconfig-list) |
|
||||||
| Patreon | [purpleidea](https://www.patreon.com/purpleidea) on Patreon |
|
| Patreon | [purpleidea](https://www.patreon.com/purpleidea) on Patreon |
|
||||||
@@ -99,6 +100,8 @@ Please read, enjoy and help improve our documentation!
|
|||||||
| [prometheus guide](docs/prometheus.md) | for everyone |
|
| [prometheus guide](docs/prometheus.md) | for everyone |
|
||||||
| [puppet guide](docs/puppet-guide.md) | for puppet sysadmins |
|
| [puppet guide](docs/puppet-guide.md) | for puppet sysadmins |
|
||||||
| [development](docs/development.md) | for mgmt developers |
|
| [development](docs/development.md) | for mgmt developers |
|
||||||
|
| [videos](docs/on-the-web.md) | for everyone |
|
||||||
|
| [blogs](docs/on-the-web.md) | for everyone |
|
||||||
|
|
||||||
## Questions:
|
## Questions:
|
||||||
|
|
||||||
@@ -107,12 +110,13 @@ If you have a well phrased question that might benefit others, consider asking
|
|||||||
it by sending a patch to the [FAQ](docs/faq.md) section. I'll merge your
|
it by sending a patch to the [FAQ](docs/faq.md) section. I'll merge your
|
||||||
question, and a patch with the answer!
|
question, and a patch with the answer!
|
||||||
|
|
||||||
## Roadmap:
|
## Get involved:
|
||||||
|
|
||||||
Feel free to grab one of the straightforward [#mgmtlove](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
Feel free to grab one of the straightforward [#mgmtlove](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
issues if you're a first time contributor to the project or if you're unsure
|
issues if you're a first time contributor to the project or if you're unsure
|
||||||
about what to hack on! Please get involved by working on one of these items or
|
about what to hack on! Please get involved by working on one of these items or
|
||||||
by suggesting something else!
|
by suggesting something else! There are some lower priority issues and harder
|
||||||
|
issues available in our [TODO](TODO.md) file. Please have a look.
|
||||||
|
|
||||||
## Bugs:
|
## Bugs:
|
||||||
|
|
||||||
@@ -126,6 +130,6 @@ We'd love to have your patches! Please send them by email, or as a pull request.
|
|||||||
|
|
||||||
## On the web:
|
## On the web:
|
||||||
|
|
||||||
[Read what people are saying and publishing about mgmt!](docs/on-the-web.md)
|
[Blog posts and recorded talks about mgmt are listed here!](docs/on-the-web.md)
|
||||||
|
|
||||||
Happy hacking!
|
Happy hacking!
|
||||||
|
|||||||
65
TODO.md
65
TODO.md
@@ -1,10 +1,18 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
If you're looking for something to do, look here!
|
Here is a TODO list of longstanding items that are either lower-priority, or
|
||||||
Let us know if you're working on one of the items.
|
more involved in terms of time, skill-level, and/or motivation.
|
||||||
If you'd like something to work on, ping @purpleidea and I'll create an issue
|
|
||||||
tailored especially for you! Just let me know your approximate golang skill
|
Please have a look, and let us know if you're working on one of the items. It's
|
||||||
level and how many hours you'd like to spend on the patch.
|
best to open an issue to track your progress and to discuss any implementation
|
||||||
|
questions you might have.
|
||||||
|
|
||||||
|
Lastly, if you'd like something different to work on, please ping @purpleidea
|
||||||
|
and I'll create an issue tailored especially for your approximate golang skill
|
||||||
|
level and available time commitment in terms of hours you'd need to spend on the
|
||||||
|
patch.
|
||||||
|
|
||||||
|
Happy Hacking!
|
||||||
|
|
||||||
## Package resource
|
## Package resource
|
||||||
|
|
||||||
@@ -19,7 +27,7 @@ level and how many hours you'd like to spend on the patch.
|
|||||||
|
|
||||||
## Svc resource
|
## Svc resource
|
||||||
|
|
||||||
- [ ] base resource improvements
|
- [ ] refreshonly support [:heart:](https://github.com/purpleidea/mgmt/issues/464)
|
||||||
|
|
||||||
## Exec resource
|
## Exec resource
|
||||||
|
|
||||||
@@ -33,33 +41,14 @@ level and how many hours you'd like to spend on the patch.
|
|||||||
|
|
||||||
- [ ] automatic edges to file resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
- [ ] automatic edges to file resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
|
|
||||||
## Virt (libvirt) resource
|
|
||||||
|
|
||||||
- [ ] base resource improvements [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
|
||||||
|
|
||||||
## Net (systemd-networkd) resource
|
|
||||||
|
|
||||||
- [ ] base resource
|
|
||||||
|
|
||||||
## Nspawn (systemd-nspawn) resource
|
|
||||||
|
|
||||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
|
||||||
|
|
||||||
## Mount (systemd-mount) resource
|
|
||||||
|
|
||||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
|
||||||
|
|
||||||
## Cron (systemd-timer) resource
|
|
||||||
|
|
||||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
|
||||||
|
|
||||||
## Http resource
|
## Http resource
|
||||||
|
|
||||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
|
|
||||||
## Etcd improvements
|
## Etcd improvements
|
||||||
|
|
||||||
- [ ] fix embedded etcd master race
|
- [ ] fix etcd race bug that only happens during CI testing (intermittently
|
||||||
|
failing test case issue)
|
||||||
|
|
||||||
## Torrent/dht file transfer
|
## Torrent/dht file transfer
|
||||||
|
|
||||||
@@ -69,17 +58,33 @@ level and how many hours you'd like to spend on the patch.
|
|||||||
|
|
||||||
- [ ] base plumbing
|
- [ ] base plumbing
|
||||||
|
|
||||||
|
## Resource improvements
|
||||||
|
|
||||||
|
- [ ] more reversible resources implemented
|
||||||
|
- [ ] more "cloud" resources
|
||||||
|
|
||||||
## Language improvements
|
## Language improvements
|
||||||
|
|
||||||
- [ ] more core functions
|
- [ ] more core functions
|
||||||
- [ ] automatic language formatter, ala `gofmt`
|
- [ ] automatic language formatter, ala `gofmt`
|
||||||
- [ ] gedit/gnome-builder/gtksourceview syntax highlighting
|
- [ ] gedit/gnome-builder/gtksourceview syntax highlighting
|
||||||
- [ ] vim syntax highlighting
|
- [ ] vim syntax highlighting
|
||||||
- [x] emacs syntax highlighting: see `misc/emacs/`
|
- [ ] emacs syntax highlighting: see `misc/emacs/` (needs updating)
|
||||||
|
- [ ] exposed $error variable for feedback in the language
|
||||||
|
- [ ] improve the printf function to add %[]s, %[]f ([]str, []float) and map,
|
||||||
|
struct, nested etc... %v would be nice too!
|
||||||
|
- [ ] add line/col/file annotations to AST so we can get locations of errors
|
||||||
|
that the parser finds
|
||||||
|
- [ ] add more error messages with the `%error` pattern in parser.y
|
||||||
|
- [ ] we should have helper functions or language sugar to pull a field out of a
|
||||||
|
struct, or a value out of a map, or an index out of a list, etc...
|
||||||
|
|
||||||
|
## Engine improvements
|
||||||
|
|
||||||
|
- [ ] add a "waiting for func" message in the func engine to notify the user
|
||||||
|
about slow functions...
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
- [ ] better error/retry handling
|
|
||||||
- [ ] deb package target in Makefile
|
|
||||||
- [ ] reproducible builds
|
- [ ] reproducible builds
|
||||||
- [ ] add your suggestions!
|
- [ ] add your suggestions!
|
||||||
|
|||||||
11
Vagrantfile
vendored
11
Vagrantfile
vendored
@@ -6,7 +6,7 @@ Vagrant.configure(2) do |config|
|
|||||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||||
|
|
||||||
config.vm.define "mgmt-dev" do |instance|
|
config.vm.define "mgmt-dev" do |instance|
|
||||||
instance.vm.box = "fedora/28-cloud-base"
|
instance.vm.box = "bento/fedora-31"
|
||||||
end
|
end
|
||||||
|
|
||||||
config.vm.provider "virtualbox" do |v|
|
config.vm.provider "virtualbox" do |v|
|
||||||
@@ -23,8 +23,7 @@ Vagrant.configure(2) do |config|
|
|||||||
config.vm.provision "file", source: "vagrant/mgmt.bashrc", destination: ".mgmt.bashrc"
|
config.vm.provision "file", source: "vagrant/mgmt.bashrc", destination: ".mgmt.bashrc"
|
||||||
config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
|
config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
|
||||||
|
|
||||||
# copied from make-deps.sh (with added git)
|
config.vm.provision "shell", inline: "dnf install -y golang git make"
|
||||||
config.vm.provision "shell", inline: "dnf install -y libvirt-devel golang golang-googlecode-tools-stringer hg git make gem"
|
|
||||||
|
|
||||||
# set up packagekit
|
# set up packagekit
|
||||||
config.vm.provision "shell" do |shell|
|
config.vm.provision "shell" do |shell|
|
||||||
@@ -39,8 +38,10 @@ Vagrant.configure(2) do |config|
|
|||||||
script = <<-SCRIPT
|
script = <<-SCRIPT
|
||||||
grep -q 'mgmt\.bashrc' ~/.bashrc || echo '. ~/.mgmt.bashrc' >>~/.bashrc
|
grep -q 'mgmt\.bashrc' ~/.bashrc || echo '. ~/.mgmt.bashrc' >>~/.bashrc
|
||||||
. ~/.mgmt.bashrc
|
. ~/.mgmt.bashrc
|
||||||
go get -u github.com/purpleidea/mgmt
|
mkdir -p ~/gopath/src/github.com/purpleidea
|
||||||
cd ~/gopath/src/github.com/purpleidea/mgmt
|
cd ~/gopath/src/github.com/purpleidea
|
||||||
|
git clone https://github.com/purpleidea/mgmt --recursive
|
||||||
|
cd mgmt
|
||||||
make deps
|
make deps
|
||||||
SCRIPT
|
SCRIPT
|
||||||
config.vm.provision "shell" do |shell|
|
config.vm.provision "shell" do |shell|
|
||||||
|
|||||||
BIN
art/mgmt.png
BIN
art/mgmt.png
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 683 KiB |
BIN
art/mgmt_poohbear_meme.jpg
Normal file
BIN
art/mgmt_poohbear_meme.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@@ -1,42 +0,0 @@
|
|||||||
# Mgmt
|
|
||||||
# Copyright (C) 2013-2019+ James Shubin and the project contributors
|
|
||||||
# Written by James Shubin <james@shubin.ca> and the project contributors
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
# The bindata target generates go files from any source defined below. To use
|
|
||||||
# the files, import the generated "bindata" package and use:
|
|
||||||
# `bytes, err := bindata.Asset("FILEPATH")`
|
|
||||||
# where FILEPATH is the path of the original input file relative to `bindata/`.
|
|
||||||
# To get a list of files stored in this "bindata" package, you can use:
|
|
||||||
# `paths := bindata.AssetNames()` and `paths, err := bindata.AssetDir(name)`
|
|
||||||
# to get a list of files with a directory prefix.
|
|
||||||
|
|
||||||
.PHONY: build clean
|
|
||||||
default: build
|
|
||||||
|
|
||||||
build: bindata.go
|
|
||||||
|
|
||||||
# add more input files as dependencies at the end here...
|
|
||||||
bindata.go: ../COPYING
|
|
||||||
@echo "Generating: bindata..."
|
|
||||||
# go-bindata --pkg bindata -o <OUTPUT> <INPUT>
|
|
||||||
go-bindata --pkg bindata -o ./$@ $^
|
|
||||||
# gofmt the output file
|
|
||||||
gofmt -s -w $@
|
|
||||||
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && $$ROOT/misc/header.sh '$@'
|
|
||||||
|
|
||||||
clean:
|
|
||||||
# remove generated bindata.go
|
|
||||||
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && rm -f bindata.go
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package converger
|
package converger
|
||||||
|
|
||||||
|
|||||||
2
debian/copyright
vendored
2
debian/copyright
vendored
@@ -3,7 +3,7 @@ Upstream-Name: mgmt
|
|||||||
Source: <https://github.com/purpleidea/mgmt>
|
Source: <https://github.com/purpleidea/mgmt>
|
||||||
|
|
||||||
Files: *
|
Files: *
|
||||||
Copyright: Copyright (C) 2013-2019+ James Shubin and the project contributors
|
Copyright: Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
2
doc.go
2
doc.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM golang:1.9
|
FROM golang:1.18
|
||||||
|
|
||||||
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
||||||
|
|
||||||
# Set the reset cache variable
|
# Set the reset cache variable
|
||||||
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
|
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
|
||||||
ENV REFRESHED_AT 2017-11-16
|
ENV REFRESHED_AT 2020-09-23
|
||||||
|
|
||||||
# Update the package list to be able to use required packages
|
# Update the package list to be able to use required packages
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ ENV PATH=/opt/rh/rh-ruby22/root/usr/bin:/root/gopath/bin:/usr/local/sbin:/sbin:/
|
|||||||
ENV LD_LIBRARY_PATH=/opt/rh/rh-ruby22/root/usr/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
|
ENV LD_LIBRARY_PATH=/opt/rh/rh-ruby22/root/usr/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
|
||||||
ENV PKG_CONFIG_PATH=/opt/rh/rh-ruby22/root/usr/lib64/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}
|
ENV PKG_CONFIG_PATH=/opt/rh/rh-ruby22/root/usr/lib64/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}
|
||||||
|
|
||||||
RUN yum -y install epel-release wget unzip git make which centos-release-scl gcc && sed -i "s/enabled=0/enabled=1/" /etc/yum.repos.d/epel-testing.repo && yum -y install rh-ruby22 && wget -O /opt/go1.9.1.linux-amd64.tar.gz https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz && tar -C /usr/local -xzf /opt/go1.9.1.linux-amd64.tar.gz
|
RUN yum -y install epel-release wget unzip git make which centos-release-scl gcc && sed -i "s/enabled=0/enabled=1/" /etc/yum.repos.d/epel-testing.repo && yum -y install rh-ruby22 && wget -O /opt/go1.18.5.linux-amd64.tar.gz https://storage.googleapis.com/golang/go1.18.5.linux-amd64.tar.gz && tar -C /usr/local -xzf /opt/go1.18.5.linux-amd64.tar.gz
|
||||||
RUN mkdir -p $GOPATH/src/github.com/purpleidea && cd $GOPATH/src/github.com/purpleidea && git clone --recursive https://github.com/purpleidea/mgmt
|
RUN mkdir -p $GOPATH/src/github.com/purpleidea && cd $GOPATH/src/github.com/purpleidea && git clone --recursive https://github.com/purpleidea/mgmt
|
||||||
RUN go get -u gopkg.in/alecthomas/gometalinter.v1 && cd $GOPATH/src/github.com/purpleidea/mgmt && make deps && make build
|
RUN go get -u gopkg.in/alecthomas/gometalinter.v1 && cd $GOPATH/src/github.com/purpleidea/mgmt && make deps && make build
|
||||||
CMD ["/bin/bash"]
|
CMD ["/bin/bash"]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.11
|
FROM golang:1.18
|
||||||
|
|
||||||
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
||||||
|
|
||||||
@@ -27,8 +27,5 @@ WORKDIR /home/$USER_NAME/mgmt
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN make deps
|
RUN make deps
|
||||||
|
|
||||||
# Chown $GOPATH
|
|
||||||
RUN chown -R ${USER_ID}:${GROUP_ID} /go
|
|
||||||
|
|
||||||
# Change user
|
# Change user
|
||||||
USER ${USER_NAME}
|
USER ${USER_NAME}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'mgmt'
|
project = u'mgmt'
|
||||||
copyright = u'2013-2019+ James Shubin and the project contributors'
|
copyright = u'2013-2023+ James Shubin and the project contributors'
|
||||||
author = u'James Shubin'
|
author = u'James Shubin'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ required for running the _test_ suite.
|
|||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
* `golang` 1.11 or higher (required, available in some distros and distributed
|
* `golang` 1.18 or higher (required, available in some distros and distributed
|
||||||
as a binary officially by [golang.org](https://golang.org/dl/))
|
as a binary officially by [golang.org](https://golang.org/dl/))
|
||||||
|
|
||||||
### Runtime
|
### Runtime
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ entire set of running mgmt agents will need to all simultaneously converge for
|
|||||||
the group to exit. This is particularly useful for bootstrapping new clusters
|
the group to exit. This is particularly useful for bootstrapping new clusters
|
||||||
which need to exchange information that is only available at run time.
|
which need to exchange information that is only available at run time.
|
||||||
|
|
||||||
|
This existed in earlier versions of mgmt as a `--remote` option, but it has been
|
||||||
|
removed and is being ported to a more powerful variant where you can remote
|
||||||
|
execute via a `remote` resource.
|
||||||
|
|
||||||
#### Blog post
|
#### Blog post
|
||||||
|
|
||||||
You can read the introductory blog post about this topic here:
|
You can read the introductory blog post about this topic here:
|
||||||
@@ -250,6 +254,43 @@ integer, then that value is the max size for that semaphore. Valid semaphore
|
|||||||
id's include: `some_id`, `hello:42`, `not:smart:4` and `:13`. It is expected
|
id's include: `some_id`, `hello:42`, `not:smart:4` and `:13`. It is expected
|
||||||
that the last bare example be only used by the engine to add a global semaphore.
|
that the last bare example be only used by the engine to add a global semaphore.
|
||||||
|
|
||||||
|
#### Rewatch
|
||||||
|
|
||||||
|
Boolean. Rewatch specifies whether we re-run the Watch worker during a graph
|
||||||
|
swap if it has errored. When doing a graph compare to swap the graphs, if this
|
||||||
|
is true, and this particular worker has errored, then we'll remove it and add it
|
||||||
|
back as a new vertex, thus causing it to run again. This is different from the
|
||||||
|
`Retry` metaparam which applies during the normal execution. It is only when
|
||||||
|
this is exhausted that we're in permanent worker failure, and only then can we
|
||||||
|
rely on this metaparam.
|
||||||
|
|
||||||
|
#### Realize
|
||||||
|
|
||||||
|
Boolean. Realize ensures that the resource is guaranteed to converge at least
|
||||||
|
once before a potential graph swap removes or changes it. This guarantee is
|
||||||
|
useful for fast changing graphs, to ensure that the brief creation of a resource
|
||||||
|
is seen. This guarantee does not prevent against the engine quitting normally,
|
||||||
|
and it can't guarantee it if the resource is blocked because of a failed
|
||||||
|
pre-requisite resource.
|
||||||
|
*XXX: This is currently not implemented!*
|
||||||
|
|
||||||
|
#### Reverse
|
||||||
|
|
||||||
|
Boolean. Reverse is a property that some resources can implement that specifies
|
||||||
|
that some "reverse" operation should happen when that resource "disappears". A
|
||||||
|
disappearance happens when a resource is defined in one instance of the graph,
|
||||||
|
and is gone in the subsequent one. This disappearance can happen if it was
|
||||||
|
previously in an if statement that then becomes false.
|
||||||
|
|
||||||
|
This is helpful for building robust programs with the engine. The engine adds a
|
||||||
|
"reversed" resource to that subsequent graph to accomplish the desired "reverse"
|
||||||
|
mechanics. The specifics of what this entails is a property of the particular
|
||||||
|
resource that is being "reversed".
|
||||||
|
|
||||||
|
It might be wise to combine the use of this meta parameter with the use of the
|
||||||
|
`realize` meta parameter to ensure that your reversed resource actually runs at
|
||||||
|
least once, if there's a chance that it might be gone for a while.
|
||||||
|
|
||||||
### Lang metadata file
|
### Lang metadata file
|
||||||
|
|
||||||
Any module *must* have a metadata file in its root. It must be named
|
Any module *must* have a metadata file in its root. It must be named
|
||||||
@@ -323,12 +364,6 @@ collision with this globally defined semaphore. The size value must be greater
|
|||||||
than zero at this time. The traditional non-parallel execution found in config
|
than zero at this time. The traditional non-parallel execution found in config
|
||||||
management tools such as `Puppet` can be obtained with `--sema 1`.
|
management tools such as `Puppet` can be obtained with `--sema 1`.
|
||||||
|
|
||||||
#### `--remote <graph.yaml>`
|
|
||||||
|
|
||||||
Point to a graph file to run on the remote host specified within. This parameter
|
|
||||||
can be used multiple times if you'd like to remotely run on multiple hosts in
|
|
||||||
parallel.
|
|
||||||
|
|
||||||
#### `--allow-interactive`
|
#### `--allow-interactive`
|
||||||
|
|
||||||
Allow interactive prompting for SSH passwords if there is no authentication
|
Allow interactive prompting for SSH passwords if there is no authentication
|
||||||
@@ -367,8 +402,8 @@ default prefix. This can't be combined with the `--prefix` option.
|
|||||||
If this option is specified, we will attempt to fall back to a temporary prefix
|
If this option is specified, we will attempt to fall back to a temporary prefix
|
||||||
if the primary prefix couldn't be created. This is useful for avoiding failures
|
if the primary prefix couldn't be created. This is useful for avoiding failures
|
||||||
in environments where the primary prefix may or may not be available, but you'd
|
in environments where the primary prefix may or may not be available, but you'd
|
||||||
like to try. The canonical example is when running `mgmt` with `--remote` there
|
like to try. The canonical example is when running `mgmt` with remote execution
|
||||||
might be a cached copy of the binary in the primary prefix, but in case there's
|
there might be a cached copy of the binary in the primary prefix, but if there's
|
||||||
no binary available continue working in a temporary directory to avoid failure.
|
no binary available continue working in a temporary directory to avoid failure.
|
||||||
|
|
||||||
### Compilation options
|
### Compilation options
|
||||||
@@ -451,7 +486,7 @@ To report any bugs, please file a ticket at: [https://github.com/purpleidea/mgmt
|
|||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
Copyright (C) 2013-2019+ James Shubin and the project contributors
|
Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
|
|
||||||
Please see the
|
Please see the
|
||||||
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file
|
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file
|
||||||
|
|||||||
68
docs/faq.md
68
docs/faq.md
@@ -53,10 +53,11 @@ find a number of tutorials online.
|
|||||||
3. Spend between four to six hours with the [golang tour](https://tour.golang.org/).
|
3. Spend between four to six hours with the [golang tour](https://tour.golang.org/).
|
||||||
Skip over the longer problems, but try and get a solid overview of everything.
|
Skip over the longer problems, but try and get a solid overview of everything.
|
||||||
If you forget something, you can always go back and repeat those parts.
|
If you forget something, you can always go back and repeat those parts.
|
||||||
4. Connect to our [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig)
|
4. Connect to our [#mgmtconfig](https://web.libera.chat/?channels=#mgmtconfig)
|
||||||
IRC channel on the [Freenode](https://freenode.net/) network. You can use any
|
IRC channel on the [Libera.Chat](https://libera.chat/) network. You can use any
|
||||||
IRC client that you'd like, but the [hosted web portal](https://webchat.freenode.net/?channels=#mgmtconfig)
|
IRC client that you'd like, but the [hosted web portal](https://web.libera.chat/?channels=#mgmtconfig)
|
||||||
will suffice if you don't know what else to use.
|
will suffice if you don't know what else to use. [Here are a few suggestions for
|
||||||
|
alternative clients.](https://libera.chat/guides/clients)
|
||||||
5. Now it's time to try and starting writing a patch! We have tagged a bunch of
|
5. Now it's time to try and starting writing a patch! We have tagged a bunch of
|
||||||
[open issues as #mgmtlove](https://github.com/purpleidea/mgmt/issues?q=is%3Aissue+is%3Aopen+label%3Amgmtlove)
|
[open issues as #mgmtlove](https://github.com/purpleidea/mgmt/issues?q=is%3Aissue+is%3Aopen+label%3Amgmtlove)
|
||||||
for new users to have somewhere to get involved. Look through them to see if
|
for new users to have somewhere to get involved. Look through them to see if
|
||||||
@@ -215,16 +216,40 @@ requires a number of seconds as an argument.
|
|||||||
./mgmt run lang examples/lang/hello0.mcl --converged-timeout=5
|
./mgmt run lang examples/lang/hello0.mcl --converged-timeout=5
|
||||||
```
|
```
|
||||||
|
|
||||||
### When I try to build `mgmt` I see: `no Go files in $GOPATH/src/github.com/purpleidea/mgmt/bindata`.
|
### Why does my file resource error with `no such file or directory`?
|
||||||
|
|
||||||
Due to the arcane way that `golang` designed its `$GOPATH`, the main project
|
If you create a file resource and only specify the content like this:
|
||||||
directory must be inside your `$GOPATH`, and at the appropriate FQDN. This is:
|
|
||||||
`$GOPATH/src/github.com/purpleidea/mgmt/`. If you have your project root outside
|
```
|
||||||
of that directory, then you may get this error when you try to build it. In this
|
file "/tmp/foo" {
|
||||||
case there is likely a `go get` version of the project at this location. Remove
|
content => "hello world\n",
|
||||||
it and replace it with your git cloned directory. In my case, I like to work on
|
}
|
||||||
things in `~/code/mgmt/`, so that path is a symlink that points to the long
|
```
|
||||||
project directory.
|
|
||||||
|
Then this will attempt to set the contents of that file to the desired string,
|
||||||
|
but *only* if that file already exists. If you'd like to ensure that it also
|
||||||
|
gets created in case it is not present, then you must also specify the state:
|
||||||
|
|
||||||
|
```
|
||||||
|
file "/tmp/foo" {
|
||||||
|
state => $const.res.file.state.exists,
|
||||||
|
content => "hello world\n",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Similar logic applies for situations when you only specify the `mode` parameter.
|
||||||
|
|
||||||
|
This all turns out to be more safe and "correct", in that it would error and
|
||||||
|
prevent masking an error for a situation when you expected a file to already be
|
||||||
|
at that location. It also turns out to simplify the internals significantly, and
|
||||||
|
remove an ambiguous scenario with the reversable file resource.
|
||||||
|
|
||||||
|
### Why do function names inside of templates include underscores?
|
||||||
|
|
||||||
|
The golang template library which we use to implement the template() function
|
||||||
|
doesn't support the dot notation, so we import all our normal functions, and
|
||||||
|
just replace dots with underscores. As an example, the standard `datetime.print`
|
||||||
|
function is shown within mcl scripts as datetime_print after being imported.
|
||||||
|
|
||||||
### On startup `mgmt` hangs after: `etcd: server: starting...`.
|
### On startup `mgmt` hangs after: `etcd: server: starting...`.
|
||||||
|
|
||||||
@@ -259,6 +284,13 @@ an instance of mgmt running, or if a related file locking issue occurred. To
|
|||||||
solve this, shutdown and running mgmt process, run `rm mgmt` to remove the file,
|
solve this, shutdown and running mgmt process, run `rm mgmt` to remove the file,
|
||||||
and then get a new one by running `make` again.
|
and then get a new one by running `make` again.
|
||||||
|
|
||||||
|
### The docs speaks of `--remote` but the CLI errors out?
|
||||||
|
|
||||||
|
The `--remote` flag existed in an earlier version of mgmt. It was removed and
|
||||||
|
will be replaced with a more powerful version, which is a "remote" resource. The
|
||||||
|
code is mostly ready but it's not finished. If you'd like to help finish it or
|
||||||
|
sponsor the work, please let me know.
|
||||||
|
|
||||||
### Does this support Windows? OSX? GNU Hurd?
|
### Does this support Windows? OSX? GNU Hurd?
|
||||||
|
|
||||||
Mgmt probably works best on Linux, because that's what most developers use for
|
Mgmt probably works best on Linux, because that's what most developers use for
|
||||||
@@ -321,6 +353,14 @@ Don't blindly use the tools that others tell you to. Learn what they do, think
|
|||||||
for yourself, and become a power user today! That process led us to using
|
for yourself, and become a power user today! That process led us to using
|
||||||
`git submodules`. Hopefully you'll come to the same conclusions that we did.
|
`git submodules`. Hopefully you'll come to the same conclusions that we did.
|
||||||
|
|
||||||
|
**UPDATE:**
|
||||||
|
|
||||||
|
After golang made it virtually impossible to build without `go.mod` stuff, we've
|
||||||
|
switched to it since golang 1.16. I still think the above approach was better,
|
||||||
|
and that the `go mod` tooling should have been a layer on top of git submodules
|
||||||
|
so that we don't grow yet another lock file format, and existing folks who are
|
||||||
|
comfortable with `git` can use those tools directly.
|
||||||
|
|
||||||
### Did you know that there is a band named `MGMT`?
|
### Did you know that there is a band named `MGMT`?
|
||||||
|
|
||||||
I didn't realize this when naming the project, and it is accidental. After much
|
I didn't realize this when naming the project, and it is accidental. After much
|
||||||
@@ -334,7 +374,7 @@ which definitely existed before the band did.
|
|||||||
|
|
||||||
### You didn't answer my question, or I have a question!
|
### You didn't answer my question, or I have a question!
|
||||||
|
|
||||||
It's best to ask on [IRC](https://webchat.freenode.net/?channels=#mgmtconfig)
|
It's best to ask on [IRC](https://web.libera.chat/?channels=#mgmtconfig)
|
||||||
to see if someone can help you. If you don't get a response from IRC, you can
|
to see if someone can help you. If you don't get a response from IRC, you can
|
||||||
contact me through my [technical blog](https://purpleidea.com/contact/) and I'll
|
contact me through my [technical blog](https://purpleidea.com/contact/) and I'll
|
||||||
do my best to help. If you have a good question, please add it as a patch to
|
do my best to help. If you have a good question, please add it as a patch to
|
||||||
|
|||||||
@@ -37,8 +37,10 @@ available types and values in the mgmt language. It is very easy to use, and
|
|||||||
should be fairly intuitive. Most of what you'll need to know can be inferred
|
should be fairly intuitive. Most of what you'll need to know can be inferred
|
||||||
from looking at example code.
|
from looking at example code.
|
||||||
|
|
||||||
To implement a function, you'll need to create a file in
|
To implement a function, you'll need to create a file that imports the
|
||||||
[`lang/funcs/simple/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simple/).
|
[`lang/funcs/simple/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simple/)
|
||||||
|
module. It should probably get created in the correct directory inside of:
|
||||||
|
[`lang/funcs/core/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
|
||||||
The function should be implemented as a `FuncValue` in our type system. It is
|
The function should be implemented as a `FuncValue` in our type system. It is
|
||||||
then registered with the engine during `init()`. An example explains it best:
|
then registered with the engine during `init()`. An example explains it best:
|
||||||
|
|
||||||
@@ -50,14 +52,15 @@ package simple
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/lang/funcs/simple"
|
||||||
"github.com/purpleidea/mgmt/lang/types"
|
"github.com/purpleidea/mgmt/lang/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// you must register your functions in init when the program starts up
|
// you must register your functions in init when the program starts up
|
||||||
func init() {
|
func init() {
|
||||||
// Example function that squares an int and prints out answer as an str.
|
// Example function that squares an int and prints out answer as an str.
|
||||||
Register("talkingsquare", &types.FuncValue{
|
simple.ModuleRegister(ModuleName, "talkingsquare", &types.FuncValue{
|
||||||
T: types.NewType("func(a int) str"), // declare the signature
|
T: types.NewType("func(int) str"), // declare the signature
|
||||||
V: func(input []types.Value) (types.Value, error) {
|
V: func(input []types.Value) (types.Value, error) {
|
||||||
i := input[0].Int() // get first arg as an int64
|
i := input[0].Int() // get first arg as an int64
|
||||||
// must return the above specified value
|
// must return the above specified value
|
||||||
@@ -109,15 +112,20 @@ As with the simple, non-polymorphic API, you can only implement [pure](https://e
|
|||||||
functions, without writing too much boilerplate code. They will be automatically
|
functions, without writing too much boilerplate code. They will be automatically
|
||||||
re-evaluated as needed when their input values change.
|
re-evaluated as needed when their input values change.
|
||||||
|
|
||||||
To implement a function, you'll need to create a file in
|
To implement a function, you'll need to create a file that imports the
|
||||||
[`lang/funcs/simplepoly/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simplepoly/).
|
[`lang/funcs/simplepoly/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simplepoly/)
|
||||||
|
module. It should probably get created in the correct directory inside of:
|
||||||
|
[`lang/funcs/core/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
|
||||||
The function should be implemented as a list of `FuncValue`'s in our type
|
The function should be implemented as a list of `FuncValue`'s in our type
|
||||||
system. It is then registered with the engine during `init()`. You may also use
|
system. It is then registered with the engine during `init()`. You may also use
|
||||||
the `variant` type in your type definitions. This special type will never be
|
the `variant` type in your type definitions. This special type will never be
|
||||||
seen inside a running program, and will get converted to a concrete type if a
|
seen inside a running program, and will get converted to a concrete type if a
|
||||||
suitable match to this signature can be found. Be warned that signatures which
|
suitable match to this signature can be found. Be warned that signatures which
|
||||||
contain too many variants, or which are very general, might be hard for the
|
contain too many variants, or which are very general, might be hard for the
|
||||||
compiler to match, and ambiguous type graphs make for user compiler errors.
|
compiler to match, and ambiguous type graphs make for user compiler errors. The
|
||||||
|
top-level type must still be a function type, it may only contain variants as
|
||||||
|
part of its signature. It is probably more difficult to unify a function if its
|
||||||
|
return type is a variant, as opposed to if one of its args was.
|
||||||
|
|
||||||
An example explains it best:
|
An example explains it best:
|
||||||
|
|
||||||
@@ -127,11 +135,13 @@ An example explains it best:
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/lang/types"
|
|
||||||
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
|
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
|
||||||
|
"github.com/purpleidea/mgmt/lang/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
// You may use the simplepoly.ModuleRegister method to register your
|
||||||
|
// function if it's in a module, as seen in the simple function example.
|
||||||
simplepoly.Register("len", []*types.FuncValue{
|
simplepoly.Register("len", []*types.FuncValue{
|
||||||
{
|
{
|
||||||
T: types.NewType("func([]variant) int"),
|
T: types.NewType("func([]variant) int"),
|
||||||
@@ -190,7 +200,7 @@ if it meets your needs. Most functions will be able to use that API. If you
|
|||||||
really need something more powerful, then you can use the regular function API.
|
really need something more powerful, then you can use the regular function API.
|
||||||
What follows are each of the method signatures and a description of each.
|
What follows are each of the method signatures and a description of each.
|
||||||
|
|
||||||
### Default
|
### Info
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
Info() *interfaces.Info
|
Info() *interfaces.Info
|
||||||
@@ -435,6 +445,11 @@ generator to build your `FuncValue` implementations, and pass in the unique
|
|||||||
signature to each one as you are building them. Using a generator is a common
|
signature to each one as you are building them. Using a generator is a common
|
||||||
technique which was mentioned previously.
|
technique which was mentioned previously.
|
||||||
|
|
||||||
|
One obvious situation where this might occur is if your function doesn't take
|
||||||
|
any inputs! An example `math.fortytwo()` function was implemented that
|
||||||
|
demonstrates the use of function generators to pass the type signatures into the
|
||||||
|
implementations.
|
||||||
|
|
||||||
### Where can I find more information about mgmt?
|
### Where can I find more information about mgmt?
|
||||||
|
|
||||||
Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md).
|
Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md).
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ can be impossible to infer the item's type.
|
|||||||
|
|
||||||
An unordered set of unique keys of the same type and corresponding value pairs
|
An unordered set of unique keys of the same type and corresponding value pairs
|
||||||
of another type, eg:
|
of another type, eg:
|
||||||
`{"boiling" => 100, "freezing" => 0, "room" => "25", "house" => 22, "canada" => -30,}`.
|
`{"boiling" => 100, "freezing" => 0, "room" => 25, "house" => 22, "canada" => -30,}`.
|
||||||
That is to say, all of the keys must have the same type, and all of the values
|
That is to say, all of the keys must have the same type, and all of the values
|
||||||
must have the same type. You can use any type for either, although it is
|
must have the same type. You can use any type for either, although it is
|
||||||
probably advisable to avoid using very complex types as map keys.
|
probably advisable to avoid using very complex types as map keys.
|
||||||
@@ -206,7 +206,7 @@ value to use if that boolean is true. You can do this with the resource-specific
|
|||||||
$b = true # change me to false and then try editing the file manually
|
$b = true # change me to false and then try editing the file manually
|
||||||
file "/tmp/mgmt-elvis" {
|
file "/tmp/mgmt-elvis" {
|
||||||
content => $b ?: "hello world\n",
|
content => $b ?: "hello world\n",
|
||||||
state => "exists",
|
state => $const.res.file.state.exists,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ send notifications. You may have multiples of these per resource, including
|
|||||||
multiple `Depend` lines if necessary. Each of these properties also supports the
|
multiple `Depend` lines if necessary. Each of these properties also supports the
|
||||||
conditional inclusion `elvis` operator as well.
|
conditional inclusion `elvis` operator as well.
|
||||||
|
|
||||||
For example, you may write is:
|
For example, you may write:
|
||||||
|
|
||||||
```mcl
|
```mcl
|
||||||
$b = true # for example purposes
|
$b = true # for example purposes
|
||||||
|
|||||||
@@ -44,3 +44,16 @@ if we missed something that you think is relevant!
|
|||||||
| James Shubin | blog | [Mgmt Configuration Language](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/) |
|
| James Shubin | blog | [Mgmt Configuration Language](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/) |
|
||||||
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2018](https://www.youtube.com/watch?v=NxObmwZDyrI) |
|
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2018](https://www.youtube.com/watch?v=NxObmwZDyrI) |
|
||||||
| Jonathan Gold | blog | [Go Netlink and Select](https://jonathangold.ca/blog/go-netlink-and-select/) |
|
| Jonathan Gold | blog | [Go Netlink and Select](https://jonathangold.ca/blog/go-netlink-and-select/) |
|
||||||
|
| James Shubin | video | [Recording from DevOpsDays Montreal 2018](https://www.youtube.com/watch?v=1i38c5cooHo) |
|
||||||
|
| James Shubin | video | [Recording from FOSDEM Minimalistic Languages Devroom 2019](https://video.fosdem.org/2019/K.4.201/mgmtconfig.webm) |
|
||||||
|
| James Shubin | video | [Recording from FOSDEM Infra Management Devroom 2019](https://video.fosdem.org/2019/UB2.252A/mgmt.webm) |
|
||||||
|
| James Shubin | video | [Recording from FOSDEM Graph Processing Devroom 2019](https://video.fosdem.org/2019/H.1308/graph_mgmt_config.webm) |
|
||||||
|
| James Shubin | video | [Recording from FOSDEM Virtualization Devroom 2019](https://video.fosdem.org/2019/H.2213/vai_real_time_virtualization_automation.webm) |
|
||||||
|
| James Shubin | video | [Recording from FOSDEM Containers Devroom 2019](https://video.fosdem.org/2019/UA2.114/containers_mgmt.webm) |
|
||||||
|
| James Shubin | video | [Recording from FOSDEM Monitoring Devroom 2019](https://video.fosdem.org/2019/UB2.252A/real_time_merging_of_config_management_and_monitoring.webm) |
|
||||||
|
| James Shubin | blog | [Mgmt Configuration Language: Class and Include](https://purpleidea.com/blog/2019/07/26/class-and-include-in-mgmt/) |
|
||||||
|
| James Shubin | video | [Recording from FOSDEM 2020, Main Track (History)](https://video.fosdem.org/2020/Janson/automation.webm) |
|
||||||
|
| James Shubin | video | [Recording from FOSDEM 2020, Infra Management Devroom](https://video.fosdem.org/2020/UA2.120/mgmt.webm) |
|
||||||
|
| James Shubin | video | [Recording from FOSDEM 2020, Minimalistic Languages Devroom](https://video.fosdem.org/2020/AW1.125/mgmtconfigmore.webm) |
|
||||||
|
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2020](https://www.youtube.com/watch?v=Kd7FAORFtsc) |
|
||||||
|
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2023](https://www.youtube.com/watch?v=FeRGRj8w0BU) |
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ You'll need some dependencies, including `golang`, and some associated tools.
|
|||||||
|
|
||||||
#### Installing golang
|
#### Installing golang
|
||||||
|
|
||||||
* You need golang version 1.11 or greater installed.
|
* You need golang version 1.18 or greater installed.
|
||||||
* To install on rpm style systems: `sudo dnf install golang`
|
* To install on rpm style systems: `sudo dnf install golang`
|
||||||
* To install on apt style systems: `sudo apt install golang`
|
* To install on apt style systems: `sudo apt install golang`
|
||||||
* To install on macOS systems install [Homebrew](https://brew.sh)
|
* To install on macOS systems install [Homebrew](https://brew.sh)
|
||||||
and run: `brew install go`
|
and run: `brew install go`
|
||||||
* You can run `go version` to check the golang version.
|
* You can run `go version` to check the golang version.
|
||||||
* If your distro is tool old, you may need to [download](https://golang.org/dl/)
|
* If your distro is too old, you may need to [download](https://golang.org/dl/)
|
||||||
a newer golang version.
|
a newer golang version.
|
||||||
|
|
||||||
#### Setting up golang
|
#### Setting up golang
|
||||||
@@ -63,13 +63,11 @@ export GOPATH=$HOME/gopath
|
|||||||
|
|
||||||
#### Getting the mgmt code and associated dependencies
|
#### Getting the mgmt code and associated dependencies
|
||||||
|
|
||||||
* Download the `mgmt` code into the `GOPATH`, and switch to that directory:
|
* Download the `mgmt` code and switch to that directory:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
[ -z "$GOPATH" ] && mkdir ~/go/ || mkdir -p $GOPATH/src/github.com/purpleidea/
|
git clone --recursive https://github.com/purpleidea/mgmt/ ~/mgmt/
|
||||||
cd $GOPATH/src/github.com/purpleidea/ || cd ~/go/
|
cd ~/mgmt/
|
||||||
git clone --recursive https://github.com/purpleidea/mgmt/
|
|
||||||
cd $GOPATH/src/github.com/purpleidea/mgmt/ || cd ~/go/src/github.com/purpleidea/mgmt/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* Add `$GOPATH/bin` to `$PATH`
|
* Add `$GOPATH/bin` to `$PATH`
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ when parameters take a zero value, whenever this is possible.)
|
|||||||
|
|
||||||
```golang
|
```golang
|
||||||
// Default returns some sensible defaults for this resource.
|
// Default returns some sensible defaults for this resource.
|
||||||
func (obj *FooRes) Default() Res {
|
func (obj *FooRes) Default() engine.Res {
|
||||||
return &FooRes{
|
return &FooRes{
|
||||||
Answer: 42, // sometimes, defaults shouldn't be the zero value
|
Answer: 42, // sometimes, defaults shouldn't be the zero value
|
||||||
}
|
}
|
||||||
@@ -642,8 +642,8 @@ The signature intentionally matches what is required to satisfy the `go-yaml`
|
|||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *FooRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *FooRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes FooRes // indirection to avoid infinite recursion
|
type rawRes FooRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ You might want to look at the [generated documentation](https://godoc.org/github
|
|||||||
for more up-to-date information about these resources.
|
for more up-to-date information about these resources.
|
||||||
|
|
||||||
* [Augeas](#Augeas): Manipulate files using augeas.
|
* [Augeas](#Augeas): Manipulate files using augeas.
|
||||||
|
* [Consul:KV](#ConsulKV): Set keys in a Consul datastore.
|
||||||
* [Docker](#Docker):[Container](#Container) Manage docker containers.
|
* [Docker](#Docker):[Container](#Container) Manage docker containers.
|
||||||
* [Exec](#Exec): Execute shell commands on the system.
|
* [Exec](#Exec): Execute shell commands on the system.
|
||||||
* [File](#File): Manage files and directories.
|
* [File](#File): Manage files and directories.
|
||||||
@@ -32,6 +33,8 @@ for more up-to-date information about these resources.
|
|||||||
* [Print](#Print): Print messages to the console.
|
* [Print](#Print): Print messages to the console.
|
||||||
* [Svc](#Svc): Manage system systemd services.
|
* [Svc](#Svc): Manage system systemd services.
|
||||||
* [Test](#Test): A mostly harmless resource that is used for internal testing.
|
* [Test](#Test): A mostly harmless resource that is used for internal testing.
|
||||||
|
* [Tftp:File](#TftpFile): Add files to the small embedded embedded tftp server.
|
||||||
|
* [Tftp:Server](#TftpServer): Run a small embedded tftp server.
|
||||||
* [Timer](#Timer): Manage system systemd services.
|
* [Timer](#Timer): Manage system systemd services.
|
||||||
* [User](#User): Manage system users.
|
* [User](#User): Manage system users.
|
||||||
* [Virt](#Virt): Manage virtual machines with libvirt.
|
* [Virt](#Virt): Manage virtual machines with libvirt.
|
||||||
@@ -69,9 +72,9 @@ identified by a trailing slash in their path name. File have no such slash.
|
|||||||
It has the following properties:
|
It has the following properties:
|
||||||
|
|
||||||
* `path`: absolute file path (directories have a trailing slash here)
|
* `path`: absolute file path (directories have a trailing slash here)
|
||||||
|
* `state`: either `exists`, `absent`, or undefined
|
||||||
* `content`: raw file content
|
* `content`: raw file content
|
||||||
* `state`: either `exists` (the default value) or `absent`
|
* `mode`: octal unix file permissions or symbolic string
|
||||||
* `mode`: octal unix file permissions
|
|
||||||
* `owner`: username or uid for the file owner
|
* `owner`: username or uid for the file owner
|
||||||
* `group`: group name or gid for the file group
|
* `group`: group name or gid for the file group
|
||||||
|
|
||||||
@@ -79,6 +82,16 @@ It has the following properties:
|
|||||||
|
|
||||||
The path property specifies the file or directory that we are managing.
|
The path property specifies the file or directory that we are managing.
|
||||||
|
|
||||||
|
### State
|
||||||
|
|
||||||
|
The state property describes the action we'd like to apply for the resource. The
|
||||||
|
possible values are: `exists` and `absent`. If you do not specify either of
|
||||||
|
these, it is undefined. Without specifying this value as `exists`, another param
|
||||||
|
cannot cause a file to get implicitly created. When specifying this value as
|
||||||
|
`absent`, you should not specify any other params that would normally change the
|
||||||
|
file. For example, if you specify `content` and this param is `absent`, then you
|
||||||
|
will get an engine validation error.
|
||||||
|
|
||||||
### Content
|
### Content
|
||||||
|
|
||||||
The content property is a string that specifies the desired file contents.
|
The content property is a string that specifies the desired file contents.
|
||||||
@@ -88,10 +101,12 @@ The content property is a string that specifies the desired file contents.
|
|||||||
The source property points to a source file or directory path that we wish to
|
The source property points to a source file or directory path that we wish to
|
||||||
copy over and use as the desired contents for our resource.
|
copy over and use as the desired contents for our resource.
|
||||||
|
|
||||||
### State
|
### Fragments
|
||||||
|
|
||||||
The state property describes the action we'd like to apply for the resource. The
|
The fragments property lets you specify a list of files to concatenate together
|
||||||
possible values are: `exists` and `absent`.
|
to make up the contents of this file. They will be combined in the order that
|
||||||
|
they are listed in. If one of the files specified is a directory, then the
|
||||||
|
files in that top-level directory will be themselves combined together and used.
|
||||||
|
|
||||||
### Recurse
|
### Recurse
|
||||||
|
|
||||||
@@ -104,6 +119,12 @@ The force property is required if we want the file resource to be able to change
|
|||||||
a file into a directory or vice-versa. If such a change is needed, but the force
|
a file into a directory or vice-versa. If such a change is needed, but the force
|
||||||
property is not set to `true`, then this file resource will error.
|
property is not set to `true`, then this file resource will error.
|
||||||
|
|
||||||
|
### Purge
|
||||||
|
|
||||||
|
The purge property is used when this file represents a directory, and we'd like
|
||||||
|
to remove any unmanaged files from within it. Please note that any unmanaged
|
||||||
|
files in a directory with this flag set will be irreversibly deleted.
|
||||||
|
|
||||||
## Group
|
## Group
|
||||||
|
|
||||||
The group resource manages the system groups from `/etc/group`.
|
The group resource manages the system groups from `/etc/group`.
|
||||||
@@ -206,6 +227,16 @@ The service resource is still very WIP. Please help us by improving it!
|
|||||||
|
|
||||||
The test resource is mostly harmless and is used for internal tests.
|
The test resource is mostly harmless and is used for internal tests.
|
||||||
|
|
||||||
|
## Tftp:File
|
||||||
|
|
||||||
|
This adds files to the running tftp server. It's useful because it allows you to
|
||||||
|
add individual files without needing to create them on disk.
|
||||||
|
|
||||||
|
## Tftp:Server
|
||||||
|
|
||||||
|
Run a small embedded tftp server. This doesn't apply any state, but instead runs
|
||||||
|
a pure golang tftp server in the Watch loop.
|
||||||
|
|
||||||
## Timer
|
## Timer
|
||||||
|
|
||||||
This resource needs better documentation. Please help us by improving it!
|
This resource needs better documentation. Please help us by improving it!
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ Occasionally inline, two line source code comments are used within a function.
|
|||||||
These should usually be balanced so that you don't have one line with 78
|
These should usually be balanced so that you don't have one line with 78
|
||||||
characters and the second with only four. Split the comment between the two.
|
characters and the second with only four. Split the comment between the two.
|
||||||
|
|
||||||
|
### Default values
|
||||||
|
|
||||||
|
Whenever a constant or function parameter is defined, try and have the safer or
|
||||||
|
default value be the `zero` value. For example, instead of `const NoDanger`, use
|
||||||
|
`const AllowDanger` so that the `false` value is the safe scenario.
|
||||||
|
|
||||||
### Method receiver naming
|
### Method receiver naming
|
||||||
|
|
||||||
[Contrary](https://github.com/golang/go/wiki/CodeReviewComments#receiver-names)
|
[Contrary](https://github.com/golang/go/wiki/CodeReviewComments#receiver-names)
|
||||||
@@ -84,6 +90,57 @@ func (obj *Foo) Bar(baz string) int {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Variable naming
|
||||||
|
|
||||||
|
We prefer shorter, scoped variables rather than `unnecessarilyLongIdentifiers`.
|
||||||
|
Remember the scoping rules and feel free to use new variables where appropriate.
|
||||||
|
For example, in a short string snippet you can use `s` instead of `myString`, as
|
||||||
|
well as other common choices. `i` is a common `int` counter, `f` for files, `fn`
|
||||||
|
for functions, `x` for something else and so on.
|
||||||
|
|
||||||
|
### Variable re-use
|
||||||
|
|
||||||
|
Feel free to create and use new variables instead of attempting to re-use the
|
||||||
|
same string. For example, if a function input arg is named `s`, you can use a
|
||||||
|
new variable to receive the first computation result on `s` instead of storing
|
||||||
|
it back into the original `s`. This avoids confusion if a different part of the
|
||||||
|
code wants to read the original input, and it avoids any chance of edit by
|
||||||
|
reference of the original callers copy of the variable.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```golang
|
||||||
|
MyNotIdealFunc(s string, b bool) string {
|
||||||
|
if !b {
|
||||||
|
return s + "hey"
|
||||||
|
}
|
||||||
|
s = strings.Replace(s, "blah", "", -1) // not ideal (re-use of `s` var)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
MyOkayFunc(s string, b bool) string {
|
||||||
|
if !b {
|
||||||
|
return s + "hey"
|
||||||
|
}
|
||||||
|
s2 := strings.Replace(s, "blah", "", -1) // doesn't re-use `s` variable
|
||||||
|
return s2
|
||||||
|
}
|
||||||
|
|
||||||
|
MyGreatFunc(s string, b bool) string {
|
||||||
|
if !b {
|
||||||
|
return s + "hey"
|
||||||
|
}
|
||||||
|
return strings.Replace(s, "blah", "", -1) // even cleaner
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constants in code
|
||||||
|
|
||||||
|
If a function takes a specifier (often a bool) it's sometimes better to name
|
||||||
|
that variable (often with a `const`) rather than leaving a naked `bool` in the
|
||||||
|
code. For example, `x := MyFoo("blah", false)` is less clear than
|
||||||
|
`const useMagic = false; x := MyFoo("blah", useMagic)`.
|
||||||
|
|
||||||
### Consistent ordering
|
### Consistent ordering
|
||||||
|
|
||||||
In general we try to preserve a logical ordering in source files which usually
|
In general we try to preserve a logical ordering in source files which usually
|
||||||
@@ -96,6 +153,23 @@ declared in the interface.
|
|||||||
When implementing code for the various types in the language, please follow this
|
When implementing code for the various types in the language, please follow this
|
||||||
order: `bool`, `str`, `int`, `float`, `list`, `map`, `struct`, `func`.
|
order: `bool`, `str`, `int`, `float`, `list`, `map`, `struct`, `func`.
|
||||||
|
|
||||||
|
For other aspects where you have a set of items, try to be internally consistent
|
||||||
|
as well. For example, if you have two switch statements with `A`, `B`, and `C`,
|
||||||
|
please use the same ordering for these elements elsewhere that they appear in
|
||||||
|
the code and in the commentary if it is not illogical to do so.
|
||||||
|
|
||||||
|
### Product identifiers
|
||||||
|
|
||||||
|
Try to avoid references in the code to `mgmt` or a specific program name string
|
||||||
|
if possible. This makes it easier to rename code if we ever pick a better name
|
||||||
|
or support `libmgmt` better if we embed it. You can use the `Program` variable
|
||||||
|
which is available in numerous places if you want a string to put in the logs.
|
||||||
|
|
||||||
|
It is also recommended to avoid the `go` (programming language name) string if
|
||||||
|
possible. Try to use `golang` if required, since the word `go` is already
|
||||||
|
overloaded, and in particular it was even already used by the
|
||||||
|
[`go!`](https://en.wikipedia.org/wiki/Go!_(programming_language)).
|
||||||
|
|
||||||
## Overview for mcl code
|
## Overview for mcl code
|
||||||
|
|
||||||
The `mcl` language is quite new, so this guide will probably change over time as
|
The `mcl` language is quite new, so this guide will probably change over time as
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -68,7 +68,8 @@ type AutoEdge interface {
|
|||||||
Test([]bool) bool // call until false
|
Test([]bool) bool // call until false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResUID is a unique identifier for a resource, namely it's name, and the kind ("type").
|
// ResUID is a unique identifier for a resource, namely it's name, and the kind
|
||||||
|
// ("type").
|
||||||
type ResUID interface {
|
type ResUID interface {
|
||||||
fmt.Stringer // String() string
|
fmt.Stringer // String() string
|
||||||
|
|
||||||
@@ -104,9 +105,9 @@ func (obj *BaseUID) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IFF looks at two UID's and if and only if they are equivalent, returns true.
|
// IFF looks at two UID's and if and only if they are equivalent, returns true.
|
||||||
// If they are not equivalent, it returns false.
|
// If they are not equivalent, it returns false. Most resources will want to
|
||||||
// Most resources will want to override this method, since it does the important
|
// override this method, since it does the important work of actually discerning
|
||||||
// work of actually discerning if two resources are identical in function.
|
// if two resources are identical in function.
|
||||||
func (obj *BaseUID) IFF(uid ResUID) bool {
|
func (obj *BaseUID) IFF(uid ResUID) bool {
|
||||||
res, ok := uid.(*BaseUID)
|
res, ok := uid.(*BaseUID)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package engine
|
package engine
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -152,6 +152,18 @@ func ResCmp(r1, r2 Res) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compare meta params for resources with reversible traits
|
||||||
|
r1v, ok1 := r1.(ReversibleRes)
|
||||||
|
r2v, ok2 := r2.(ReversibleRes)
|
||||||
|
if ok1 != ok2 {
|
||||||
|
return fmt.Errorf("reversible differs") // they must be different (optional)
|
||||||
|
}
|
||||||
|
if ok1 && ok2 {
|
||||||
|
if r1v.ReversibleMeta().Cmp(r2v.ReversibleMeta()) != nil {
|
||||||
|
return fmt.Errorf("reversible differs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +292,18 @@ func AdaptCmp(r1, r2 CompatibleRes) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compare meta params for resources with reversible traits
|
||||||
|
r1v, ok1 := r1.(ReversibleRes)
|
||||||
|
r2v, ok2 := r2.(ReversibleRes)
|
||||||
|
if ok1 != ok2 {
|
||||||
|
return fmt.Errorf("reversible differs") // they must be different (optional)
|
||||||
|
}
|
||||||
|
if ok1 && ok2 {
|
||||||
|
if r1v.ReversibleMeta().Cmp(r2v.ReversibleMeta()) != nil {
|
||||||
|
return fmt.Errorf("reversible differs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -106,6 +106,16 @@ func ResCopy(r CopyableRes) (CopyableRes, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copy meta params for resources with reversible traits
|
||||||
|
if x, ok := r.(ReversibleRes); ok {
|
||||||
|
dst, ok := res.(ReversibleRes)
|
||||||
|
if !ok {
|
||||||
|
// programming error
|
||||||
|
panic("reversible interfaces are illogical")
|
||||||
|
}
|
||||||
|
dst.SetReversibleMeta(x.ReversibleMeta()) // no need to copy atm
|
||||||
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
engine/doc.go
Normal file
21
engine/doc.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// Package engine represents the implementation of the resource engine that runs
|
||||||
|
// the graph of resources in real-time. This package has the common imports that
|
||||||
|
// most consumers use directly.
|
||||||
|
package engine
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -89,6 +89,9 @@ func AutoEdge(graph *pgraph.Graph, debug bool, logf func(format string, v ...int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It would be great to ensure we didn't add any graph cycles here, but
|
||||||
|
// instead of checking now, we'll move the check into the main loop.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -74,10 +74,10 @@ func (obj *wrappedGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
|
|||||||
return fmt.Errorf("v2 is not a GroupableRes")
|
return fmt.Errorf("v2 is not a GroupableRes")
|
||||||
}
|
}
|
||||||
|
|
||||||
if r1.Kind() != r2.Kind() { // we must group similar kinds
|
// Some resources of different kinds can now group together!
|
||||||
// TODO: maybe future resources won't need this limitation?
|
//if r1.Kind() != r2.Kind() { // we must group similar kinds
|
||||||
return fmt.Errorf("the two resources aren't the same kind")
|
// return fmt.Errorf("the two resources aren't the same kind")
|
||||||
}
|
//}
|
||||||
// someone doesn't want to group!
|
// someone doesn't want to group!
|
||||||
if r1.AutoGroupMeta().Disabled || r2.AutoGroupMeta().Disabled {
|
if r1.AutoGroupMeta().Disabled || r2.AutoGroupMeta().Disabled {
|
||||||
return fmt.Errorf("one of the autogroup flags is false")
|
return fmt.Errorf("one of the autogroup flags is false")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -54,7 +54,7 @@ func AutoGroup(ag engine.AutoGrouper, g *pgraph.Graph, debug bool, logf func(for
|
|||||||
logf("!VertexMerge for: %s into: %s", wStr, vStr)
|
logf("!VertexMerge for: %s into: %s", wStr, vStr)
|
||||||
|
|
||||||
} else { // success!
|
} else { // success!
|
||||||
logf("success for: %s into: %s", wStr, vStr)
|
logf("%s into %s", wStr, vStr)
|
||||||
merged = true // woo
|
merged = true // woo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,5 +66,8 @@ func AutoGroup(ag engine.AutoGrouper, g *pgraph.Graph, debug bool, logf func(for
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It would be great to ensure we didn't add any graph cycles here, but
|
||||||
|
// instead of checking now, we'll move the check into the main loop.
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package autogroup
|
package autogroup
|
||||||
|
|
||||||
@@ -595,10 +595,12 @@ func TestPgraphGrouping11(t *testing.T) {
|
|||||||
runGraphCmp(t, g1, g2)
|
runGraphCmp(t, g1, g2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// simple merge 1
|
// simple merge 1
|
||||||
// a1 a2 a1,a2
|
// a1 a2 a1,a2
|
||||||
// \ / >>> | (arrows point downwards)
|
// \ / >>> | (arrows point downwards)
|
||||||
// b b
|
// b b
|
||||||
|
*/
|
||||||
func TestPgraphGrouping12(t *testing.T) {
|
func TestPgraphGrouping12(t *testing.T) {
|
||||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||||
{
|
{
|
||||||
@@ -620,10 +622,12 @@ func TestPgraphGrouping12(t *testing.T) {
|
|||||||
runGraphCmp(t, g1, g2)
|
runGraphCmp(t, g1, g2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// simple merge 2
|
// simple merge 2
|
||||||
// b b
|
// b b
|
||||||
// / \ >>> | (arrows point downwards)
|
// / \ >>> | (arrows point downwards)
|
||||||
// a1 a2 a1,a2
|
// a1 a2 a1,a2
|
||||||
|
*/
|
||||||
func TestPgraphGrouping13(t *testing.T) {
|
func TestPgraphGrouping13(t *testing.T) {
|
||||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||||
{
|
{
|
||||||
@@ -645,10 +649,12 @@ func TestPgraphGrouping13(t *testing.T) {
|
|||||||
runGraphCmp(t, g1, g2)
|
runGraphCmp(t, g1, g2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// triple merge
|
// triple merge
|
||||||
// a1 a2 a3 a1,a2,a3
|
// a1 a2 a3 a1,a2,a3
|
||||||
// \ | / >>> | (arrows point downwards)
|
// \ | / >>> | (arrows point downwards)
|
||||||
// b b
|
// b b
|
||||||
|
*/
|
||||||
func TestPgraphGrouping14(t *testing.T) {
|
func TestPgraphGrouping14(t *testing.T) {
|
||||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||||
{
|
{
|
||||||
@@ -673,12 +679,14 @@ func TestPgraphGrouping14(t *testing.T) {
|
|||||||
runGraphCmp(t, g1, g2)
|
runGraphCmp(t, g1, g2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// chain merge
|
// chain merge
|
||||||
// a1 a1
|
// a1 a1
|
||||||
// / \ |
|
// / \ |
|
||||||
// b1 b2 >>> b1,b2 (arrows point downwards)
|
// b1 b2 >>> b1,b2 (arrows point downwards)
|
||||||
// \ / |
|
// \ / |
|
||||||
// c1 c1
|
// c1 c1
|
||||||
|
*/
|
||||||
func TestPgraphGrouping15(t *testing.T) {
|
func TestPgraphGrouping15(t *testing.T) {
|
||||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||||
{
|
{
|
||||||
@@ -708,6 +716,7 @@ func TestPgraphGrouping15(t *testing.T) {
|
|||||||
runGraphCmp(t, g1, g2)
|
runGraphCmp(t, g1, g2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// re-attach 1 (outer)
|
// re-attach 1 (outer)
|
||||||
// technically the second possibility is valid too, depending on which order we
|
// technically the second possibility is valid too, depending on which order we
|
||||||
// merge edges in, and if we don't filter out any unnecessary edges afterwards!
|
// merge edges in, and if we don't filter out any unnecessary edges afterwards!
|
||||||
@@ -716,6 +725,7 @@ func TestPgraphGrouping15(t *testing.T) {
|
|||||||
// b1 / >>> b1 OR b1 / (arrows point downwards)
|
// b1 / >>> b1 OR b1 / (arrows point downwards)
|
||||||
// | / | | /
|
// | / | | /
|
||||||
// c1 c1 c1
|
// c1 c1 c1
|
||||||
|
*/
|
||||||
func TestPgraphGrouping16(t *testing.T) {
|
func TestPgraphGrouping16(t *testing.T) {
|
||||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||||
{
|
{
|
||||||
@@ -743,12 +753,14 @@ func TestPgraphGrouping16(t *testing.T) {
|
|||||||
runGraphCmp(t, g1, g2)
|
runGraphCmp(t, g1, g2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// re-attach 2 (inner)
|
// re-attach 2 (inner)
|
||||||
// a1 b2 a1
|
// a1 b2 a1
|
||||||
// | / |
|
// | / |
|
||||||
// b1 / >>> b1,b2 (arrows point downwards)
|
// b1 / >>> b1,b2 (arrows point downwards)
|
||||||
// | / |
|
// | / |
|
||||||
// c1 c1
|
// c1 c1
|
||||||
|
*/
|
||||||
func TestPgraphGrouping17(t *testing.T) {
|
func TestPgraphGrouping17(t *testing.T) {
|
||||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||||
{
|
{
|
||||||
@@ -776,6 +788,7 @@ func TestPgraphGrouping17(t *testing.T) {
|
|||||||
runGraphCmp(t, g1, g2)
|
runGraphCmp(t, g1, g2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// re-attach 3 (double)
|
// re-attach 3 (double)
|
||||||
// similar to "re-attach 1", technically there is a second possibility for this
|
// similar to "re-attach 1", technically there is a second possibility for this
|
||||||
// a2 a1 b2 a1,a2
|
// a2 a1 b2 a1,a2
|
||||||
@@ -783,6 +796,7 @@ func TestPgraphGrouping17(t *testing.T) {
|
|||||||
// \ b1 / >>> b1,b2 (arrows point downwards)
|
// \ b1 / >>> b1,b2 (arrows point downwards)
|
||||||
// \ | / |
|
// \ | / |
|
||||||
// c1 c1
|
// c1 c1
|
||||||
|
*/
|
||||||
func TestPgraphGrouping18(t *testing.T) {
|
func TestPgraphGrouping18(t *testing.T) {
|
||||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||||
{
|
{
|
||||||
@@ -813,10 +827,12 @@ func TestPgraphGrouping18(t *testing.T) {
|
|||||||
runGraphCmp(t, g1, g2)
|
runGraphCmp(t, g1, g2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// connected merge 0, (no change!)
|
// connected merge 0, (no change!)
|
||||||
// a1 a1
|
// a1 a1
|
||||||
// \ >>> \ (arrows point downwards)
|
// \ >>> \ (arrows point downwards)
|
||||||
// a2 a2
|
// a2 a2
|
||||||
|
*/
|
||||||
func TestPgraphGroupingConnected0(t *testing.T) {
|
func TestPgraphGroupingConnected0(t *testing.T) {
|
||||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||||
{
|
{
|
||||||
@@ -835,12 +851,14 @@ func TestPgraphGroupingConnected0(t *testing.T) {
|
|||||||
runGraphCmp(t, g1, g2)
|
runGraphCmp(t, g1, g2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// connected merge 1, (no change!)
|
// connected merge 1, (no change!)
|
||||||
// a1 a1
|
// a1 a1
|
||||||
// \ \
|
// \ \
|
||||||
// b >>> b (arrows point downwards)
|
// b >>> b (arrows point downwards)
|
||||||
// \ \
|
// \ \
|
||||||
// a2 a2
|
// a2 a2
|
||||||
|
*/
|
||||||
func TestPgraphGroupingConnected1(t *testing.T) {
|
func TestPgraphGroupingConnected1(t *testing.T) {
|
||||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -89,6 +89,19 @@ func (ag *baseGrouper) VertexNext() (v1, v2 pgraph.Vertex, err error) {
|
|||||||
ag.done = true
|
ag.done = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO: is this index swap better or even valid?
|
||||||
|
//if ag.i < l {
|
||||||
|
// ag.i++
|
||||||
|
//}
|
||||||
|
//if ag.i == l {
|
||||||
|
// ag.i = 0
|
||||||
|
// if ag.j < l {
|
||||||
|
// ag.j++
|
||||||
|
// }
|
||||||
|
// if ag.j == l {
|
||||||
|
// ag.done = true
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -110,7 +123,7 @@ func (ag *baseGrouper) VertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err e
|
|||||||
return nil, fmt.Errorf("vertexMerge needs to be overridden")
|
return nil, fmt.Errorf("vertexMerge needs to be overridden")
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeMerge can be overridden, since it just simple returns the first edge.
|
// EdgeMerge can be overridden, since it just simply returns the first edge.
|
||||||
func (ag *baseGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
|
func (ag *baseGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
|
||||||
return e1 // noop
|
return e1 // noop
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -18,13 +18,16 @@
|
|||||||
package autogroup
|
package autogroup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
"github.com/purpleidea/mgmt/pgraph"
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VertexMerge merges v2 into v1 by reattaching the edges where appropriate,
|
// VertexMerge merges v2 into v1 by reattaching the edges where appropriate, and
|
||||||
// and then by deleting v2 from the graph. Since more than one edge between two
|
// then by deleting v2 from the graph. Since more than one edge between two
|
||||||
// vertices is not allowed, duplicate edges are merged as well. an edge merge
|
// vertices is not allowed, duplicate edges are merged as well. An edge merge
|
||||||
// function can be provided if you'd like to control how you merge the edges!
|
// function can be provided if you'd like to control how you merge the edges!
|
||||||
func VertexMerge(g *pgraph.Graph, v1, v2 pgraph.Vertex, vertexMergeFn func(pgraph.Vertex, pgraph.Vertex) (pgraph.Vertex, error), edgeMergeFn func(pgraph.Edge, pgraph.Edge) pgraph.Edge) error {
|
func VertexMerge(g *pgraph.Graph, v1, v2 pgraph.Vertex, vertexMergeFn func(pgraph.Vertex, pgraph.Vertex) (pgraph.Vertex, error), edgeMergeFn func(pgraph.Edge, pgraph.Edge) pgraph.Edge) error {
|
||||||
// methodology
|
// methodology
|
||||||
@@ -112,8 +115,17 @@ func VertexMerge(g *pgraph.Graph, v1, v2 pgraph.Vertex, vertexMergeFn func(pgrap
|
|||||||
// note: This branch isn't used if the vertexMergeFn
|
// note: This branch isn't used if the vertexMergeFn
|
||||||
// decides to just merge logically on its own instead
|
// decides to just merge logically on its own instead
|
||||||
// of actually returning something that we then merge.
|
// of actually returning something that we then merge.
|
||||||
v1 = v // TODO: ineffassign?
|
v1 = v // XXX: ineffassign?
|
||||||
//*v1 = *v
|
//*v1 = *v
|
||||||
|
|
||||||
|
// Ensure that everything still validates. (For safety!)
|
||||||
|
r, ok := v1.(engine.Res) // TODO: v ?
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("not a Res")
|
||||||
|
}
|
||||||
|
if err := engine.Validate(r); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "the Res did not Validate")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
g.DeleteVertex(v2) // remove grouped vertex
|
g.DeleteVertex(v2) // remove grouped vertex
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// Package graph contains the actual implementation of the resource graph engine
|
||||||
|
// that runs the graph of resources in real-time. This package has the algorithm
|
||||||
|
// that runs all the graph transitions.
|
||||||
package graph
|
package graph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -25,14 +28,22 @@ import (
|
|||||||
|
|
||||||
"github.com/purpleidea/mgmt/converger"
|
"github.com/purpleidea/mgmt/converger"
|
||||||
"github.com/purpleidea/mgmt/engine"
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||||
"github.com/purpleidea/mgmt/pgraph"
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
"github.com/purpleidea/mgmt/util/semaphore"
|
"github.com/purpleidea/mgmt/util/semaphore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// StateDir is the name of the sub directory where all the local
|
||||||
|
// resource state is stored.
|
||||||
|
StateDir = "state"
|
||||||
|
)
|
||||||
|
|
||||||
// Engine encapsulates a generic graph and manages its operations.
|
// Engine encapsulates a generic graph and manages its operations.
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
Program string
|
Program string
|
||||||
|
Version string
|
||||||
Hostname string
|
Hostname string
|
||||||
World engine.World
|
World engine.World
|
||||||
|
|
||||||
@@ -174,9 +185,9 @@ func (obj *Engine) Commit() error {
|
|||||||
return errwrap.Wrapf(err, "the Res did not Validate")
|
return errwrap.Wrapf(err, "the Res did not Validate")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: is res.Name() sufficiently unique to use as a UID here?
|
pathUID := engineUtil.ResPathUID(res)
|
||||||
pathUID := fmt.Sprintf("%s-%s", res.Kind(), res.Name())
|
statePrefix := fmt.Sprintf("%s/", path.Join(obj.statePrefix(), pathUID))
|
||||||
statePrefix := fmt.Sprintf("%s/", path.Join(obj.Prefix, "state", pathUID))
|
|
||||||
// don't create this unless it *will* be used
|
// don't create this unless it *will* be used
|
||||||
//if err := os.MkdirAll(statePrefix, 0770); err != nil {
|
//if err := os.MkdirAll(statePrefix, 0770); err != nil {
|
||||||
// return errwrap.Wrapf(err, "can't create state prefix")
|
// return errwrap.Wrapf(err, "can't create state prefix")
|
||||||
@@ -184,10 +195,11 @@ func (obj *Engine) Commit() error {
|
|||||||
|
|
||||||
obj.waits[vertex] = &sync.WaitGroup{}
|
obj.waits[vertex] = &sync.WaitGroup{}
|
||||||
obj.state[vertex] = &State{
|
obj.state[vertex] = &State{
|
||||||
//Graph: obj.graph, // TODO: what happens if we swap the graph?
|
Graph: obj.graph, // Update if we swap the graph!
|
||||||
Vertex: vertex,
|
Vertex: vertex,
|
||||||
|
|
||||||
Program: obj.Program,
|
Program: obj.Program,
|
||||||
|
Version: obj.Version,
|
||||||
Hostname: obj.Hostname,
|
Hostname: obj.Hostname,
|
||||||
|
|
||||||
World: obj.World,
|
World: obj.World,
|
||||||
@@ -322,14 +334,14 @@ func (obj *Engine) Commit() error {
|
|||||||
// the changes that we'd made to the previously primary graph. This is
|
// the changes that we'd made to the previously primary graph. This is
|
||||||
// because this function is meant to atomically swap the graphs safely.
|
// because this function is meant to atomically swap the graphs safely.
|
||||||
|
|
||||||
// TODO: update all the `State` structs with the new Graph pointer
|
// Update all the `State` structs with the new Graph pointer.
|
||||||
//for _, vertex := range obj.graph.Vertices() {
|
for _, vertex := range obj.graph.Vertices() {
|
||||||
// state, exists := obj.state[vertex]
|
state, exists := obj.state[vertex]
|
||||||
// if !exists {
|
if !exists {
|
||||||
// continue
|
continue
|
||||||
// }
|
}
|
||||||
// state.Graph = obj.graph // update pointer to graph
|
state.Graph = obj.graph // update pointer to graph
|
||||||
//}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -416,3 +428,8 @@ func (obj *Engine) Close() error {
|
|||||||
func (obj *Engine) Graph() *pgraph.Graph {
|
func (obj *Engine) Graph() *pgraph.Graph {
|
||||||
return obj.graph
|
return obj.graph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// statePrefix returns the dir where all the resource state is stored locally.
|
||||||
|
func (obj *Engine) statePrefix() string {
|
||||||
|
return fmt.Sprintf("%s/", path.Join(obj.Prefix, StateDir))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package graph
|
package graph
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
300
engine/graph/reverse.go
Normal file
300
engine/graph/reverse.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||||
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ReverseFile is the file name in the resource state dir where any
|
||||||
|
// reversal information is stored.
|
||||||
|
ReverseFile = "reverse"
|
||||||
|
|
||||||
|
// ReversePerm is the permissions mode used to create the ReverseFile.
|
||||||
|
ReversePerm = 0600
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reversals adds the reversals onto the loaded graph. This should happen last,
|
||||||
|
// and before Commit.
|
||||||
|
func (obj *Engine) Reversals() error {
|
||||||
|
if obj.nextGraph == nil {
|
||||||
|
return fmt.Errorf("there is no active graph to add reversals to")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initially get all of the reversals to seek out all possible errors.
|
||||||
|
// XXX: The engine needs to know where data might have been stored if we
|
||||||
|
// XXX: want to potentially allow alternate read/write paths, like etcd.
|
||||||
|
// XXX: In this scenario, we'd have to store a token somewhere to let us
|
||||||
|
// XXX: know to look elsewhere for the special ReversalList read method.
|
||||||
|
data, err := obj.ReversalList() // (map[string]string, error)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "the reversals had errors")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil // end early
|
||||||
|
}
|
||||||
|
|
||||||
|
resMatch := func(r1, r2 engine.Res) bool { // simple match on UID only!
|
||||||
|
if r1.Kind() != r2.Kind() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r1.Name() != r2.Name() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
resInList := func(needle engine.Res, haystack []engine.Res) bool {
|
||||||
|
for _, res := range haystack {
|
||||||
|
if resMatch(needle, res) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Debug {
|
||||||
|
obj.Logf("decoding %d reversals...", len(data))
|
||||||
|
}
|
||||||
|
resources := []engine.Res{}
|
||||||
|
|
||||||
|
// do this in a sorted order so that it errors deterministically
|
||||||
|
sorted := []string{}
|
||||||
|
for key := range data {
|
||||||
|
sorted = append(sorted, key)
|
||||||
|
}
|
||||||
|
sort.Strings(sorted)
|
||||||
|
for _, key := range sorted {
|
||||||
|
val := data[key]
|
||||||
|
// XXX: replace this ResToB64 method with one that stores it in
|
||||||
|
// a human readable format, in case someone wants to hack and
|
||||||
|
// edit it manually.
|
||||||
|
// XXX: we probably want this to be YAML, it works with the diff
|
||||||
|
// too...
|
||||||
|
r, err := engineUtil.B64ToRes(val)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error decoding res with UID: `%s`", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, ok := r.(engine.ReversibleRes)
|
||||||
|
if !ok {
|
||||||
|
// this requirement is here to keep things simpler...
|
||||||
|
return errwrap.Wrapf(err, "decoded res with UID: `%s` was not reversible", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
matchFn := func(vertex pgraph.Vertex) (bool, error) {
|
||||||
|
r, ok := vertex.(engine.Res)
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("not a Res")
|
||||||
|
}
|
||||||
|
if !resMatch(r, res) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: not efficient, we could build a cache-map first
|
||||||
|
vertex, err := obj.nextGraph.VertexMatchFn(matchFn) // (Vertex, error)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error searching graph for match")
|
||||||
|
}
|
||||||
|
if vertex != nil { // found one!
|
||||||
|
continue // it doesn't need reversing yet
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check for (incompatible?) duplicates instead
|
||||||
|
if resInList(res, resources) { // we've already got this one...
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We set this in two different places to be safe. It ensures
|
||||||
|
// that we erase the reversal state file after we've used it.
|
||||||
|
res.ReversibleMeta().Reversal = true // set this for later...
|
||||||
|
|
||||||
|
resources = append(resources, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resources) == 0 {
|
||||||
|
return nil // end early
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've passed the chance of any errors, we modify the graph.
|
||||||
|
obj.Logf("adding %d reversals...", len(resources))
|
||||||
|
for _, res := range resources {
|
||||||
|
obj.nextGraph.AddVertex(res)
|
||||||
|
}
|
||||||
|
// TODO: Do we want a way for stored reversals to add edges too?
|
||||||
|
|
||||||
|
// It would be great to ensure we didn't add any graph cycles here, but
|
||||||
|
// instead of checking now, we'll move the check into the main loop.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReversalList returns all the available pending reversal data on this host. It
|
||||||
|
// can then be decoded by whatever method is appropriate for.
|
||||||
|
func (obj *Engine) ReversalList() (map[string]string, error) {
|
||||||
|
result := make(map[string]string) // some key to contents
|
||||||
|
|
||||||
|
dir := obj.statePrefix() // loop through this dir...
|
||||||
|
files, err := ioutil.ReadDir(dir)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, errwrap.Wrapf(err, "error reading list of state dirs")
|
||||||
|
} else if err != nil {
|
||||||
|
return result, nil // nothing found, no state dir exists yet
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, x := range files {
|
||||||
|
key := x.Name() // some uid for the resource
|
||||||
|
file := path.Join(dir, key, ReverseFile)
|
||||||
|
content, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, errwrap.Wrapf(err, "could not read reverse file: %s", file)
|
||||||
|
} else if err != nil {
|
||||||
|
continue // file does not exist, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// file exists!
|
||||||
|
str := string(content)
|
||||||
|
result[key] = str // save
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReversalInit performs the reversal initialization steps if necessary for this
|
||||||
|
// resource.
|
||||||
|
func (obj *State) ReversalInit() error {
|
||||||
|
res, ok := obj.Vertex.(engine.ReversibleRes)
|
||||||
|
if !ok {
|
||||||
|
return nil // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.ReversibleMeta().Disabled {
|
||||||
|
return nil // nothing to do, reversal isn't enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the reversal is enabled, but we are the result of a previous
|
||||||
|
// reversal, then this will overwrite that older reversal request, and
|
||||||
|
// our resource should be designed to deal with that. This happens if we
|
||||||
|
// return a reversible resource as the reverse of a resource that was
|
||||||
|
// reversed. It's probably fairly rare.
|
||||||
|
if res.ReversibleMeta().Reversal {
|
||||||
|
obj.Logf("triangle reversal") // warn!
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := res.Reversed()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not reverse: %s", res.String())
|
||||||
|
}
|
||||||
|
if r == nil {
|
||||||
|
return nil // this can't be reversed, or isn't implemented here
|
||||||
|
}
|
||||||
|
|
||||||
|
// We set this in two different places to be safe. It ensures that we
|
||||||
|
// erase the reversal state file after we've used it.
|
||||||
|
r.ReversibleMeta().Reversal = true // set this for later...
|
||||||
|
|
||||||
|
// XXX: replace this ResToB64 method with one that stores it in a human
|
||||||
|
// readable format, in case someone wants to hack and edit it manually.
|
||||||
|
// XXX: we probably want this to be YAML, it works with the diff too...
|
||||||
|
str, err := engineUtil.ResToB64(r)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not encode: %s", res.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: put this method on traits.Reversible as part of the interface?
|
||||||
|
return obj.ReversalWrite(str, res.ReversibleMeta().Overwrite) // Store!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReversalClose performs the reversal shutdown steps if necessary for this
|
||||||
|
// resource.
|
||||||
|
func (obj *State) ReversalClose() error {
|
||||||
|
res, ok := obj.Vertex.(engine.ReversibleRes)
|
||||||
|
if !ok {
|
||||||
|
return nil // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't check res.ReversibleMeta().Disabled because we're removing the
|
||||||
|
// previous one. That value only applies if we're doing a new reversal.
|
||||||
|
|
||||||
|
if !res.ReversibleMeta().Reversal {
|
||||||
|
return nil // nothing to erase, we're not a reversal resource
|
||||||
|
}
|
||||||
|
|
||||||
|
if !obj.isStateOK { // did we successfully reverse?
|
||||||
|
obj.Logf("did not complete reversal") // warn
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: put this method on traits.Reversible as part of the interface?
|
||||||
|
return obj.ReversalDelete() // Erase our reversal instructions.
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReversalWrite stores the reversal state information for this resource.
|
||||||
|
func (obj *State) ReversalWrite(str string, overwrite bool) error {
|
||||||
|
dir, err := obj.varDir("") // private version
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not get VarDir for reverse")
|
||||||
|
}
|
||||||
|
file := path.Join(dir, ReverseFile) // return a unique file
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return errwrap.Wrapf(err, "could not read reverse file: %s", file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// file exists and we shouldn't overwrite if different
|
||||||
|
if err == nil && !overwrite {
|
||||||
|
// compare to existing file
|
||||||
|
oldStr := string(content)
|
||||||
|
if str != oldStr {
|
||||||
|
obj.Logf("existing, pending, reversible resource exists")
|
||||||
|
//obj.Logf("diff:")
|
||||||
|
//obj.Logf("") // TODO: print the diff w/o and secret values
|
||||||
|
return fmt.Errorf("existing, pending, reversible resource exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutil.WriteFile(file, []byte(str), ReversePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReversalDelete removes the reversal state information for this resource.
|
||||||
|
func (obj *State) ReversalDelete() error {
|
||||||
|
dir, err := obj.varDir("") // private version
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not get VarDir for reverse")
|
||||||
|
}
|
||||||
|
file := path.Join(dir, ReverseFile) // return a unique file
|
||||||
|
|
||||||
|
// FIXME: why do we see these removals when there isn't a state file?
|
||||||
|
if err = os.Remove(file); os.IsNotExist(err) {
|
||||||
|
return nil // ignore missing files
|
||||||
|
}
|
||||||
|
|
||||||
|
return errwrap.Wrapf(err, "could not remove reverse state file")
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package graph
|
package graph
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -32,7 +32,7 @@ import (
|
|||||||
// State stores some state about the resource it is mapped to.
|
// State stores some state about the resource it is mapped to.
|
||||||
type State struct {
|
type State struct {
|
||||||
// Graph is a pointer to the graph that this vertex is part of.
|
// Graph is a pointer to the graph that this vertex is part of.
|
||||||
//Graph pgraph.Graph
|
Graph *pgraph.Graph
|
||||||
|
|
||||||
// Vertex is the pointer in the graph that this state corresponds to. It
|
// Vertex is the pointer in the graph that this state corresponds to. It
|
||||||
// can be converted to a `Res` if necessary.
|
// can be converted to a `Res` if necessary.
|
||||||
@@ -40,6 +40,7 @@ type State struct {
|
|||||||
Vertex pgraph.Vertex
|
Vertex pgraph.Vertex
|
||||||
|
|
||||||
Program string
|
Program string
|
||||||
|
Version string
|
||||||
Hostname string
|
Hostname string
|
||||||
World engine.World
|
World engine.World
|
||||||
|
|
||||||
@@ -154,6 +155,7 @@ func (obj *State) Init() error {
|
|||||||
|
|
||||||
obj.init = &engine.Init{
|
obj.init = &engine.Init{
|
||||||
Program: obj.Program,
|
Program: obj.Program,
|
||||||
|
Version: obj.Version,
|
||||||
Hostname: obj.Hostname,
|
Hostname: obj.Hostname,
|
||||||
|
|
||||||
// Watch:
|
// Watch:
|
||||||
@@ -169,25 +171,63 @@ func (obj *State) Init() error {
|
|||||||
}
|
}
|
||||||
return res.Refresh()
|
return res.Refresh()
|
||||||
},
|
},
|
||||||
Send: func(st interface{}) error {
|
|
||||||
res, ok := obj.Vertex.(engine.SendableRes)
|
|
||||||
if !ok {
|
|
||||||
panic("res does not support the Sendable trait")
|
|
||||||
}
|
|
||||||
// XXX: type check this
|
|
||||||
//expected := res.Sends()
|
|
||||||
//if err := XXX_TYPE_CHECK(expected, st); err != nil {
|
|
||||||
// return err
|
|
||||||
//}
|
|
||||||
|
|
||||||
return res.Send(st) // send the struct
|
Send: engine.GenerateSendFunc(res),
|
||||||
},
|
Recv: engine.GenerateRecvFunc(res),
|
||||||
Recv: func() map[string]*engine.Send { // TODO: change this API?
|
|
||||||
res, ok := obj.Vertex.(engine.RecvableRes)
|
// FIXME: pass in a safe, limited query func instead?
|
||||||
if !ok {
|
// TODO: not implemented, use FilteredGraph
|
||||||
panic("res does not support the Recvable trait")
|
//Graph: func() *pgraph.Graph {
|
||||||
|
// _, ok := obj.Vertex.(engine.CanGraphQueryRes)
|
||||||
|
// if !ok {
|
||||||
|
// panic("res does not support the GraphQuery trait")
|
||||||
|
// }
|
||||||
|
// return obj.Graph // we return in a func so it's fresh!
|
||||||
|
//},
|
||||||
|
|
||||||
|
FilteredGraph: func() (*pgraph.Graph, error) {
|
||||||
|
graph, err := pgraph.NewGraph("filtered")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return res.Recv()
|
|
||||||
|
// filter graph and build a new one...
|
||||||
|
adjacency := obj.Graph.Adjacency()
|
||||||
|
for v1 := range adjacency {
|
||||||
|
// check we're allowed
|
||||||
|
r1, ok := v1.(engine.GraphQueryableRes)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// pass in information on requestor...
|
||||||
|
if err := r1.GraphQueryAllowed(
|
||||||
|
engine.GraphQueryableOptionKind(res.Kind()),
|
||||||
|
engine.GraphQueryableOptionName(res.Name()),
|
||||||
|
// TODO: add more information...
|
||||||
|
); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
graph.AddVertex(v1)
|
||||||
|
|
||||||
|
for v2, edge := range adjacency[v1] {
|
||||||
|
r2, ok := v2.(engine.GraphQueryableRes)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// pass in information on requestor...
|
||||||
|
if err := r2.GraphQueryAllowed(
|
||||||
|
engine.GraphQueryableOptionKind(res.Kind()),
|
||||||
|
engine.GraphQueryableOptionName(res.Name()),
|
||||||
|
// TODO: add more information...
|
||||||
|
); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
//graph.AddVertex(v2) // redundant
|
||||||
|
graph.AddEdge(v1, v2, edge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph, nil // we return in a func so it's fresh!
|
||||||
},
|
},
|
||||||
|
|
||||||
World: obj.World,
|
World: obj.World,
|
||||||
@@ -203,6 +243,12 @@ func (obj *State) Init() error {
|
|||||||
if obj.Debug {
|
if obj.Debug {
|
||||||
obj.Logf("Init(%s)", res)
|
obj.Logf("Init(%s)", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// write the reverse request to the disk...
|
||||||
|
if err := obj.ReversalInit(); err != nil {
|
||||||
|
return err // TODO: test this code path...
|
||||||
|
}
|
||||||
|
|
||||||
err := res.Init(obj.init)
|
err := res.Init(obj.init)
|
||||||
if obj.Debug {
|
if obj.Debug {
|
||||||
obj.Logf("Init(%s): Return(%+v)", res, err)
|
obj.Logf("Init(%s): Return(%+v)", res, err)
|
||||||
@@ -236,12 +282,23 @@ func (obj *State) Close() error {
|
|||||||
if obj.Debug {
|
if obj.Debug {
|
||||||
obj.Logf("Close(%s)", res)
|
obj.Logf("Close(%s)", res)
|
||||||
}
|
}
|
||||||
err := res.Close()
|
|
||||||
if obj.Debug {
|
var reverr error
|
||||||
obj.Logf("Close(%s): Return(%+v)", res, err)
|
// clear the reverse request from the disk...
|
||||||
|
if err := obj.ReversalClose(); err != nil {
|
||||||
|
// TODO: test this code path...
|
||||||
|
// TODO: should this be an error or a warning?
|
||||||
|
reverr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
reterr := res.Close()
|
||||||
|
if obj.Debug {
|
||||||
|
obj.Logf("Close(%s): Return(%+v)", res, reterr)
|
||||||
|
}
|
||||||
|
|
||||||
|
reterr = errwrap.Append(reterr, reverr)
|
||||||
|
|
||||||
|
return reterr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poke sends a notification on the poke channel. This channel is used to notify
|
// Poke sends a notification on the poke channel. This channel is used to notify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
70
engine/graphqueryable.go
Normal file
70
engine/graphqueryable.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package engine
|
||||||
|
|
||||||
|
// GraphQueryableRes is the interface that must be implemented if you want your
|
||||||
|
// resource to be allowed to be queried from another resource in the graph. This
|
||||||
|
// is done as a form of explicit authorization tracking so that we can consider
|
||||||
|
// security aspects more easily. Ultimately, all resource code should be
|
||||||
|
// trusted, but it's still a good idea to know if a particular resource is even
|
||||||
|
// able to access information about another one, and if your resource doesn't
|
||||||
|
// add the trait supporting this, then it won't be allowed.
|
||||||
|
type GraphQueryableRes interface {
|
||||||
|
Res // implement everything in Res but add the additional requirements
|
||||||
|
|
||||||
|
// GraphQueryAllowed returns nil if you're allowed to query the graph.
|
||||||
|
GraphQueryAllowed(...GraphQueryableOption) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQueryableOption is an option that can be used to specify the
|
||||||
|
// authentication.
|
||||||
|
type GraphQueryableOption func(*GraphQueryableOptions)
|
||||||
|
|
||||||
|
// GraphQueryableOptions represents the different possible configurable options.
|
||||||
|
type GraphQueryableOptions struct {
|
||||||
|
// Kind is the kind of the resource making the access.
|
||||||
|
Kind string
|
||||||
|
// Name is the name of the resource making the access.
|
||||||
|
Name string
|
||||||
|
// TODO: add more options if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply is a helper function to apply a list of options to the struct. You
|
||||||
|
// should initialize it with defaults you want, and then apply any you've
|
||||||
|
// received like this.
|
||||||
|
func (obj *GraphQueryableOptions) Apply(opts ...GraphQueryableOption) {
|
||||||
|
for _, optionFunc := range opts { // apply the options
|
||||||
|
optionFunc(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQueryableOptionKind tells the GraphQueryAllowed function what the
|
||||||
|
// resource kind is.
|
||||||
|
func GraphQueryableOptionKind(kind string) GraphQueryableOption {
|
||||||
|
return func(gqo *GraphQueryableOptions) {
|
||||||
|
gqo.Kind = kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphQueryableOptionName tells the GraphQueryAllowed function what the
|
||||||
|
// resource name is.
|
||||||
|
func GraphQueryableOptionName(name string) GraphQueryableOption {
|
||||||
|
return func(gqo *GraphQueryableOptions) {
|
||||||
|
gqo.Name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package engine
|
package engine
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
@@ -29,8 +30,8 @@ import (
|
|||||||
// TODO: should each resource be a sub-package?
|
// TODO: should each resource be a sub-package?
|
||||||
var registeredResources = map[string]func() Res{}
|
var registeredResources = map[string]func() Res{}
|
||||||
|
|
||||||
// RegisterResource registers a new resource by providing a constructor
|
// RegisterResource registers a new resource by providing a constructor function
|
||||||
// function that returns a resource object ready to be unmarshalled from YAML.
|
// that returns a resource object ready to be unmarshalled from YAML.
|
||||||
func RegisterResource(kind string, fn func() Res) {
|
func RegisterResource(kind string, fn func() Res) {
|
||||||
f := fn()
|
f := fn()
|
||||||
if kind == "" {
|
if kind == "" {
|
||||||
@@ -86,6 +87,9 @@ type Init struct {
|
|||||||
// Program is the name of the program.
|
// Program is the name of the program.
|
||||||
Program string
|
Program string
|
||||||
|
|
||||||
|
// Version is the version of the program.
|
||||||
|
Version string
|
||||||
|
|
||||||
// Hostname is the uuid for the host.
|
// Hostname is the uuid for the host.
|
||||||
Hostname string
|
Hostname string
|
||||||
|
|
||||||
@@ -120,6 +124,20 @@ type Init struct {
|
|||||||
|
|
||||||
// Other functionality:
|
// Other functionality:
|
||||||
|
|
||||||
|
// Graph is a function that returns the current graph. The returned
|
||||||
|
// value won't be valid after a graphsync so make sure to call this when
|
||||||
|
// you are about to use it, and discard it right after.
|
||||||
|
// FIXME: it might be better to offer a safer, more limited, GraphQuery?
|
||||||
|
//Graph func() *pgraph.Graph // TODO: not implemented, use FilteredGraph
|
||||||
|
|
||||||
|
// FilteredGraph is a function that returns a filtered variant of the
|
||||||
|
// current graph. Only resource that have allowed themselves to be added
|
||||||
|
// into this graph will appear. If they did not consent, then those
|
||||||
|
// vertices and any associated edges, will not be present.
|
||||||
|
FilteredGraph func() (*pgraph.Graph, error)
|
||||||
|
|
||||||
|
// TODO: GraphQuery offers an interface to query the resource graph.
|
||||||
|
|
||||||
// World provides a connection to the outside world. This is most often
|
// World provides a connection to the outside world. This is most often
|
||||||
// used for communicating with the distributed database.
|
// used for communicating with the distributed database.
|
||||||
World World
|
World World
|
||||||
@@ -227,8 +245,8 @@ func Validate(res Res) error {
|
|||||||
// the Interrupt method to shutdown the resource quickly. Running this method
|
// the Interrupt method to shutdown the resource quickly. Running this method
|
||||||
// may leave the resource in a partial state, however this may be desired if you
|
// may leave the resource in a partial state, however this may be desired if you
|
||||||
// want a faster exit or if you'd prefer a partial state over letting the
|
// want a faster exit or if you'd prefer a partial state over letting the
|
||||||
// resource complete in a situation where you made an error and you wish to
|
// resource complete in a situation where you made an error and you wish to exit
|
||||||
// exit quickly to avoid data loss. It is usually triggered after multiple ^C
|
// quickly to avoid data loss. It is usually triggered after multiple ^C
|
||||||
// signals.
|
// signals.
|
||||||
type InterruptableRes interface {
|
type InterruptableRes interface {
|
||||||
Res
|
Res
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !noaugeas
|
//go:build !noaugeas
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
@@ -29,8 +29,6 @@ import (
|
|||||||
"github.com/purpleidea/mgmt/recwatch"
|
"github.com/purpleidea/mgmt/recwatch"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
|
||||||
// FIXME: we vendor go/augeas because master requires augeas 1.6.0
|
|
||||||
// and libaugeas-dev-1.6.0 is not yet available in a PPA.
|
|
||||||
"honnef.co/go/augeas"
|
"honnef.co/go/augeas"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,8 +122,8 @@ func (obj *AugeasRes) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch is the primary listener for this resource and it outputs events.
|
// Watch is the primary listener for this resource and it outputs events. This
|
||||||
// Taken from the File resource.
|
// was taken from the File resource.
|
||||||
// FIXME: DRY - This is taken from the file resource
|
// FIXME: DRY - This is taken from the file resource
|
||||||
func (obj *AugeasRes) Watch() error {
|
func (obj *AugeasRes) Watch() error {
|
||||||
var err error
|
var err error
|
||||||
@@ -301,8 +299,8 @@ func (obj *AugeasRes) UIDs() []engine.ResUID {
|
|||||||
return []engine.ResUID{x}
|
return []engine.ResUID{x}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *AugeasRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *AugeasRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes AugeasRes // indirection to avoid infinite recursion
|
type rawRes AugeasRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -121,8 +121,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AwsRegions is a list of all AWS regions generated using ec2.DescribeRegions.
|
// AwsRegions is a list of all AWS regions generated using ec2.DescribeRegions.
|
||||||
// cn-north-1 and us-gov-west-1 are not returned, probably due to security.
|
// cn-north-1 and us-gov-west-1 are not returned, probably due to security. List
|
||||||
// List available at http://docs.aws.amazon.com/general/latest/gr/rande.html
|
// available at http://docs.aws.amazon.com/general/latest/gr/rande.html
|
||||||
var AwsRegions = []string{
|
var AwsRegions = []string{
|
||||||
"ap-northeast-1",
|
"ap-northeast-1",
|
||||||
"ap-northeast-2",
|
"ap-northeast-2",
|
||||||
@@ -187,7 +187,8 @@ type AwsEc2Res struct {
|
|||||||
InstanceID string
|
InstanceID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// chanStruct defines the type for a channel used to pass events and errors to watch.
|
// chanStruct defines the type for a channel used to pass events and errors to
|
||||||
|
// watch.
|
||||||
type chanStruct struct {
|
type chanStruct struct {
|
||||||
event awsEc2Event
|
event awsEc2Event
|
||||||
state string
|
state string
|
||||||
@@ -233,7 +234,8 @@ type ruleDetail struct {
|
|||||||
State []string `json:"state"`
|
State []string `json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// postData is the format of the messages received and decoded by snsPostHandler().
|
// postData is the format of the messages received and decoded by
|
||||||
|
// snsPostHandler().
|
||||||
type postData struct {
|
type postData struct {
|
||||||
Type string `json:"Type"`
|
Type string `json:"Type"`
|
||||||
MessageID string `json:"MessageId"`
|
MessageID string `json:"MessageId"`
|
||||||
@@ -247,7 +249,8 @@ type postData struct {
|
|||||||
SigningCertURL string `json:"SigningCertURL"`
|
SigningCertURL string `json:"SigningCertURL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// postMsg is used to unmarshal the postData message if it's an event notification.
|
// postMsg is used to unmarshal the postData message if it's an event
|
||||||
|
// notification.
|
||||||
type postMsg struct {
|
type postMsg struct {
|
||||||
InstanceID string `json:"instance-id"`
|
InstanceID string `json:"instance-id"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
@@ -413,7 +416,8 @@ func (obj *AwsEc2Res) Watch() error {
|
|||||||
return obj.longpollWatch()
|
return obj.longpollWatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
// longpollWatch uses the ec2 api's built in methods to watch ec2 resource state.
|
// longpollWatch uses the ec2 api's built in methods to watch ec2 resource
|
||||||
|
// state.
|
||||||
func (obj *AwsEc2Res) longpollWatch() error {
|
func (obj *AwsEc2Res) longpollWatch() error {
|
||||||
send := false
|
send := false
|
||||||
|
|
||||||
@@ -510,10 +514,10 @@ func (obj *AwsEc2Res) longpollWatch() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// snsWatch uses amazon's SNS and CloudWatchEvents APIs to get instance state-
|
// snsWatch uses amazon's SNS and CloudWatchEvents APIs to get instance state-
|
||||||
// change notifications pushed to the http endpoint (snsServer) set up below.
|
// change notifications pushed to the http endpoint (snsServer) set up below. In
|
||||||
// In Init() a CloudWatch rule is created along with a corresponding SNS topic
|
// Init() a CloudWatch rule is created along with a corresponding SNS topic that
|
||||||
// that it can publish to. snsWatch creates an http server which listens for
|
// it can publish to. snsWatch creates an http server which listens for messages
|
||||||
// messages published to the topic and processes them accordingly.
|
// published to the topic and processes them accordingly.
|
||||||
func (obj *AwsEc2Res) snsWatch() error {
|
func (obj *AwsEc2Res) snsWatch() error {
|
||||||
send := false
|
send := false
|
||||||
defer obj.wg.Wait()
|
defer obj.wg.Wait()
|
||||||
@@ -751,45 +755,37 @@ func (obj *AwsEc2Res) CheckApply(apply bool) (bool, error) {
|
|||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *AwsEc2Res) Cmp(r engine.Res) error {
|
func (obj *AwsEc2Res) Cmp(r engine.Res) error {
|
||||||
if !obj.Compare(r) {
|
|
||||||
return fmt.Errorf("did not compare")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare two resources and return if they are equivalent.
|
|
||||||
func (obj *AwsEc2Res) Compare(r engine.Res) bool {
|
|
||||||
// we can only compare AwsEc2Res to others of the same resource kind
|
// we can only compare AwsEc2Res to others of the same resource kind
|
||||||
res, ok := r.(*AwsEc2Res)
|
res, ok := r.(*AwsEc2Res)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.State != res.State {
|
if obj.State != res.State {
|
||||||
return false
|
return fmt.Errorf("the State differs")
|
||||||
}
|
}
|
||||||
if obj.Region != res.Region {
|
if obj.Region != res.Region {
|
||||||
return false
|
return fmt.Errorf("the Region differs")
|
||||||
}
|
}
|
||||||
if obj.Type != res.Type {
|
if obj.Type != res.Type {
|
||||||
return false
|
return fmt.Errorf("the Type differs")
|
||||||
}
|
}
|
||||||
if obj.ImageID != res.ImageID {
|
if obj.ImageID != res.ImageID {
|
||||||
return false
|
return fmt.Errorf("the ImageID differs")
|
||||||
}
|
}
|
||||||
if obj.WatchEndpoint != res.WatchEndpoint {
|
if obj.WatchEndpoint != res.WatchEndpoint {
|
||||||
return false
|
return fmt.Errorf("the WatchEndpoint differs")
|
||||||
}
|
}
|
||||||
if obj.WatchListenAddr != res.WatchListenAddr {
|
if obj.WatchListenAddr != res.WatchListenAddr {
|
||||||
return false
|
return fmt.Errorf("the WatchListenAddr differs")
|
||||||
}
|
}
|
||||||
if obj.ErrorOnMalformedPost != res.ErrorOnMalformedPost {
|
if obj.ErrorOnMalformedPost != res.ErrorOnMalformedPost {
|
||||||
return false
|
return fmt.Errorf("the ErrorOnMalformedPost differs")
|
||||||
}
|
}
|
||||||
if obj.UserData != res.UserData {
|
if obj.UserData != res.UserData {
|
||||||
return false
|
return fmt.Errorf("the UserData differs")
|
||||||
}
|
}
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj *AwsEc2Res) prependName() string {
|
func (obj *AwsEc2Res) prependName() string {
|
||||||
@@ -803,8 +799,8 @@ type AwsEc2UID struct {
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one, although some resources can return multiple.
|
// resources only return one, although some resources can return multiple.
|
||||||
func (obj *AwsEc2Res) UIDs() []engine.ResUID {
|
func (obj *AwsEc2Res) UIDs() []engine.ResUID {
|
||||||
x := &AwsEc2UID{
|
x := &AwsEc2UID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -813,8 +809,8 @@ func (obj *AwsEc2Res) UIDs() []engine.ResUID {
|
|||||||
return []engine.ResUID{x}
|
return []engine.ResUID{x}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *AwsEc2Res) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *AwsEc2Res) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes AwsEc2Res // indirection to avoid infinite recursion
|
type rawRes AwsEc2Res // indirection to avoid infinite recursion
|
||||||
|
|
||||||
@@ -950,8 +946,8 @@ func (obj *AwsEc2Res) snsVerifySignature(post postData) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// snsGetCert downloads and parses the signing certificate from the provided
|
// snsGetCert downloads and parses the signing certificate from the provided URL
|
||||||
// URL for message verification.
|
// for message verification.
|
||||||
func (obj *AwsEc2Res) snsGetCert(url string) (*x509.Certificate, error) {
|
func (obj *AwsEc2Res) snsGetCert(url string) (*x509.Certificate, error) {
|
||||||
// only download valid certificates from amazon
|
// only download valid certificates from amazon
|
||||||
matchURL, err := regexp.MatchString(SnsCertURLRegex, url)
|
matchURL, err := regexp.MatchString(SnsCertURLRegex, url)
|
||||||
@@ -1025,7 +1021,7 @@ func (obj *AwsEc2Res) snsMakeTopic() (string, error) {
|
|||||||
}
|
}
|
||||||
obj.init.Logf("Created SNS Topic")
|
obj.init.Logf("Created SNS Topic")
|
||||||
if topic.TopicArn == nil {
|
if topic.TopicArn == nil {
|
||||||
return "", fmt.Errorf("TopicArn is nil")
|
return "", fmt.Errorf("the TopicArn is nil")
|
||||||
}
|
}
|
||||||
return *topic.TopicArn, nil
|
return *topic.TopicArn, nil
|
||||||
}
|
}
|
||||||
@@ -1043,8 +1039,8 @@ func (obj *AwsEc2Res) snsDeleteTopic(topicArn string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// snsSubscribe subscribes the endpoint to the sns topic.
|
// snsSubscribe subscribes the endpoint to the sns topic. Returning
|
||||||
// Returning SubscriptionArn here is useless as it is still pending confirmation.
|
// SubscriptionArn here is useless as it is still pending confirmation.
|
||||||
func (obj *AwsEc2Res) snsSubscribe(endpoint string, topicArn string) error {
|
func (obj *AwsEc2Res) snsSubscribe(endpoint string, topicArn string) error {
|
||||||
// subscribe to the topic
|
// subscribe to the topic
|
||||||
subInput := &sns.SubscribeInput{
|
subInput := &sns.SubscribeInput{
|
||||||
@@ -1060,8 +1056,8 @@ func (obj *AwsEc2Res) snsSubscribe(endpoint string, topicArn string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// snsConfirmSubscription confirms the sns subscription.
|
// snsConfirmSubscription confirms the sns subscription. Returning
|
||||||
// Returning SubscriptionArn here is useless as it is still pending confirmation.
|
// SubscriptionArn here is useless as it is still pending confirmation.
|
||||||
func (obj *AwsEc2Res) snsConfirmSubscription(topicArn string, token string) error {
|
func (obj *AwsEc2Res) snsConfirmSubscription(topicArn string, token string) error {
|
||||||
// confirm the subscription
|
// confirm the subscription
|
||||||
csInput := &sns.ConfirmSubscriptionInput{
|
csInput := &sns.ConfirmSubscriptionInput{
|
||||||
@@ -1113,7 +1109,8 @@ func (obj *AwsEc2Res) snsProcessEvent(message, instanceName string) (awsEc2Event
|
|||||||
return awsEc2EventNone, nil
|
return awsEc2EventNone, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// snsAuthorize adds the necessary permission for cloudwatch to publish to the SNS topic.
|
// snsAuthorize adds the necessary permission for cloudwatch to publish to the
|
||||||
|
// SNS topic.
|
||||||
func (obj *AwsEc2Res) snsAuthorizeCloudWatch(topicArn string) error {
|
func (obj *AwsEc2Res) snsAuthorizeCloudWatch(topicArn string) error {
|
||||||
// get the topic attributes, including the security policy
|
// get the topic attributes, including the security policy
|
||||||
gaInput := &sns.GetTopicAttributesInput{
|
gaInput := &sns.GetTopicAttributesInput{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -229,8 +229,8 @@ func (obj *ConfigEtcdRes) Interrupt() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *ConfigEtcdRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *ConfigEtcdRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes ConfigEtcdRes // indirection to avoid infinite recursion
|
type rawRes ConfigEtcdRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
283
engine/resources/consul_kv.go
Normal file
283
engine/resources/consul_kv.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("consul:kv", func() engine.Res { return &ConsulKVRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsulKVRes is a resource that writes a value into a Consul datastore. The
|
||||||
|
// name of the resource can either be the key name, or the concatenation of the
|
||||||
|
// server address and the key name: http://127.0.0.1:8500/my-key. If the param
|
||||||
|
// keys are specified, then those are used. If the Name cannot be properly
|
||||||
|
// parsed by url.Parse, then it will be considered as the Key's value. If the
|
||||||
|
// Key is specified explicitly, then we won't use anything from the Name.
|
||||||
|
type ConsulKVRes struct {
|
||||||
|
traits.Base
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
// Key is the name of the key. Defaults to the name of the resource.
|
||||||
|
Key string `lang:"key" yaml:"key"`
|
||||||
|
|
||||||
|
// Value is the value for the key.
|
||||||
|
Value string `lang:"value" yaml:"value"`
|
||||||
|
|
||||||
|
// Scheme is the URI scheme for the Consul server. Default: http.
|
||||||
|
Scheme string `lang:"scheme" yaml:"scheme"`
|
||||||
|
|
||||||
|
// Address is the address of the Consul server. Default: 127.0.0.1:8500.
|
||||||
|
Address string `lang:"address" yaml:"address"`
|
||||||
|
|
||||||
|
// Token is used to provide an ACL token to use for this resource.
|
||||||
|
Token string `lang:"token" yaml:"token"`
|
||||||
|
|
||||||
|
client *api.Client
|
||||||
|
config *api.Config // needed to close the idle connections
|
||||||
|
once bool // safety token
|
||||||
|
key string // cache the key name to avoid re-running the parser
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *ConsulKVRes) Default() engine.Res {
|
||||||
|
return &ConsulKVRes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if the params passed in are valid data.
|
||||||
|
func (obj *ConsulKVRes) Validate() error {
|
||||||
|
s, _, k := obj.inputParser()
|
||||||
|
if k == "" {
|
||||||
|
return fmt.Errorf("the Key is empty")
|
||||||
|
}
|
||||||
|
if s != "" && s != "http" && s != "https" {
|
||||||
|
return fmt.Errorf("unknown Scheme")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *ConsulKVRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
s, a, k := obj.inputParser()
|
||||||
|
|
||||||
|
obj.config = api.DefaultConfig()
|
||||||
|
if s != "" {
|
||||||
|
obj.config.Scheme = s
|
||||||
|
}
|
||||||
|
if a != "" {
|
||||||
|
obj.config.Address = obj.Address
|
||||||
|
}
|
||||||
|
obj.key = k // store the key
|
||||||
|
obj.init.Logf("using consul key: %s", obj.key)
|
||||||
|
|
||||||
|
if obj.Token != "" {
|
||||||
|
obj.config.Token = obj.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
obj.client, err = api.NewClient(obj.config)
|
||||||
|
return errwrap.Wrapf(err, "could not create Consul client")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *ConsulKVRes) Close() error {
|
||||||
|
if obj.config != nil && obj.config.Transport != nil {
|
||||||
|
obj.config.Transport.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the listener and main loop for this resource and it outputs events.
|
||||||
|
func (obj *ConsulKVRes) Watch() error {
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
ch := make(chan error)
|
||||||
|
exit := make(chan struct{})
|
||||||
|
|
||||||
|
kv := obj.client.KV()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
opts := &api.QueryOptions{RequireConsistent: true}
|
||||||
|
ctx, cancel := util.ContextWithCloser(context.Background(), exit)
|
||||||
|
defer cancel()
|
||||||
|
opts = opts.WithContext(ctx)
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, meta, err := kv.Get(obj.key, opts)
|
||||||
|
select {
|
||||||
|
case ch <- err: // send
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitIndex = 0, which means that it is the
|
||||||
|
// first time we run the query, as we are about
|
||||||
|
// to change the WaitIndex to make a blocking
|
||||||
|
// query, we can consider the watch started.
|
||||||
|
opts.WaitIndex = meta.LastIndex
|
||||||
|
if opts.WaitIndex != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !obj.once {
|
||||||
|
obj.init.Running()
|
||||||
|
obj.once = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unexpected situation, bug in consul API...
|
||||||
|
select {
|
||||||
|
case ch <- fmt.Errorf("unexpected behaviour in Consul API"):
|
||||||
|
case <-obj.init.Done: // signal for shutdown request
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-obj.init.Done: // signal for shutdown request
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer close(exit)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err, ok := <-ch:
|
||||||
|
if !ok { // channel shutdown
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("event!")
|
||||||
|
}
|
||||||
|
obj.init.Event()
|
||||||
|
|
||||||
|
case <-obj.init.Done: // signal for shutdown request
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply is run to check the state and, if apply is true, to apply the
|
||||||
|
// necessary changes to reach the desired state. This is run before Watch and
|
||||||
|
// again if Watch finds a change occurring to the state.
|
||||||
|
func (obj *ConsulKVRes) CheckApply(apply bool) (bool, error) {
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("consul key: %s", obj.key)
|
||||||
|
}
|
||||||
|
kv := obj.client.KV()
|
||||||
|
pair, _, err := kv.Get(obj.key, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair != nil && string(pair.Value) == obj.Value {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &api.KVPair{Key: obj.key, Value: []byte(obj.Value)}
|
||||||
|
_, err = kv.Put(p, nil)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and return if they are equivalent.
|
||||||
|
func (obj *ConsulKVRes) Cmp(r engine.Res) error {
|
||||||
|
res, ok := r.(*ConsulKVRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Key != res.Key {
|
||||||
|
return fmt.Errorf("the Key param differs")
|
||||||
|
}
|
||||||
|
if obj.Value != res.Value {
|
||||||
|
return fmt.Errorf("the Value param differs")
|
||||||
|
}
|
||||||
|
if obj.Scheme != res.Scheme {
|
||||||
|
return fmt.Errorf("the Scheme param differs")
|
||||||
|
}
|
||||||
|
if obj.Address != res.Address {
|
||||||
|
return fmt.Errorf("the Address param differs")
|
||||||
|
}
|
||||||
|
if obj.Token != res.Token {
|
||||||
|
return fmt.Errorf("the Token param differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputParser parses the Name() of a resource and extracts the scheme, address,
|
||||||
|
// and key name of a consul key. We don't have an error, because if we have one,
|
||||||
|
// then it means the input must be a raw key. Output of this function is scheme,
|
||||||
|
// address (includes hostname and port), and key. This also takes our parameters
|
||||||
|
// in to account, and applies the correct overrides if they are specified there.
|
||||||
|
func (obj *ConsulKVRes) inputParser() (string, string, string) {
|
||||||
|
// If the key is specified explicitly, then we're not going to parse the
|
||||||
|
// resource name for a pattern, and we use our given params as they are.
|
||||||
|
if obj.Key != "" {
|
||||||
|
return obj.Scheme, obj.Address, obj.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we parse...
|
||||||
|
u, err := url.Parse(obj.Name())
|
||||||
|
if err != nil {
|
||||||
|
// If this didn't work, then we know it's explicitly a raw key.
|
||||||
|
return obj.Scheme, obj.Address, obj.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we use the parse result, and we overwrite any of the
|
||||||
|
// fields if we have an explicit param that was specified.
|
||||||
|
k := u.Path
|
||||||
|
s := u.Scheme
|
||||||
|
a := u.Host
|
||||||
|
|
||||||
|
//if obj.Key != "" { // this is now guaranteed to never happen
|
||||||
|
// k = obj.Key
|
||||||
|
//}
|
||||||
|
if obj.Scheme != "" {
|
||||||
|
s = obj.Scheme
|
||||||
|
}
|
||||||
|
if obj.Address != "" {
|
||||||
|
a = obj.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, a, k
|
||||||
|
}
|
||||||
71
engine/resources/consul_kv_test.go
Normal file
71
engine/resources/consul_kv_test.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createConsulRes(name string) *ConsulKVRes {
|
||||||
|
r, err := engine.NewNamedResource("consul:kv", name)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("could not create resource: %+v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
res := r.(*ConsulKVRes) // if this panics, the test will panic
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseConsulName(t *testing.T) {
|
||||||
|
n1 := "test"
|
||||||
|
r1 := createConsulRes(n1)
|
||||||
|
if s, a, k := r1.inputParser(); s != "" || a != "" || k != "test" {
|
||||||
|
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n1, s, a, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
n2 := "http://127.0.0.1:8500/test"
|
||||||
|
r2 := createConsulRes(n2)
|
||||||
|
if s, a, k := r2.inputParser(); s != "http" || a != "127.0.0.1:8500" || k != "/test" {
|
||||||
|
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n2, s, a, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
n3 := "http://127.0.0.1:8500/test"
|
||||||
|
r3 := createConsulRes(n3)
|
||||||
|
r3.Scheme = "https"
|
||||||
|
r3.Address = "example.com"
|
||||||
|
if s, a, k := r3.inputParser(); s != "https" || a != "example.com" || k != "/test" {
|
||||||
|
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n3, s, a, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
n4 := "http:://127.0.0.1..5:8500/test" // wtf, url.Parse is on drugs...
|
||||||
|
r4 := createConsulRes(n4)
|
||||||
|
//if s, a, k := r4.inputParser(); s != "" || a != "" || k != n4 { // what i really expect
|
||||||
|
if s, a, k := r4.inputParser(); s != "http" || a != "" || k != "" { // what i get
|
||||||
|
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n4, s, a, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
n5 := "http://127.0.0.1:8500/test" // whatever, it's ignored
|
||||||
|
r5 := createConsulRes(n3)
|
||||||
|
r5.Key = "some key"
|
||||||
|
if s, a, k := r5.inputParser(); s != "" || a != "" || k != "some key" {
|
||||||
|
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n5, s, a, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -33,10 +33,10 @@ import (
|
|||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
|
||||||
sdbus "github.com/coreos/go-systemd/dbus"
|
sdbus "github.com/coreos/go-systemd/v22/dbus"
|
||||||
"github.com/coreos/go-systemd/unit"
|
"github.com/coreos/go-systemd/v22/unit"
|
||||||
systemdUtil "github.com/coreos/go-systemd/util"
|
systemdUtil "github.com/coreos/go-systemd/v22/util"
|
||||||
"github.com/godbus/dbus"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -139,9 +139,9 @@ func (obj *CronRes) Default() engine.Res {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeComposite creates a pointer to a FileRes. The pointer is used to
|
// makeComposite creates a pointer to a FileRes. The pointer is used to validate
|
||||||
// validate and initialize the nested file resource and to apply the file state
|
// and initialize the nested file resource and to apply the file state in
|
||||||
// in CheckApply.
|
// CheckApply.
|
||||||
func (obj *CronRes) makeComposite() (*FileRes, error) {
|
func (obj *CronRes) makeComposite() (*FileRes, error) {
|
||||||
p, err := obj.UnitFilePath()
|
p, err := obj.UnitFilePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -466,8 +466,8 @@ func (obj *CronRes) AutoEdges() (engine.AutoEdge, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one although some resources can return multiple.
|
// resources only return one although some resources can return multiple.
|
||||||
func (obj *CronRes) UIDs() []engine.ResUID {
|
func (obj *CronRes) UIDs() []engine.ResUID {
|
||||||
unit := fmt.Sprintf("%s.service", obj.Name())
|
unit := fmt.Sprintf("%s.service", obj.Name())
|
||||||
if obj.Unit != "" {
|
if obj.Unit != "" {
|
||||||
@@ -486,8 +486,8 @@ func (obj *CronRes) UIDs() []engine.ResUID {
|
|||||||
return uids
|
return uids
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *CronRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *CronRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes CronRes // indirection to avoid infinite recursion
|
type rawRes CronRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
1177
engine/resources/dhcp.go
Normal file
1177
engine/resources/dhcp.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,5 +15,5 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// Package bindata stores core mcl code that is built-in at compile time.
|
// Package resources contains the implementations of all the core resources.
|
||||||
package bindata
|
package resources
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !nodocker
|
//go:build !nodocker
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
@@ -50,8 +50,8 @@ const (
|
|||||||
// initCtxTimeout is the length of time, in seconds, before requests are
|
// initCtxTimeout is the length of time, in seconds, before requests are
|
||||||
// cancelled in Init.
|
// cancelled in Init.
|
||||||
initCtxTimeout = 20
|
initCtxTimeout = 20
|
||||||
// checkApplyCtxTimeout is the length of time, in seconds, before requests
|
// checkApplyCtxTimeout is the length of time, in seconds, before
|
||||||
// are cancelled in CheckApply.
|
// requests are cancelled in CheckApply.
|
||||||
checkApplyCtxTimeout = 120
|
checkApplyCtxTimeout = 120
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,11 +74,12 @@ type DockerContainerRes struct {
|
|||||||
Env []string `yaml:"env"`
|
Env []string `yaml:"env"`
|
||||||
// Ports is a map of port bindings. E.g. {"tcp" => {80 => 8080},}.
|
// Ports is a map of port bindings. E.g. {"tcp" => {80 => 8080},}.
|
||||||
Ports map[string]map[int64]int64 `yaml:"ports"`
|
Ports map[string]map[int64]int64 `yaml:"ports"`
|
||||||
// APIVersion allows you to override the host's default client API version.
|
// APIVersion allows you to override the host's default client API
|
||||||
|
// version.
|
||||||
APIVersion string `yaml:"apiversion"`
|
APIVersion string `yaml:"apiversion"`
|
||||||
|
|
||||||
// Force, if true, will destroy and redeploy the container if the image is
|
// Force, if true, this will destroy and redeploy the container if the
|
||||||
// incorrect.
|
// image is incorrect.
|
||||||
Force bool `yaml:"force"`
|
Force bool `yaml:"force"`
|
||||||
|
|
||||||
client *client.Client // docker api client
|
client *client.Client // docker api client
|
||||||
@@ -88,7 +89,9 @@ type DockerContainerRes struct {
|
|||||||
|
|
||||||
// Default returns some sensible defaults for this resource.
|
// Default returns some sensible defaults for this resource.
|
||||||
func (obj *DockerContainerRes) Default() engine.Res {
|
func (obj *DockerContainerRes) Default() engine.Res {
|
||||||
return &DockerContainerRes{}
|
return &DockerContainerRes{
|
||||||
|
State: "running",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate if the params passed in are valid data.
|
// Validate if the params passed in are valid data.
|
||||||
@@ -98,6 +101,11 @@ func (obj *DockerContainerRes) Validate() error {
|
|||||||
return fmt.Errorf("state must be running, stopped or removed")
|
return fmt.Errorf("state must be running, stopped or removed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure an image is specified
|
||||||
|
if obj.Image == "" {
|
||||||
|
return fmt.Errorf("image must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
// validate env
|
// validate env
|
||||||
for _, env := range obj.Env {
|
for _, env := range obj.Env {
|
||||||
if !strings.Contains(env, "=") || strings.Contains(env, " ") {
|
if !strings.Contains(env, "=") || strings.Contains(env, " ") {
|
||||||
@@ -140,7 +148,7 @@ func (obj *DockerContainerRes) Init(init *engine.Init) error {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Initialize the docker client.
|
// Initialize the docker client.
|
||||||
obj.client, err = client.NewClient(client.DefaultDockerHost, obj.APIVersion, nil, nil)
|
obj.client, err = client.NewClientWithOpts(client.WithVersion(obj.APIVersion))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errwrap.Wrapf(err, "error creating docker client")
|
return errwrap.Wrapf(err, "error creating docker client")
|
||||||
}
|
}
|
||||||
@@ -302,7 +310,7 @@ func (obj *DockerContainerRes) CheckApply(apply bool) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := obj.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, obj.Name())
|
c, err := obj.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, obj.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errwrap.Wrapf(err, "error creating container")
|
return false, errwrap.Wrapf(err, "error creating container")
|
||||||
}
|
}
|
||||||
@@ -367,52 +375,105 @@ func (obj *DockerContainerRes) Cmp(r engine.Res) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("error casting r to *DockerContainerRes")
|
return fmt.Errorf("error casting r to *DockerContainerRes")
|
||||||
}
|
}
|
||||||
if obj.Name() != res.Name() {
|
|
||||||
return fmt.Errorf("names differ")
|
if obj.State != res.State {
|
||||||
|
return fmt.Errorf("the State differs")
|
||||||
|
}
|
||||||
|
if obj.Image != res.Image {
|
||||||
|
return fmt.Errorf("the Image differs")
|
||||||
}
|
}
|
||||||
if err := util.SortedStrSliceCompare(obj.Cmd, res.Cmd); err != nil {
|
if err := util.SortedStrSliceCompare(obj.Cmd, res.Cmd); err != nil {
|
||||||
return errwrap.Wrapf(err, "cmd differs")
|
return errwrap.Wrapf(err, "the Cmd field differs")
|
||||||
}
|
}
|
||||||
if err := util.SortedStrSliceCompare(obj.Env, res.Env); err != nil {
|
if err := util.SortedStrSliceCompare(obj.Env, res.Env); err != nil {
|
||||||
return errwrap.Wrapf(err, "env differs")
|
return errwrap.Wrapf(err, "tne Env field differs")
|
||||||
}
|
}
|
||||||
if len(obj.Ports) != len(res.Ports) {
|
if len(obj.Ports) != len(res.Ports) {
|
||||||
return fmt.Errorf("ports length differs")
|
return fmt.Errorf("the Ports length differs")
|
||||||
}
|
}
|
||||||
for k, v := range obj.Ports {
|
for k, v := range obj.Ports {
|
||||||
for p, q := range v {
|
for p, q := range v {
|
||||||
if w, ok := res.Ports[k][p]; !ok || q != w {
|
if w, ok := res.Ports[k][p]; !ok || q != w {
|
||||||
return fmt.Errorf("ports differ")
|
return fmt.Errorf("the Ports field differs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if obj.APIVersion != res.APIVersion {
|
if obj.APIVersion != res.APIVersion {
|
||||||
return fmt.Errorf("apiversions differ")
|
return fmt.Errorf("the APIVersion differs")
|
||||||
}
|
}
|
||||||
if obj.Force != res.Force {
|
if obj.Force != res.Force {
|
||||||
return fmt.Errorf("forces differ")
|
return fmt.Errorf("the Force field differs")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DockerUID is the UID struct for DockerContainerRes.
|
// DockerContainerUID is the UID struct for DockerContainerRes.
|
||||||
type DockerUID struct {
|
type DockerContainerUID struct {
|
||||||
engine.BaseUID
|
engine.BaseUID
|
||||||
|
|
||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// DockerContainerResAutoEdges holds the state of the auto edge generator.
|
||||||
// Most resources only return one, although some resources can return multiple.
|
type DockerContainerResAutoEdges struct {
|
||||||
|
UIDs []engine.ResUID
|
||||||
|
pointer int
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoEdges returns edges to any docker:image resource that matches the image
|
||||||
|
// specified in the docker:container resource definition.
|
||||||
|
func (obj *DockerContainerRes) AutoEdges() (engine.AutoEdge, error) {
|
||||||
|
var result []engine.ResUID
|
||||||
|
var reversed bool
|
||||||
|
if obj.State != "removed" {
|
||||||
|
reversed = true
|
||||||
|
}
|
||||||
|
result = append(result, &DockerImageUID{
|
||||||
|
BaseUID: engine.BaseUID{
|
||||||
|
Reversed: &reversed,
|
||||||
|
},
|
||||||
|
image: dockerImageNameTag(obj.Image),
|
||||||
|
})
|
||||||
|
return &DockerContainerResAutoEdges{
|
||||||
|
UIDs: result,
|
||||||
|
pointer: 0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returnes the next automatic edge.
|
||||||
|
func (obj *DockerContainerResAutoEdges) Next() []engine.ResUID {
|
||||||
|
if len(obj.UIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value := obj.UIDs[obj.pointer]
|
||||||
|
obj.pointer++
|
||||||
|
return []engine.ResUID{value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test gets results of the earlier Next() call, & returns if we should
|
||||||
|
// continue.
|
||||||
|
func (obj *DockerContainerResAutoEdges) Test(input []bool) bool {
|
||||||
|
if len(obj.UIDs) <= obj.pointer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(input) != 1 { // in case we get given bad data
|
||||||
|
panic(fmt.Sprintf("Expecting a single value!"))
|
||||||
|
}
|
||||||
|
return true // keep going
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
|
// resources only return one, although some resources can return multiple.
|
||||||
func (obj *DockerContainerRes) UIDs() []engine.ResUID {
|
func (obj *DockerContainerRes) UIDs() []engine.ResUID {
|
||||||
x := &DockerUID{
|
x := &DockerContainerUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
name: obj.Name(),
|
name: obj.Name(),
|
||||||
}
|
}
|
||||||
return []engine.ResUID{x}
|
return []engine.ResUID{x}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *DockerContainerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *DockerContainerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes DockerContainerRes // indirection to avoid infinite recursion
|
type rawRes DockerContainerRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !nodocker
|
//go:build !nodocker
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
@@ -165,6 +165,7 @@ func setup() error {
|
|||||||
},
|
},
|
||||||
&container.HostConfig{},
|
&container.HostConfig{},
|
||||||
nil,
|
nil,
|
||||||
|
nil,
|
||||||
"mgmt-test",
|
"mgmt-test",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
295
engine/resources/docker_image.go
Normal file
295
engine/resources/docker_image.go
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build !nodocker
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// dockerImageInitCtxTimeout is the length of time, in seconds, before
|
||||||
|
// requests are cancelled in Init.
|
||||||
|
dockerImageInitCtxTimeout = 20
|
||||||
|
// dockerImageCheckApplyCtxTimeout is the length of time, in seconds,
|
||||||
|
// before requests are cancelled in CheckApply.
|
||||||
|
dockerImageCheckApplyCtxTimeout = 120
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("docker:image", func() engine.Res { return &DockerImageRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
// DockerImageRes is a docker image resource. The resource's name must be a
|
||||||
|
// docker image in any supported format (url, image, or image:tag).
|
||||||
|
type DockerImageRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
traits.Edgeable
|
||||||
|
|
||||||
|
// State of the image must be exists or absent.
|
||||||
|
State string `yaml:"state"`
|
||||||
|
// APIVersion allows you to override the host's default client API
|
||||||
|
// version.
|
||||||
|
APIVersion string `yaml:"apiversion"`
|
||||||
|
|
||||||
|
image string // full image:tag format
|
||||||
|
client *client.Client // docker api client
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *DockerImageRes) Default() engine.Res {
|
||||||
|
return &DockerImageRes{
|
||||||
|
// TODO: eventually if image supports other properties, this can
|
||||||
|
// be left out and we could have the state be "unmanaged".
|
||||||
|
State: "exists",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if the params passed in are valid data.
|
||||||
|
func (obj *DockerImageRes) Validate() error {
|
||||||
|
// validate state
|
||||||
|
if obj.State != "exists" && obj.State != "absent" {
|
||||||
|
return fmt.Errorf("state must be exists or absent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate APIVersion
|
||||||
|
if obj.APIVersion != "" {
|
||||||
|
verOK, err := regexp.MatchString(`^(v)[1-9]\.[0-9]\d*$`, obj.APIVersion)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error matching apiversion string")
|
||||||
|
}
|
||||||
|
if !verOK {
|
||||||
|
return fmt.Errorf("invalid apiversion: %s", obj.APIVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *DockerImageRes) Init(init *engine.Init) error {
|
||||||
|
var err error
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
// Save the full image name and tag.
|
||||||
|
obj.image = dockerImageNameTag(obj.Name())
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), dockerImageInitCtxTimeout*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Initialize the docker client.
|
||||||
|
obj.client, err = client.NewClientWithOpts(client.WithVersion(obj.APIVersion))
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error creating docker client")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the image.
|
||||||
|
resp, err := obj.client.ImageSearch(ctx, obj.image, types.ImageSearchOptions{Limit: 1})
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error searching for image")
|
||||||
|
}
|
||||||
|
if len(resp) == 0 {
|
||||||
|
return fmt.Errorf("image: %s not found", obj.image)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *DockerImageRes) Close() error {
|
||||||
|
return obj.client.Close() // close the docker client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
|
func (obj *DockerImageRes) Watch() error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
eventChan, errChan := obj.client.Events(ctx, types.EventsOptions{})
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
obj.init.Running()
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-eventChan:
|
||||||
|
if !ok { // channel shutdown
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("%+v", event)
|
||||||
|
}
|
||||||
|
send = true
|
||||||
|
|
||||||
|
case err, ok := <-errChan:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
|
||||||
|
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
obj.init.Event() // notify engine of an event (this can block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply method for Docker resource.
|
||||||
|
func (obj *DockerImageRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), dockerImageCheckApplyCtxTimeout*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s, err := obj.client.ImageList(ctx, types.ImageListOptions{
|
||||||
|
Filters: filters.NewArgs(filters.Arg("reference", obj.image)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error listing images")
|
||||||
|
}
|
||||||
|
if len(s) > 1 {
|
||||||
|
return false, fmt.Errorf("more than one image found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.State == "absent" && len(s) == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if obj.State == "exists" && len(s) == 1 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.State == "absent" {
|
||||||
|
// TODO: force? prune children?
|
||||||
|
if _, err := obj.client.ImageRemove(ctx, obj.image, types.ImageRemoveOptions{}); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error removing image")
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pull the image
|
||||||
|
p, err := obj.client.ImagePull(ctx, obj.image, types.ImagePullOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error pulling image")
|
||||||
|
}
|
||||||
|
// Wait for the image to download, EOF signals that it's done.
|
||||||
|
if _, err := ioutil.ReadAll(p); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error reading image pull result")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *DockerImageRes) Cmp(r engine.Res) error {
|
||||||
|
// we can only compare DockerImageRes to others of the same resource kind
|
||||||
|
res, ok := r.(*DockerImageRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("error casting r to *DockerImageRes")
|
||||||
|
}
|
||||||
|
if obj.State != res.State {
|
||||||
|
return fmt.Errorf("the State differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.APIVersion != res.APIVersion {
|
||||||
|
return fmt.Errorf("the APIVersion differs")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DockerImageUID is the UID struct for DockerImageRes.
|
||||||
|
type DockerImageUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
|
||||||
|
image string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
|
// resources only return one, although some resources can return multiple.
|
||||||
|
func (obj *DockerImageRes) UIDs() []engine.ResUID {
|
||||||
|
x := &DockerImageUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
image: dockerImageNameTag(obj.Name()),
|
||||||
|
}
|
||||||
|
return []engine.ResUID{x}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoEdges returns the AutoEdge interface.
|
||||||
|
func (obj *DockerImageRes) AutoEdges() (engine.AutoEdge, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||||
|
func (obj *DockerImageUID) IFF(uid engine.ResUID) bool {
|
||||||
|
res, ok := uid.(*DockerImageUID)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return obj.image == res.image
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
|
// primarily useful for setting the defaults.
|
||||||
|
func (obj *DockerImageRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes DockerImageRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*DockerImageRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to DockerImageRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = DockerImageRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dockerImageNameTag does a naive check to see if the input includes a tag or
|
||||||
|
// is a url, and if not, appends the `:latest` tag to ensure disambiguation.
|
||||||
|
func dockerImageNameTag(image string) string {
|
||||||
|
if strings.Contains(image, ":") {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
return image + ":latest"
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -54,7 +55,7 @@ type ExecRes struct {
|
|||||||
// only be used when a Shell is *not* specified. The advantage of this
|
// only be used when a Shell is *not* specified. The advantage of this
|
||||||
// is that you don't have to worry about escape characters.
|
// is that you don't have to worry about escape characters.
|
||||||
Args []string `yaml:"args"`
|
Args []string `yaml:"args"`
|
||||||
// Cmd is the dir to run the command in. If empty, then this will use
|
// Cwd is the dir to run the command in. If empty, then this will use
|
||||||
// the working directory of the calling process. (This process is mgmt,
|
// the working directory of the calling process. (This process is mgmt,
|
||||||
// not the process being run here.)
|
// not the process being run here.)
|
||||||
Cwd string `yaml:"cwd"`
|
Cwd string `yaml:"cwd"`
|
||||||
@@ -65,6 +66,9 @@ type ExecRes struct {
|
|||||||
// running command. If the Kill is received before the process exits,
|
// running command. If the Kill is received before the process exits,
|
||||||
// then this be treated as an error.
|
// then this be treated as an error.
|
||||||
Timeout uint64 `yaml:"timeout"`
|
Timeout uint64 `yaml:"timeout"`
|
||||||
|
// Env allows the user to specify environment variables for script
|
||||||
|
// execution. These are taken using a map of format of VAR_NAME -> value.
|
||||||
|
Env map[string]string `yaml:"env"`
|
||||||
|
|
||||||
// Watch is the command to run to detect event changes. Each line of
|
// Watch is the command to run to detect event changes. Each line of
|
||||||
// output from this command is treated as an event.
|
// output from this command is treated as an event.
|
||||||
@@ -138,6 +142,12 @@ func (obj *ExecRes) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check that environment variables' format is valid
|
||||||
|
for key := range obj.Env {
|
||||||
|
if err := isNameValid(key); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "invalid variable name")
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,19 +224,21 @@ func (obj *ExecRes) Watch() error {
|
|||||||
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
||||||
if !ok {
|
if !ok {
|
||||||
// command failed in some bad way
|
// command failed in some bad way
|
||||||
return errwrap.Wrapf(err, "unknown error")
|
return errwrap.Wrapf(err, "watchcmd failed in some bad way")
|
||||||
}
|
}
|
||||||
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
|
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
|
||||||
wStatus, ok := pStateSys.(syscall.WaitStatus)
|
wStatus, ok := pStateSys.(syscall.WaitStatus)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errwrap.Wrapf(err, "error running cmd")
|
return errwrap.Wrapf(err, "could not get exit status of watchcmd")
|
||||||
}
|
}
|
||||||
exitStatus := wStatus.ExitStatus()
|
exitStatus := wStatus.ExitStatus()
|
||||||
obj.init.Logf("watchcmd exited with: %d", exitStatus)
|
if exitStatus == 0 {
|
||||||
if exitStatus != 0 {
|
// i'm not sure if this could happen
|
||||||
return errwrap.Wrapf(err, "unexpected exit status of zero")
|
return errwrap.Wrapf(err, "unexpected watchcmd exit status of zero")
|
||||||
}
|
}
|
||||||
return err // i'm not sure if this could happen
|
|
||||||
|
obj.init.Logf("watchcmd exited with: %d", exitStatus)
|
||||||
|
return errwrap.Wrapf(err, "watchcmd errored")
|
||||||
}
|
}
|
||||||
|
|
||||||
// each time we get a line of output, we loop!
|
// each time we get a line of output, we loop!
|
||||||
@@ -298,16 +310,17 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
|
|||||||
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
||||||
if !ok {
|
if !ok {
|
||||||
// command failed in some bad way
|
// command failed in some bad way
|
||||||
return false, err
|
return false, errwrap.Wrapf(err, "ifcmd failed in some bad way")
|
||||||
}
|
}
|
||||||
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
|
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
|
||||||
wStatus, ok := pStateSys.(syscall.WaitStatus)
|
wStatus, ok := pStateSys.(syscall.WaitStatus)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errwrap.Wrapf(err, "error running cmd")
|
return false, errwrap.Wrapf(err, "could not get exit status of ifcmd")
|
||||||
}
|
}
|
||||||
exitStatus := wStatus.ExitStatus()
|
exitStatus := wStatus.ExitStatus()
|
||||||
if exitStatus == 0 {
|
if exitStatus == 0 {
|
||||||
return false, fmt.Errorf("unexpected exit status of zero")
|
// i'm not sure if this could happen
|
||||||
|
return false, errwrap.Wrapf(err, "unexpected ifcmd exit status of zero")
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.init.Logf("ifcmd exited with: %d", exitStatus)
|
obj.init.Logf("ifcmd exited with: %d", exitStatus)
|
||||||
@@ -368,6 +381,18 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
|
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
|
||||||
cmd.Dir = obj.Cwd // run program in pwd if ""
|
cmd.Dir = obj.Cwd // run program in pwd if ""
|
||||||
|
|
||||||
|
envKeys := []string{}
|
||||||
|
for key := range obj.Env {
|
||||||
|
envKeys = append(envKeys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(envKeys)
|
||||||
|
cmdEnv := []string{}
|
||||||
|
for _, k := range envKeys {
|
||||||
|
cmdEnv = append(cmdEnv, k+"="+obj.Env[k])
|
||||||
|
}
|
||||||
|
cmd.Env = cmdEnv
|
||||||
|
|
||||||
// ignore signals sent to parent process (we're in our own group)
|
// ignore signals sent to parent process (we're in our own group)
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
Setpgid: true,
|
Setpgid: true,
|
||||||
@@ -438,7 +463,7 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
|
|||||||
return false, errwrap.Wrapf(err, "cmd timeout, exit status: %d", exitStatus)
|
return false, errwrap.Wrapf(err, "cmd timeout, exit status: %d", exitStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, fmt.Errorf("unknown cmd error, signal: %s, exit status: %d", sig, exitStatus)
|
return false, errwrap.Wrapf(err, "unknown cmd error, signal: %s, exit status: %d", sig, exitStatus)
|
||||||
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return false, errwrap.Wrapf(err, "general cmd error")
|
return false, errwrap.Wrapf(err, "general cmd error")
|
||||||
@@ -546,24 +571,37 @@ type ExecUID struct {
|
|||||||
// ExecResAutoEdges holds the state of the auto edge generator.
|
// ExecResAutoEdges holds the state of the auto edge generator.
|
||||||
type ExecResAutoEdges struct {
|
type ExecResAutoEdges struct {
|
||||||
edges []engine.ResUID
|
edges []engine.ResUID
|
||||||
|
pointer int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next returns the next automatic edge.
|
// Next returns the next automatic edge.
|
||||||
func (obj *ExecResAutoEdges) Next() []engine.ResUID {
|
func (obj *ExecResAutoEdges) Next() []engine.ResUID {
|
||||||
return obj.edges
|
if len(obj.edges) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value := obj.edges[obj.pointer]
|
||||||
|
obj.pointer++
|
||||||
|
return []engine.ResUID{value}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test gets results of the earlier Next() call, & returns if we should continue!
|
// Test gets results of the earlier Next() call, & returns if we should
|
||||||
|
// continue!
|
||||||
func (obj *ExecResAutoEdges) Test(input []bool) bool {
|
func (obj *ExecResAutoEdges) Test(input []bool) bool {
|
||||||
return false // never keep going
|
if len(obj.edges) <= obj.pointer {
|
||||||
// TODO: we could return false if we find as many edges as the number of different path's in cmdFiles()
|
return false
|
||||||
|
}
|
||||||
|
if len(input) != 1 { // in case we get given bad data
|
||||||
|
panic(fmt.Sprintf("Expecting a single value!"))
|
||||||
|
}
|
||||||
|
return true // keep going
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoEdges returns the AutoEdge interface. In this case the systemd units.
|
// AutoEdges returns the AutoEdge interface. In this case the systemd units.
|
||||||
func (obj *ExecRes) AutoEdges() (engine.AutoEdge, error) {
|
func (obj *ExecRes) AutoEdges() (engine.AutoEdge, error) {
|
||||||
var data []engine.ResUID
|
var data []engine.ResUID
|
||||||
for _, x := range obj.cmdFiles() {
|
|
||||||
var reversed = true
|
var reversed = true
|
||||||
|
|
||||||
|
for _, x := range obj.cmdFiles() {
|
||||||
data = append(data, &PkgFileUID{
|
data = append(data, &PkgFileUID{
|
||||||
BaseUID: engine.BaseUID{
|
BaseUID: engine.BaseUID{
|
||||||
Name: obj.Name(),
|
Name: obj.Name(),
|
||||||
@@ -572,14 +610,44 @@ func (obj *ExecRes) AutoEdges() (engine.AutoEdge, error) {
|
|||||||
},
|
},
|
||||||
path: x, // what matters
|
path: x, // what matters
|
||||||
})
|
})
|
||||||
|
data = append(data, &FileUID{
|
||||||
|
BaseUID: engine.BaseUID{
|
||||||
|
Name: obj.Name(),
|
||||||
|
Kind: obj.Kind(),
|
||||||
|
Reversed: &reversed,
|
||||||
|
},
|
||||||
|
path: x,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
if obj.User != "" {
|
||||||
|
data = append(data, &UserUID{
|
||||||
|
BaseUID: engine.BaseUID{
|
||||||
|
Name: obj.Name(),
|
||||||
|
Kind: obj.Kind(),
|
||||||
|
Reversed: &reversed,
|
||||||
|
},
|
||||||
|
name: obj.User,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if obj.Group != "" {
|
||||||
|
data = append(data, &GroupUID{
|
||||||
|
BaseUID: engine.BaseUID{
|
||||||
|
Name: obj.Name(),
|
||||||
|
Kind: obj.Kind(),
|
||||||
|
Reversed: &reversed,
|
||||||
|
},
|
||||||
|
name: obj.Group,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return &ExecResAutoEdges{
|
return &ExecResAutoEdges{
|
||||||
edges: data,
|
edges: data,
|
||||||
|
pointer: 0,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one, although some resources can return multiple.
|
// resources only return one, although some resources can return multiple.
|
||||||
func (obj *ExecRes) UIDs() []engine.ResUID {
|
func (obj *ExecRes) UIDs() []engine.ResUID {
|
||||||
x := &ExecUID{
|
x := &ExecUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -609,8 +677,8 @@ func (obj *ExecRes) Sends() interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *ExecRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *ExecRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes ExecRes // indirection to avoid infinite recursion
|
type rawRes ExecRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
@@ -690,9 +758,9 @@ type cmdOutput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cmdOutputRunner wraps the Cmd in with a StdoutPipe scanner and reads for
|
// cmdOutputRunner wraps the Cmd in with a StdoutPipe scanner and reads for
|
||||||
// errors. It runs Start and Wait, and errors runtime things in the channel.
|
// errors. It runs Start and Wait, and errors runtime things in the channel. If
|
||||||
// If it can't start up the command, it will fail early. Once it's running, it
|
// it can't start up the command, it will fail early. Once it's running, it will
|
||||||
// will return the channel which can be used for the duration of the process.
|
// return the channel which can be used for the duration of the process.
|
||||||
// Cancelling the context merely unblocks the sending on the output channel, it
|
// Cancelling the context merely unblocks the sending on the output channel, it
|
||||||
// does not Kill the cmd process. For that you must do it yourself elsewhere.
|
// does not Kill the cmd process. For that you must do it yourself elsewhere.
|
||||||
func (obj *ExecRes) cmdOutputRunner(ctx context.Context, cmd *exec.Cmd) (chan *cmdOutput, error) {
|
func (obj *ExecRes) cmdOutputRunner(ctx context.Context, cmd *exec.Cmd) (chan *cmdOutput, error) {
|
||||||
@@ -800,3 +868,20 @@ func (obj *wrapWriter) Write(p []byte) (int, error) {
|
|||||||
func (obj *wrapWriter) String() string {
|
func (obj *wrapWriter) String() string {
|
||||||
return obj.Buffer.String()
|
return obj.Buffer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isNameValid checks that environment variable name is valid.
|
||||||
|
func isNameValid(varName string) error {
|
||||||
|
if varName == "" {
|
||||||
|
return fmt.Errorf("variable name cannot be an empty string")
|
||||||
|
}
|
||||||
|
for i := range varName {
|
||||||
|
c := varName[i]
|
||||||
|
if i == 0 && '0' <= c && c <= '9' {
|
||||||
|
return fmt.Errorf("variable name cannot begin with number")
|
||||||
|
}
|
||||||
|
if !(c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
|
||||||
|
return fmt.Errorf("invalid character in variable name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/engine"
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/graph/autoedge"
|
||||||
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
)
|
)
|
||||||
|
|
||||||
func fakeExecInit(t *testing.T) (*engine.Init, *ExecSends) {
|
func fakeExecInit(t *testing.T) (*engine.Init, *ExecSends) {
|
||||||
@@ -257,3 +259,77 @@ func TestExecTimeoutBehaviour(t *testing.T) {
|
|||||||
|
|
||||||
// no error
|
// no error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecAutoEdge1(t *testing.T) {
|
||||||
|
g, err := pgraph.NewGraph("TestGraph")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating graph: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resUser, err := engine.NewNamedResource("user", "someuser")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating user resource: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resGroup, err := engine.NewNamedResource("group", "somegroup")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating group resource: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resFile, err := engine.NewNamedResource("file", "/somefile")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating group resource: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resExec, err := engine.NewNamedResource("exec", "somefile")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating exec resource: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exc := resExec.(*ExecRes)
|
||||||
|
exc.Cmd = resFile.Name()
|
||||||
|
exc.User = resUser.Name()
|
||||||
|
exc.Group = resGroup.Name()
|
||||||
|
|
||||||
|
g.AddVertex(resUser, resGroup, resFile, resExec)
|
||||||
|
|
||||||
|
if i := g.NumEdges(); i != 0 {
|
||||||
|
t.Errorf("should have 0 edges instead of: %d", i)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
||||||
|
logf := func(format string, v ...interface{}) {
|
||||||
|
t.Logf("test: "+format, v...)
|
||||||
|
}
|
||||||
|
if err := autoedge.AutoEdge(g, debug, logf); err != nil {
|
||||||
|
t.Errorf("error running autoedges: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expected, err := pgraph.NewGraph("Expected")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating graph: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expectEdge := func(from, to pgraph.Vertex) {
|
||||||
|
edge := &engine.Edge{Name: fmt.Sprintf("%s -> %s (expected)", from, to)}
|
||||||
|
expected.AddEdge(from, to, edge)
|
||||||
|
}
|
||||||
|
expectEdge(resFile, resExec)
|
||||||
|
expectEdge(resUser, resExec)
|
||||||
|
expectEdge(resGroup, resExec)
|
||||||
|
|
||||||
|
vertexCmp := func(v1, v2 pgraph.Vertex) (bool, error) { return v1 == v2, nil } // pointer compare is sufficient
|
||||||
|
edgeCmp := func(e1, e2 pgraph.Edge) (bool, error) { return true, nil } // we don't care about edges here
|
||||||
|
|
||||||
|
if err := expected.GraphCmp(g, vertexCmp, edgeCmp); err != nil {
|
||||||
|
t.Errorf("graph doesn't match expected: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ func TestMiscEncodeDecode1(t *testing.T) {
|
|||||||
e := gob.NewEncoder(&b1)
|
e := gob.NewEncoder(&b1)
|
||||||
err = e.Encode(&input) // pass with &
|
err = e.Encode(&input) // pass with &
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Gob failed to Encode: %v", err)
|
t.Errorf("gob failed to Encode: %v", err)
|
||||||
}
|
}
|
||||||
str := base64.StdEncoding.EncodeToString(b1.Bytes())
|
str := base64.StdEncoding.EncodeToString(b1.Bytes())
|
||||||
|
|
||||||
@@ -86,27 +86,27 @@ func TestMiscEncodeDecode1(t *testing.T) {
|
|||||||
var output interface{}
|
var output interface{}
|
||||||
bb, err := base64.StdEncoding.DecodeString(str)
|
bb, err := base64.StdEncoding.DecodeString(str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Base64 failed to Decode: %v", err)
|
t.Errorf("base64 failed to Decode: %v", err)
|
||||||
}
|
}
|
||||||
b2 := bytes.NewBuffer(bb)
|
b2 := bytes.NewBuffer(bb)
|
||||||
d := gob.NewDecoder(b2)
|
d := gob.NewDecoder(b2)
|
||||||
err = d.Decode(&output) // pass with &
|
err = d.Decode(&output) // pass with &
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Gob failed to Decode: %v", err)
|
t.Errorf("gob failed to Decode: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res1, ok := input.(engine.Res)
|
res1, ok := input.(engine.Res)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("Input %v is not a Res", res1)
|
t.Errorf("input %v is not a Res", res1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res2, ok := output.(engine.Res)
|
res2, ok := output.(engine.Res)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("Output %v is not a Res", res2)
|
t.Errorf("output %v is not a Res", res2)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := res1.Cmp(res2); err != nil {
|
if err := res1.Cmp(res2); err != nil {
|
||||||
t.Errorf("The input and output Res values do not match: %+v", err)
|
t.Errorf("the input and output Res values do not match: %+v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ func TestMiscEncodeDecode2(t *testing.T) {
|
|||||||
// encode
|
// encode
|
||||||
input, err := engine.NewNamedResource("file", "file1")
|
input, err := engine.NewNamedResource("file", "file1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Can't create: %v", err)
|
t.Errorf("can't create: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// NOTE: Do not add this bit of code, because it would cause the path to
|
// NOTE: Do not add this bit of code, because it would cause the path to
|
||||||
@@ -128,29 +128,29 @@ func TestMiscEncodeDecode2(t *testing.T) {
|
|||||||
|
|
||||||
b64, err := engineUtil.ResToB64(input)
|
b64, err := engineUtil.ResToB64(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Can't encode: %v", err)
|
t.Errorf("can't encode: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := engineUtil.B64ToRes(b64)
|
output, err := engineUtil.B64ToRes(b64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Can't decode: %v", err)
|
t.Errorf("can't decode: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res1, ok := input.(engine.Res)
|
res1, ok := input.(engine.Res)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("Input %v is not a Res", res1)
|
t.Errorf("input %v is not a Res", res1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res2, ok := output.(engine.Res)
|
res2, ok := output.(engine.Res)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("Output %v is not a Res", res2)
|
t.Errorf("output %v is not a Res", res2)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// this uses the standalone file cmp function
|
// this uses the standalone file cmp function
|
||||||
if err := res1.Cmp(res2); err != nil {
|
if err := res1.Cmp(res2); err != nil {
|
||||||
t.Errorf("The input and output Res values do not match: %+v", err)
|
t.Errorf("the input and output Res values do not match: %+v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ func TestMiscEncodeDecode3(t *testing.T) {
|
|||||||
// encode
|
// encode
|
||||||
input, err := engine.NewNamedResource("file", "file1")
|
input, err := engine.NewNamedResource("file", "file1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Can't create: %v", err)
|
t.Errorf("can't create: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileRes := input.(*FileRes) // must not panic
|
fileRes := input.(*FileRes) // must not panic
|
||||||
@@ -169,29 +169,82 @@ func TestMiscEncodeDecode3(t *testing.T) {
|
|||||||
|
|
||||||
b64, err := engineUtil.ResToB64(input)
|
b64, err := engineUtil.ResToB64(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Can't encode: %v", err)
|
t.Errorf("can't encode: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := engineUtil.B64ToRes(b64)
|
output, err := engineUtil.B64ToRes(b64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Can't decode: %v", err)
|
t.Errorf("can't decode: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res1, ok := input.(engine.Res)
|
res1, ok := input.(engine.Res)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("Input %v is not a Res", res1)
|
t.Errorf("input %v is not a Res", res1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res2, ok := output.(engine.Res)
|
res2, ok := output.(engine.Res)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("Output %v is not a Res", res2)
|
t.Errorf("output %v is not a Res", res2)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// this uses the more complete, engine cmp function
|
// this uses the more complete, engine cmp function
|
||||||
if err := engine.ResCmp(res1, res2); err != nil {
|
if err := engine.ResCmp(res1, res2); err != nil {
|
||||||
t.Errorf("The input and output Res values do not match: %+v", err)
|
t.Errorf("the input and output Res values do not match: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiscEncodeDecode4(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
const (
|
||||||
|
Kind = "file"
|
||||||
|
Name = "file1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encode
|
||||||
|
input, err := engine.NewNamedResource(Kind, Name)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("can't create: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileRes := input.(*FileRes) // must not panic
|
||||||
|
fileRes.Path = "/tmp/whatever"
|
||||||
|
// TODO: add other params/traits/etc here!
|
||||||
|
|
||||||
|
b64, err := engineUtil.ResToB64(input)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("can't encode: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := engineUtil.B64ToRes(b64)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("can't decode: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res1, ok := input.(engine.Res)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("input %v is not a Res", res1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res2, ok := output.(engine.Res)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("output %v is not a Res", res2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// this uses the more complete, engine cmp function
|
||||||
|
if err := engine.ResCmp(res1, res2); err != nil {
|
||||||
|
t.Errorf("the input and output Res values do not match: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure the kind and name are correctly decoded too!
|
||||||
|
if kind := res2.Kind(); kind != Kind {
|
||||||
|
t.Errorf("the output kind was `%s`, expected `%s`", kind, Kind)
|
||||||
|
}
|
||||||
|
if name := res2.Name(); name != Name {
|
||||||
|
t.Errorf("the output name was `%s`, expected `%s`", name, Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -58,7 +58,7 @@ func (obj *GroupRes) Default() engine.Res {
|
|||||||
// Validate if the params passed in are valid data.
|
// Validate if the params passed in are valid data.
|
||||||
func (obj *GroupRes) Validate() error {
|
func (obj *GroupRes) Validate() error {
|
||||||
if obj.State != "exists" && obj.State != "absent" {
|
if obj.State != "exists" && obj.State != "absent" {
|
||||||
return fmt.Errorf("State must be 'exists' or 'absent'")
|
return fmt.Errorf("state must be 'exists' or 'absent'")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -220,32 +220,24 @@ func (obj *GroupRes) CheckApply(apply bool) (bool, error) {
|
|||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *GroupRes) Cmp(r engine.Res) error {
|
func (obj *GroupRes) Cmp(r engine.Res) error {
|
||||||
if !obj.Compare(r) {
|
|
||||||
return fmt.Errorf("did not compare")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare two resources and return if they are equivalent.
|
|
||||||
func (obj *GroupRes) Compare(r engine.Res) bool {
|
|
||||||
// we can only compare GroupRes to others of the same resource kind
|
// we can only compare GroupRes to others of the same resource kind
|
||||||
res, ok := r.(*GroupRes)
|
res, ok := r.(*GroupRes)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.State != res.State {
|
if obj.State != res.State {
|
||||||
return false
|
return fmt.Errorf("the State differs")
|
||||||
}
|
}
|
||||||
if (obj.GID == nil) != (res.GID == nil) {
|
if (obj.GID == nil) != (res.GID == nil) {
|
||||||
return false
|
return fmt.Errorf("the GID differs")
|
||||||
}
|
}
|
||||||
if obj.GID != nil && res.GID != nil {
|
if obj.GID != nil && res.GID != nil {
|
||||||
if *obj.GID != *res.GID {
|
if *obj.GID != *res.GID {
|
||||||
return false
|
return fmt.Errorf("the GID differs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupUID is the UID struct for GroupRes.
|
// GroupUID is the UID struct for GroupRes.
|
||||||
@@ -279,8 +271,8 @@ func (obj *GroupUID) IFF(uid engine.ResUID) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one, although some resources can return multiple.
|
// resources only return one, although some resources can return multiple.
|
||||||
func (obj *GroupRes) UIDs() []engine.ResUID {
|
func (obj *GroupRes) UIDs() []engine.ResUID {
|
||||||
x := &GroupUID{
|
x := &GroupUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -290,8 +282,8 @@ func (obj *GroupRes) UIDs() []engine.ResUID {
|
|||||||
return []engine.ResUID{x}
|
return []engine.ResUID{x}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *GroupRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *GroupRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes GroupRes // indirection to avoid infinite recursion
|
type rawRes GroupRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
1227
engine/resources/hetzner_vm.go
Normal file
1227
engine/resources/hetzner_vm.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
|
||||||
"github.com/godbus/dbus"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -46,12 +46,12 @@ var ErrResourceInsufficientParameters = errors.New("insufficient parameters for
|
|||||||
|
|
||||||
// HostnameRes is a resource that allows setting and watching the hostname.
|
// HostnameRes is a resource that allows setting and watching the hostname.
|
||||||
//
|
//
|
||||||
// StaticHostname is the one configured in /etc/hostname or a similar file.
|
// StaticHostname is the one configured in /etc/hostname or a similar file. It
|
||||||
// It is chosen by the local user. It is not always in sync with the current
|
// is chosen by the local user. It is not always in sync with the current host
|
||||||
// host name as returned by the gethostname() system call.
|
// name as returned by the gethostname() system call.
|
||||||
//
|
//
|
||||||
// TransientHostname is the one configured via the kernel's sethostbyname().
|
// TransientHostname is the one configured via the kernel's sethostbyname(). It
|
||||||
// It can be different from the static hostname in case DHCP or mDNS have been
|
// can be different from the static hostname in case DHCP or mDNS have been
|
||||||
// configured to change the name based on network information.
|
// configured to change the name based on network information.
|
||||||
//
|
//
|
||||||
// PrettyHostname is a free-form UTF8 host name for presentation to the user.
|
// PrettyHostname is a free-form UTF8 host name for presentation to the user.
|
||||||
@@ -219,31 +219,23 @@ func (obj *HostnameRes) CheckApply(apply bool) (bool, error) {
|
|||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *HostnameRes) Cmp(r engine.Res) error {
|
func (obj *HostnameRes) Cmp(r engine.Res) error {
|
||||||
if !obj.Compare(r) {
|
|
||||||
return fmt.Errorf("did not compare")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare two resources and return if they are equivalent.
|
|
||||||
func (obj *HostnameRes) Compare(r engine.Res) bool {
|
|
||||||
// we can only compare HostnameRes to others of the same resource kind
|
// we can only compare HostnameRes to others of the same resource kind
|
||||||
res, ok := r.(*HostnameRes)
|
res, ok := r.(*HostnameRes)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.PrettyHostname != res.PrettyHostname {
|
if obj.PrettyHostname != res.PrettyHostname {
|
||||||
return false
|
return fmt.Errorf("the PrettyHostname differs")
|
||||||
}
|
}
|
||||||
if obj.StaticHostname != res.StaticHostname {
|
if obj.StaticHostname != res.StaticHostname {
|
||||||
return false
|
return fmt.Errorf("the StaticHostname differs")
|
||||||
}
|
}
|
||||||
if obj.TransientHostname != res.TransientHostname {
|
if obj.TransientHostname != res.TransientHostname {
|
||||||
return false
|
return fmt.Errorf("the TransientHostname differs")
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostnameUID is the UID struct for HostnameRes.
|
// HostnameUID is the UID struct for HostnameRes.
|
||||||
@@ -256,8 +248,8 @@ type HostnameUID struct {
|
|||||||
transientHostname string
|
transientHostname string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one, although some resources can return multiple.
|
// resources only return one, although some resources can return multiple.
|
||||||
func (obj *HostnameRes) UIDs() []engine.ResUID {
|
func (obj *HostnameRes) UIDs() []engine.ResUID {
|
||||||
x := &HostnameUID{
|
x := &HostnameUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -269,8 +261,8 @@ func (obj *HostnameRes) UIDs() []engine.ResUID {
|
|||||||
return []engine.ResUID{x}
|
return []engine.ResUID{x}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *HostnameRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *HostnameRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes HostnameRes // indirection to avoid infinite recursion
|
type rawRes HostnameRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
808
engine/resources/http.go
Normal file
808
engine/resources/http.go
Normal file
@@ -0,0 +1,808 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
|
||||||
|
securefilepath "github.com/cyphar/filepath-securejoin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("http:server", func() engine.Res { return &HTTPServerRes{} })
|
||||||
|
engine.RegisterResource("http:file", func() engine.Res { return &HTTPFileRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HTTPUseSecureJoin specifies that we should add in a "secure join" lib
|
||||||
|
// so that we avoid the ../../etc/passwd and symlink problems.
|
||||||
|
HTTPUseSecureJoin = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPServerRes is an http server resource. It serves files, but does not
|
||||||
|
// actually apply any state. The name is used as the address to listen on,
|
||||||
|
// unless the Address field is specified, and in that case it is used instead.
|
||||||
|
// This resource can offer up files for serving that are specified either inline
|
||||||
|
// in this resource by specifying an http root, or as http:file resources which
|
||||||
|
// will get autogrouped into this resource at runtime. The two methods can be
|
||||||
|
// combined as well.
|
||||||
|
//
|
||||||
|
// This server also supports autogrouping some more magical resources into it.
|
||||||
|
// For example, the http:flag and http:ui resources add in magic endpoints.
|
||||||
|
//
|
||||||
|
// This server is not meant as a featureful replacement for the venerable and
|
||||||
|
// modern httpd servers out there, but rather as a simple, dynamic, integrated
|
||||||
|
// alternative for bootstrapping new machines and clusters in an elegant way.
|
||||||
|
//
|
||||||
|
// TODO: add support for TLS
|
||||||
|
// XXX: Add an http:flag resource that lets an http client set a flag somewhere!
|
||||||
|
// XXX: Add a http:ui resource that functions can read data from!
|
||||||
|
// XXX: The http:ui resource can also take in values from those functions!
|
||||||
|
type HTTPServerRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
traits.Edgeable // XXX: add autoedge support
|
||||||
|
traits.Groupable // can have HTTPFileRes grouped into it
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
// Address is the listen address to use for the http server. It is
|
||||||
|
// common to use `:80` (the standard) to listen on TCP port 80 on all
|
||||||
|
// addresses.
|
||||||
|
Address string `lang:"address" yaml:"address"`
|
||||||
|
|
||||||
|
// Timeout is the maximum duration in seconds to use for unspecified
|
||||||
|
// timeouts. In other words, when this value is specified, it is used as
|
||||||
|
// the value for the other *Timeout values when they aren't used. Put
|
||||||
|
// another way, this makes it easy to set all the different timeouts
|
||||||
|
// with a single parameter.
|
||||||
|
Timeout *uint64 `lang:"timeout" yaml:"timeout"`
|
||||||
|
|
||||||
|
// ReadTimeout is the maximum duration in seconds for reading during the
|
||||||
|
// http request. If it is zero, then there is no timeout. If this is
|
||||||
|
// unspecified, then the value of Timeout is used instead if it is set.
|
||||||
|
// For more information, see the golang net/http Server documentation.
|
||||||
|
ReadTimeout *uint64 `lang:"read_timeout" yaml:"read_timeout"`
|
||||||
|
|
||||||
|
// WriteTimeout is the maximum duration in seconds for writing during
|
||||||
|
// the http request. If it is zero, then there is no timeout. If this is
|
||||||
|
// unspecified, then the value of Timeout is used instead if it is set.
|
||||||
|
// For more information, see the golang net/http Server documentation.
|
||||||
|
WriteTimeout *uint64 `lang:"write_timeout" yaml:"write_timeout"`
|
||||||
|
|
||||||
|
// ShutdownTimeout is the maximum duration in seconds to wait for the
|
||||||
|
// server to shutdown gracefully before calling Close. By default it is
|
||||||
|
// nice to let client connections terminate gracefully, however it might
|
||||||
|
// take longer than we are willing to wait, particularly if one is long
|
||||||
|
// polling or running a very long download. As a result, you can set a
|
||||||
|
// timeout here. The default is zero which means it will wait
|
||||||
|
// indefinitely. The shutdown process can also be cancelled by the
|
||||||
|
// interrupt handler which this resource supports. If this is
|
||||||
|
// unspecified, then the value of Timeout is used instead if it is set.
|
||||||
|
ShutdownTimeout *uint64 `lang:"shutdown_timeout" yaml:"shutdown_timeout"`
|
||||||
|
|
||||||
|
// Root is the root directory that we should serve files from. If it is
|
||||||
|
// not specified, then it is not used. Any http file resources will have
|
||||||
|
// precedence over anything in here, in case the same path exists twice.
|
||||||
|
// TODO: should we have a flag to determine the precedence rules here?
|
||||||
|
Root string `lang:"root" yaml:"root"`
|
||||||
|
|
||||||
|
// TODO: should we allow adding a list of one-of files directly here?
|
||||||
|
|
||||||
|
interruptChan chan struct{}
|
||||||
|
|
||||||
|
conn net.Listener
|
||||||
|
serveMux *http.ServeMux // can't share the global one between resources!
|
||||||
|
server *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *HTTPServerRes) Default() engine.Res {
|
||||||
|
return &HTTPServerRes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAddress returns the actual address to use. When Address is not specified,
|
||||||
|
// we use the Name.
|
||||||
|
func (obj *HTTPServerRes) getAddress() string {
|
||||||
|
if obj.Address != "" {
|
||||||
|
return obj.Address
|
||||||
|
}
|
||||||
|
return obj.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getReadTimeout determines the value for ReadTimeout, because if unspecified,
|
||||||
|
// this will default to the value of Timeout.
|
||||||
|
func (obj *HTTPServerRes) getReadTimeout() *uint64 {
|
||||||
|
if obj.ReadTimeout != nil {
|
||||||
|
return obj.ReadTimeout
|
||||||
|
}
|
||||||
|
return obj.Timeout // might be nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWriteTimeout determines the value for WriteTimeout, because if
|
||||||
|
// unspecified, this will default to the value of Timeout.
|
||||||
|
func (obj *HTTPServerRes) getWriteTimeout() *uint64 {
|
||||||
|
if obj.WriteTimeout != nil {
|
||||||
|
return obj.WriteTimeout
|
||||||
|
}
|
||||||
|
return obj.Timeout // might be nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getShutdownTimeout determines the value for ShutdownTimeout, because if
|
||||||
|
// unspecified, this will default to the value of Timeout.
|
||||||
|
func (obj *HTTPServerRes) getShutdownTimeout() *uint64 {
|
||||||
|
if obj.ShutdownTimeout != nil {
|
||||||
|
return obj.ShutdownTimeout
|
||||||
|
}
|
||||||
|
return obj.Timeout // might be nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
|
func (obj *HTTPServerRes) Validate() error {
|
||||||
|
if obj.getAddress() == "" {
|
||||||
|
return fmt.Errorf("empty address")
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(obj.getAddress())
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "the Address is in an invalid format: %s", obj.getAddress())
|
||||||
|
}
|
||||||
|
if host != "" {
|
||||||
|
// TODO: should we allow fqdn's here?
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
|
return fmt.Errorf("the Address is not a valid IP: %s", host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Root != "" && !strings.HasPrefix(obj.Root, "/") {
|
||||||
|
return fmt.Errorf("the Root must be absolute")
|
||||||
|
}
|
||||||
|
if obj.Root != "" && !strings.HasSuffix(obj.Root, "/") {
|
||||||
|
return fmt.Errorf("the Root must be a dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: validate that the autogrouped resources don't have paths that
|
||||||
|
// conflict with each other. We can only have a single unique entry for
|
||||||
|
// what handles a /whatever URL.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *HTTPServerRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
// No need to error in Validate if Timeout is ignored, but log it.
|
||||||
|
// These are all specified, so Timeout effectively does nothing.
|
||||||
|
a := obj.ReadTimeout != nil
|
||||||
|
b := obj.WriteTimeout != nil
|
||||||
|
c := obj.ShutdownTimeout != nil
|
||||||
|
if obj.Timeout != nil && (a && b && c) {
|
||||||
|
obj.init.Logf("the Timeout param is being ignored")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: If we don't Init anything that's autogrouped, then it won't
|
||||||
|
// even get an Init call on it.
|
||||||
|
// TODO: should we do this in the engine? Do we want to decide it here?
|
||||||
|
for _, res := range obj.GetGroup() { // grouped elements
|
||||||
|
if err := res.Init(init); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "autogrouped Init failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.interruptChan = make(chan struct{})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *HTTPServerRes) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
|
func (obj *HTTPServerRes) Watch() error {
|
||||||
|
// TODO: I think we could replace all this with:
|
||||||
|
//obj.conn, err := net.Listen("tcp", obj.getAddress())
|
||||||
|
// ...but what is the advantage?
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", obj.getAddress())
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not resolve address")
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.conn, err = net.ListenTCP("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not start listener")
|
||||||
|
}
|
||||||
|
defer obj.conn.Close()
|
||||||
|
|
||||||
|
obj.serveMux = http.NewServeMux() // do it here in case Watch restarts!
|
||||||
|
obj.serveMux.HandleFunc("/", obj.handler())
|
||||||
|
|
||||||
|
readTimeout := uint64(0)
|
||||||
|
if i := obj.getReadTimeout(); i != nil {
|
||||||
|
readTimeout = *i
|
||||||
|
}
|
||||||
|
writeTimeout := uint64(0)
|
||||||
|
if i := obj.getWriteTimeout(); i != nil {
|
||||||
|
writeTimeout = *i
|
||||||
|
}
|
||||||
|
obj.server = &http.Server{
|
||||||
|
Addr: obj.getAddress(),
|
||||||
|
Handler: obj.serveMux,
|
||||||
|
ReadTimeout: time.Duration(readTimeout) * time.Second,
|
||||||
|
WriteTimeout: time.Duration(writeTimeout) * time.Second,
|
||||||
|
//MaxHeaderBytes: 1 << 20, XXX: should we add a param for this?
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.init.Running() // when started, notify engine that we're running
|
||||||
|
|
||||||
|
var closeError error
|
||||||
|
closeSignal := make(chan struct{})
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
shutdownChan := make(chan struct{}) // server shutdown finished signal
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
select {
|
||||||
|
case <-obj.interruptChan:
|
||||||
|
// TODO: should we bubble up the error from Close?
|
||||||
|
// TODO: do we need a mutex around this Close?
|
||||||
|
obj.server.Close() // kill it quickly!
|
||||||
|
case <-shutdownChan:
|
||||||
|
// let this exit
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer close(closeSignal)
|
||||||
|
|
||||||
|
err := obj.server.Serve(obj.conn) // blocks until Shutdown() is called!
|
||||||
|
if err == nil || err == http.ErrServerClosed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if this returned on its own, then closeSignal can be used...
|
||||||
|
closeError = errwrap.Wrapf(err, "the server errored")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS
|
||||||
|
// immediately return ErrServerClosed. Make sure the program doesn't
|
||||||
|
// exit and waits instead for Shutdown to return.
|
||||||
|
defer func() {
|
||||||
|
defer close(shutdownChan) // signal that shutdown is finished
|
||||||
|
ctx := context.Background()
|
||||||
|
if i := obj.getShutdownTimeout(); i != nil && *i > 0 {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, time.Duration(*i)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
err := obj.server.Shutdown(ctx) // shutdown gracefully
|
||||||
|
if err == context.DeadlineExceeded {
|
||||||
|
// TODO: should we bubble up the error from Close?
|
||||||
|
// TODO: do we need a mutex around this Close?
|
||||||
|
obj.server.Close() // kill it now
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
startupChan := make(chan struct{})
|
||||||
|
close(startupChan) // send one initial signal
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Looping...")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-startupChan:
|
||||||
|
startupChan = nil
|
||||||
|
send = true
|
||||||
|
|
||||||
|
case <-closeSignal: // something shut us down early
|
||||||
|
return closeError
|
||||||
|
|
||||||
|
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
obj.init.Event() // notify engine of an event (this can block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply never has anything to do for this resource, so it always succeeds.
|
||||||
|
// It does however check that certain runtime requirements (such as the Root dir
|
||||||
|
// existing if one was specified) are fulfilled.
|
||||||
|
func (obj *HTTPServerRes) CheckApply(apply bool) (bool, error) {
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("CheckApply")
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: We don't want the initial CheckApply to return true until the
|
||||||
|
// Watch has started up, so we must block here until that's the case...
|
||||||
|
|
||||||
|
// Cheap runtime validation!
|
||||||
|
if obj.Root != "" {
|
||||||
|
fileInfo, err := os.Stat(obj.Root)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "can't stat Root dir")
|
||||||
|
}
|
||||||
|
if !fileInfo.IsDir() {
|
||||||
|
return false, fmt.Errorf("the Root path is not a dir")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil // always succeeds, with nothing to do!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *HTTPServerRes) Cmp(r engine.Res) error {
|
||||||
|
// we can only compare HTTPServerRes to others of the same resource kind
|
||||||
|
res, ok := r.(*HTTPServerRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("res is not the same kind")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Address != res.Address {
|
||||||
|
return fmt.Errorf("the Address differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.Timeout == nil) != (res.Timeout == nil) { // xor
|
||||||
|
return fmt.Errorf("the Timeout differs")
|
||||||
|
}
|
||||||
|
if obj.Timeout != nil && res.Timeout != nil {
|
||||||
|
if *obj.Timeout != *res.Timeout { // compare the values
|
||||||
|
return fmt.Errorf("the value of Timeout differs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.ReadTimeout == nil) != (res.ReadTimeout == nil) {
|
||||||
|
return fmt.Errorf("the ReadTimeout differs")
|
||||||
|
}
|
||||||
|
if obj.ReadTimeout != nil && res.ReadTimeout != nil {
|
||||||
|
if *obj.ReadTimeout != *res.ReadTimeout {
|
||||||
|
return fmt.Errorf("the value of ReadTimeout differs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.WriteTimeout == nil) != (res.WriteTimeout == nil) {
|
||||||
|
return fmt.Errorf("the WriteTimeout differs")
|
||||||
|
}
|
||||||
|
if obj.WriteTimeout != nil && res.WriteTimeout != nil {
|
||||||
|
if *obj.WriteTimeout != *res.WriteTimeout {
|
||||||
|
return fmt.Errorf("the value of WriteTimeout differs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.ShutdownTimeout == nil) != (res.ShutdownTimeout == nil) {
|
||||||
|
return fmt.Errorf("the ShutdownTimeout differs")
|
||||||
|
}
|
||||||
|
if obj.ShutdownTimeout != nil && res.ShutdownTimeout != nil {
|
||||||
|
if *obj.ShutdownTimeout != *res.ShutdownTimeout {
|
||||||
|
return fmt.Errorf("the value of ShutdownTimeout differs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We could do this sort of thing to skip checking Timeout when it
|
||||||
|
// is not used, but for the moment, this is overkill and not needed yet.
|
||||||
|
//a := obj.ReadTimeout != nil
|
||||||
|
//b := obj.WriteTimeout != nil
|
||||||
|
//c := obj.ShutdownTimeout != nil
|
||||||
|
//if !(obj.Timeout != nil && (a && b && c)) {
|
||||||
|
// // the Timeout param is not being ignored
|
||||||
|
//}
|
||||||
|
|
||||||
|
if obj.Root != res.Root {
|
||||||
|
return fmt.Errorf("the Root differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interrupt is called to ask the execution of this resource to end early. It
|
||||||
|
// will cause the server Shutdown to end abruptly instead of leading open client
|
||||||
|
// connections terminate gracefully. It does this by causing the server Close
|
||||||
|
// method to run.
|
||||||
|
func (obj *HTTPServerRes) Interrupt() error {
|
||||||
|
close(obj.interruptChan) // this should cause obj.server.Close() to run!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies the resource. Don't call it directly, use engine.ResCopy instead.
|
||||||
|
// TODO: should this copy internal state?
|
||||||
|
func (obj *HTTPServerRes) Copy() engine.CopyableRes {
|
||||||
|
var timeout, readTimeout, writeTimeout, shutdownTimeout *uint64
|
||||||
|
if obj.Timeout != nil {
|
||||||
|
x := *obj.Timeout
|
||||||
|
timeout = &x
|
||||||
|
}
|
||||||
|
if obj.ReadTimeout != nil {
|
||||||
|
x := *obj.ReadTimeout
|
||||||
|
readTimeout = &x
|
||||||
|
}
|
||||||
|
if obj.WriteTimeout != nil {
|
||||||
|
x := *obj.WriteTimeout
|
||||||
|
writeTimeout = &x
|
||||||
|
}
|
||||||
|
if obj.ShutdownTimeout != nil {
|
||||||
|
x := *obj.ShutdownTimeout
|
||||||
|
shutdownTimeout = &x
|
||||||
|
}
|
||||||
|
return &HTTPServerRes{
|
||||||
|
Address: obj.Address,
|
||||||
|
Timeout: timeout,
|
||||||
|
ReadTimeout: readTimeout,
|
||||||
|
WriteTimeout: writeTimeout,
|
||||||
|
ShutdownTimeout: shutdownTimeout,
|
||||||
|
Root: obj.Root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
|
// primarily useful for setting the defaults.
|
||||||
|
func (obj *HTTPServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes HTTPServerRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*HTTPServerRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to HTTPServerRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = HTTPServerRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupCmp returns whether two resources can be grouped together or not. Can
|
||||||
|
// these two resources be merged, aka, does this resource support doing so? Will
|
||||||
|
// resource allow itself to be grouped _into_ this obj?
|
||||||
|
func (obj *HTTPServerRes) GroupCmp(r engine.GroupableRes) error {
|
||||||
|
res1, ok1 := r.(*HTTPFileRes) // different from what we usually do!
|
||||||
|
if ok1 {
|
||||||
|
// If the http file resource has the Server field specified,
|
||||||
|
// then it must match against our name field if we want it to
|
||||||
|
// group with us.
|
||||||
|
if res1.Server != "" && res1.Server != obj.Name() {
|
||||||
|
return fmt.Errorf("resource groups with a different server name")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("resource is not the right kind")
|
||||||
|
}
|
||||||
|
|
||||||
|
// readHandler handles all the incoming download requests from clients.
|
||||||
|
func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) {
|
||||||
|
// TODO: we could statically pre-compute some stuff here...
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Client: %s", req.RemoteAddr)
|
||||||
|
}
|
||||||
|
// TODO: would this leak anything security sensitive in our log?
|
||||||
|
obj.init.Logf("URL: %s", req.URL)
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Path: %s", req.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only allow GET at the moment.
|
||||||
|
if req.Method != http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||||
|
|
||||||
|
//var handle io.Reader // TODO: simplify?
|
||||||
|
var handle io.ReadSeeker
|
||||||
|
|
||||||
|
// Look through the autogrouped resources!
|
||||||
|
// TODO: can we improve performance by only searching here once?
|
||||||
|
for _, x := range obj.GetGroup() { // grouped elements
|
||||||
|
res, ok := x.(*HTTPFileRes) // convert from Res
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if requestPath != res.getPath() {
|
||||||
|
continue // not me
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Got grouped file: %s", res.String())
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
handle, err = res.getContent()
|
||||||
|
if err != nil {
|
||||||
|
obj.init.Logf("could not get content for: %s", requestPath)
|
||||||
|
msg, httpStatus := toHTTPError(err)
|
||||||
|
http.Error(w, msg, httpStatus)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look in root if we have one, and we haven't got a file yet...
|
||||||
|
if obj.Root != "" && handle == nil {
|
||||||
|
|
||||||
|
p := filepath.Join(obj.Root, requestPath) // normal unsafe!
|
||||||
|
if !strings.HasPrefix(p, obj.Root) { // root ends with /
|
||||||
|
// user might have tried a ../../etc/passwd hack
|
||||||
|
obj.init.Logf("join inconsistency: %s", p)
|
||||||
|
http.NotFound(w, req) // lie to them...
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if HTTPUseSecureJoin {
|
||||||
|
var err error
|
||||||
|
p, err = securefilepath.SecureJoin(obj.Root, requestPath)
|
||||||
|
if err != nil {
|
||||||
|
obj.init.Logf("secure join fail: %s", p)
|
||||||
|
http.NotFound(w, req) // lie to them...
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Got file at root: %s", p)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
handle, err = os.Open(p)
|
||||||
|
if err != nil {
|
||||||
|
obj.init.Logf("could not open: %s", p)
|
||||||
|
msg, httpStatus := toHTTPError(err)
|
||||||
|
http.Error(w, msg, httpStatus)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We never found a file...
|
||||||
|
if handle == nil {
|
||||||
|
if obj.init.Debug || true { // XXX: maybe we should always do this?
|
||||||
|
obj.init.Logf("File not found: %s", requestPath)
|
||||||
|
}
|
||||||
|
http.NotFound(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the last-modified time if we can.
|
||||||
|
modtime := time.Now()
|
||||||
|
if f, ok := handle.(*os.File); ok {
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err == nil {
|
||||||
|
modtime = fi.ModTime()
|
||||||
|
}
|
||||||
|
// TODO: if Stat errors, should we fail the whole thing?
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: is requestPath what we want for the name field?
|
||||||
|
http.ServeContent(w, req, requestPath, modtime, handle)
|
||||||
|
//obj.init.Logf("%d bytes sent", n) // XXX: how do we know (on the server-side) if it worked?
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPFileRes is a file that exists within an http server. The name is used as
|
||||||
|
// the public path of the file, unless the filename field is specified, and in
|
||||||
|
// that case it is used instead. The way this works is that it autogroups at
|
||||||
|
// runtime with an existing http resource, and in doing so makes the file
|
||||||
|
// associated with this resource available for serving from that http server.
|
||||||
|
type HTTPFileRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
traits.Edgeable // XXX: add autoedge support
|
||||||
|
traits.Groupable // can be grouped into HTTPServerRes
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
// Server is the name of the http server resource to group this into. If
|
||||||
|
// it is omitted, and there is only a single http resource, then it will
|
||||||
|
// be grouped into it automatically. If there is more than one main http
|
||||||
|
// resource being used, then the grouping behaviour is *undefined* when
|
||||||
|
// this is not specified, and it is not recommended to leave this blank!
|
||||||
|
Server string `lang:"server" yaml:"server"`
|
||||||
|
|
||||||
|
// Filename is the name of the file this data should appear as on the
|
||||||
|
// http server.
|
||||||
|
Filename string `lang:"filename" yaml:"filename"`
|
||||||
|
|
||||||
|
// Path is the absolute path to a file that should be used as the source
|
||||||
|
// for this file resource. It must not be combined with the data field.
|
||||||
|
Path string `lang:"path" yaml:"path"`
|
||||||
|
|
||||||
|
// Data is the file content that should be used as the source for this
|
||||||
|
// file resource. It must not be combined with the path field.
|
||||||
|
// TODO: should this be []byte instead?
|
||||||
|
Data string `lang:"data" yaml:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *HTTPFileRes) Default() engine.Res {
|
||||||
|
return &HTTPFileRes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPath returns the actual path we respond to. When Filename is not
|
||||||
|
// specified, we use the Name. Note that this is the filename that will be seen
|
||||||
|
// on the http server, it is *not* the source path to the actual file contents
|
||||||
|
// being sent by the server.
|
||||||
|
func (obj *HTTPFileRes) getPath() string {
|
||||||
|
if obj.Filename != "" {
|
||||||
|
return obj.Filename
|
||||||
|
}
|
||||||
|
return obj.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContent returns the content that we expect from this resource. It depends
|
||||||
|
// on whether the user specified the Path or Data fields, and whether the Path
|
||||||
|
// exists or not.
|
||||||
|
func (obj *HTTPFileRes) getContent() (io.ReadSeeker, error) {
|
||||||
|
if obj.Path != "" && obj.Data != "" {
|
||||||
|
// programming error! this should have been caught in Validate!
|
||||||
|
return nil, fmt.Errorf("must not specify Path and Data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Path != "" {
|
||||||
|
return os.Open(obj.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewReader([]byte(obj.Data)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
|
func (obj *HTTPFileRes) Validate() error {
|
||||||
|
if obj.getPath() == "" {
|
||||||
|
return fmt.Errorf("empty filename")
|
||||||
|
}
|
||||||
|
// FIXME: does getPath need to start with a slash?
|
||||||
|
|
||||||
|
if obj.Path != "" && !strings.HasPrefix(obj.Path, "/") {
|
||||||
|
return fmt.Errorf("the Path must be absolute")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Path != "" && obj.Data != "" {
|
||||||
|
return fmt.Errorf("must not specify Path and Data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: if obj.Path == "" && obj.Data == "" then we have an empty file!
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *HTTPFileRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *HTTPFileRes) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the primary listener for this resource and it outputs events. This
|
||||||
|
// particular one does absolutely nothing but block until we've received a done
|
||||||
|
// signal.
|
||||||
|
func (obj *HTTPFileRes) Watch() error {
|
||||||
|
obj.init.Running() // when started, notify engine that we're running
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
//obj.init.Event() // notify engine of an event (this can block)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply never has anything to do for this resource, so it always succeeds.
|
||||||
|
func (obj *HTTPFileRes) CheckApply(apply bool) (bool, error) {
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("CheckApply")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil // always succeeds, with nothing to do!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *HTTPFileRes) Cmp(r engine.Res) error {
|
||||||
|
// we can only compare HTTPFileRes to others of the same resource kind
|
||||||
|
res, ok := r.(*HTTPFileRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("res is not the same kind")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Server != res.Server {
|
||||||
|
return fmt.Errorf("the Server field differs")
|
||||||
|
}
|
||||||
|
if obj.Filename != res.Filename {
|
||||||
|
return fmt.Errorf("the Filename differs")
|
||||||
|
}
|
||||||
|
if obj.Path != res.Path {
|
||||||
|
return fmt.Errorf("the Path differs")
|
||||||
|
}
|
||||||
|
if obj.Data != res.Data {
|
||||||
|
return fmt.Errorf("the Data differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
|
// primarily useful for setting the defaults.
|
||||||
|
func (obj *HTTPFileRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes HTTPFileRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*HTTPFileRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to HTTPFileRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = HTTPFileRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toHTTPError returns a non-specific HTTP error message and status code for a
|
||||||
|
// given non-nil error value. It's important that toHTTPError does not actually
|
||||||
|
// return err.Error(), since msg and httpStatus are returned to users, and
|
||||||
|
// historically Go's ServeContent always returned just "404 Not Found" for all
|
||||||
|
// errors. We don't want to start leaking information in error messages.
|
||||||
|
// NOTE: This was copied and modified slightly from the golang net/http package.
|
||||||
|
// See: https://github.com/golang/go/issues/38375
|
||||||
|
func toHTTPError(err error) (msg string, httpStatus int) {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
//return "404 page not found", http.StatusNotFound
|
||||||
|
return http.StatusText(http.StatusNotFound), http.StatusNotFound
|
||||||
|
}
|
||||||
|
if os.IsPermission(err) {
|
||||||
|
//return "403 Forbidden", http.StatusForbidden
|
||||||
|
return http.StatusText(http.StatusForbidden), http.StatusForbidden
|
||||||
|
}
|
||||||
|
// Default:
|
||||||
|
//return "500 Internal Server Error", http.StatusInternalServerError
|
||||||
|
return http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -34,10 +34,12 @@ func init() {
|
|||||||
engine.RegisterResource("kv", func() engine.Res { return &KVRes{} })
|
engine.RegisterResource("kv", func() engine.Res { return &KVRes{} })
|
||||||
}
|
}
|
||||||
|
|
||||||
// KVResSkipCmpStyle represents the different styles of comparison when using SkipLessThan.
|
// KVResSkipCmpStyle represents the different styles of comparison when using
|
||||||
|
// SkipLessThan.
|
||||||
type KVResSkipCmpStyle int
|
type KVResSkipCmpStyle int
|
||||||
|
|
||||||
// These are the different allowed comparison styles. Most folks will want SkipCmpStyleInt.
|
// These are the different allowed comparison styles. Most folks will want
|
||||||
|
// SkipCmpStyleInt.
|
||||||
const (
|
const (
|
||||||
SkipCmpStyleInt KVResSkipCmpStyle = iota
|
SkipCmpStyleInt KVResSkipCmpStyle = iota
|
||||||
SkipCmpStyleString
|
SkipCmpStyleString
|
||||||
@@ -308,8 +310,8 @@ type KVUID struct {
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one, although some resources can return multiple.
|
// resources only return one, although some resources can return multiple.
|
||||||
func (obj *KVRes) UIDs() []engine.ResUID {
|
func (obj *KVRes) UIDs() []engine.ResUID {
|
||||||
x := &KVUID{
|
x := &KVUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -318,8 +320,8 @@ func (obj *KVRes) UIDs() []engine.ResUID {
|
|||||||
return []engine.ResUID{x}
|
return []engine.ResUID{x}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *KVRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *KVRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes KVRes // indirection to avoid infinite recursion
|
type rawRes KVRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -35,11 +35,11 @@ import (
|
|||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
|
||||||
sdbus "github.com/coreos/go-systemd/dbus"
|
sdbus "github.com/coreos/go-systemd/v22/dbus"
|
||||||
"github.com/coreos/go-systemd/unit"
|
"github.com/coreos/go-systemd/v22/unit"
|
||||||
systemdUtil "github.com/coreos/go-systemd/util"
|
systemdUtil "github.com/coreos/go-systemd/v22/util"
|
||||||
fstab "github.com/deniswernert/go-fstab"
|
fstab "github.com/deniswernert/go-fstab"
|
||||||
"github.com/godbus/dbus"
|
"github.com/godbus/dbus/v5"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -403,8 +403,8 @@ func (obj *MountUID) IFF(uid engine.ResUID) bool {
|
|||||||
return obj.name == res.name
|
return obj.name == res.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one although some resources can return multiple.
|
// resources only return one although some resources can return multiple.
|
||||||
func (obj *MountRes) UIDs() []engine.ResUID {
|
func (obj *MountRes) UIDs() []engine.ResUID {
|
||||||
x := &MountUID{
|
x := &MountUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -413,8 +413,8 @@ func (obj *MountRes) UIDs() []engine.ResUID {
|
|||||||
return []engine.ResUID{x}
|
return []engine.ResUID{x}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *MountRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *MountRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes MountRes // indirection to avoid infinite recursion
|
type rawRes MountRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
@@ -499,8 +499,8 @@ func (obj *MountRes) fstabEntryRemove(file string, mount *fstab.Mount) error {
|
|||||||
return obj.fstabWrite(file, mounts)
|
return obj.fstabWrite(file, mounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fstabWrite generates an fstab file with the given mounts, and writes them
|
// fstabWrite generates an fstab file with the given mounts, and writes them to
|
||||||
// to the provided fstab file.
|
// the provided fstab file.
|
||||||
func (obj *MountRes) fstabWrite(file string, mounts fstab.Mounts) error {
|
func (obj *MountRes) fstabWrite(file string, mounts fstab.Mounts) error {
|
||||||
// build the file contents
|
// build the file contents
|
||||||
contents := fmt.Sprintf("# Generated by %s at %d", obj.init.Program, time.Now().UnixNano()) + "\n"
|
contents := fmt.Sprintf("# Generated by %s at %d", obj.init.Program, time.Now().UnixNano()) + "\n"
|
||||||
@@ -541,9 +541,9 @@ func mountExists(file string, mount *fstab.Mount) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mountCompare compares two mounts. It is assumed that the first comes from
|
// mountCompare compares two mounts. It is assumed that the first comes from a
|
||||||
// a resource definition, and the second comes from /proc/mounts. It compares
|
// resource definition, and the second comes from /proc/mounts. It compares the
|
||||||
// the two after resolving the loopback device's file path (if necessary,) and
|
// two after resolving the loopback device's file path (if necessary,) and
|
||||||
// ignores freq and passno, as they may differ between the definition and
|
// ignores freq and passno, as they may differ between the definition and
|
||||||
// /proc/mounts.
|
// /proc/mounts.
|
||||||
func mountCompare(def, proc *fstab.Mount) (bool, error) {
|
func mountCompare(def, proc *fstab.Mount) (bool, error) {
|
||||||
@@ -599,8 +599,8 @@ func mountReload() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// restartUnit restarts the given dbus unit and waits for it to finish
|
// restartUnit restarts the given dbus unit and waits for it to finish starting
|
||||||
// starting up. If restartTimeout is exceeded, it will return an error.
|
// up. If restartTimeout is exceeded, it will return an error.
|
||||||
func restartUnit(conn *dbus.Conn, unit string) error {
|
func restartUnit(conn *dbus.Conn, unit string) error {
|
||||||
// timeout if we don't get the JobRemoved event
|
// timeout if we don't get the JobRemoved event
|
||||||
ctx, cancel := context.WithTimeout(context.TODO(), dbusRestartCtxTimeout*time.Second)
|
ctx, cancel := context.WithTimeout(context.TODO(), dbusRestartCtxTimeout*time.Second)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root !darwin
|
//go:build !root || !darwin
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
"github.com/purpleidea/mgmt/engine"
|
"github.com/purpleidea/mgmt/engine"
|
||||||
"github.com/purpleidea/mgmt/engine/traits"
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
|
||||||
"github.com/coreos/go-systemd/journal"
|
"github.com/coreos/go-systemd/v22/journal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -200,36 +200,28 @@ func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
|
|||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *MsgRes) Cmp(r engine.Res) error {
|
func (obj *MsgRes) Cmp(r engine.Res) error {
|
||||||
if !obj.Compare(r) {
|
|
||||||
return fmt.Errorf("did not compare")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare two resources and return if they are equivalent.
|
|
||||||
func (obj *MsgRes) Compare(r engine.Res) bool {
|
|
||||||
// we can only compare MsgRes to others of the same resource kind
|
// we can only compare MsgRes to others of the same resource kind
|
||||||
res, ok := r.(*MsgRes)
|
res, ok := r.(*MsgRes)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.Body != res.Body {
|
if obj.Body != res.Body {
|
||||||
return false
|
return fmt.Errorf("the Body differs")
|
||||||
}
|
}
|
||||||
if obj.Priority != res.Priority {
|
if obj.Priority != res.Priority {
|
||||||
return false
|
return fmt.Errorf("the Priority differs")
|
||||||
}
|
}
|
||||||
if len(obj.Fields) != len(res.Fields) {
|
if len(obj.Fields) != len(res.Fields) {
|
||||||
return false
|
return fmt.Errorf("the length of Fields differs")
|
||||||
}
|
}
|
||||||
for field, value := range obj.Fields {
|
for field, value := range obj.Fields {
|
||||||
if res.Fields[field] != value {
|
if res.Fields[field] != value {
|
||||||
return false
|
return fmt.Errorf("the Fields differ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MsgUID is a unique representation for a MsgRes object.
|
// MsgUID is a unique representation for a MsgRes object.
|
||||||
@@ -239,8 +231,8 @@ type MsgUID struct {
|
|||||||
body string
|
body string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one, although some resources can return multiple.
|
// resources only return one, although some resources can return multiple.
|
||||||
func (obj *MsgRes) UIDs() []engine.ResUID {
|
func (obj *MsgRes) UIDs() []engine.ResUID {
|
||||||
x := &MsgUID{
|
x := &MsgUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -249,8 +241,8 @@ func (obj *MsgRes) UIDs() []engine.ResUID {
|
|||||||
return []engine.ResUID{x}
|
return []engine.ResUID{x}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *MsgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *MsgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes MsgRes // indirection to avoid infinite recursion
|
type rawRes MsgRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !darwin
|
//go:build !darwin
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
@@ -81,17 +81,32 @@ const (
|
|||||||
socketFile = "pipe.sock" // path in vardir to store our socket file
|
socketFile = "pipe.sock" // path in vardir to store our socket file
|
||||||
)
|
)
|
||||||
|
|
||||||
// NetRes is a network interface resource based on netlink. It manages the
|
// NetRes is a network interface resource based on netlink. It manages the state
|
||||||
// state of a network link. Configuration is also stored in a networkd
|
// of a network link. Configuration is also stored in a networkd configuration
|
||||||
// configuration file, so the network is available upon reboot.
|
// file, so the network is available upon reboot. The name of the resource is
|
||||||
|
// the string representing the network interface name. This could be "eth0" for
|
||||||
|
// example.
|
||||||
type NetRes struct {
|
type NetRes struct {
|
||||||
traits.Base // add the base methods without re-implementation
|
traits.Base // add the base methods without re-implementation
|
||||||
|
|
||||||
init *engine.Init
|
init *engine.Init
|
||||||
|
|
||||||
State string `yaml:"state"` // up, down, or empty
|
// State is the desired state of the interface. It can be "up", "down",
|
||||||
Addrs []string `yaml:"addrs"` // list of addresses in cidr format
|
// or the empty string to leave that unspecified.
|
||||||
Gateway string `yaml:"gateway"` // gateway address
|
State string `lang:"state" yaml:"state"`
|
||||||
|
|
||||||
|
// Addrs is the list of addresses to set on the interface. They must
|
||||||
|
// each be in CIDR notation such as: 192.0.2.42/24 for example.
|
||||||
|
Addrs []string `lang:"addrs" yaml:"addrs"`
|
||||||
|
|
||||||
|
// Gateway represents the default route to set for the interface.
|
||||||
|
Gateway string `lang:"gateway" yaml:"gateway"`
|
||||||
|
|
||||||
|
// IPForward is a boolean that sets whether we should forward incoming
|
||||||
|
// packets onward when this is set. It default to unspecified, which
|
||||||
|
// downstream (in the systemd-networkd configuration) defaults to false.
|
||||||
|
// XXX: this could also be "ipv4" or "ipv6", add those as a second option?
|
||||||
|
IPForward *bool `lang:"ip_forward" yaml:"ip_forward"`
|
||||||
|
|
||||||
iface *iface // a struct containing the net.Interface and netlink.Link
|
iface *iface // a struct containing the net.Interface and netlink.Link
|
||||||
unitFilePath string // the interface unit file path
|
unitFilePath string // the interface unit file path
|
||||||
@@ -99,8 +114,8 @@ type NetRes struct {
|
|||||||
socketFile string // path for storing the pipe socket file
|
socketFile string // path for storing the pipe socket file
|
||||||
}
|
}
|
||||||
|
|
||||||
// nlChanStruct defines the channel used to send netlink messages and errors
|
// nlChanStruct defines the channel used to send netlink messages and errors to
|
||||||
// to the event processing loop in Watch.
|
// the event processing loop in Watch.
|
||||||
type nlChanStruct struct {
|
type nlChanStruct struct {
|
||||||
msg []syscall.NetlinkMessage
|
msg []syscall.NetlinkMessage
|
||||||
err error
|
err error
|
||||||
@@ -371,8 +386,8 @@ func (obj *NetRes) addrCheckApply(apply bool) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// gatewayCheckApply checks if the interface has the correct default gateway
|
// gatewayCheckApply checks if the interface has the correct default gateway and
|
||||||
// and adds/deletes routes as necessary.
|
// adds/deletes routes as necessary.
|
||||||
func (obj *NetRes) gatewayCheckApply(apply bool) (bool, error) {
|
func (obj *NetRes) gatewayCheckApply(apply bool) (bool, error) {
|
||||||
// get all routes from the interface
|
// get all routes from the interface
|
||||||
routes, err := netlink.RouteList(obj.iface.link, netlink.FAMILY_V4)
|
routes, err := netlink.RouteList(obj.iface.link, netlink.FAMILY_V4)
|
||||||
@@ -506,34 +521,26 @@ func (obj *NetRes) CheckApply(apply bool) (bool, error) {
|
|||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *NetRes) Cmp(r engine.Res) error {
|
func (obj *NetRes) Cmp(r engine.Res) error {
|
||||||
if !obj.Compare(r) {
|
|
||||||
return fmt.Errorf("did not compare")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare two resources and return if they are equivalent.
|
|
||||||
func (obj *NetRes) Compare(r engine.Res) bool {
|
|
||||||
// we can only compare NetRes to others of the same resource kind
|
// we can only compare NetRes to others of the same resource kind
|
||||||
res, ok := r.(*NetRes)
|
res, ok := r.(*NetRes)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.State != res.State {
|
if obj.State != res.State {
|
||||||
return false
|
return fmt.Errorf("the State differs")
|
||||||
}
|
}
|
||||||
if (obj.Addrs == nil) != (res.Addrs == nil) {
|
if (obj.Addrs == nil) != (res.Addrs == nil) {
|
||||||
return false
|
return fmt.Errorf("the Addrs differ")
|
||||||
}
|
}
|
||||||
if err := util.SortedStrSliceCompare(obj.Addrs, res.Addrs); err != nil {
|
if err := util.SortedStrSliceCompare(obj.Addrs, res.Addrs); err != nil {
|
||||||
return false
|
return fmt.Errorf("the Addrs differ")
|
||||||
}
|
}
|
||||||
if obj.Gateway != res.Gateway {
|
if obj.Gateway != res.Gateway {
|
||||||
return false
|
return fmt.Errorf("the Gateway differs")
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetUID is a unique resource identifier.
|
// NetUID is a unique resource identifier.
|
||||||
@@ -556,8 +563,8 @@ func (obj *NetUID) IFF(uid engine.ResUID) bool {
|
|||||||
return obj.name == res.name
|
return obj.name == res.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one although some resources can return multiple.
|
// resources only return one although some resources can return multiple.
|
||||||
func (obj *NetRes) UIDs() []engine.ResUID {
|
func (obj *NetRes) UIDs() []engine.ResUID {
|
||||||
x := &NetUID{
|
x := &NetUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -566,8 +573,8 @@ func (obj *NetRes) UIDs() []engine.ResUID {
|
|||||||
return []engine.ResUID{x}
|
return []engine.ResUID{x}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *NetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *NetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes NetRes // indirection to avoid infinite recursion
|
type rawRes NetRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
@@ -598,6 +605,13 @@ func (obj *NetRes) unitFileContents() []byte {
|
|||||||
if obj.Gateway != "" {
|
if obj.Gateway != "" {
|
||||||
u = append(u, fmt.Sprintf("Gateway=%s", obj.Gateway))
|
u = append(u, fmt.Sprintf("Gateway=%s", obj.Gateway))
|
||||||
}
|
}
|
||||||
|
if obj.IPForward != nil {
|
||||||
|
b := "false"
|
||||||
|
if *obj.IPForward {
|
||||||
|
b = "true"
|
||||||
|
}
|
||||||
|
u = append(u, fmt.Sprintf("IPForward=%s", b))
|
||||||
|
}
|
||||||
c := strings.Join(u, "\n")
|
c := strings.Join(u, "\n")
|
||||||
return []byte(c)
|
return []byte(c)
|
||||||
}
|
}
|
||||||
@@ -633,8 +647,8 @@ func (obj *iface) linkUpDown(state string) error {
|
|||||||
return netlink.LinkSetDown(obj.link)
|
return netlink.LinkSetDown(obj.link)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getAddrs returns a list of strings containing all of the interface's
|
// getAddrs returns a list of strings containing all of the interface's IP
|
||||||
// IP addresses in CIDR format.
|
// addresses in CIDR format.
|
||||||
func (obj *iface) getAddrs() ([]string, error) {
|
func (obj *iface) getAddrs() ([]string, error) {
|
||||||
var ifaceAddrs []string
|
var ifaceAddrs []string
|
||||||
a, err := obj.iface.Addrs()
|
a, err := obj.iface.Addrs()
|
||||||
@@ -702,8 +716,8 @@ func (obj *iface) kernelApply(addrs []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addrApplyDelete, checks the interface's addresses and deletes any that are not
|
// addrApplyDelete, checks the interface's addresses and deletes any that are
|
||||||
// in the list/definition.
|
// not in the list/definition.
|
||||||
func (obj *iface) addrApplyDelete(objAddrs []string) error {
|
func (obj *iface) addrApplyDelete(objAddrs []string) error {
|
||||||
ifaceAddrs, err := obj.getAddrs()
|
ifaceAddrs, err := obj.getAddrs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !darwin
|
//go:build !darwin
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -103,8 +103,8 @@ type NoopUID struct {
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one, although some resources can return multiple.
|
// resources only return one, although some resources can return multiple.
|
||||||
func (obj *NoopRes) UIDs() []engine.ResUID {
|
func (obj *NoopRes) UIDs() []engine.ResUID {
|
||||||
x := &NoopUID{
|
x := &NoopUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -126,8 +126,8 @@ func (obj *NoopRes) GroupCmp(r engine.GroupableRes) error {
|
|||||||
return nil // noop resources can always be grouped together!
|
return nil // noop resources can always be grouped together!
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *NoopRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *NoopRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes NoopRes // indirection to avoid infinite recursion
|
type rawRes NoopRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// +build !root
|
//go:build !root
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -30,10 +30,10 @@ import (
|
|||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
|
||||||
systemdDbus "github.com/coreos/go-systemd/dbus"
|
systemdDbus "github.com/coreos/go-systemd/v22/dbus"
|
||||||
machined "github.com/coreos/go-systemd/machine1"
|
machined "github.com/coreos/go-systemd/v22/machine1"
|
||||||
systemdUtil "github.com/coreos/go-systemd/util"
|
systemdUtil "github.com/coreos/go-systemd/v22/util"
|
||||||
"github.com/godbus/dbus"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -73,8 +73,8 @@ func (obj *NspawnRes) Default() engine.Res {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeComposite creates a pointer to a SvcRes. The pointer is used to
|
// makeComposite creates a pointer to a SvcRes. The pointer is used to validate
|
||||||
// validate and initialize the nested svc.
|
// and initialize the nested svc.
|
||||||
func (obj *NspawnRes) makeComposite() (*SvcRes, error) {
|
func (obj *NspawnRes) makeComposite() (*SvcRes, error) {
|
||||||
res, err := engine.NewNamedResource("svc", fmt.Sprintf(nspawnServiceTmpl, obj.Name()))
|
res, err := engine.NewNamedResource("svc", fmt.Sprintf(nspawnServiceTmpl, obj.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,7 +113,7 @@ func (obj *NspawnRes) Validate() error {
|
|||||||
return errwrap.Wrapf(err, "makeComposite failed in validate")
|
return errwrap.Wrapf(err, "makeComposite failed in validate")
|
||||||
}
|
}
|
||||||
if err := svc.Validate(); err != nil { // composite resource
|
if err := svc.Validate(); err != nil { // composite resource
|
||||||
return errwrap.Wrapf(err, "validate failed for embedded svc: %s", obj.svc)
|
return errwrap.Wrapf(err, "validate failed for embedded svc: %s", svc)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -128,10 +128,7 @@ func (obj *NspawnRes) Init(init *engine.Init) error {
|
|||||||
}
|
}
|
||||||
obj.svc = svc
|
obj.svc = svc
|
||||||
// TODO: we could build a new init that adds a prefix to the logger...
|
// TODO: we could build a new init that adds a prefix to the logger...
|
||||||
if err := obj.svc.Init(init); err != nil {
|
return obj.svc.Init(init)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close is run by the engine to clean up after the resource is done.
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
@@ -261,35 +258,27 @@ func (obj *NspawnRes) CheckApply(apply bool) (bool, error) {
|
|||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *NspawnRes) Cmp(r engine.Res) error {
|
func (obj *NspawnRes) Cmp(r engine.Res) error {
|
||||||
if !obj.Compare(r) {
|
|
||||||
return fmt.Errorf("did not compare")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare two resources and return if they are equivalent.
|
|
||||||
func (obj *NspawnRes) Compare(r engine.Res) bool {
|
|
||||||
// we can only compare NspawnRes to others of the same resource kind
|
// we can only compare NspawnRes to others of the same resource kind
|
||||||
res, ok := r.(*NspawnRes)
|
res, ok := r.(*NspawnRes)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.State != res.State {
|
if obj.State != res.State {
|
||||||
return false
|
return fmt.Errorf("the State differs")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: why is res.svc ever nil?
|
// TODO: why is res.svc ever nil?
|
||||||
if (obj.svc == nil) != (res.svc == nil) { // xor
|
if (obj.svc == nil) != (res.svc == nil) { // xor
|
||||||
return false
|
return fmt.Errorf("the svc differs")
|
||||||
}
|
}
|
||||||
if obj.svc != nil && res.svc != nil {
|
if obj.svc != nil && res.svc != nil {
|
||||||
if !obj.svc.Compare(res.svc) {
|
if err := obj.svc.Cmp(res.svc); err != nil {
|
||||||
return false
|
return errwrap.Wrapf(err, "the svc differs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NspawnUID is a unique resource identifier.
|
// NspawnUID is a unique resource identifier.
|
||||||
@@ -312,8 +301,8 @@ func (obj *NspawnUID) IFF(uid engine.ResUID) bool {
|
|||||||
return obj.name == res.name
|
return obj.name == res.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one although some resources can return multiple.
|
// resources only return one although some resources can return multiple.
|
||||||
func (obj *NspawnRes) UIDs() []engine.ResUID {
|
func (obj *NspawnRes) UIDs() []engine.ResUID {
|
||||||
x := &NspawnUID{
|
x := &NspawnUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -322,8 +311,8 @@ func (obj *NspawnRes) UIDs() []engine.ResUID {
|
|||||||
return append([]engine.ResUID{x}, obj.svc.UIDs()...)
|
return append([]engine.ResUID{x}, obj.svc.UIDs()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *NspawnRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *NspawnRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes NspawnRes // indirection to avoid infinite recursion
|
type rawRes NspawnRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
|
||||||
"github.com/godbus/dbus"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// global tweaks of verbosity and code path
|
// global tweaks of verbosity and code path
|
||||||
@@ -37,7 +37,8 @@ const (
|
|||||||
Paranoid = false // enable if you see any ghosts
|
Paranoid = false // enable if you see any ghosts
|
||||||
)
|
)
|
||||||
|
|
||||||
// constants which might need to be tweaked or which contain special dbus strings.
|
// constants which might need to be tweaked or which contain special dbus
|
||||||
|
// strings.
|
||||||
const (
|
const (
|
||||||
// FIXME: if PkBufferSize is too low, install seems to drop signals
|
// FIXME: if PkBufferSize is too low, install seems to drop signals
|
||||||
PkBufferSize = 1000
|
PkBufferSize = 1000
|
||||||
@@ -154,7 +155,8 @@ type Conn struct {
|
|||||||
Logf func(format string, v ...interface{})
|
Logf func(format string, v ...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in the map values.
|
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in
|
||||||
|
// the map values.
|
||||||
type PkPackageIDActionData struct {
|
type PkPackageIDActionData struct {
|
||||||
Found bool
|
Found bool
|
||||||
Installed bool
|
Installed bool
|
||||||
@@ -185,7 +187,8 @@ func (obj *Conn) Close() error {
|
|||||||
return obj.conn.Close()
|
return obj.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal helper to add signal matches to the bus, should only be called once
|
// matchSignal is an internal helper to add signal matches to the bus. It should
|
||||||
|
// only be called once.
|
||||||
func (obj *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) (func() error, error) {
|
func (obj *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) (func() error, error) {
|
||||||
if obj.Debug {
|
if obj.Debug {
|
||||||
obj.Logf("matchSignal(%v, %v, %s, %v)", ch, path, iface, signals)
|
obj.Logf("matchSignal(%v, %v, %s, %v)", ch, path, iface, signals)
|
||||||
@@ -352,7 +355,7 @@ loop:
|
|||||||
// should already be broken
|
// should already be broken
|
||||||
break loop
|
break loop
|
||||||
} else {
|
} else {
|
||||||
return []string{}, fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
return []string{}, fmt.Errorf("error in body: %v", signal.Body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,9 +366,9 @@ loop:
|
|||||||
func (obj *Conn) IsInstalledList(packages []string) ([]bool, error) {
|
func (obj *Conn) IsInstalledList(packages []string) ([]bool, error) {
|
||||||
var filter uint64 // initializes at the "zero" value of 0
|
var filter uint64 // initializes at the "zero" value of 0
|
||||||
filter += PkFilterEnumArch // always search in our arch
|
filter += PkFilterEnumArch // always search in our arch
|
||||||
packageIDs, e := obj.ResolvePackages(packages, filter)
|
packageIDs, err := obj.ResolvePackages(packages, filter)
|
||||||
if e != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ResolvePackages error: %v", e)
|
return nil, errwrap.Wrapf(err, "error resolving packages")
|
||||||
}
|
}
|
||||||
|
|
||||||
var m = make(map[string]int)
|
var m = make(map[string]int)
|
||||||
@@ -443,7 +446,7 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
||||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
return fmt.Errorf("error in body: %v", signal.Body)
|
||||||
} else if signal.Name == FmtTransactionMethod("Package") {
|
} else if signal.Name == FmtTransactionMethod("Package") {
|
||||||
// a package was installed...
|
// a package was installed...
|
||||||
// only start the timer once we're here...
|
// only start the timer once we're here...
|
||||||
@@ -454,14 +457,14 @@ loop:
|
|||||||
} else if signal.Name == FmtTransactionMethod("Destroy") {
|
} else if signal.Name == FmtTransactionMethod("Destroy") {
|
||||||
return nil // success
|
return nil // success
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
return fmt.Errorf("error in body: %v", signal.Body)
|
||||||
}
|
}
|
||||||
case <-util.TimeAfterOrBlock(timeout):
|
case <-util.TimeAfterOrBlock(timeout):
|
||||||
if finished {
|
if finished {
|
||||||
obj.Logf("Timeout: InstallPackages: Waiting for 'Destroy'")
|
obj.Logf("Timeout: InstallPackages: Waiting for 'Destroy'")
|
||||||
return nil // got tired of waiting for Destroy
|
return nil // got tired of waiting for Destroy
|
||||||
}
|
}
|
||||||
return fmt.Errorf("PackageKit: Timeout: InstallPackages: %s", strings.Join(packageIDs, ", "))
|
return fmt.Errorf("timeout installing packages: %s", strings.Join(packageIDs, ", "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,7 +503,7 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
||||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
return fmt.Errorf("error in body: %v", signal.Body)
|
||||||
} else if signal.Name == FmtTransactionMethod("Package") {
|
} else if signal.Name == FmtTransactionMethod("Package") {
|
||||||
// a package was installed...
|
// a package was installed...
|
||||||
continue loop
|
continue loop
|
||||||
@@ -511,7 +514,7 @@ loop:
|
|||||||
// should already be broken
|
// should already be broken
|
||||||
break loop
|
break loop
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
return fmt.Errorf("error in body: %v", signal.Body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,7 +552,7 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
||||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
return fmt.Errorf("error in body: %v", signal.Body)
|
||||||
} else if signal.Name == FmtTransactionMethod("Package") {
|
} else if signal.Name == FmtTransactionMethod("Package") {
|
||||||
} else if signal.Name == FmtTransactionMethod("Finished") {
|
} else if signal.Name == FmtTransactionMethod("Finished") {
|
||||||
// TODO: should we wait for the Destroy signal?
|
// TODO: should we wait for the Destroy signal?
|
||||||
@@ -558,14 +561,15 @@ loop:
|
|||||||
// should already be broken
|
// should already be broken
|
||||||
break loop
|
break loop
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
return fmt.Errorf("error in body: %v", signal.Body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFilesByPackageID gets the list of files that are contained inside a list of packageIDs.
|
// GetFilesByPackageID gets the list of files that are contained inside a list
|
||||||
|
// of packageIDs.
|
||||||
func (obj *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) {
|
func (obj *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) {
|
||||||
// NOTE: the maximum number of files in an RPM is 52116 in Fedora 23
|
// NOTE: the maximum number of files in an RPM is 52116 in Fedora 23
|
||||||
// https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a
|
// https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a
|
||||||
@@ -601,7 +605,7 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
||||||
err = fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
err = fmt.Errorf("error in body: %v", signal.Body)
|
||||||
return
|
return
|
||||||
|
|
||||||
// one signal returned per packageID found...
|
// one signal returned per packageID found...
|
||||||
@@ -626,7 +630,7 @@ loop:
|
|||||||
// should already be broken
|
// should already be broken
|
||||||
break loop
|
break loop
|
||||||
} else {
|
} else {
|
||||||
err = fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
err = fmt.Errorf("error in body: %v", signal.Body)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,7 +638,8 @@ loop:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUpdates gets a list of packages that are installed and which can be updated, mod filter.
|
// GetUpdates gets a list of packages that are installed and which can be
|
||||||
|
// updated, mod filter.
|
||||||
func (obj *Conn) GetUpdates(filter uint64) ([]string, error) {
|
func (obj *Conn) GetUpdates(filter uint64) ([]string, error) {
|
||||||
if obj.Debug {
|
if obj.Debug {
|
||||||
obj.Logf("GetUpdates()")
|
obj.Logf("GetUpdates()")
|
||||||
@@ -669,7 +674,7 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
||||||
return nil, fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
return nil, fmt.Errorf("error in body: %v", signal.Body)
|
||||||
} else if signal.Name == FmtTransactionMethod("Package") {
|
} else if signal.Name == FmtTransactionMethod("Package") {
|
||||||
|
|
||||||
//pkg_int, ok := signal.Body[0].(int)
|
//pkg_int, ok := signal.Body[0].(int)
|
||||||
@@ -692,7 +697,7 @@ loop:
|
|||||||
// should already be broken
|
// should already be broken
|
||||||
break loop
|
break loop
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
return nil, fmt.Errorf("error in body: %v", signal.Body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -718,9 +723,9 @@ func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
if obj.Debug {
|
if obj.Debug {
|
||||||
obj.Logf("PackagesToPackageIDs(): %s", strings.Join(packages, ", "))
|
obj.Logf("PackagesToPackageIDs(): %s", strings.Join(packages, ", "))
|
||||||
}
|
}
|
||||||
resolved, e := obj.ResolvePackages(packages, filter)
|
resolved, err := obj.ResolvePackages(packages, filter)
|
||||||
if e != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Resolve error: %v", e)
|
return nil, errwrap.Wrapf(err, "error resolving")
|
||||||
}
|
}
|
||||||
|
|
||||||
found := make([]bool, count) // default false
|
found := make([]bool, count) // default false
|
||||||
@@ -758,7 +763,7 @@ func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
}
|
}
|
||||||
state := packageMap[pkg] // lookup the requested state/version
|
state := packageMap[pkg] // lookup the requested state/version
|
||||||
if state == "" {
|
if state == "" {
|
||||||
return nil, fmt.Errorf("Empty package state for %v", pkg)
|
return nil, fmt.Errorf("empty package state for: `%s`", pkg)
|
||||||
}
|
}
|
||||||
found[index] = true
|
found[index] = true
|
||||||
stateIsVersion := (state != "installed" && state != "uninstalled" && state != "newest") // must be a ver. string
|
stateIsVersion := (state != "installed" && state != "uninstalled" && state != "newest") // must be a ver. string
|
||||||
@@ -794,9 +799,9 @@ func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
// to be done, and if so, anything that needs updating isn't newest!
|
// to be done, and if so, anything that needs updating isn't newest!
|
||||||
// if something isn't installed, we can't verify it with this method
|
// if something isn't installed, we can't verify it with this method
|
||||||
// FIXME: https://github.com/hughsie/PackageKit/issues/116
|
// FIXME: https://github.com/hughsie/PackageKit/issues/116
|
||||||
updates, e := obj.GetUpdates(filter)
|
updates, err := obj.GetUpdates(filter)
|
||||||
if e != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Updates error: %v", e)
|
return nil, errwrap.Wrapf(err, "updates error")
|
||||||
}
|
}
|
||||||
for _, packageID := range updates {
|
for _, packageID := range updates {
|
||||||
//obj.Logf("* %v", packageID)
|
//obj.Logf("* %v", packageID)
|
||||||
@@ -844,9 +849,9 @@ func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
if obj.Debug {
|
if obj.Debug {
|
||||||
obj.Logf("PackagesToPackageIDs(): Recurse: %s", strings.Join(checkPackages, ", "))
|
obj.Logf("PackagesToPackageIDs(): Recurse: %s", strings.Join(checkPackages, ", "))
|
||||||
}
|
}
|
||||||
recursion, e = obj.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest)
|
recursion, err = obj.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest)
|
||||||
if e != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Recursion error: %v", e)
|
return nil, errwrap.Wrapf(err, "recursion error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -876,7 +881,8 @@ func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterPackageIDs returns a list of packageIDs which match the set of package names in packages.
|
// FilterPackageIDs returns a list of packageIDs which match the set of package
|
||||||
|
// names in packages.
|
||||||
func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([]string, error) {
|
func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([]string, error) {
|
||||||
result := []string{}
|
result := []string{}
|
||||||
for _, k := range packages {
|
for _, k := range packages {
|
||||||
@@ -890,7 +896,8 @@ func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterState returns a map of whether each package queried matches the particular state.
|
// FilterState returns a map of whether each package queried matches the
|
||||||
|
// particular state.
|
||||||
func FilterState(m map[string]*PkPackageIDActionData, packages []string, state string) (result map[string]bool, err error) {
|
func FilterState(m map[string]*PkPackageIDActionData, packages []string, state string) (result map[string]bool, err error) {
|
||||||
result = make(map[string]bool)
|
result = make(map[string]bool)
|
||||||
pkgs := []string{} // bad pkgs that don't have a bool state
|
pkgs := []string{} // bad pkgs that don't have a bool state
|
||||||
@@ -920,7 +927,8 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterPackageState returns all packages that are in package and match the specific state.
|
// FilterPackageState returns all packages that are in package and match the
|
||||||
|
// specific state.
|
||||||
func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string, state string) (result []string, err error) {
|
func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string, state string) (result []string, err error) {
|
||||||
result = []string{}
|
result = []string{}
|
||||||
for _, k := range packages {
|
for _, k := range packages {
|
||||||
@@ -946,7 +954,8 @@ func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string,
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlagInData asks whether a flag exists inside the data portion of a packageID field?
|
// FlagInData asks whether a flag exists inside the data portion of a packageID
|
||||||
|
// field?
|
||||||
func FlagInData(flag, data string) bool {
|
func FlagInData(flag, data string) bool {
|
||||||
flags := strings.Split(data, ":")
|
flags := strings.Split(data, ":")
|
||||||
for _, f := range flags {
|
for _, f := range flags {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2019+ James Shubin and the project contributors
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -295,33 +295,25 @@ func (obj *PasswordRes) CheckApply(apply bool) (bool, error) {
|
|||||||
|
|
||||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
func (obj *PasswordRes) Cmp(r engine.Res) error {
|
func (obj *PasswordRes) Cmp(r engine.Res) error {
|
||||||
if !obj.Compare(r) {
|
|
||||||
return fmt.Errorf("did not compare")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare two resources and return if they are equivalent.
|
|
||||||
func (obj *PasswordRes) Compare(r engine.Res) bool {
|
|
||||||
// we can only compare PasswordRes to others of the same resource kind
|
// we can only compare PasswordRes to others of the same resource kind
|
||||||
res, ok := r.(*PasswordRes)
|
res, ok := r.(*PasswordRes)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.Length != res.Length {
|
if obj.Length != res.Length {
|
||||||
return false
|
return fmt.Errorf("the Length differs")
|
||||||
}
|
}
|
||||||
// TODO: we *could* optimize by allowing CheckApply to move from
|
// TODO: we *could* optimize by allowing CheckApply to move from
|
||||||
// saved->!saved, by removing the file, but not likely worth it!
|
// saved->!saved, by removing the file, but not likely worth it!
|
||||||
if obj.Saved != res.Saved {
|
if obj.Saved != res.Saved {
|
||||||
return false
|
return fmt.Errorf("the Saved differs")
|
||||||
}
|
}
|
||||||
if obj.CheckRecovery != res.CheckRecovery {
|
if obj.CheckRecovery != res.CheckRecovery {
|
||||||
return false
|
return fmt.Errorf("the CheckRecovery differs")
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PasswordUID is the UID struct for PasswordRes.
|
// PasswordUID is the UID struct for PasswordRes.
|
||||||
@@ -330,8 +322,8 @@ type PasswordUID struct {
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDs includes all params to make a unique identification of this object.
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
// Most resources only return one, although some resources can return multiple.
|
// resources only return one, although some resources can return multiple.
|
||||||
func (obj *PasswordRes) UIDs() []engine.ResUID {
|
func (obj *PasswordRes) UIDs() []engine.ResUID {
|
||||||
x := &PasswordUID{
|
x := &PasswordUID{
|
||||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
@@ -355,8 +347,8 @@ func (obj *PasswordRes) Sends() interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
// It is primarily useful for setting the defaults.
|
// primarily useful for setting the defaults.
|
||||||
func (obj *PasswordRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (obj *PasswordRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
type rawRes PasswordRes // indirection to avoid infinite recursion
|
type rawRes PasswordRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
|||||||
329
engine/resources/pippet.go
Normal file
329
engine/resources/pippet.go
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pippetReceiverInstance *pippetReceiver
|
||||||
|
var pippetReceiverOnce sync.Once
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("pippet", func() engine.Res { return &PippetRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
// PippetRes is a wrapper resource for puppet. It implements the functional
|
||||||
|
// equivalent of an exec resource that calls "puppet resource <type> <title>
|
||||||
|
// <params>", but offers superior performance through a long-running Puppet
|
||||||
|
// process that receives resources through a pipe (hence the name).
|
||||||
|
type PippetRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
traits.Refreshable
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
// Type is the exact name of the wrapped Puppet resource type, e.g.
|
||||||
|
// "file", "mount". This needs not be a core type. It can be a type
|
||||||
|
// from a module. The Puppet installation local to the mgmt agent
|
||||||
|
// machine must be able recognize it. It has to be a native type though,
|
||||||
|
// as opposed to defined types from your Puppet manifest code.
|
||||||
|
Type string `yaml:"type" json:"type"`
|
||||||
|
// Title is used by Puppet as the resource title. Puppet will often
|
||||||
|
// assign special meaning to the title, e.g. use it as the path for a
|
||||||
|
// file resource, or the name of a package.
|
||||||
|
Title string `yaml:"title" json:"title"`
|
||||||
|
// Params is expected to be a hash in YAML format, pairing resource
|
||||||
|
// parameter names with their respective values, e.g. { ensure: present
|
||||||
|
// }
|
||||||
|
Params string `yaml:"params" json:"params"`
|
||||||
|
|
||||||
|
runner *pippetReceiver
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns an example Pippet resource.
|
||||||
|
func (obj *PippetRes) Default() engine.Res {
|
||||||
|
return &PippetRes{
|
||||||
|
Params: "{}", // use an empty params hash per default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate never errors out. We don't know the set of potential types that
|
||||||
|
// Puppet supports. Resource names are arbitrary. We cannot really validate the
|
||||||
|
// parameter YAML, because we cannot assume that it can be unmarshalled into a
|
||||||
|
// map[string]string; Puppet supports complex parameter values.
|
||||||
|
func (obj *PippetRes) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init makes sure that the PippetReceiver object is initialized.
|
||||||
|
func (obj *PippetRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
obj.runner = getPippetReceiverInstance()
|
||||||
|
return obj.runner.Register()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *PippetRes) Close() error {
|
||||||
|
return obj.runner.Unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
|
func (obj *PippetRes) Watch() error {
|
||||||
|
obj.init.Running() // when started, notify engine that we're running
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
//obj.init.Event() // notify engine of an event (this can block)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply synchronizes the resource if required.
|
||||||
|
func (obj *PippetRes) CheckApply(apply bool) (bool, error) {
|
||||||
|
changed, err := applyPippetRes(obj.runner, obj)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("pippet: %s[%s]: ERROR - %v", obj.Type, obj.Title, err)
|
||||||
|
}
|
||||||
|
return !changed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *PippetRes) Cmp(r engine.Res) error {
|
||||||
|
res, ok := r.(*PippetRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Type != res.Type {
|
||||||
|
return fmt.Errorf("the Type param differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Title != res.Title {
|
||||||
|
return fmt.Errorf("the Title param differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: This is a lie. Parameter lists can be equivalent but not
|
||||||
|
// lexically identical (e.g. whitespace differences, parameter order).
|
||||||
|
// This is difficult to handle because we cannot casually unmarshall the
|
||||||
|
// YAML content.
|
||||||
|
if obj.Params != res.Params {
|
||||||
|
return fmt.Errorf("the Param param differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PippetUID is the UID struct for PippetRes.
|
||||||
|
type PippetUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
resourceType string
|
||||||
|
resourceTitle string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object. Most
|
||||||
|
// resources only return one, although some resources can return multiple.
|
||||||
|
func (obj *PippetRes) UIDs() []engine.ResUID {
|
||||||
|
x := &PippetUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
resourceType: obj.Type,
|
||||||
|
resourceTitle: obj.Title,
|
||||||
|
}
|
||||||
|
return []engine.ResUID{x}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||||
|
// primarily useful for setting the defaults.
|
||||||
|
func (obj *PippetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes PippetRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*PippetRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to PippetRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = PippetRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PippetRunner is the interface used to communicate with the PippetReceiver
|
||||||
|
// object. Its main purpose is dependency injection.
|
||||||
|
type PippetRunner interface {
|
||||||
|
LockApply()
|
||||||
|
UnlockApply()
|
||||||
|
InputStream() io.WriteCloser
|
||||||
|
OutputStream() io.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
// PippetResult is the structured return value type for the PippetReceiver's
|
||||||
|
// Apply function.
|
||||||
|
type PippetResult struct {
|
||||||
|
Error bool
|
||||||
|
Failed bool
|
||||||
|
Changed bool
|
||||||
|
Exception string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPippetReceiverInstance returns a pointer to the PippetReceiver object. The
|
||||||
|
// PippetReceiver is supposed to be a singleton object. The pippet resource code
|
||||||
|
// should always use the PippetReceiverInstance function to gain access to the
|
||||||
|
// pippetReceiver object. Other objects of type pippetReceiver should not be
|
||||||
|
// created.
|
||||||
|
func getPippetReceiverInstance() *pippetReceiver {
|
||||||
|
for pippetReceiverInstance == nil {
|
||||||
|
pippetReceiverOnce.Do(func() { pippetReceiverInstance = &pippetReceiver{} })
|
||||||
|
}
|
||||||
|
return pippetReceiverInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
type pippetReceiver struct {
|
||||||
|
stdin io.WriteCloser
|
||||||
|
stdout io.ReadCloser
|
||||||
|
registerMutex sync.Mutex
|
||||||
|
applyMutex sync.Mutex
|
||||||
|
registered int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs the Puppet process that will perform the work of synchronizing
|
||||||
|
// resources that are sent to its stdin. The process will keep running until
|
||||||
|
// Close is called. Init should not be called directly. It is implicitly called
|
||||||
|
// by the Register function.
|
||||||
|
func (obj *pippetReceiver) Init() error {
|
||||||
|
cmd := exec.Command("puppet", "yamlresource", "receive", "--color=no")
|
||||||
|
var err error
|
||||||
|
obj.stdin, err = cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obj.stdout, err = cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Append(err, obj.stdin.Close())
|
||||||
|
}
|
||||||
|
if err = cmd.Start(); err != nil {
|
||||||
|
return errwrap.Append(err, obj.stdin.Close())
|
||||||
|
}
|
||||||
|
buf := make([]byte, 80)
|
||||||
|
if _, err = obj.stdout.Read(buf); err != nil {
|
||||||
|
return errwrap.Append(err, obj.stdin.Close())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register should be called by any user (i.e., any pippet resource) before
|
||||||
|
// using the PippetRunner functions on this receiver object. Register implicitly
|
||||||
|
// takes care of calling Init if required.
|
||||||
|
func (obj *pippetReceiver) Register() error {
|
||||||
|
obj.registerMutex.Lock()
|
||||||
|
defer obj.registerMutex.Unlock()
|
||||||
|
obj.registered = obj.registered + 1
|
||||||
|
if obj.registered > 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// count was increased from 0 to 1, we need to (re-)init
|
||||||
|
var err error
|
||||||
|
if err = obj.Init(); err != nil {
|
||||||
|
obj.registered = 0
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister should be called by any object that registered itself using the
|
||||||
|
// Register function, and which no longer needs the receiver. This should
|
||||||
|
// typically happen at closing time of the pippet resource that registered
|
||||||
|
// itself. Unregister implicitly calls Close in case all registered resources
|
||||||
|
// have unregistered.
|
||||||
|
func (obj *pippetReceiver) Unregister() error {
|
||||||
|
obj.registerMutex.Lock()
|
||||||
|
defer obj.registerMutex.Unlock()
|
||||||
|
obj.registered = obj.registered - 1
|
||||||
|
if obj.registered == 0 {
|
||||||
|
return obj.Close()
|
||||||
|
}
|
||||||
|
if obj.registered < 0 {
|
||||||
|
return fmt.Errorf("pippet runner: ERROR: unregistered more resources than were registered")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockApply locks the pippetReceiver's mutex for an "Apply" transaction.
|
||||||
|
func (obj *pippetReceiver) LockApply() {
|
||||||
|
obj.applyMutex.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlockApply unlocks the pippetReceiver's mutex for an "Apply" transaction.
|
||||||
|
func (obj *pippetReceiver) UnlockApply() {
|
||||||
|
obj.applyMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputStream returns the pippetReceiver's pipe writer.
|
||||||
|
func (obj *pippetReceiver) InputStream() io.WriteCloser {
|
||||||
|
return obj.stdin
|
||||||
|
}
|
||||||
|
|
||||||
|
// OutputStream returns the pippetReceiver's pipe reader.
|
||||||
|
func (obj *pippetReceiver) OutputStream() io.ReadCloser {
|
||||||
|
return obj.stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops the backend puppet process by closing its stdin handle. It should
|
||||||
|
// not be called directly. It is implicitly called by the Unregister function if
|
||||||
|
// appropriate.
|
||||||
|
func (obj *pippetReceiver) Close() error {
|
||||||
|
return obj.stdin.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyPippetRes does the actual work of making Puppet synchronize a resource.
|
||||||
|
func applyPippetRes(runner PippetRunner, resource *PippetRes) (bool, error) {
|
||||||
|
runner.LockApply()
|
||||||
|
defer runner.UnlockApply()
|
||||||
|
if err := json.NewEncoder(runner.InputStream()).Encode(resource); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "failed to send resource to puppet")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := PippetResult{
|
||||||
|
Error: true,
|
||||||
|
Exception: "missing output fields",
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(runner.OutputStream()).Decode(&result); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "failed to read response from puppet")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Error {
|
||||||
|
return false, fmt.Errorf("puppet did not sync: %s", result.Exception)
|
||||||
|
}
|
||||||
|
if result.Failed {
|
||||||
|
return false, fmt.Errorf("puppet failed to sync")
|
||||||
|
}
|
||||||
|
return result.Changed, nil
|
||||||
|
}
|
||||||
136
engine/resources/pippet_test.go
Normal file
136
engine/resources/pippet_test.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build !root
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nullWriteCloser struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakePippetReceiver struct {
|
||||||
|
stdin nullWriteCloser
|
||||||
|
stdout *io.PipeReader
|
||||||
|
Locked bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj nullWriteCloser) Write(data []byte) (int, error) {
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj nullWriteCloser) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj *fakePippetReceiver) LockApply() {
|
||||||
|
obj.Locked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj *fakePippetReceiver) UnlockApply() {
|
||||||
|
obj.Locked = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj *fakePippetReceiver) InputStream() io.WriteCloser {
|
||||||
|
return obj.stdin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj *fakePippetReceiver) OutputStream() io.ReadCloser {
|
||||||
|
return obj.stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFakePippetReceiver(jsonTestOutput string) *fakePippetReceiver {
|
||||||
|
output, input := io.Pipe()
|
||||||
|
|
||||||
|
result := &fakePippetReceiver{
|
||||||
|
stdout: output,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// this will appear on the fake stdout
|
||||||
|
input.Write([]byte(jsonTestOutput))
|
||||||
|
}()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var pippetTestRes = &PippetRes{
|
||||||
|
Type: "notify",
|
||||||
|
Title: "testmessage",
|
||||||
|
Params: `{msg: "This is a test"}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalPuppetOutput(t *testing.T) {
|
||||||
|
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":true,"noop":false,"error":false,"exception":null}`)
|
||||||
|
changed, err := applyPippetRes(r, pippetTestRes)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("normal Puppet output led to an apply error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !changed {
|
||||||
|
t.Errorf("return values of applyPippetRes did not reflect the changed state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnchangedPuppetOutput(t *testing.T) {
|
||||||
|
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":false,"noop":false,"error":false,"exception":null}`)
|
||||||
|
changed, err := applyPippetRes(r, pippetTestRes)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("normal Puppet output led to an apply error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
t.Errorf("return values of applyPippetRes did not reflect the changed state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailingPuppetOutput(t *testing.T) {
|
||||||
|
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":false,"noop":false,"error":true,"exception":"I failed!"}`)
|
||||||
|
_, err := applyPippetRes(r, pippetTestRes)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("failing Puppet output led to an apply error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyPuppetOutput(t *testing.T) {
|
||||||
|
t.Skip("empty output will currently make the application (and the test) hang")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPartialPuppetOutput(t *testing.T) {
|
||||||
|
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":true}`)
|
||||||
|
_, err := applyPippetRes(r, pippetTestRes)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("partial Puppet output did not lead to an apply error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMalformedPuppetOutput(t *testing.T) {
|
||||||
|
r := newFakePippetReceiver(`oops something went wrong!!1!eleven`)
|
||||||
|
_, err := applyPippetRes(r, pippetTestRes)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("malformed Puppet output did not lead to an apply error")
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user