diff --git a/config.yaml b/config.yaml index a2aede1..883151f 100644 --- a/config.yaml +++ b/config.yaml @@ -7,6 +7,12 @@ status: address: ":8888" profiling: true +reconciler: + maxReconcileCount: 10 + targets: + - name: agent-aws + address: localhost:9091 + kmip: tcp: enabled: true @@ -15,4 +21,4 @@ kmip: enabled: true address: ":8080" operation: - only: [ 0x1, 0x2, 0x3 ] \ No newline at end of file + only: [ 0x1, 0x2, 0x3 ] diff --git a/go.mod b/go.mod index a2b1651..0a46787 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/google/uuid v1.6.0 github.com/lib/pq v1.12.3 github.com/moby/moby/api v1.54.2 + github.com/openkcm/orbital v0.5.1 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.42.0 @@ -28,18 +29,23 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/creasty/defaults v1.8.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davidhoo/jsonpath v1.0.4 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.5 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect @@ -49,23 +55,40 @@ require ( github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/openkcm/common-sdk v1.12.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/samber/lo v1.52.0 // indirect + github.com/samber/oops v1.21.0 // indirect + github.com/samber/slog-common v0.20.0 // indirect + github.com/samber/slog-formatter v1.2.2 // indirect + github.com/samber/slog-multi v1.7.1 // indirect github.com/shirou/gopsutil/v4 v4.26.3 // indirect github.com/sirupsen/logrus v1.9.4 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect + github.com/veqryn/slog-context v0.9.0 // indirect + github.com/veqryn/slog-context/otel v0.9.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect go.opentelemetry.io/otel v1.41.0 // indirect go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect ) diff --git a/go.sum b/go.sum index e292933..1ac0bbb 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,12 @@ github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHf github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= +github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidhoo/jsonpath v1.0.4 h1:f31VxArp5bWUV4wbuYI4KsnVpirMq57B5PnDQId4giI= +github.com/davidhoo/jsonpath v1.0.4/go.mod h1:QUzoZq6f3h3XtLXPCCgapLAQMNuwGCCMwfKiTAf8e5s= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= @@ -35,16 +39,24 @@ github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -67,8 +79,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= @@ -91,10 +103,19 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/openkcm/common-sdk v1.12.0 h1:18BMOuEDhYXO+gw/xmX+WpTUbOagYWQG58rnlDIQqUI= +github.com/openkcm/common-sdk v1.12.0/go.mod h1:jnOzx2bSNIMRbdSIZugtNBJ2nq0CCS5MlNGSzJ90Cqk= +github.com/openkcm/orbital v0.5.1 h1:fCFyGm059h2IkD8/u+GI7Ofqd54lFGj7zoujKI+7MQk= +github.com/openkcm/orbital v0.5.1/go.mod h1:PqAOg75iYDrDQrL9uBQKAKfuBlNJgeG0pex1780Wo0E= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= @@ -102,19 +123,41 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:Om github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/oops v1.21.0 h1:18atcO4oEigNFuGXqr3NZWZ6P0XOSEXyBSAMXdQRxTc= +github.com/samber/oops v1.21.0/go.mod h1:Hsm/sKPxtCfPh0w/cE3xVoRfSiE1joDRiStPAsmG9bo= +github.com/samber/slog-common v0.20.0 h1:WaLnm/aCvBJSk5nR5aXZTFBaV0B47A+AEaEOiZDeUnc= +github.com/samber/slog-common v0.20.0/go.mod h1:+Ozat1jgnnE59UAlmNX1IF3IByHsODnnwf9jUcBZ+m8= +github.com/samber/slog-formatter v1.2.2 h1:/JSzXcF0TUA1GRt/4g1AJc7h0ofyn7wx21oUjzpPh54= +github.com/samber/slog-formatter v1.2.2/go.mod h1:zBYmoFkeV2LT3tyiaAehpJ1pOI+CtQz/xjXvbedx26Q= +github.com/samber/slog-multi v1.7.1 h1:aCLXHRxgU+2v0PVlEOh7phynzM7CRo89ZgFtOwaqVEE= +github.com/samber/slog-multi v1.7.1/go.mod h1:A4KQC99deqfkCDJcL/cO3kX6McX7FffQAx/8QHink+c= github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= @@ -123,22 +166,27 @@ github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYI github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/veqryn/slog-context v0.9.0 h1:VNXHBWufRGfKiumi7cYoh7p2iElquZ4v8AnAumFOhEI= +github.com/veqryn/slog-context v0.9.0/go.mod h1:l953waOLsWW6hArZeJDGGKZYLrsOIPBeJ/QQnOA8RU0= +github.com/veqryn/slog-context/otel v0.9.0 h1:jGUEZ7dbgFv1ZmngPyOJEYxfeZHWe1YpcL5xoEaMUds= +github.com/veqryn/slog-context/otel v0.9.0/go.mod h1:eLmCq9MQ0FOEGJEKa2Sz4fiT1xdmr8Z0ZrU2WSnbRBs= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= @@ -149,17 +197,17 @@ golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/integration/setup_test.go b/integration/setup_test.go index a2bf7d3..79a8486 100644 --- a/integration/setup_test.go +++ b/integration/setup_test.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "testing" @@ -102,8 +103,8 @@ func setupPostgres() ([]func(), error) { } func runCleanups(cleanups []func()) { - for i := len(cleanups) - 1; i >= 0; i-- { - cleanups[i]() + for _, v := range slices.Backward(cleanups) { + v() } } diff --git a/internal/config/config.go b/internal/config/config.go index 8f9d6e7..3aba719 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -45,6 +45,7 @@ type RootConfig struct { KeyBindings map[string]spec.KeyBinding `yaml:"key_bindings"` Hierarchy spec.KeyHierarchy `yaml:"hierarchy"` Topology spec.Topology `yaml:"topology"` + Reconciler ReconcilerConfig `yaml:"reconciler"` } // AgentBootstrapConfig is the minimal configuration that agents load from file on startup. It contains just enough information to connect to root. @@ -59,6 +60,9 @@ func (cfg *RootConfig) Validate() error { if cfg.Name == "" { return ErrConfigNameEmpty } + if err := cfg.Reconciler.Validate(); err != nil { + return fmt.Errorf("reconciler: %w", err) + } if cfg.Role != spec.RootRole { return fmt.Errorf("%w: must be %q", ErrRoleInvalid, spec.RootRole) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index af8d7b9..088eb0e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -338,6 +338,11 @@ topology: type: "in-memory" labels: cloud: "aws" +reconciler: + maxReconcileCount: 7 + targets: + - name: agent-aws + address: localhost:9091 ` tests := []struct { @@ -366,6 +371,8 @@ topology: assert.Equal(t, spec.KeyKind("K0"), cfg.Hierarchy.KeySpecs[0].Kind) assert.Len(t, cfg.Topology.Segments, 1) assert.Equal(t, "agent-aws", cfg.Topology.Segments[0].Name) + assert.Equal(t, uint64(7), cfg.Reconciler.MaxReconcileCount) + assert.Equal(t, "localhost:9091", cfg.Reconciler.Targets[0].Address) }, }, { diff --git a/internal/config/reconciler.go b/internal/config/reconciler.go new file mode 100644 index 0000000..b334a41 --- /dev/null +++ b/internal/config/reconciler.go @@ -0,0 +1,75 @@ +package config + +import ( + "errors" + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +var ( + ErrReconcilerConfigNil = errors.New("reconciler config cannot be nil") + ErrReconcilerTargetNameEmpty = errors.New("target name cannot be empty") + ErrReconcilerTargetAddressEmpty = errors.New("target address cannot be empty") + ErrReconcilerTargetDuplicate = errors.New("duplicate target name") +) + +// ReconcilerConfig describes the process-level Orbital reconciler service. +type ReconcilerConfig struct { + Targets []ReconcilerTarget `yaml:"targets"` + MaxReconcileCount uint64 `yaml:"maxReconcileCount"` +} + +// ReconcilerTarget describes one gRPC target that can receive Orbital task requests. +type ReconcilerTarget struct { + Name string `yaml:"name"` + Address string `yaml:"address"` +} + +// LoadReconcilerConfig reads either a direct reconciler config document or a +// document with a top-level "reconciler" section. +func LoadReconcilerConfig(path string) (*ReconcilerConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read reconciler config: %w", err) + } + + var wrapped struct { + Reconciler *ReconcilerConfig `yaml:"reconciler"` + } + if err := yaml.Unmarshal(data, &wrapped); err != nil { + return nil, fmt.Errorf("parse reconciler config: %w", err) + } + + var cfg ReconcilerConfig + if wrapped.Reconciler != nil { + cfg = *wrapped.Reconciler + } else if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse reconciler config: %w", err) + } + + if err := cfg.Validate(); err != nil { + return nil, err + } + + return &cfg, nil +} + +func (c *ReconcilerConfig) Validate() error { + seen := make(map[string]struct{}, len(c.Targets)) + for _, target := range c.Targets { + if target.Name == "" { + return ErrReconcilerTargetNameEmpty + } + if target.Address == "" { + return fmt.Errorf("%w: %s", ErrReconcilerTargetAddressEmpty, target.Name) + } + if _, ok := seen[target.Name]; ok { + return fmt.Errorf("%w: %s", ErrReconcilerTargetDuplicate, target.Name) + } + seen[target.Name] = struct{}{} + } + + return nil +} diff --git a/internal/config/reconciler_test.go b/internal/config/reconciler_test.go new file mode 100644 index 0000000..ad43c66 --- /dev/null +++ b/internal/config/reconciler_test.go @@ -0,0 +1,105 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadReconcilerConfig(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.yaml") + err := os.WriteFile(path, []byte(` +reconciler: + maxReconcileCount: 5 + targets: + - name: agent-aws + address: localhost:9091 +`), 0o600) + require.NoError(t, err) + + cfg, err := LoadReconcilerConfig(path) + require.NoError(t, err) + + assert.Equal(t, uint64(5), cfg.MaxReconcileCount) + require.Len(t, cfg.Targets, 1) + assert.Equal(t, ReconcilerTarget{Name: "agent-aws", Address: "localhost:9091"}, cfg.Targets[0]) +} + +func TestRootConfigIncludesReconcilerConfig(t *testing.T) { + cfg := validRootConfig() + cfg.Reconciler = ReconcilerConfig{ + MaxReconcileCount: 12, + Targets: []ReconcilerTarget{ + {Name: "agent-aws", Address: "localhost:9091"}, + }, + } + + require.NoError(t, cfg.Validate()) + + assert.Equal(t, uint64(12), cfg.Reconciler.MaxReconcileCount) + assert.Equal(t, []ReconcilerTarget{{Name: "agent-aws", Address: "localhost:9091"}}, cfg.Reconciler.Targets) +} + +func TestRootConfigLeavesEmptyReconcilerConfigAlone(t *testing.T) { + cfg := validRootConfig() + + require.NoError(t, cfg.Validate()) + + assert.Equal(t, ReconcilerConfig{}, cfg.Reconciler) +} + +func TestReconcilerConfigValidate(t *testing.T) { + tests := []struct { + name string + modify func(*ReconcilerConfig) + wantErr error + }{ + { + name: "valid default config", + modify: func(*ReconcilerConfig) {}, + }, + { + name: "duplicate target", + modify: func(c *ReconcilerConfig) { + target := validReconcilerTarget("agent-aws") + c.Targets = []ReconcilerTarget{target, target} + }, + wantErr: ErrReconcilerTargetDuplicate, + }, + { + name: "target name required", + modify: func(c *ReconcilerConfig) { + c.Targets = []ReconcilerTarget{{Address: "localhost:9091"}} + }, + wantErr: ErrReconcilerTargetNameEmpty, + }, + { + name: "target address required", + modify: func(c *ReconcilerConfig) { + c.Targets = []ReconcilerTarget{{Name: "agent-aws"}} + }, + wantErr: ErrReconcilerTargetAddressEmpty, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := ReconcilerConfig{} + tt.modify(&cfg) + + err := cfg.Validate() + if tt.wantErr == nil { + assert.NoError(t, err) + return + } + assert.ErrorIs(t, err, tt.wantErr, "got %v, want %v", err, tt.wantErr) + }) + } +} + +func validReconcilerTarget(name string) ReconcilerTarget { + return ReconcilerTarget{Name: name, Address: "localhost:9091"} +} diff --git a/internal/reconciler/handler.go b/internal/reconciler/handler.go new file mode 100644 index 0000000..b9f7cb7 --- /dev/null +++ b/internal/reconciler/handler.go @@ -0,0 +1,17 @@ +package reconciler + +import ( + "context" + + "github.com/openkcm/orbital" +) + +// JobHandler owns the Orbital lifecycle for a single Krypton job type. +type JobHandler interface { + JobType() string + ConfirmJob(ctx context.Context, job orbital.Job) (orbital.JobConfirmerResult, error) + ResolveTasks(ctx context.Context, job orbital.Job, cursor orbital.TaskResolverCursor) (orbital.TaskResolverResult, error) + OnJobDone(ctx context.Context, job orbital.Job) error + OnJobFailed(ctx context.Context, job orbital.Job) error + OnJobCanceled(ctx context.Context, job orbital.Job) error +} diff --git a/internal/reconciler/manager.go b/internal/reconciler/manager.go new file mode 100644 index 0000000..b2685fa --- /dev/null +++ b/internal/reconciler/manager.go @@ -0,0 +1,213 @@ +package reconciler + +import ( + "context" + "errors" + "fmt" + + "github.com/openkcm/orbital" + + "github.com/openkcm/krypton/internal/config" +) + +const ( + noJobHandlerRegisteredMessage = "no job handler registered for job type" + defaultMaxPendingReconciles uint64 = 10 +) + +var ( + ErrRepositoryNil = errors.New("orbital repository cannot be nil") + ErrTargetFactoryRequired = errors.New("target client factory is required when targets are configured") + ErrJobHandlerRequired = errors.New("at least one job handler is required") + ErrJobHandlerNil = errors.New("job handler cannot be nil") + ErrJobTypeEmpty = errors.New("job handler type cannot be empty") + ErrJobHandlerDuplicate = errors.New("duplicate job handler") + ErrTargetClientNil = errors.New("target client cannot be nil") + ErrJobHandlerNotFound = errors.New(noJobHandlerRegisteredMessage) +) + +type TargetProvider func(context.Context, config.ReconcilerTarget) (orbital.Initiator, error) + +// Manager owns the Orbital manager lifecycle for a Krypton process. +type Manager struct { + orbitalManager *orbital.Manager + handlers map[string]JobHandler + targets map[string]orbital.TargetManager +} + +// NewManager wires handlers, target clients, and Orbital worker config. +func NewManager( + ctx context.Context, + cfg *config.ReconcilerConfig, + repo *orbital.Repository, + targetProvider TargetProvider, + handlers []JobHandler, + options ...Option, +) (*Manager, error) { + if cfg == nil { + return nil, config.ErrReconcilerConfigNil + } + if repo == nil { + return nil, ErrRepositoryNil + } + + if err := cfg.Validate(); err != nil { + return nil, err + } + + handlerMap, err := buildHandlerMap(handlers) + if err != nil { + return nil, err + } + + targets, err := buildTargets(ctx, cfg.Targets, targetProvider) + if err != nil { + return nil, err + } + + manager := &Manager{handlers: handlerMap, targets: targets} + orbitalManager, err := orbital.NewManager( + repo, + manager.resolveTasks, + orbital.WithTargets(targets), + orbital.WithJobConfirmFunc(manager.confirmJob), + orbital.WithJobDoneEventFunc(manager.jobDone), + orbital.WithJobFailedEventFunc(manager.jobFailed), + orbital.WithJobCanceledEventFunc(manager.jobCanceled), + ) + if err != nil { + return nil, errors.Join(err, closeTargets(ctx, targets)) + } + + WithMaxPendingReconciles(maxPendingReconciles(cfg.MaxReconcileCount))(orbitalManager) + for _, option := range options { + option(orbitalManager) + } + manager.orbitalManager = orbitalManager + + return manager, nil +} + +func maxPendingReconciles(configured uint64) uint64 { + if configured == 0 { + return defaultMaxPendingReconciles + } + + return configured +} + +func (m *Manager) Start(ctx context.Context) error { + return m.orbitalManager.Start(ctx) +} + +func (m *Manager) Stop(ctx context.Context) error { + return errors.Join(m.orbitalManager.Stop(ctx), closeTargets(ctx, m.targets)) +} + +func (m *Manager) confirmJob(ctx context.Context, job orbital.Job) (orbital.JobConfirmerResult, error) { + handler, ok := m.handlers[job.Type] + if !ok { + return orbital.CancelJobConfirmer(jobHandlerNotFoundError(job.Type).Error()), nil + } + + return handler.ConfirmJob(ctx, job) +} + +func (m *Manager) resolveTasks(ctx context.Context, job orbital.Job, cursor orbital.TaskResolverCursor) (orbital.TaskResolverResult, error) { + handler, ok := m.handlers[job.Type] + if !ok { + return orbital.CancelTaskResolver(jobHandlerNotFoundError(job.Type).Error()), nil + } + + return handler.ResolveTasks(ctx, job, cursor) +} + +func (m *Manager) jobDone(ctx context.Context, job orbital.Job) error { + if handler, ok := m.handlers[job.Type]; ok { + return handler.OnJobDone(ctx, job) + } + + return jobHandlerNotFoundError(job.Type) +} + +func (m *Manager) jobFailed(ctx context.Context, job orbital.Job) error { + if handler, ok := m.handlers[job.Type]; ok { + return handler.OnJobFailed(ctx, job) + } + + return jobHandlerNotFoundError(job.Type) +} + +func (m *Manager) jobCanceled(ctx context.Context, job orbital.Job) error { + if handler, ok := m.handlers[job.Type]; ok { + return handler.OnJobCanceled(ctx, job) + } + + return jobHandlerNotFoundError(job.Type) +} + +func jobHandlerNotFoundError(jobType string) error { + return fmt.Errorf("%w: %s", ErrJobHandlerNotFound, jobType) +} + +func buildHandlerMap(handlers []JobHandler) (map[string]JobHandler, error) { + if len(handlers) == 0 { + return nil, ErrJobHandlerRequired + } + + result := make(map[string]JobHandler, len(handlers)) + for _, handler := range handlers { + if handler == nil { + return nil, ErrJobHandlerNil + } + + jobType := handler.JobType() + if jobType == "" { + return nil, ErrJobTypeEmpty + } + + if _, ok := result[jobType]; ok { + return nil, fmt.Errorf("%w: %s", ErrJobHandlerDuplicate, jobType) + } + result[jobType] = handler + } + + return result, nil +} + +func buildTargets(ctx context.Context, targetConfigs []config.ReconcilerTarget, targetProvider TargetProvider) (map[string]orbital.TargetManager, error) { + targets := make(map[string]orbital.TargetManager, len(targetConfigs)) + if len(targetConfigs) == 0 { + return targets, nil + } + if targetProvider == nil { + return nil, ErrTargetFactoryRequired + } + + for _, targetConfig := range targetConfigs { + client, err := targetProvider(ctx, targetConfig) + if err != nil { + return nil, errors.Join(fmt.Errorf("create target %s client: %w", targetConfig.Name, err), closeTargets(ctx, targets)) + } + if client == nil { + return nil, errors.Join(fmt.Errorf("%w: %s", ErrTargetClientNil, targetConfig.Name), closeTargets(ctx, targets)) + } + + targets[targetConfig.Name] = orbital.TargetManager{Client: client} + } + + return targets, nil +} + +func closeTargets(ctx context.Context, targets map[string]orbital.TargetManager) error { + var errs []error + for name, target := range targets { + if target.Client != nil { + if err := target.Client.Close(ctx); err != nil { + errs = append(errs, fmt.Errorf("target %s: %w", name, err)) + } + } + } + + return errors.Join(errs...) +} diff --git a/internal/reconciler/manager_test.go b/internal/reconciler/manager_test.go new file mode 100644 index 0000000..62466b8 --- /dev/null +++ b/internal/reconciler/manager_test.go @@ -0,0 +1,323 @@ +package reconciler + +import ( + "context" + "errors" + "go/ast" + "go/parser" + "go/token" + "testing" + "time" + + "github.com/openkcm/orbital" + "github.com/openkcm/orbital/store/query" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openkcm/krypton/internal/config" +) + +func TestNewManager(t *testing.T) { + var createdTargets []config.ReconcilerTarget + targetProvider := TargetProvider(func(_ context.Context, target config.ReconcilerTarget) (orbital.Initiator, error) { + createdTargets = append(createdTargets, target) + return &fakeInitiator{}, nil + }) + + cfg := config.ReconcilerConfig{MaxReconcileCount: 6} + cfg.Targets = []config.ReconcilerTarget{validTarget("agent-aws"), validTarget("agent-gcp")} + + manager, err := NewManager(t.Context(), &cfg, newNoopRepo(), targetProvider, []JobHandler{&fakeJobHandler{jobType: "job.type"}}) + require.NoError(t, err) + + assert.Equal(t, cfg.MaxReconcileCount, manager.orbitalManager.Config.MaxPendingReconciles) + assert.Len(t, createdTargets, 2) +} + +func TestNewManagerUsesDefaultMaxPendingReconciles(t *testing.T) { + manager, err := NewManager( + t.Context(), + new(config.ReconcilerConfig), + newNoopRepo(), + nil, + []JobHandler{&fakeJobHandler{jobType: "job.type"}}, + ) + require.NoError(t, err) + + assert.Equal(t, defaultMaxPendingReconciles, manager.orbitalManager.Config.MaxPendingReconciles) +} + +func TestNewManagerOptions(t *testing.T) { + manager, err := NewManager( + t.Context(), + &config.ReconcilerConfig{}, + newNoopRepo(), + nil, + []JobHandler{&fakeJobHandler{jobType: "job.type"}}, + WithMaxPendingReconciles(42), + WithConfirmJobAfter(3*time.Second), + WithExecInterval(250*time.Millisecond), + ) + require.NoError(t, err) + + assert.Equal(t, uint64(42), manager.orbitalManager.Config.MaxPendingReconciles) + assert.Equal(t, 3*time.Second, manager.orbitalManager.Config.ConfirmJobAfter) + assert.Equal(t, 250*time.Millisecond, manager.orbitalManager.Config.ConfirmJobWorkerConfig.ExecInterval) + assert.Equal(t, 250*time.Millisecond, manager.orbitalManager.Config.CreateTasksWorkerConfig.ExecInterval) + assert.Equal(t, 250*time.Millisecond, manager.orbitalManager.Config.ReconcileWorkerConfig.ExecInterval) + assert.Equal(t, 250*time.Millisecond, manager.orbitalManager.Config.NotifyWorkerConfig.ExecInterval) +} + +func TestNewManagerValidation(t *testing.T) { + tests := []struct { + name string + cfg *config.ReconcilerConfig + repo *orbital.Repository + targetProvider TargetProvider + handlers []JobHandler + wantErr error + }{ + { + name: "nil config", + repo: newNoopRepo(), + handlers: []JobHandler{&fakeJobHandler{jobType: "job.type"}}, + wantErr: config.ErrReconcilerConfigNil, + }, + { + name: "nil repo", + cfg: &config.ReconcilerConfig{}, + handlers: []JobHandler{&fakeJobHandler{jobType: "job.type"}}, + wantErr: ErrRepositoryNil, + }, + { + name: "target factory required", + cfg: configWithTargets(), + repo: newNoopRepo(), + handlers: []JobHandler{&fakeJobHandler{jobType: "job.type"}}, + wantErr: ErrTargetFactoryRequired, + }, + { + name: "handler required", + cfg: &config.ReconcilerConfig{}, + repo: newNoopRepo(), + wantErr: ErrJobHandlerRequired, + }, + { + name: "nil handler", + cfg: &config.ReconcilerConfig{}, + repo: newNoopRepo(), + handlers: []JobHandler{nil}, + wantErr: ErrJobHandlerNil, + }, + { + name: "empty handler type", + cfg: &config.ReconcilerConfig{}, + repo: newNoopRepo(), + handlers: []JobHandler{&fakeJobHandler{}}, + wantErr: ErrJobTypeEmpty, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewManager(t.Context(), tt.cfg, tt.repo, tt.targetProvider, tt.handlers) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestManagerRoutesJobHandler(t *testing.T) { + handler := &fakeJobHandler{jobType: "job.type"} + manager, err := NewManager(t.Context(), &config.ReconcilerConfig{}, newNoopRepo(), nil, []JobHandler{handler}) + require.NoError(t, err) + + confirmResult, err := manager.confirmJob(t.Context(), orbital.Job{Type: "job.type"}) + require.NoError(t, err) + assert.Equal(t, orbital.CompleteJobConfirmer().Type(), confirmResult.Type()) + + resolveResult, err := manager.resolveTasks(t.Context(), orbital.Job{Type: "job.type"}, "") + require.NoError(t, err) + assert.Equal(t, orbital.CompleteTaskResolver().Type(), resolveResult.Type()) + + assert.NoError(t, manager.jobDone(t.Context(), orbital.Job{Type: "job.type"})) + assert.NoError(t, manager.jobFailed(t.Context(), orbital.Job{Type: "job.type"})) + assert.NoError(t, manager.jobCanceled(t.Context(), orbital.Job{Type: "job.type"})) + + assert.True(t, handler.confirmed) + assert.True(t, handler.resolved) + assert.True(t, handler.done) + assert.True(t, handler.failed) + assert.True(t, handler.canceled) +} + +func TestManagerUnknownJobTypeCancels(t *testing.T) { + manager, err := NewManager(t.Context(), &config.ReconcilerConfig{}, newNoopRepo(), nil, []JobHandler{&fakeJobHandler{jobType: "known"}}) + require.NoError(t, err) + + confirmResult, err := manager.confirmJob(t.Context(), orbital.Job{Type: "unknown"}) + require.NoError(t, err) + assert.Equal(t, orbital.CancelJobConfirmer("missing").Type(), confirmResult.Type()) + + resolveResult, err := manager.resolveTasks(t.Context(), orbital.Job{Type: "unknown"}, "") + require.NoError(t, err) + assert.Equal(t, orbital.CancelTaskResolver("missing").Type(), resolveResult.Type()) + + assert.ErrorIs(t, manager.jobDone(t.Context(), orbital.Job{Type: "unknown"}), ErrJobHandlerNotFound) + assert.ErrorIs(t, manager.jobFailed(t.Context(), orbital.Job{Type: "unknown"}), ErrJobHandlerNotFound) + assert.ErrorIs(t, manager.jobCanceled(t.Context(), orbital.Job{Type: "unknown"}), ErrJobHandlerNotFound) + assert.Contains(t, jobHandlerNotFoundError("unknown").Error(), noJobHandlerRegisteredMessage) +} + +func TestBuildTargetsClosesCreatedClientsOnError(t *testing.T) { + first := &fakeInitiator{} + providerErr := errors.New("boom") + targetProvider := TargetProvider(func(_ context.Context, target config.ReconcilerTarget) (orbital.Initiator, error) { + if target.Name == "first" { + return first, nil + } + return nil, providerErr + }) + + targets := []config.ReconcilerTarget{validTarget("first"), validTarget("second")} + _, err := buildTargets(t.Context(), targets, targetProvider) + + assert.ErrorIs(t, err, providerErr) + assert.True(t, first.closed) +} + +func TestStopClosesTargetsWhenOrbitalWasNotStarted(t *testing.T) { + initiator := &fakeInitiator{} + targetProvider := TargetProvider(func(context.Context, config.ReconcilerTarget) (orbital.Initiator, error) { + return initiator, nil + }) + + cfg := config.ReconcilerConfig{Targets: []config.ReconcilerTarget{validTarget("agent-aws")}} + manager, err := NewManager(t.Context(), &cfg, newNoopRepo(), targetProvider, []JobHandler{&fakeJobHandler{jobType: "job.type"}}) + require.NoError(t, err) + + err = manager.Stop(t.Context()) + + assert.ErrorIs(t, err, orbital.ErrManagerNotStarted) + assert.True(t, initiator.closed) + assert.Equal(t, 1, initiator.closeCount) +} + +func TestManagerOnlyExposesStartAndStop(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "manager.go", nil, 0) + require.NoError(t, err) + + exported := map[string]struct{}{} + for _, decl := range file.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Recv == nil || !fn.Name.IsExported() { + continue + } + if len(fn.Recv.List) == 0 { + continue + } + star, ok := fn.Recv.List[0].Type.(*ast.StarExpr) + if !ok { + continue + } + ident, ok := star.X.(*ast.Ident) + if ok && ident.Name == "Manager" { + exported[fn.Name.Name] = struct{}{} + } + } + + assert.Equal(t, map[string]struct{}{"Start": {}, "Stop": {}}, exported) +} + +func configWithTargets() *config.ReconcilerConfig { + return &config.ReconcilerConfig{ + Targets: []config.ReconcilerTarget{validTarget("agent-aws")}, + } +} + +func newNoopRepo() *orbital.Repository { + return orbital.NewRepository(noopStore{}) +} + +type fakeJobHandler struct { + jobType string + confirmed bool + resolved bool + done bool + failed bool + canceled bool +} + +func (f *fakeJobHandler) JobType() string { + return f.jobType +} + +func (f *fakeJobHandler) ConfirmJob(context.Context, orbital.Job) (orbital.JobConfirmerResult, error) { + f.confirmed = true + return orbital.CompleteJobConfirmer(), nil +} + +func (f *fakeJobHandler) ResolveTasks(context.Context, orbital.Job, orbital.TaskResolverCursor) (orbital.TaskResolverResult, error) { + f.resolved = true + return orbital.CompleteTaskResolver().WithTaskInfo([]orbital.TaskInfo{{Type: "task.type", Target: "root"}}), nil +} + +func (f *fakeJobHandler) OnJobDone(context.Context, orbital.Job) error { + f.done = true + return nil +} + +func (f *fakeJobHandler) OnJobFailed(context.Context, orbital.Job) error { + f.failed = true + return nil +} + +func (f *fakeJobHandler) OnJobCanceled(context.Context, orbital.Job) error { + f.canceled = true + return nil +} + +type noopStore struct{} + +func (noopStore) Create(_ context.Context, entities ...orbital.Entity) ([]orbital.Entity, error) { + return entities, nil +} + +func (noopStore) Update(_ context.Context, entities ...orbital.Entity) ([]orbital.Entity, error) { + return entities, nil +} + +func (noopStore) Find(context.Context, query.Query) (orbital.FindResult, error) { + return orbital.FindResult{}, nil +} + +func (noopStore) List(context.Context, query.Query) (orbital.ListResult, error) { + return orbital.ListResult{}, nil +} + +func (s noopStore) Transaction(ctx context.Context, txFunc orbital.TransactionFunc) error { + return txFunc(ctx, *orbital.NewRepository(s)) +} + +type fakeInitiator struct { + closed bool + closeCount int +} + +func (*fakeInitiator) SendTaskRequest(context.Context, orbital.TaskRequest) error { + return nil +} + +func (*fakeInitiator) ReceiveTaskResponse(ctx context.Context) (orbital.TaskResponse, error) { + return orbital.TaskResponse{}, ctx.Err() +} + +func (f *fakeInitiator) Close(context.Context) error { + f.closed = true + f.closeCount++ + return nil +} + +func validTarget(name string) config.ReconcilerTarget { + return config.ReconcilerTarget{Name: name, Address: "localhost:9091"} +} diff --git a/internal/reconciler/options.go b/internal/reconciler/options.go new file mode 100644 index 0000000..f32ce79 --- /dev/null +++ b/internal/reconciler/options.go @@ -0,0 +1,30 @@ +package reconciler + +import ( + "time" + + "github.com/openkcm/orbital" +) + +type Option func(manager *orbital.Manager) + +func WithMaxPendingReconciles(n uint64) Option { + return func(m *orbital.Manager) { + m.Config.MaxPendingReconciles = n + } +} + +func WithConfirmJobAfter(d time.Duration) Option { + return func(m *orbital.Manager) { + m.Config.ConfirmJobAfter = d + } +} + +func WithExecInterval(d time.Duration) Option { + return func(m *orbital.Manager) { + m.Config.ReconcileWorkerConfig.ExecInterval = d + m.Config.CreateTasksWorkerConfig.ExecInterval = d + m.Config.ConfirmJobWorkerConfig.ExecInterval = d + m.Config.NotifyWorkerConfig.ExecInterval = d + } +}