diff --git a/config/config.development.yaml b/config/config.development.yaml index ecd8d113..40461de9 100644 --- a/config/config.development.yaml +++ b/config/config.development.yaml @@ -2,7 +2,7 @@ server: port: "8889" log: dir: "./logs" - level: "debug" + level: "warn" jwt: key1: "ebe3vxIP7sblVvUHXb7ZaiMPuz4oXo0l" key2: "ebe3vxIP7sblVvUHXb7ZaiMPuz4oXo0z" diff --git a/config/config.production.yaml b/config/config.production.yaml index 236a4230..7c4bab18 100644 --- a/config/config.production.yaml +++ b/config/config.production.yaml @@ -2,7 +2,7 @@ server: port: "8889" log: dir: "./logs" - level: "debug" + level: "warn" jwt: key1: "ebe3vxIP7sblVvUHXb7ZaiMPuz4oXo0l" key2: "ebe3vxIP7sblVvUHXb7ZaiMPuz4oXo0z" diff --git a/go.mod b/go.mod index aa439431..010d0f1a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/GoSimplicity/AI-CloudOps go 1.24.6 require ( + github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 github.com/casbin/casbin/v2 v2.93.0 github.com/fatih/color v1.16.0 github.com/gin-contrib/cors v1.7.2 @@ -169,6 +170,7 @@ require ( github.com/oklog/run v1.2.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/onsi/ginkgo v1.16.5 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect diff --git a/go.sum b/go.sum index 6cd4482c..0de748f3 100644 --- a/go.sum +++ b/go.sum @@ -55,12 +55,14 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -68,6 +70,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -145,6 +149,7 @@ github.com/coder/quartz v0.1.2/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKm github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -181,6 +186,7 @@ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 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.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -257,12 +263,14 @@ github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -434,6 +442,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -514,6 +523,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -539,6 +549,8 @@ github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrB github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/openkruise/kruise-api v1.7.0 h1:Mg13oePPZZ1XfOEXqXTFgg4wNC07CnPK6fcPePLkv/U= github.com/openkruise/kruise-api v1.7.0/go.mod h1:BXZAyzIPmaF0JEI0YT1fWEYTAcOCJRCzOdCg4BpXbXQ= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -665,6 +677,10 @@ github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= @@ -691,6 +707,7 @@ go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCRE go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= @@ -717,7 +734,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= @@ -729,6 +749,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -902,8 +923,10 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -955,6 +978,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1043,6 +1070,7 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -1068,6 +1096,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= @@ -1117,6 +1146,7 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/controller-runtime v0.16.6 h1:FiXwTuFF5ZJKmozfP2Z0j7dh6kmxP4Ou1KLfxgKKC3I= diff --git a/internal/k8s/manager/pod_manager.go b/internal/k8s/manager/pod_manager.go index fdf035c9..195bf5bb 100644 --- a/internal/k8s/manager/pod_manager.go +++ b/internal/k8s/manager/pod_manager.go @@ -210,7 +210,6 @@ func (m *podManager) DeletePod(ctx context.Context, clusterID int, namespace, na func (m *podManager) GetPodLogs(ctx context.Context, clusterID int, namespace, name string, logOptions *corev1.PodLogOptions) (io.ReadCloser, error) { kubeClient, err := m.getKubeClient(clusterID) - if err != nil { return nil, err } @@ -230,18 +229,14 @@ func (m *podManager) GetPodLogs(ctx context.Context, clusterID int, namespace, n } // BatchDeletePods 批量删除 Pod -// 使用并发+重试机制提高批量操作的效率和可靠性 -// 并发度为3是经过测试的平衡值:既能提高性能,又不会对API Server造成过大压力 func (m *podManager) BatchDeletePods(ctx context.Context, clusterID int, namespace string, podNames []string, deleteOpts metav1.DeleteOptions) error { kubeClient, err := m.getKubeClient(clusterID) - if err != nil { return err } tasks := make([]retry.WrapperTask, 0, len(podNames)) for _, name := range podNames { - tasks = append(tasks, retry.WrapperTask{ Backoff: retry.DefaultBackoff, @@ -279,7 +274,6 @@ func (m *podManager) PodTerminalSession( namespace, pod, container, shell string, conn *websocket.Conn, ) error { - kubeClient, err := m.clientFactory.GetKubeClient(clusterID) if err != nil { return err @@ -301,7 +295,6 @@ func (m *podManager) PodTerminalSession( } func (m *podManager) UploadFileToPod(ctx *gin.Context, clusterID int, namespace, pod, container, filePath string) error { - if namespace == "" { return fmt.Errorf("命名空间不能为空") } @@ -440,7 +433,6 @@ func (m *podManager) UploadFileToPod(ctx *gin.Context, clusterID int, namespace, Stderr: &uploadStderr, TerminalSizeQueue: nil, }) - if err != nil { m.logger.Error("执行上传失败", zap.Error(err), @@ -463,7 +455,6 @@ func (m *podManager) UploadFileToPod(ctx *gin.Context, clusterID int, namespace, } func (m *podManager) PortForward(ctx context.Context, ports []string, dialer httpstream.Dialer) error { - // 创建 PortForwarder stopChan := make(chan struct{}, 1) readyChan := make(chan struct{}) @@ -574,7 +565,6 @@ func (m *podManager) PodPortForward(ctx context.Context, clusterID int, namespac } func (m *podManager) DownloadPodFile(ctx context.Context, clusterID int, namespace, pod, container, filePath string) (*k8sutils.PodFileStreamPipe, error) { - if namespace == "" { return nil, fmt.Errorf("命名空间不能为空") } @@ -631,7 +621,6 @@ func (m *podManager) DownloadPodFile(ctx context.Context, clusterID int, namespa reader, err := k8sutils.NewPodFileStreamPipe( ctx, restConfig, kubeClient, namespace, pod, container, filePath) - if err != nil { m.logger.Error("创建Pod文件流失败", zap.Error(err), @@ -776,7 +765,7 @@ func writeFilesToTar(files []fileWithHeader, w io.Writer) error { hdr := &tar.Header{ Name: filename, - Mode: 0644, + Mode: 0o644, Size: fileInfo.header.Size, } @@ -796,7 +785,6 @@ func writeFilesToTar(files []fileWithHeader, w io.Writer) error { return nil }(f, i) - if err != nil { return err } diff --git a/internal/model/tree_cloud.go b/internal/model/tree_cloud.go new file mode 100644 index 00000000..bc241da7 --- /dev/null +++ b/internal/model/tree_cloud.go @@ -0,0 +1,362 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package model + +import "time" + +// 变更类型常量 +const ( + ChangeTypeCreated = "created" // 创建 + ChangeTypeUpdated = "updated" // 更新 + ChangeTypeDeleted = "deleted" // 删除 + ChangeTypeStatusChanged = "status_changed" // 状态变更 +) + +// 变更来源常量 +const ( + ChangeSourceManual = "manual" // 手动操作 + ChangeSourceSync = "sync" // 同步操作 +) + +// CloudProvider 云厂商类型 +type CloudProvider int8 + +const ( + ProviderAliyun CloudProvider = iota + 1 // 阿里云 + ProviderTencent // 腾讯云 + ProviderAWS // AWS + ProviderHuawei // 华为云 + ProviderAzure // Azure + ProviderGCP // Google Cloud +) + +// String 返回云厂商的字符串表示(用于日志和调试) +func (p CloudProvider) String() string { + switch p { + case ProviderAliyun: + return "阿里云" + case ProviderTencent: + return "腾讯云" + case ProviderAWS: + return "AWS" + case ProviderHuawei: + return "华为云" + case ProviderAzure: + return "Azure" + case ProviderGCP: + return "Google Cloud" + default: + return "未知" + } +} + +// CloudResourceType 云资源类型 +type CloudResourceType int8 + +const ( + ResourceTypeECS CloudResourceType = iota + 1 // 云服务器 + ResourceTypeRDS // 云数据库 + ResourceTypeSLB // 负载均衡 + ResourceTypeOSS // 对象存储 + ResourceTypeVPC // 虚拟私有云 + ResourceTypeOther // 其他资源 +) + +// String 返回资源类型的字符串表示(用于日志和调试) +func (r CloudResourceType) String() string { + switch r { + case ResourceTypeECS: + return "云服务器" + case ResourceTypeRDS: + return "云数据库" + case ResourceTypeSLB: + return "负载均衡" + case ResourceTypeOSS: + return "对象存储" + case ResourceTypeVPC: + return "虚拟私有云" + case ResourceTypeOther: + return "其他" + default: + return "未知" + } +} + +// CloudResourceStatus 云资源状态 +type CloudResourceStatus int8 + +const ( + CloudResourceRunning CloudResourceStatus = iota + 1 // 运行中 + CloudResourceStopped // 已停止 + CloudResourceStarting // 启动中 + CloudResourceStopping // 停止中 + CloudResourceDeleted // 已删除 + CloudResourceUnknown // 未知状态 +) + +// String 返回资源状态的字符串表示(用于日志和调试) +func (s CloudResourceStatus) String() string { + switch s { + case CloudResourceRunning: + return "运行中" + case CloudResourceStopped: + return "已停止" + case CloudResourceStarting: + return "启动中" + case CloudResourceStopping: + return "停止中" + case CloudResourceDeleted: + return "已删除" + case CloudResourceUnknown: + return "未知" + default: + return "未知" + } +} + +// Currency 货币单位 +type Currency string + +const ( + CurrencyCNY Currency = "CNY" // 人民币 + CurrencyUSD Currency = "USD" // 美元 +) + +// ChargeType 计费方式 +type ChargeType string + +const ( + ChargeTypePostPaid ChargeType = "PostPaid" // 按量付费 + ChargeTypePrePaid ChargeType = "PrePaid" // 包年包月 +) + +// SyncMode 同步模式 +type SyncMode string + +const ( + SyncModeFull SyncMode = "full" // 全量同步 + SyncModeIncremental SyncMode = "incremental" // 增量同步 +) + +// TreeCloudResource 云资源管理 +type TreeCloudResource struct { + Model + + Name string `json:"name" gorm:"type:varchar(100);not null;comment:资源名称"` + ResourceType CloudResourceType `json:"resource_type" gorm:"type:tinyint(1);not null;comment:资源类型;default:1"` + Status CloudResourceStatus `json:"status" gorm:"type:tinyint(1);not null;comment:资源状态;default:1"` + Environment string `json:"environment" gorm:"type:varchar(50);comment:环境标识(dev/test/prod)"` + Description string `json:"description" gorm:"type:text;comment:资源描述"` + Tags KeyValueList `json:"tags" gorm:"type:text;serializer:json;comment:资源标签集合"` + CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` + CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` + CloudAccountID int `json:"cloud_account_id" gorm:"not null;comment:云账户ID"` + CloudAccount *CloudAccount `json:"cloud_account,omitempty" gorm:"foreignKey:CloudAccountID"` + Region string `json:"region" gorm:"type:varchar(50);comment:区域,如cn-hangzhou"` + InstanceID string `json:"instance_id" gorm:"type:varchar(100);comment:云资源实例ID"` + InstanceType string `json:"instance_type" gorm:"type:varchar(100);comment:实例规格(如ecs.g6.large)"` + Cpu int `json:"cpu" gorm:"comment:CPU核数;default:0"` + Memory int `json:"memory" gorm:"comment:内存大小(GiB);default:0"` + Disk int `json:"disk" gorm:"comment:磁盘大小(GiB);default:0"` + PublicIP string `json:"public_ip" gorm:"type:varchar(45);comment:公网IP"` + PrivateIP string `json:"private_ip" gorm:"type:varchar(45);comment:私网IP"` + VpcID string `json:"vpc_id" gorm:"type:varchar(100);comment:VPC ID"` + ZoneID string `json:"zone_id" gorm:"type:varchar(50);comment:可用区ID"` + ChargeType ChargeType `json:"charge_type" gorm:"type:varchar(50);comment:计费方式(PostPaid/PrePaid)"` + ExpireTime *time.Time `json:"expire_time" gorm:"type:datetime;comment:到期时间"` + MonthlyCost float64 `json:"monthly_cost" gorm:"type:decimal(10,2);comment:月度成本;default:0"` + Currency Currency `json:"currency" gorm:"type:varchar(10);not null;comment:货币单位;default:'CNY'"` + OSType string `json:"os_type" gorm:"type:varchar(50);comment:操作系统类型(linux/windows)"` + OSName string `json:"os_name" gorm:"type:varchar(100);comment:操作系统名称"` + ImageID string `json:"image_id" gorm:"type:varchar(100);comment:镜像ID"` + ImageName string `json:"image_name" gorm:"type:varchar(100);comment:镜像名称"` + Port int `json:"port" gorm:"comment:SSH端口号;default:22"` + Username string `json:"username" gorm:"type:varchar(100);comment:SSH用户名"` + Password string `json:"-" gorm:"type:varchar(500);comment:SSH密码(加密存储)"` + Key string `json:"-" gorm:"type:text;comment:SSH密钥"` + AuthMode AuthMode `json:"auth_mode" gorm:"type:tinyint(1);comment:SSH认证方式(1:密码,2:密钥);default:1"` + TreeNodes []*TreeNode `json:"tree_nodes" gorm:"many2many:cl_tree_node_cloud"` +} + +func (t *TreeCloudResource) TableName() string { + return "cl_tree_cloud_resource" +} + +// GetTreeCloudResourceListReq 获取云资源列表请求 +type GetTreeCloudResourceListReq struct { + ListReq + CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` + ResourceType CloudResourceType `json:"resource_type" form:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` + Status CloudResourceStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2 3 4 5 6"` + Environment string `json:"environment" form:"environment"` +} + +// GetTreeCloudResourceDetailReq 获取云资源详情请求 +type GetTreeCloudResourceDetailReq struct { + ID int `json:"id" form:"id" binding:"required,gt=0"` +} + +// UpdateTreeCloudResourceReq 更新云资源本地元数据请求(不影响云上资源) +type UpdateTreeCloudResourceReq struct { + ID int `json:"id" binding:"required,gt=0"` + Environment string `json:"environment"` // 环境标识 + Description string `json:"description"` // 资源描述 + Tags KeyValueList `json:"tags"` // 自定义标签 + Port int `json:"port" binding:"omitempty,gte=1,lte=65535"` // SSH端口 + Username string `json:"username"` // SSH用户名 + Password string `json:"password"` // SSH密码 + Key string `json:"key"` // SSH密钥 + AuthMode AuthMode `json:"auth_mode" binding:"omitempty,oneof=1 2"` // SSH认证方式 + OperatorID int `json:"-"` // 操作人ID (不通过JSON传递) + OperatorName string `json:"-"` // 操作人姓名 (不通过JSON传递) +} + +// DeleteTreeCloudResourceReq 删除云资源请求(仅从平台删除,不影响云上资源) +type DeleteTreeCloudResourceReq struct { + ID int `json:"id" binding:"required,gt=0"` + OperatorID int `json:"-"` // 操作人ID (不通过JSON传递) + OperatorName string `json:"-"` // 操作人姓名 (不通过JSON传递) +} + +// SyncTreeCloudResourceReq 从云厂商同步资源请求 +type SyncTreeCloudResourceReq struct { + CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` + ResourceTypes []CloudResourceType `json:"resource_types" binding:"omitempty"` // 同步的资源类型列表,为空则同步所有 + Regions []string `json:"regions"` // 指定同步的区域列表,为空则同步账号配置的区域 + InstanceIDs []string `json:"instance_ids"` // 指定同步的实例ID列表,为空则同步所有 + SyncMode SyncMode `json:"sync_mode" binding:"omitempty,oneof=full incremental"` // 同步模式: full-全量, incremental-增量 + AutoBind bool `json:"auto_bind"` // 是否自动绑定到服务树节点 + BindNodeID int `json:"bind_node_id" binding:"omitempty,gt=0"` // 自动绑定的目标节点ID + OperatorID int `json:"-"` // 操作人ID (不通过JSON传递) + OperatorName string `json:"-"` // 操作人姓名 (不通过JSON传递) +} + +// SyncCloudResourceResp 同步云资源响应 +type SyncCloudResourceResp struct { + TotalCount int `json:"total_count"` // 总共同步的资源数量 + NewCount int `json:"new_count"` // 新增的资源数量 + UpdateCount int `json:"update_count"` // 更新的资源数量 + DeleteCount int `json:"delete_count"` // 删除的资源数量(全量同步时) + FailedCount int `json:"failed_count"` // 同步失败的数量 + FailedInstances []string `json:"failed_instances"` // 同步失败的实例ID列表 + SyncTime time.Time `json:"sync_time"` // 同步时间 +} + +// VerifyCloudCredentialsReq 验证云厂商凭证请求 +// Deprecated: 使用 cloud_account.go 中的 VerifyCloudAccountReq +type VerifyCloudCredentialsReq struct { + Provider CloudProvider `json:"provider" binding:"required,oneof=1 2 3 4 5 6"` + Region string `json:"region" binding:"required"` + AccessKey string `json:"access_key" binding:"required"` + SecretKey string `json:"secret_key" binding:"required"` +} + +// GetTreeNodeCloudResourcesReq 获取树节点下的云资源请求 +type GetTreeNodeCloudResourcesReq struct { + NodeID int `json:"node_id" form:"node_id" binding:"required,gt=0"` + CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` + ResourceType CloudResourceType `json:"resource_type" form:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` + Status CloudResourceStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2 3 4 5 6"` +} + +// BindTreeCloudResourceReq 绑定云资源到树节点请求 +type BindTreeCloudResourceReq struct { + ID int `json:"id" binding:"required,gt=0"` + TreeNodeIDs []int `json:"tree_node_ids" binding:"required,min=1,dive,gt=0"` +} + +// UnBindTreeCloudResourceReq 解绑云资源与树节点请求 +type UnBindTreeCloudResourceReq struct { + ID int `json:"id" binding:"required,gt=0"` + TreeNodeIDs []int `json:"tree_node_ids" binding:"required,min=1,dive,gt=0"` +} + +// ConnectTreeCloudResourceTerminalReq 连接云资源终端请求(针对ECS) +type ConnectTreeCloudResourceTerminalReq struct { + ID int `json:"id" form:"id" binding:"required,gt=0"` + UserID int `json:"user_id"` +} + +// UpdateCloudResourceStatusReq 更新云资源状态请求 +type UpdateCloudResourceStatusReq struct { + ID int `json:"id" binding:"required,gt=0"` + Status CloudResourceStatus `json:"status" binding:"required,oneof=1 2 3 4 5 6"` +} + +// CloudResourceSyncHistory 云资源同步历史 +type CloudResourceSyncHistory struct { + Model + CloudAccountID int `json:"cloud_account_id" gorm:"not null;comment:云账户ID"` + SyncMode SyncMode `json:"sync_mode" gorm:"type:varchar(20);comment:同步模式"` + TotalCount int `json:"total_count" gorm:"comment:同步总数"` + NewCount int `json:"new_count" gorm:"comment:新增数量"` + UpdateCount int `json:"update_count" gorm:"comment:更新数量"` + DeleteCount int `json:"delete_count" gorm:"comment:删除数量"` + FailedCount int `json:"failed_count" gorm:"comment:失败数量"` + FailedInstances string `json:"failed_instances" gorm:"type:text;comment:失败的实例ID列表(JSON)"` + SyncStatus string `json:"sync_status" gorm:"type:varchar(20);comment:同步状态(success/failed/partial)"` + ErrorMessage string `json:"error_message" gorm:"type:text;comment:错误信息"` + StartTime time.Time `json:"start_time" gorm:"comment:开始时间"` + EndTime time.Time `json:"end_time" gorm:"comment:结束时间"` + Duration int `json:"duration" gorm:"comment:同步耗时(秒)"` +} + +func (c *CloudResourceSyncHistory) TableName() string { + return "cl_cloud_resource_sync_history" +} + +// GetCloudResourceSyncHistoryReq 获取同步历史请求 +type GetCloudResourceSyncHistoryReq struct { + ListReq + CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` + SyncStatus string `json:"sync_status" form:"sync_status"` +} + +// CloudResourceChangeLog 云资源变更日志 +type CloudResourceChangeLog struct { + Model + ResourceID int `json:"resource_id" gorm:"not null;comment:云资源ID"` + InstanceID string `json:"instance_id" gorm:"type:varchar(100);comment:实例ID"` + ChangeType string `json:"change_type" gorm:"type:varchar(20);comment:变更类型(created/updated/deleted/status_changed)"` + FieldName string `json:"field_name" gorm:"type:varchar(100);comment:变更字段名"` + OldValue string `json:"old_value" gorm:"type:text;comment:旧值"` + NewValue string `json:"new_value" gorm:"type:text;comment:新值"` + ChangeSource string `json:"change_source" gorm:"type:varchar(50);comment:变更来源(sync/manual)"` + OperatorID int `json:"operator_id" gorm:"comment:操作人ID"` + OperatorName string `json:"operator_name" gorm:"type:varchar(100);comment:操作人姓名"` + ChangeTime time.Time `json:"change_time" gorm:"comment:变更时间"` +} + +func (c *CloudResourceChangeLog) TableName() string { + return "cl_cloud_resource_change_log" +} + +// GetCloudResourceChangeLogReq 获取资源变更日志请求 +type GetCloudResourceChangeLogReq struct { + ListReq + ResourceID int `json:"resource_id" form:"resource_id" binding:"omitempty,gt=0"` + ChangeType string `json:"change_type" form:"change_type"` +} diff --git a/internal/model/tree_cloud_account.go b/internal/model/tree_cloud_account.go new file mode 100644 index 00000000..995097a6 --- /dev/null +++ b/internal/model/tree_cloud_account.go @@ -0,0 +1,112 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package model + +// CloudAccountStatus 云账户状态 +type CloudAccountStatus int8 + +const ( + CloudAccountEnabled CloudAccountStatus = iota + 1 // 启用 + CloudAccountDisabled // 禁用 +) + +// CloudAccount 云账户管理 +type CloudAccount struct { + Model + Name string `json:"name" gorm:"type:varchar(100);not null;comment:账户名称"` + Provider CloudProvider `json:"provider" gorm:"type:tinyint(1);not null;comment:云厂商类型;default:1"` + Region string `json:"region" gorm:"type:varchar(50);not null;comment:区域,如cn-hangzhou"` + AccessKey string `json:"-" gorm:"type:varchar(500);not null;comment:访问密钥ID,加密存储"` + SecretKey string `json:"-" gorm:"type:varchar(500);not null;comment:访问密钥Secret,加密存储"` + AccountID string `json:"account_id" gorm:"type:varchar(100);comment:云账号ID"` + AccountName string `json:"account_name" gorm:"type:varchar(100);comment:云账号名称"` + AccountAlias string `json:"account_alias" gorm:"type:varchar(100);comment:账号别名"` + Description string `json:"description" gorm:"type:text;comment:账户描述"` + Status CloudAccountStatus `json:"status" gorm:"type:tinyint(1);not null;comment:账户状态,1:启用,2:禁用;default:1"` + CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` + CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` + CloudResources []*TreeCloudResource `json:"cloud_resources,omitempty" gorm:"foreignKey:CloudAccountID"` +} + +func (c *CloudAccount) TableName() string { + return "cl_cloud_account" +} + +// GetCloudAccountListReq 获取云账户列表请求 +type GetCloudAccountListReq struct { + ListReq + Provider CloudProvider `json:"provider" form:"provider" binding:"omitempty,oneof=1 2 3 4 5 6"` + Region string `json:"region" form:"region"` + Status CloudAccountStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2"` +} + +// GetCloudAccountDetailReq 获取云账户详情请求 +type GetCloudAccountDetailReq struct { + ID int `json:"id" form:"id" binding:"required,gt=0"` +} + +// CreateCloudAccountReq 创建云账户请求 +type CreateCloudAccountReq struct { + Name string `json:"name" binding:"required"` + Provider CloudProvider `json:"provider" binding:"required,oneof=1 2 3 4 5 6"` + Region string `json:"region" binding:"required"` + AccessKey string `json:"access_key" binding:"required"` + SecretKey string `json:"secret_key" binding:"required"` + AccountID string `json:"account_id"` + AccountName string `json:"account_name"` + AccountAlias string `json:"account_alias"` + Description string `json:"description"` + CreateUserID int `json:"create_user_id"` + CreateUserName string `json:"create_user_name"` +} + +// UpdateCloudAccountReq 更新云账户请求 +type UpdateCloudAccountReq struct { + ID int `json:"id" binding:"required,gt=0"` + Name string `json:"name"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + AccountID string `json:"account_id"` + AccountName string `json:"account_name"` + AccountAlias string `json:"account_alias"` + Description string `json:"description"` +} + +// DeleteCloudAccountReq 删除云账户请求 +type DeleteCloudAccountReq struct { + ID int `json:"id" binding:"required,gt=0"` +} + +// UpdateCloudAccountStatusReq 更新云账户状态请求 +type UpdateCloudAccountStatusReq struct { + ID int `json:"id" binding:"required,gt=0"` + Status CloudAccountStatus `json:"status" binding:"required,oneof=1 2"` +} + +// VerifyCloudAccountReq 验证云账户凭证请求 +type VerifyCloudAccountReq struct { + ID int `json:"id" binding:"required,gt=0"` +} diff --git a/internal/model/tree_local.go b/internal/model/tree_local.go index c67108b7..71ce8ad2 100644 --- a/internal/model/tree_local.go +++ b/internal/model/tree_local.go @@ -50,7 +50,7 @@ type TreeLocalResource struct { Status ResourceStatus `json:"status" gorm:"type:tinyint(1);comment:资源状态;default:1"` Environment string `json:"environment" gorm:"type:varchar(50);comment:环境标识,如dev,prod"` Description string `json:"description" gorm:"type:text;comment:资源描述"` - Tags StringList `json:"tags" gorm:"type:varchar(500);comment:资源标签集合"` + Tags KeyValueList `json:"tags" gorm:"type:text;serializer:json;comment:资源标签集合"` Cpu int `json:"cpu" gorm:"comment:CPU核数"` Memory int `json:"memory" gorm:"comment:内存大小,单位GiB"` Disk int `json:"disk" gorm:"comment:系统盘大小,单位GiB"` @@ -62,7 +62,7 @@ type TreeLocalResource struct { CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` Key string `json:"key" gorm:"type:text;comment:密钥"` AuthMode AuthMode `json:"auth_mode" gorm:"type:tinyint(1);comment:认证方式,1:密码,2:密钥;default:1"` - OsType string `json:"os_type" gorm:"type:varchar(50);comment:操作系统类型,如win,linux"` + OSType string `json:"os_type" gorm:"type:varchar(50);comment:操作系统类型,如win,linux"` OSName string `json:"os_name" gorm:"type:varchar(100);comment:操作系统名称"` ImageName string `json:"image_name" gorm:"type:varchar(100);comment:镜像名称"` TreeNodes []*TreeNode `json:"tree_nodes" gorm:"many2many:cl_tree_node_local"` @@ -85,58 +85,58 @@ type GetTreeLocalResourceDetailReq struct { // CreateTreeLocalReq 创建本地树资源请求 type CreateTreeLocalResourceReq struct { - Name string `json:"name" binding:"required"` - Environment string `json:"environment"` - Description string `json:"description"` - Tags StringList `json:"tags"` - IpAddr string `json:"ip_addr" binding:"required"` - Port int `json:"port"` - Username string `json:"username"` - Password string `json:"password"` - CreateUserID int `json:"create_user_id"` - CreateUserName string `json:"create_user_name"` - OsType string `json:"os_type"` - OSName string `json:"os_name"` - ImageName string `json:"image_name"` - Key string `json:"key"` - AuthMode AuthMode `json:"auth_mode"` + Name string `json:"name" binding:"required"` + Environment string `json:"environment"` + Description string `json:"description"` + Tags KeyValueList `json:"tags"` + IpAddr string `json:"ip_addr" binding:"required"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + CreateUserID int `json:"create_user_id"` + CreateUserName string `json:"create_user_name"` + OSType string `json:"os_type"` + OSName string `json:"os_name"` + ImageName string `json:"image_name"` + Key string `json:"key"` + AuthMode AuthMode `json:"auth_mode"` } // UpdateTreeLocalReq 更新本地树资源请求 type UpdateTreeLocalResourceReq struct { - ID int `json:"id" form:"id"` - Name string `json:"name"` - Environment string `json:"environment"` - Description string `json:"description"` - Tags StringList `json:"tags"` - IpAddr string `json:"ip_addr"` - Port int `json:"port"` - OsType string `json:"os_type"` - OSName string `json:"os_name"` - ImageName string `json:"image_name"` - Username string `json:"username"` - Password string `json:"password"` - Key string `json:"key"` - AuthMode AuthMode `json:"auth_mode"` + ID int `json:"id" form:"id"` + Name string `json:"name"` + Environment string `json:"environment"` + Description string `json:"description"` + Tags KeyValueList `json:"tags"` + IpAddr string `json:"ip_addr"` + Port int `json:"port"` + OSType string `json:"os_type"` + OSName string `json:"os_name"` + ImageName string `json:"image_name"` + Username string `json:"username"` + Password string `json:"password"` + Key string `json:"key"` + AuthMode AuthMode `json:"auth_mode"` } -// DeleteTreeLocalReq 删除本地树资源请求 +// DeleteTreeLocalResourceReq 删除本地树资源请求 type DeleteTreeLocalResourceReq struct { - ID int `json:"id" form:"id"` + ID int `json:"id" form:"id" binding:"required"` } -// ConnectTerminalReq 连接终端请求 -type ConnectTerminalResourceReq struct { - ID int `json:"id" form:"id"` +// ConnectTreeLocalResourceTerminalReq 连接本地资源终端请求 +type ConnectTreeLocalResourceTerminalReq struct { + ID int `json:"id" form:"id" binding:"required"` UserID int `json:"user_id"` } type BindTreeLocalResourceReq struct { - ID int `json:"id" form:"id"` - TreeNodeIDs []int `json:"tree_node_ids" form:"tree_node_ids"` + ID int `json:"id" form:"id" binding:"required"` + TreeNodeIDs []int `json:"tree_node_ids" form:"tree_node_ids" binding:"required,min=1"` } type UnBindTreeLocalResourceReq struct { - ID int `json:"id" form:"id"` - TreeNodeIDs []int `json:"tree_node_ids" form:"tree_node_ids"` + ID int `json:"id" form:"id" binding:"required"` + TreeNodeIDs []int `json:"tree_node_ids" form:"tree_node_ids" binding:"required,min=1"` } diff --git a/internal/model/tree_node.go b/internal/model/tree_node.go index cfcd14fa..12a23df8 100644 --- a/internal/model/tree_node.go +++ b/internal/model/tree_node.go @@ -48,18 +48,19 @@ const ( // TreeNode 服务树节点结构 type TreeNode struct { Model - Name string `json:"name" gorm:"type:varchar(50);not null;comment:节点名称"` // 节点名称 - ParentID int `json:"parent_id" gorm:"index;comment:父节点ID;default:0"` // 父节点ID - Level int `json:"level" gorm:"comment:节点层级,默认在第1层;default:1"` // 节点层级 - Description string `json:"description" gorm:"type:text;comment:节点描述"` // 节点描述 - CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` // 创建者ID - CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` // 创建者姓名 - Status TreeNodeStatus `json:"status" gorm:"default:1;comment:节点状态, 1:活跃 2:非活跃"` // 节点状态 - AdminUsers []User `json:"admins" gorm:"many2many:cl_tree_node_admin;"` // 管理员多对多关系 - MemberUsers []User `json:"members" gorm:"many2many:cl_tree_node_member;"` // 成员多对多关系 - IsLeaf int8 `json:"is_leaf" gorm:"comment:是否为叶子节点1:是 2:不是;default:2"` // 是否为叶子节点 - Children []*TreeNode `json:"children" gorm:"-"` // 子节点列表 - TreeLocalResources []*TreeLocalResource `json:"tree_local_resources" gorm:"many2many:cl_tree_node_local;"` + Name string `json:"name" gorm:"type:varchar(50);not null;comment:节点名称"` // 节点名称 + ParentID int `json:"parent_id" gorm:"index;comment:父节点ID;default:0"` // 父节点ID + Level int `json:"level" gorm:"comment:节点层级,默认在第1层;default:1"` // 节点层级 + Description string `json:"description" gorm:"type:text;comment:节点描述"` // 节点描述 + CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` // 创建者ID + CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` // 创建者姓名 + Status TreeNodeStatus `json:"status" gorm:"default:1;comment:节点状态, 1:活跃 2:非活跃"` // 节点状态 + AdminUsers []User `json:"admins" gorm:"many2many:cl_tree_node_admin;"` // 管理员多对多关系 + MemberUsers []User `json:"members" gorm:"many2many:cl_tree_node_member;"` // 成员多对多关系 + IsLeaf int8 `json:"is_leaf" gorm:"comment:是否为叶子节点1:是 2:不是;default:2"` // 是否为叶子节点 + Children []*TreeNode `json:"children" gorm:"-"` // 子节点列表 + TreeLocalResources []*TreeLocalResource `json:"tree_local_resources" gorm:"many2many:cl_tree_node_local;"` // 本地资源关联 + TreeCloudResources []*TreeCloudResource `json:"tree_cloud_resources" gorm:"many2many:cl_tree_node_cloud;"` // 云资源关联 } func (t *TreeNode) TableName() string { @@ -68,9 +69,9 @@ func (t *TreeNode) TableName() string { // GetTreeNodeListReq 获取树节点列表请求 type GetTreeNodeListReq struct { + ListReq Level int `json:"level" form:"level" binding:"omitempty,min=1"` Status TreeNodeStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2"` - Search string `json:"search" form:"search" binding:"omitempty"` } // GetTreeNodeDetailReq 获取节点详情请求 diff --git a/internal/tree/api/cloud_account_handler.go b/internal/tree/api/cloud_account_handler.go new file mode 100644 index 00000000..6d59a202 --- /dev/null +++ b/internal/tree/api/cloud_account_handler.go @@ -0,0 +1,164 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package api + +import ( + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "github.com/GoSimplicity/AI-CloudOps/internal/tree/service" + "github.com/GoSimplicity/AI-CloudOps/pkg/utils" + "github.com/gin-gonic/gin" +) + +type CloudAccountHandler struct { + service service.CloudAccountService +} + +func NewCloudAccountHandler(service service.CloudAccountService) *CloudAccountHandler { + return &CloudAccountHandler{ + service: service, + } +} + +func (h *CloudAccountHandler) RegisterRouters(server *gin.Engine) { + accountGroup := server.Group("/api/tree/cloud/account") + { + accountGroup.GET("/list", h.GetCloudAccountList) + accountGroup.GET("/:id/detail", h.GetCloudAccountDetail) + accountGroup.POST("/create", h.CreateCloudAccount) + accountGroup.PUT("/:id/update", h.UpdateCloudAccount) + accountGroup.DELETE("/:id/delete", h.DeleteCloudAccount) + accountGroup.PUT("/:id/status", h.UpdateCloudAccountStatus) + accountGroup.POST("/:id/verify", h.VerifyCloudAccount) + } +} + +// GetCloudAccountList 获取云账户列表 +func (h *CloudAccountHandler) GetCloudAccountList(ctx *gin.Context) { + var req model.GetCloudAccountListReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetCloudAccountList(ctx, &req) + }) +} + +// GetCloudAccountDetail 获取云账户详情 +func (h *CloudAccountHandler) GetCloudAccountDetail(ctx *gin.Context) { + var req model.GetCloudAccountDetailReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的账户ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetCloudAccountDetail(ctx, &req) + }) +} + +// CreateCloudAccount 创建云账户 +func (h *CloudAccountHandler) CreateCloudAccount(ctx *gin.Context) { + var req model.CreateCloudAccountReq + + user := ctx.MustGet("user").(utils.UserClaims) + + req.CreateUserID = user.Uid + req.CreateUserName = user.Username + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.CreateCloudAccount(ctx, &req) + }) +} + +// UpdateCloudAccount 更新云账户 +func (h *CloudAccountHandler) UpdateCloudAccount(ctx *gin.Context) { + var req model.UpdateCloudAccountReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的账户ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.UpdateCloudAccount(ctx, &req) + }) +} + +// DeleteCloudAccount 删除云账户 +func (h *CloudAccountHandler) DeleteCloudAccount(ctx *gin.Context) { + var req model.DeleteCloudAccountReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的账户ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.DeleteCloudAccount(ctx, &req) + }) +} + +// UpdateCloudAccountStatus 更新云账户状态 +func (h *CloudAccountHandler) UpdateCloudAccountStatus(ctx *gin.Context) { + var req model.UpdateCloudAccountStatusReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的账户ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.UpdateCloudAccountStatus(ctx, &req) + }) +} + +// VerifyCloudAccount 验证云账户凭证 +func (h *CloudAccountHandler) VerifyCloudAccount(ctx *gin.Context) { + var req model.VerifyCloudAccountReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的账户ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.VerifyCloudAccount(ctx, &req) + }) +} diff --git a/internal/tree/api/tree_cloud_handler.go b/internal/tree/api/tree_cloud_handler.go new file mode 100644 index 00000000..e781fb74 --- /dev/null +++ b/internal/tree/api/tree_cloud_handler.go @@ -0,0 +1,322 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package api + +import ( + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "github.com/GoSimplicity/AI-CloudOps/internal/tree/service" + "github.com/GoSimplicity/AI-CloudOps/pkg/ssh" + "github.com/GoSimplicity/AI-CloudOps/pkg/utils" + "github.com/gin-gonic/gin" +) + +type TreeCloudHandler struct { + service service.TreeCloudService + sshClient ssh.Client +} + +func NewTreeCloudHandler(service service.TreeCloudService, sshClient ssh.Client) *TreeCloudHandler { + return &TreeCloudHandler{ + service: service, + sshClient: sshClient, + } +} + +func (h *TreeCloudHandler) RegisterRouters(server *gin.Engine) { + cloudGroup := server.Group("/api/tree/cloud") + { + cloudGroup.GET("/list", h.GetTreeCloudResourceList) + cloudGroup.GET("/:id/detail", h.GetTreeCloudResourceDetail) + cloudGroup.GET("/:id/node", h.GetTreeNodeCloudResources) + cloudGroup.GET("/:id/terminal", h.ConnectCloudResourceTerminal) + cloudGroup.POST("/sync", h.SyncTreeCloudResource) + cloudGroup.GET("/sync/history", h.GetSyncHistory) + cloudGroup.PUT("/:id/update", h.UpdateTreeCloudResource) + cloudGroup.DELETE("/:id/delete", h.DeleteTreeCloudResource) + cloudGroup.PUT("/:id/status", h.UpdateCloudResourceStatus) + cloudGroup.POST("/:id/bind", h.BindTreeCloudResource) + cloudGroup.POST("/:id/unbind", h.UnBindTreeCloudResource) + cloudGroup.GET("/changelog", h.GetChangeLog) + } +} + +// GetTreeCloudResourceList 获取云资源列表 +func (h *TreeCloudHandler) GetTreeCloudResourceList(ctx *gin.Context) { + var req model.GetTreeCloudResourceListReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetTreeCloudResourceList(ctx, &req) + }) +} + +// GetTreeCloudResourceDetail 获取云资源详情 +func (h *TreeCloudHandler) GetTreeCloudResourceDetail(ctx *gin.Context) { + var req model.GetTreeCloudResourceDetailReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetTreeCloudResourceDetail(ctx, &req) + }) +} + +// UpdateTreeCloudResource 更新云资源本地元数据 +func (h *TreeCloudHandler) UpdateTreeCloudResource(ctx *gin.Context) { + var req model.UpdateTreeCloudResourceReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + // 获取当前用户信息 + uc := ctx.MustGet("user").(utils.UserClaims) + req.ID = id + req.OperatorID = uc.Uid + req.OperatorName = uc.Username + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.UpdateTreeCloudResource(ctx, &req) + }) +} + +// DeleteTreeCloudResource 删除云资源 +func (h *TreeCloudHandler) DeleteTreeCloudResource(ctx *gin.Context) { + var req model.DeleteTreeCloudResourceReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + // 获取当前用户信息 + uc := ctx.MustGet("user").(utils.UserClaims) + req.ID = id + req.OperatorID = uc.Uid + req.OperatorName = uc.Username + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.DeleteTreeCloudResource(ctx, &req) + }) +} + +// BindTreeCloudResource 绑定云资源到树节点 +func (h *TreeCloudHandler) BindTreeCloudResource(ctx *gin.Context) { + var req model.BindTreeCloudResourceReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.BindTreeCloudResource(ctx, &req) + }) +} + +// UnBindTreeCloudResource 解绑云资源与树节点 +func (h *TreeCloudHandler) UnBindTreeCloudResource(ctx *gin.Context) { + var req model.UnBindTreeCloudResourceReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.UnBindTreeCloudResource(ctx, &req) + }) +} + +// SyncTreeCloudResource 从云厂商同步资源 +func (h *TreeCloudHandler) SyncTreeCloudResource(ctx *gin.Context) { + var req model.SyncTreeCloudResourceReq + + // 获取当前用户信息 + uc := ctx.MustGet("user").(utils.UserClaims) + req.OperatorID = uc.Uid + req.OperatorName = uc.Username + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.SyncTreeCloudResource(ctx, &req) + }) +} + +// GetSyncHistory 获取云资源同步历史 +func (h *TreeCloudHandler) GetSyncHistory(ctx *gin.Context) { + var req model.GetCloudResourceSyncHistoryReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetSyncHistory(ctx, &req) + }) +} + +// GetChangeLog 获取云资源变更日志 +func (h *TreeCloudHandler) GetChangeLog(ctx *gin.Context) { + var req model.GetCloudResourceChangeLogReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetChangeLog(ctx, &req) + }) +} + +// GetTreeNodeCloudResources 获取树节点下的云资源 +func (h *TreeCloudHandler) GetTreeNodeCloudResources(ctx *gin.Context) { + var req model.GetTreeNodeCloudResourcesReq + + nodeId, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的节点ID") + return + } + + req.NodeID = nodeId + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.GetTreeNodeCloudResources(ctx, &req) + }) +} + +// ConnectCloudResourceTerminal 连接云资源终端 +func (h *TreeCloudHandler) ConnectCloudResourceTerminal(ctx *gin.Context) { + var req model.ConnectTreeCloudResourceTerminalReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + uc := ctx.MustGet("user").(utils.UserClaims) + req.ID = id + req.UserID = uc.Uid + + // 获取云资源详情 + detailReq := &model.GetTreeCloudResourceDetailReq{ID: req.ID} + cloud, err := h.service.GetTreeCloudResourceForConnection(ctx, detailReq) + if err != nil { + utils.ErrorWithMessage(ctx, "获取云资源信息失败: "+err.Error()) + return + } + + // 仅支持ECS类型的云资源连接终端 + if cloud.ResourceType != model.ResourceTypeECS { + utils.ErrorWithMessage(ctx, "仅支持ECS类型的云资源连接终端") + return + } + + // 如果没有公网IP,尝试使用私网IP + ipAddr := cloud.PublicIP + if ipAddr == "" { + ipAddr = cloud.PrivateIP + } + + if ipAddr == "" { + utils.ErrorWithMessage(ctx, "云资源没有可用的IP地址") + return + } + + // 设置默认端口 + port := cloud.Port + if port == 0 { + port = 22 + } + + // 设置默认用户名 + username := cloud.Username + if username == "" { + username = "root" + } + + // 配置SSH连接 + sshConfig := &ssh.Config{ + Host: ipAddr, + Port: port, + Username: username, + Password: cloud.Password, + Key: cloud.Key, + Mode: ssh.AuthMode(cloud.AuthMode), + Timeout: 10, + } + + // 建立SSH连接 + if err := h.sshClient.Connect(sshConfig); err != nil { + utils.ErrorWithMessage(ctx, "连接SSH失败: "+err.Error()) + return + } + + // 确保SSH连接在函数退出时关闭 + defer func() { + if closeErr := h.sshClient.Close(); closeErr != nil { + utils.ErrorWithMessage(ctx, "关闭SSH连接失败: "+closeErr.Error()) + } + }() + + // 升级WebSocket连接 + ws, err := utils.UpGrader.Upgrade(ctx.Writer, ctx.Request, nil) + if err != nil { + utils.ErrorWithMessage(ctx, "升级WebSocket连接失败: "+err.Error()) + return + } + defer ws.Close() + + // 启动终端会话 + if err := h.sshClient.WebTerminal(uc.Uid, ws); err != nil { + utils.ErrorWithMessage(ctx, "启动Web终端失败: "+err.Error()) + return + } +} + +// UpdateCloudResourceStatus 更新云资源状态 +func (h *TreeCloudHandler) UpdateCloudResourceStatus(ctx *gin.Context) { + var req model.UpdateCloudResourceStatusReq + + id, err := utils.GetParamID(ctx) + if err != nil { + utils.ErrorWithMessage(ctx, "无效的资源ID") + return + } + + req.ID = id + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.UpdateCloudResourceStatus(ctx, &req) + }) +} diff --git a/internal/tree/api/tree_local_handler.go b/internal/tree/api/tree_local_handler.go index ba0a3ffb..ebef9422 100644 --- a/internal/tree/api/tree_local_handler.go +++ b/internal/tree/api/tree_local_handler.go @@ -30,24 +30,18 @@ import ( "github.com/GoSimplicity/AI-CloudOps/internal/tree/service" "github.com/GoSimplicity/AI-CloudOps/pkg/ssh" "github.com/GoSimplicity/AI-CloudOps/pkg/utils" - "github.com/GoSimplicity/AI-CloudOps/pkg/websocket" "github.com/gin-gonic/gin" ) type TreeLocalHandler struct { service service.TreeLocalService sshClient ssh.Client - wsManager websocket.Manager } func NewTreeLocalHandler(service service.TreeLocalService, sshClient ssh.Client) *TreeLocalHandler { - // 初始化WebSocket管理器 - wsManager := websocket.NewManager(nil, nil) - return &TreeLocalHandler{ service: service, sshClient: sshClient, - wsManager: wsManager, } } @@ -80,7 +74,7 @@ func (h *TreeLocalHandler) GetTreeLocalDetail(ctx *gin.Context) { id, err := utils.GetParamID(ctx) if err != nil { - utils.ErrorWithMessage(ctx, "invalid param id") + utils.ErrorWithMessage(ctx, "无效的资源ID") return } diff --git a/internal/tree/dao/cloud_account_dao.go b/internal/tree/dao/cloud_account_dao.go new file mode 100644 index 00000000..ca70f24e --- /dev/null +++ b/internal/tree/dao/cloud_account_dao.go @@ -0,0 +1,176 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package dao + +import ( + "context" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type CloudAccountDAO interface { + Create(ctx context.Context, account *model.CloudAccount) error + Update(ctx context.Context, account *model.CloudAccount) error + Delete(ctx context.Context, id int) error + GetByID(ctx context.Context, id int) (*model.CloudAccount, error) + GetList(ctx context.Context, req *model.GetCloudAccountListReq) ([]*model.CloudAccount, int64, error) + UpdateStatus(ctx context.Context, id int, status model.CloudAccountStatus) error + GetByProviderAndRegion(ctx context.Context, provider model.CloudProvider, region string) ([]*model.CloudAccount, error) +} + +type cloudAccountDAO struct { + logger *zap.Logger + db *gorm.DB +} + +func NewCloudAccountDAO(db *gorm.DB, logger *zap.Logger) CloudAccountDAO { + return &cloudAccountDAO{ + logger: logger, + db: db, + } +} + +// Create 创建云账户 +func (d *cloudAccountDAO) Create(ctx context.Context, account *model.CloudAccount) error { + if err := d.db.WithContext(ctx).Create(account).Error; err != nil { + d.logger.Error("创建云账户失败", zap.Error(err)) + return err + } + + return nil +} + +// Update 更新云账户 +func (d *cloudAccountDAO) Update(ctx context.Context, account *model.CloudAccount) error { + if err := d.db.WithContext(ctx).Model(account).Updates(account).Error; err != nil { + d.logger.Error("更新云账户失败", zap.Error(err)) + return err + } + + return nil +} + +// Delete 删除云账户 +func (d *cloudAccountDAO) Delete(ctx context.Context, id int) error { + if err := d.db.WithContext(ctx).Delete(&model.CloudAccount{}, id).Error; err != nil { + d.logger.Error("删除云账户失败", zap.Error(err), zap.Int("id", id)) + return err + } + + return nil +} + +// GetByID 根据ID获取云账户详情 +func (d *cloudAccountDAO) GetByID(ctx context.Context, id int) (*model.CloudAccount, error) { + var account model.CloudAccount + + err := d.db.WithContext(ctx).Preload("CloudResources").Where("id = ?", id).First(&account).Error + if err != nil { + d.logger.Error("根据ID获取云账户详情失败", zap.Error(err), zap.Int("id", id)) + return nil, err + } + + return &account, nil +} + +// GetList 获取云账户列表 +func (d *cloudAccountDAO) GetList(ctx context.Context, req *model.GetCloudAccountListReq) ([]*model.CloudAccount, int64, error) { + var accounts []*model.CloudAccount + var total int64 + + query := d.db.WithContext(ctx).Model(&model.CloudAccount{}) + + // 添加查询条件 + if req.Provider != 0 { + query = query.Where("provider = ?", req.Provider) + } + + if req.Region != "" { + query = query.Where("region = ?", req.Region) + } + + if req.Status != 0 { + query = query.Where("status = ?", req.Status) + } + + if req.Search != "" { + query = query.Where("name LIKE ? OR account_name LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") + } + + // 计算总数 + err := query.Count(&total).Error + if err != nil { + d.logger.Error("获取云账户总数失败", zap.Error(err)) + return nil, 0, err + } + + // 分页查询 + offset := (req.Page - 1) * req.Size + err = query. + Order("created_at DESC"). + Limit(req.Size). + Offset(offset). + Find(&accounts).Error + if err != nil { + d.logger.Error("获取云账户列表失败", zap.Error(err)) + return nil, 0, err + } + + return accounts, total, nil +} + +// UpdateStatus 更新云账户状态 +func (d *cloudAccountDAO) UpdateStatus(ctx context.Context, id int, status model.CloudAccountStatus) error { + if err := d.db.WithContext(ctx). + Model(&model.CloudAccount{}). + Where("id = ?", id). + Update("status", status).Error; err != nil { + d.logger.Error("更新云账户状态失败", zap.Error(err), zap.Int("id", id), zap.Int8("status", int8(status))) + return err + } + + return nil +} + +// GetByProviderAndRegion 根据云厂商和区域获取云账户列表 +func (d *cloudAccountDAO) GetByProviderAndRegion(ctx context.Context, provider model.CloudProvider, region string) ([]*model.CloudAccount, error) { + var accounts []*model.CloudAccount + + query := d.db.WithContext(ctx).Where("provider = ?", provider) + if region != "" { + query = query.Where("region = ?", region) + } + + err := query.Find(&accounts).Error + if err != nil { + d.logger.Error("根据云厂商和区域获取云账户列表失败", zap.Error(err)) + return nil, err + } + + return accounts, nil +} diff --git a/internal/tree/dao/tree_cloud_dao.go b/internal/tree/dao/tree_cloud_dao.go new file mode 100644 index 00000000..20f5ffa6 --- /dev/null +++ b/internal/tree/dao/tree_cloud_dao.go @@ -0,0 +1,432 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package dao + +import ( + "context" + "errors" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type TreeCloudDAO interface { + Create(ctx context.Context, cloud *model.TreeCloudResource) error + Update(ctx context.Context, cloud *model.TreeCloudResource) error + UpdateMetadata(ctx context.Context, id int, metadata map[string]interface{}) error + Delete(ctx context.Context, id int) error + GetByID(ctx context.Context, id int) (*model.TreeCloudResource, error) + GetList(ctx context.Context, req *model.GetTreeCloudResourceListReq) ([]*model.TreeCloudResource, int64, error) + GetByAccountAndInstanceID(ctx context.Context, cloudAccountID int, instanceID string) (*model.TreeCloudResource, error) + GetByNodeID(ctx context.Context, nodeID int, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) + BindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error + UnBindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error + BatchGetByIDs(ctx context.Context, ids []int) ([]*model.TreeCloudResource, error) + BatchCreate(ctx context.Context, clouds []*model.TreeCloudResource) error + UpdateStatus(ctx context.Context, id int, status model.CloudResourceStatus) error + CreateSyncHistory(ctx context.Context, history *model.CloudResourceSyncHistory) error + GetSyncHistoryList(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) ([]*model.CloudResourceSyncHistory, int64, error) + CreateChangeLog(ctx context.Context, log *model.CloudResourceChangeLog) error + GetChangeLogList(ctx context.Context, req *model.GetCloudResourceChangeLogReq) ([]*model.CloudResourceChangeLog, int64, error) +} + +type treeCloudDAO struct { + logger *zap.Logger + db *gorm.DB +} + +func NewTreeCloudDAO(db *gorm.DB, logger *zap.Logger) TreeCloudDAO { + return &treeCloudDAO{ + logger: logger, + db: db, + } +} + +// Create 创建云资源 +func (d *treeCloudDAO) Create(ctx context.Context, cloud *model.TreeCloudResource) error { + if err := d.db.WithContext(ctx).Create(cloud).Error; err != nil { + d.logger.Error("创建云资源失败", zap.Error(err)) + return err + } + + return nil +} + +// Update 更新云资源 +func (d *treeCloudDAO) Update(ctx context.Context, cloud *model.TreeCloudResource) error { + if err := d.db.WithContext(ctx).Model(cloud).Updates(cloud).Error; err != nil { + d.logger.Error("更新云资源失败", zap.Error(err)) + return err + } + + return nil +} + +// UpdateMetadata 更新云资源元数据 +func (d *treeCloudDAO) UpdateMetadata(ctx context.Context, id int, metadata map[string]interface{}) error { + if err := d.db.WithContext(ctx). + Model(&model.TreeCloudResource{}). + Where("id = ?", id). + Updates(metadata).Error; err != nil { + d.logger.Error("更新云资源元数据失败", zap.Error(err), zap.Int("id", id)) + return err + } + + return nil +} + +// Delete 删除云资源 +func (d *treeCloudDAO) Delete(ctx context.Context, id int) error { + if err := d.db.WithContext(ctx).Delete(&model.TreeCloudResource{}, id).Error; err != nil { + d.logger.Error("删除云资源失败", zap.Error(err), zap.Int("id", id)) + return err + } + + return nil +} + +// GetByID 根据ID获取云资源详情 +func (d *treeCloudDAO) GetByID(ctx context.Context, id int) (*model.TreeCloudResource, error) { + var cloud model.TreeCloudResource + + err := d.db.WithContext(ctx).Preload("TreeNodes").Where("id = ?", id).First(&cloud).Error + if err != nil { + d.logger.Error("根据ID获取云资源详情失败", zap.Error(err), zap.Int("id", id)) + return nil, err + } + + return &cloud, nil +} + +// GetList 获取云资源列表 +func (d *treeCloudDAO) GetList(ctx context.Context, req *model.GetTreeCloudResourceListReq) ([]*model.TreeCloudResource, int64, error) { + var clouds []*model.TreeCloudResource + var total int64 + + query := d.db.WithContext(ctx).Model(&model.TreeCloudResource{}) + + // 添加查询条件 + if req.CloudAccountID != 0 { + query = query.Where("cloud_account_id = ?", req.CloudAccountID) + } + + if req.ResourceType != 0 { + query = query.Where("resource_type = ?", req.ResourceType) + } + + if req.Status != 0 { + query = query.Where("status = ?", req.Status) + } + + if req.Environment != "" { + query = query.Where("environment = ?", req.Environment) + } + + if req.Search != "" { + query = query.Where("name LIKE ? OR instance_id LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") + } + + // 计算总数 + err := query.Count(&total).Error + if err != nil { + d.logger.Error("获取云资源总数失败", zap.Error(err)) + return nil, 0, err + } + + // 分页查询,关联云账户信息 + offset := (req.Page - 1) * req.Size + err = query. + Order("created_at DESC"). + Preload("CloudAccount"). + Preload("TreeNodes"). + Limit(req.Size). + Offset(offset). + Find(&clouds).Error + if err != nil { + d.logger.Error("获取云资源列表失败", zap.Error(err)) + return nil, 0, err + } + + return clouds, total, nil +} + +// GetByAccountAndInstanceID 根据云账户ID和实例ID获取云资源 +func (d *treeCloudDAO) GetByAccountAndInstanceID(ctx context.Context, cloudAccountID int, instanceID string) (*model.TreeCloudResource, error) { + var cloud model.TreeCloudResource + + err := d.db.WithContext(ctx). + Where("cloud_account_id = ? AND instance_id = ?", cloudAccountID, instanceID). + First(&cloud).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + d.logger.Error("根据云账户和实例ID获取云资源失败", zap.Error(err), zap.Int("cloudAccountID", cloudAccountID), zap.String("instanceID", instanceID)) + return nil, err + } + + return &cloud, nil +} + +// GetByNodeID 根据树节点ID获取云资源列表 +func (d *treeCloudDAO) GetByNodeID(ctx context.Context, nodeID int, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) { + var clouds []*model.TreeCloudResource + + query := d.db.WithContext(ctx). + Joins("JOIN cl_tree_node_cloud ON cl_tree_node_cloud.tree_cloud_resource_id = cl_tree_cloud_resource.id"). + Where("cl_tree_node_cloud.tree_node_id = ?", nodeID) + + // 添加过滤条件 + if req.CloudAccountID != 0 { + query = query.Where("cl_tree_cloud_resource.cloud_account_id = ?", req.CloudAccountID) + } + + if req.ResourceType != 0 { + query = query.Where("cl_tree_cloud_resource.resource_type = ?", req.ResourceType) + } + + if req.Status != 0 { + query = query.Where("cl_tree_cloud_resource.status = ?", req.Status) + } + + err := query.Preload("CloudAccount").Find(&clouds).Error + if err != nil { + d.logger.Error("根据节点ID获取云资源失败", zap.Error(err), zap.Int("nodeID", nodeID)) + return nil, err + } + + return clouds, nil +} + +// BatchGetByIDs 批量获取云资源 +func (d *treeCloudDAO) BatchGetByIDs(ctx context.Context, ids []int) ([]*model.TreeCloudResource, error) { + if len(ids) == 0 { + return nil, nil + } + + var clouds []*model.TreeCloudResource + + if err := d.db.WithContext(ctx).Where("id IN ?", ids).Find(&clouds).Error; err != nil { + d.logger.Error("批量获取云资源失败", zap.Error(err), zap.Ints("ids", ids)) + return nil, err + } + + return clouds, nil +} + +// BatchCreate 批量创建云资源 +func (d *treeCloudDAO) BatchCreate(ctx context.Context, clouds []*model.TreeCloudResource) error { + if len(clouds) == 0 { + return nil + } + + if err := d.db.WithContext(ctx).Create(&clouds).Error; err != nil { + d.logger.Error("批量创建云资源失败", zap.Error(err)) + return err + } + + d.logger.Info("批量创建云资源成功", zap.Int("count", len(clouds))) + return nil +} + +// UpdateStatus 更新云资源状态 +func (d *treeCloudDAO) UpdateStatus(ctx context.Context, id int, status model.CloudResourceStatus) error { + if err := d.db.WithContext(ctx). + Model(&model.TreeCloudResource{}). + Where("id = ?", id). + Update("status", status).Error; err != nil { + d.logger.Error("更新云资源状态失败", zap.Error(err), zap.Int("id", id), zap.Int8("status", int8(status))) + return err + } + + return nil +} + +// BindTreeNodes 绑定树节点 +func (d *treeCloudDAO) BindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error { + if !treeUtils.ValidateTreeNodeIDs(treeNodeIds) { + d.logger.Info("没有需要绑定的树节点") + return nil + } + + // 获取云资源 + var cloud model.TreeCloudResource + if err := d.db.WithContext(ctx).First(&cloud, cloudID).Error; err != nil { + d.logger.Error("获取云资源失败", zap.Error(err), zap.Int("cloudID", cloudID)) + return err + } + + // 构建要绑定的树节点列表 + var treeNodes []model.TreeNode + for _, nodeID := range treeNodeIds { + treeNodes = append(treeNodes, model.TreeNode{Model: model.Model{ID: nodeID}}) + } + + // 通过many2many关系绑定树节点 + if err := d.db.WithContext(ctx).Model(&cloud).Association("TreeNodes").Append(treeNodes); err != nil { + d.logger.Error("绑定树节点失败", zap.Error(err), zap.Int("cloudID", cloudID), zap.Ints("treeNodeIds", treeNodeIds)) + return err + } + + d.logger.Info("绑定树节点成功", zap.Int("cloudID", cloudID), zap.Ints("treeNodeIds", treeNodeIds)) + + return nil +} + +// UnBindTreeNodes 解绑树节点 +func (d *treeCloudDAO) UnBindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error { + if !treeUtils.ValidateTreeNodeIDs(treeNodeIds) { + d.logger.Info("没有需要解绑的树节点") + return nil + } + + // 获取云资源 + var cloud model.TreeCloudResource + if err := d.db.WithContext(ctx).First(&cloud, cloudID).Error; err != nil { + d.logger.Error("获取云资源失败", zap.Error(err), zap.Int("cloudID", cloudID)) + return err + } + + // 构建要解绑的树节点列表 + var treeNodes []model.TreeNode + for _, nodeID := range treeNodeIds { + treeNodes = append(treeNodes, model.TreeNode{Model: model.Model{ID: nodeID}}) + } + + // 通过many2many关系解绑树节点 + if err := d.db.WithContext(ctx).Model(&cloud).Association("TreeNodes").Delete(treeNodes); err != nil { + d.logger.Error("解绑树节点失败", zap.Error(err), zap.Int("cloudID", cloudID), zap.Ints("treeNodeIds", treeNodeIds)) + return err + } + + d.logger.Info("解绑树节点成功", zap.Int("cloudID", cloudID), zap.Ints("treeNodeIds", treeNodeIds)) + + return nil +} + +// CreateSyncHistory 创建同步历史记录 +func (d *treeCloudDAO) CreateSyncHistory(ctx context.Context, history *model.CloudResourceSyncHistory) error { + if err := d.db.WithContext(ctx).Create(history).Error; err != nil { + d.logger.Error("创建同步历史失败", zap.Error(err)) + return err + } + + d.logger.Info("创建同步历史成功", + zap.Int("cloudAccountID", history.CloudAccountID), + zap.String("syncStatus", history.SyncStatus)) + return nil +} + +// GetSyncHistoryList 获取同步历史列表 +func (d *treeCloudDAO) GetSyncHistoryList(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) ([]*model.CloudResourceSyncHistory, int64, error) { + var histories []*model.CloudResourceSyncHistory + var total int64 + + query := d.db.WithContext(ctx).Model(&model.CloudResourceSyncHistory{}) + + // 添加查询条件 + if req.CloudAccountID != 0 { + query = query.Where("cloud_account_id = ?", req.CloudAccountID) + } + + if req.SyncStatus != "" { + query = query.Where("sync_status = ?", req.SyncStatus) + } + + if req.Search != "" { + query = query.Where("error_message LIKE ?", "%"+req.Search+"%") + } + + // 计算总数 + if err := query.Count(&total).Error; err != nil { + d.logger.Error("获取同步历史总数失败", zap.Error(err)) + return nil, 0, err + } + + // 分页查询 + offset := (req.Page - 1) * req.Size + if err := query.Order("created_at DESC"). + Limit(req.Size). + Offset(offset). + Find(&histories).Error; err != nil { + d.logger.Error("获取同步历史列表失败", zap.Error(err)) + return nil, 0, err + } + + return histories, total, nil +} + +// CreateChangeLog 创建变更日志 +func (d *treeCloudDAO) CreateChangeLog(ctx context.Context, log *model.CloudResourceChangeLog) error { + if err := d.db.WithContext(ctx).Create(log).Error; err != nil { + d.logger.Error("创建变更日志失败", zap.Error(err)) + return err + } + + return nil +} + +// GetChangeLogList 获取变更日志列表 +func (d *treeCloudDAO) GetChangeLogList(ctx context.Context, req *model.GetCloudResourceChangeLogReq) ([]*model.CloudResourceChangeLog, int64, error) { + var logs []*model.CloudResourceChangeLog + var total int64 + + query := d.db.WithContext(ctx).Model(&model.CloudResourceChangeLog{}) + + // 添加查询条件 + if req.ResourceID != 0 { + query = query.Where("resource_id = ?", req.ResourceID) + } + + if req.ChangeType != "" { + query = query.Where("change_type = ?", req.ChangeType) + } + + if req.Search != "" { + query = query.Where("instance_id LIKE ? OR field_name LIKE ?", + "%"+req.Search+"%", "%"+req.Search+"%") + } + + // 计算总数 + if err := query.Count(&total).Error; err != nil { + d.logger.Error("获取变更日志总数失败", zap.Error(err)) + return nil, 0, err + } + + // 分页查询 + offset := (req.Page - 1) * req.Size + if err := query.Order("change_time DESC"). + Limit(req.Size). + Offset(offset). + Find(&logs).Error; err != nil { + d.logger.Error("获取变更日志列表失败", zap.Error(err)) + return nil, 0, err + } + + return logs, total, nil +} diff --git a/internal/tree/dao/tree_local_dao.go b/internal/tree/dao/tree_local_dao.go index 01d6cabc..cb9d8f89 100644 --- a/internal/tree/dao/tree_local_dao.go +++ b/internal/tree/dao/tree_local_dao.go @@ -30,6 +30,7 @@ import ( "errors" "github.com/GoSimplicity/AI-CloudOps/internal/model" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" "go.uber.org/zap" "gorm.io/gorm" ) @@ -172,7 +173,7 @@ func (d *treeLocalDAO) BatchGetByIDs(ctx context.Context, ids []int) ([]*model.T // BindTreeNodes 绑定树节点 func (d *treeLocalDAO) BindTreeNodes(ctx context.Context, localID int, treeNodeIds []int) error { - if len(treeNodeIds) == 0 { + if !treeUtils.ValidateTreeNodeIDs(treeNodeIds) { d.logger.Info("没有需要绑定的树节点") return nil } @@ -202,7 +203,7 @@ func (d *treeLocalDAO) BindTreeNodes(ctx context.Context, localID int, treeNodeI } func (d *treeLocalDAO) UnBindTreeNodes(ctx context.Context, localID int, treeNodeIds []int) error { - if len(treeNodeIds) == 0 { + if !treeUtils.ValidateTreeNodeIDs(treeNodeIds) { d.logger.Info("没有需要解绑的树节点") return nil } diff --git a/internal/tree/dao/tree_node_dao.go b/internal/tree/dao/tree_node_dao.go index 6a650ab3..921db318 100644 --- a/internal/tree/dao/tree_node_dao.go +++ b/internal/tree/dao/tree_node_dao.go @@ -31,6 +31,7 @@ import ( "strings" "github.com/GoSimplicity/AI-CloudOps/internal/model" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" "go.uber.org/zap" "gorm.io/gorm" ) @@ -94,47 +95,27 @@ func (t *treeNodeDAO) GetTreeList(ctx context.Context, req *model.GetTreeNodeLis return nil, 0, err } - // 预加载关联数据并查询 - if err := query.Preload("AdminUsers"). + // 分页查询 + offset := (req.Page - 1) * req.Size + if err := query. + Preload("AdminUsers"). Preload("MemberUsers"). Preload("TreeLocalResources"). Order("level ASC, parent_id ASC, name ASC"). + Limit(req.Size). + Offset(offset). Find(&nodes).Error; err != nil { t.logger.Error("获取树节点列表失败", zap.Error(err)) return nil, 0, err } - // 如果指定了层级,直接返回列表 + // 指定层级时直接返回列表 if req.Level > 0 { return nodes, count, nil } // 构建树形结构 - return t.buildTreeStructure(nodes), count, nil -} - -// buildTreeStructure 构建树形结构 -func (t *treeNodeDAO) buildTreeStructure(nodes []*model.TreeNode) []*model.TreeNode { - nodeMap := make(map[int]*model.TreeNode) - var rootNodes []*model.TreeNode - - for _, node := range nodes { - nodeClone := *node - nodeClone.Children = make([]*model.TreeNode, 0) - nodeMap[node.ID] = &nodeClone - } - - for _, node := range nodes { - currentNode := nodeMap[node.ID] - if node.ParentID == 0 || nodeMap[node.ParentID] == nil { - rootNodes = append(rootNodes, currentNode) - } else { - parent := nodeMap[node.ParentID] - parent.Children = append(parent.Children, currentNode) - } - } - - return rootNodes + return treeUtils.BuildTreeStructure(nodes), count, nil } // GetNode 获取节点详情 @@ -172,41 +153,53 @@ func (t *treeNodeDAO) GetChildNodes(ctx context.Context, parentID int) ([]*model // GetTreeStatistics 获取服务树统计数据 func (t *treeNodeDAO) GetTreeStatistics(ctx context.Context) (*model.TreeNodeStatisticsResp, error) { var stats model.TreeNodeStatisticsResp + var count int64 // 节点总数 - if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Count((*int64)(&[]int64{0}[0])).Error; err != nil { + if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Count(&count).Error; err != nil { t.logger.Error("统计节点总数失败", zap.Error(err)) - } - // 为了避免使用中间变量,这里分别统计并赋值 - var c int64 - if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Count(&c).Error; err == nil { - stats.TotalNodes = int(c) + } else { + stats.TotalNodes = int(count) } - // 活跃/非活跃 - c = 0 - if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("status = ?", model.ACTIVE).Count(&c).Error; err == nil { - stats.ActiveNodes = int(c) + // 活跃节点数 + count = 0 + if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("status = ?", model.ACTIVE).Count(&count).Error; err != nil { + t.logger.Error("统计活跃节点失败", zap.Error(err)) + } else { + stats.ActiveNodes = int(count) } - c = 0 - if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("status = ?", model.INACTIVE).Count(&c).Error; err == nil { - stats.InactiveNodes = int(c) + + // 非活跃节点数 + count = 0 + if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("status = ?", model.INACTIVE).Count(&count).Error; err != nil { + t.logger.Error("统计非活跃节点失败", zap.Error(err)) + } else { + stats.InactiveNodes = int(count) } // 资源总数 - c = 0 - if err := t.db.WithContext(ctx).Model(&model.TreeLocalResource{}).Count(&c).Error; err == nil { - stats.TotalResources = int(c) + count = 0 + if err := t.db.WithContext(ctx).Model(&model.TreeLocalResource{}).Count(&count).Error; err != nil { + t.logger.Error("统计资源总数失败", zap.Error(err)) + } else { + stats.TotalResources = int(count) } - // 管理员与成员总数(关联关系条目数) - c = 0 - if err := t.db.WithContext(ctx).Table("cl_tree_node_admin").Count(&c).Error; err == nil { - stats.TotalAdmins = int(c) + // 管理员总数 + count = 0 + if err := t.db.WithContext(ctx).Table("cl_tree_node_admin").Count(&count).Error; err != nil { + t.logger.Error("统计管理员总数失败", zap.Error(err)) + } else { + stats.TotalAdmins = int(count) } - c = 0 - if err := t.db.WithContext(ctx).Table("cl_tree_node_member").Count(&c).Error; err == nil { - stats.TotalMembers = int(c) + + // 成员总数 + count = 0 + if err := t.db.WithContext(ctx).Table("cl_tree_node_member").Count(&count).Error; err != nil { + t.logger.Error("统计成员总数失败", zap.Error(err)) + } else { + stats.TotalMembers = int(count) } return &stats, nil @@ -300,7 +293,7 @@ func (t *treeNodeDAO) UpdateNode(ctx context.Context, node *model.TreeNode) erro // 如果父节点发生变化,需要验证和计算层级 if node.ParentID != existingNode.ParentID { - // 验证新父节点存在(如果不是根节点) + // 验证新父节点存在 if node.ParentID != 0 { var count int64 if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("id = ?", node.ParentID).Count(&count).Error; err != nil { @@ -464,6 +457,11 @@ func (t *treeNodeDAO) DeleteNode(ctx context.Context, id int) error { // BindResource 绑定资源到节点 func (t *treeNodeDAO) BindResource(ctx context.Context, nodeId int, resourceIds []int) error { + // 验证资源ID列表 + if err := treeUtils.ValidateResourceIDs(resourceIds); err != nil { + return err + } + // 验证节点存在 var count int64 if err := t.db.WithContext(ctx).Model(&model.TreeNode{}).Where("id = ?", nodeId).Count(&count).Error; err != nil { @@ -560,7 +558,7 @@ func (t *treeNodeDAO) GetNodeMembers(ctx context.Context, nodeId int, memberType return nil, err } case "all", "": - // 获取所有用户(管理员+成员) + // 获取所有用户 var adminUsers []*model.User var memberUsers []*model.User diff --git a/internal/tree/service/cloud_account_service.go b/internal/tree/service/cloud_account_service.go new file mode 100644 index 00000000..9828c92b --- /dev/null +++ b/internal/tree/service/cloud_account_service.go @@ -0,0 +1,307 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package service + +import ( + "context" + "errors" + "fmt" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "github.com/GoSimplicity/AI-CloudOps/internal/tree/dao" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type CloudAccountService interface { + GetCloudAccountList(ctx context.Context, req *model.GetCloudAccountListReq) (model.ListResp[*model.CloudAccount], error) + GetCloudAccountDetail(ctx context.Context, req *model.GetCloudAccountDetailReq) (*model.CloudAccount, error) + CreateCloudAccount(ctx context.Context, req *model.CreateCloudAccountReq) error + UpdateCloudAccount(ctx context.Context, req *model.UpdateCloudAccountReq) error + DeleteCloudAccount(ctx context.Context, req *model.DeleteCloudAccountReq) error + UpdateCloudAccountStatus(ctx context.Context, req *model.UpdateCloudAccountStatusReq) error + VerifyCloudAccount(ctx context.Context, req *model.VerifyCloudAccountReq) error +} + +type cloudAccountService struct { + logger *zap.Logger + dao dao.CloudAccountDAO +} + +func NewCloudAccountService(logger *zap.Logger, dao dao.CloudAccountDAO) CloudAccountService { + return &cloudAccountService{ + logger: logger, + dao: dao, + } +} + +// GetCloudAccountList 获取云账户列表 +func (s *cloudAccountService) GetCloudAccountList(ctx context.Context, req *model.GetCloudAccountListReq) (model.ListResp[*model.CloudAccount], error) { + // 兜底分页参数 + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) + + accounts, total, err := s.dao.GetList(ctx, req) + if err != nil { + s.logger.Error("获取云账户列表失败", zap.Error(err)) + return model.ListResp[*model.CloudAccount]{}, err + } + + return model.ListResp[*model.CloudAccount]{ + Items: accounts, + Total: total, + }, nil +} + +// GetCloudAccountDetail 获取云账户详情 +func (s *cloudAccountService) GetCloudAccountDetail(ctx context.Context, req *model.GetCloudAccountDetailReq) (*model.CloudAccount, error) { + if err := treeUtils.ValidateID(req.ID); err != nil { + return nil, fmt.Errorf("无效的云账户ID: %w", err) + } + + account, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("云账户不存在") + } + s.logger.Error("获取云账户详情失败", zap.Int("id", req.ID), zap.Error(err)) + return nil, err + } + + return account, nil +} + +// CreateCloudAccount 创建云账户 +func (s *cloudAccountService) CreateCloudAccount(ctx context.Context, req *model.CreateCloudAccountReq) error { + // 加密 AccessKey 和 SecretKey + encryptedAccessKey, err := treeUtils.EncryptPassword(req.AccessKey) + if err != nil { + s.logger.Error("加密AccessKey失败", zap.Error(err)) + return fmt.Errorf("加密AccessKey失败: %w", err) + } + + encryptedSecretKey, err := treeUtils.EncryptPassword(req.SecretKey) + if err != nil { + s.logger.Error("加密SecretKey失败", zap.Error(err)) + return fmt.Errorf("加密SecretKey失败: %w", err) + } + + // 创建云账户对象 + account := &model.CloudAccount{ + Name: req.Name, + Provider: req.Provider, + Region: req.Region, + AccessKey: encryptedAccessKey, + SecretKey: encryptedSecretKey, + AccountID: req.AccountID, + AccountName: req.AccountName, + AccountAlias: req.AccountAlias, + Description: req.Description, + Status: model.CloudAccountEnabled, // 默认启用 + CreateUserID: req.CreateUserID, + CreateUserName: req.CreateUserName, + } + + if err := s.dao.Create(ctx, account); err != nil { + s.logger.Error("创建云账户失败", zap.Error(err)) + return err + } + + return nil +} + +// UpdateCloudAccount 更新云账户 +func (s *cloudAccountService) UpdateCloudAccount(ctx context.Context, req *model.UpdateCloudAccountReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云账户ID: %w", err) + } + + // 检查云账户是否存在 + _, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云账户不存在") + } + return err + } + + // 构建更新对象 + account := &model.CloudAccount{ + Model: model.Model{ID: req.ID}, + Name: req.Name, + AccountID: req.AccountID, + AccountName: req.AccountName, + AccountAlias: req.AccountAlias, + Description: req.Description, + } + + // 如果需要更新 AccessKey + if req.AccessKey != "" { + encryptedAccessKey, err := treeUtils.EncryptPassword(req.AccessKey) + if err != nil { + s.logger.Error("加密AccessKey失败", zap.Error(err)) + return fmt.Errorf("加密AccessKey失败: %w", err) + } + account.AccessKey = encryptedAccessKey + } + + // 如果需要更新 SecretKey + if req.SecretKey != "" { + encryptedSecretKey, err := treeUtils.EncryptPassword(req.SecretKey) + if err != nil { + s.logger.Error("加密SecretKey失败", zap.Error(err)) + return fmt.Errorf("加密SecretKey失败: %w", err) + } + account.SecretKey = encryptedSecretKey + } + + if err := s.dao.Update(ctx, account); err != nil { + s.logger.Error("更新云账户失败", zap.Error(err)) + return err + } + + return nil +} + +// DeleteCloudAccount 删除云账户 +func (s *cloudAccountService) DeleteCloudAccount(ctx context.Context, req *model.DeleteCloudAccountReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云账户ID: %w", err) + } + + // 检查云账户是否存在 + account, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云账户不存在") + } + return err + } + + // 检查是否有关联的云资源 + if len(account.CloudResources) > 0 { + return fmt.Errorf("云账户下还有 %d 个云资源,请先删除相关资源", len(account.CloudResources)) + } + + if err := s.dao.Delete(ctx, req.ID); err != nil { + s.logger.Error("删除云账户失败", zap.Error(err)) + return err + } + + return nil +} + +// UpdateCloudAccountStatus 更新云账户状态 +func (s *cloudAccountService) UpdateCloudAccountStatus(ctx context.Context, req *model.UpdateCloudAccountStatusReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云账户ID: %w", err) + } + + if err := s.dao.UpdateStatus(ctx, req.ID, req.Status); err != nil { + s.logger.Error("更新云账户状态失败", zap.Error(err)) + return err + } + + return nil +} + +// VerifyCloudAccount 验证云账户凭证 +func (s *cloudAccountService) VerifyCloudAccount(ctx context.Context, req *model.VerifyCloudAccountReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云账户ID: %w", err) + } + + account, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云账户不存在") + } + return err + } + + // 解密密钥 + accessKey, err := treeUtils.DecryptPassword(account.AccessKey) + if err != nil { + s.logger.Error("解密AccessKey失败", zap.Error(err)) + return fmt.Errorf("解密AccessKey失败: %w", err) + } + + secretKey, err := treeUtils.DecryptPassword(account.SecretKey) + if err != nil { + s.logger.Error("解密SecretKey失败", zap.Error(err)) + return fmt.Errorf("解密SecretKey失败: %w", err) + } + + // 根据 Provider 调用相应的云厂商 SDK 验证凭证 + verifyReq := &model.VerifyCloudCredentialsReq{ + Provider: account.Provider, + Region: account.Region, + AccessKey: accessKey, + SecretKey: secretKey, + } + + switch account.Provider { + case model.ProviderAliyun: + if err := treeUtils.VerifyAliyunCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("阿里云凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("阿里云凭证验证失败: %w", err) + } + case model.ProviderTencent: + if err := treeUtils.VerifyTencentCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("腾讯云凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("腾讯云凭证验证失败: %w", err) + } + case model.ProviderAWS: + if err := treeUtils.VerifyAWSCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("AWS凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("AWS凭证验证失败: %w", err) + } + case model.ProviderHuawei: + if err := treeUtils.VerifyHuaweiCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("华为云凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("华为云凭证验证失败: %w", err) + } + case model.ProviderAzure: + if err := treeUtils.VerifyAzureCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("Azure凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("Azure凭证验证失败: %w", err) + } + case model.ProviderGCP: + if err := treeUtils.VerifyGCPCredentials(ctx, verifyReq, s.logger); err != nil { + s.logger.Error("GCP凭证验证失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("GCP凭证验证失败: %w", err) + } + default: + return fmt.Errorf("不支持的云厂商: %d", account.Provider) + } + + s.logger.Info("云账户凭证验证成功", + zap.Int("id", req.ID), + zap.Int8("provider", int8(account.Provider)), + zap.String("region", account.Region)) + + return nil +} diff --git a/internal/tree/service/tree_cloud_service.go b/internal/tree/service/tree_cloud_service.go new file mode 100644 index 00000000..e263086e --- /dev/null +++ b/internal/tree/service/tree_cloud_service.go @@ -0,0 +1,684 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package service + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "github.com/GoSimplicity/AI-CloudOps/internal/tree/dao" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" + "go.uber.org/zap" + "gorm.io/gorm" +) + +type TreeCloudService interface { + GetTreeCloudResourceList(ctx context.Context, req *model.GetTreeCloudResourceListReq) (model.ListResp[*model.TreeCloudResource], error) + GetTreeCloudResourceDetail(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) + GetTreeCloudResourceForConnection(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) + GetTreeNodeCloudResources(ctx context.Context, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) + SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) (*model.SyncCloudResourceResp, error) + GetSyncHistory(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) (model.ListResp[*model.CloudResourceSyncHistory], error) + UpdateTreeCloudResource(ctx context.Context, req *model.UpdateTreeCloudResourceReq) error + DeleteTreeCloudResource(ctx context.Context, req *model.DeleteTreeCloudResourceReq) error + UpdateCloudResourceStatus(ctx context.Context, req *model.UpdateCloudResourceStatusReq) error + BindTreeCloudResource(ctx context.Context, req *model.BindTreeCloudResourceReq) error + UnBindTreeCloudResource(ctx context.Context, req *model.UnBindTreeCloudResourceReq) error + GetChangeLog(ctx context.Context, req *model.GetCloudResourceChangeLogReq) (model.ListResp[*model.CloudResourceChangeLog], error) +} + +type treeCloudService struct { + logger *zap.Logger + dao dao.TreeCloudDAO + cloudAccountDAO dao.CloudAccountDAO +} + +func NewTreeCloudService(logger *zap.Logger, dao dao.TreeCloudDAO, cloudAccountDAO dao.CloudAccountDAO) TreeCloudService { + return &treeCloudService{ + logger: logger, + dao: dao, + cloudAccountDAO: cloudAccountDAO, + } +} + +// GetTreeCloudResourceList 获取云资源列表 +func (s *treeCloudService) GetTreeCloudResourceList(ctx context.Context, req *model.GetTreeCloudResourceListReq) (model.ListResp[*model.TreeCloudResource], error) { + // 兜底分页参数,避免offset为负或size为0 + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) + + clouds, total, err := s.dao.GetList(ctx, req) + if err != nil { + s.logger.Error("获取云资源列表失败", zap.Error(err)) + return model.ListResp[*model.TreeCloudResource]{}, err + } + + return model.ListResp[*model.TreeCloudResource]{ + Items: clouds, + Total: total, + }, nil +} + +// GetTreeCloudResourceDetail 获取云资源详情 +func (s *treeCloudService) GetTreeCloudResourceDetail(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) { + if err := treeUtils.ValidateID(req.ID); err != nil { + return nil, fmt.Errorf("无效的云资源ID: %w", err) + } + + cloud, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("云资源不存在") + } + s.logger.Error("获取云资源详情失败", zap.Int("id", req.ID), zap.Error(err)) + return nil, err + } + + return cloud, nil +} + +// GetTreeCloudResourceForConnection 获取用于连接的云资源详情(包含解密后的密码) +func (s *treeCloudService) GetTreeCloudResourceForConnection(ctx context.Context, req *model.GetTreeCloudResourceDetailReq) (*model.TreeCloudResource, error) { + if err := treeUtils.ValidateID(req.ID); err != nil { + return nil, fmt.Errorf("无效的云资源ID: %w", err) + } + + cloud, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("云资源不存在") + } + s.logger.Error("获取云资源详情失败", zap.Int("id", req.ID), zap.Error(err)) + return nil, err + } + + // 解密SSH密码 + if cloud.AuthMode == model.AuthModePassword && cloud.Password != "" { + plainPassword, err := treeUtils.DecryptPassword(cloud.Password) + if err != nil { + s.logger.Error("SSH密码解密失败", zap.Int("id", req.ID), zap.Error(err)) + return nil, fmt.Errorf("SSH密码解密失败: %w", err) + } + cloud.Password = plainPassword + } + + return cloud, nil +} + +// UpdateTreeCloudResource 更新云资源本地元数据 +func (s *treeCloudService) UpdateTreeCloudResource(ctx context.Context, req *model.UpdateTreeCloudResourceReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云资源ID: %w", err) + } + + // 检查资源是否存在 + _, err := s.dao.GetByID(ctx, req.ID) + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + return errors.New("云资源不存在") + case err != nil: + s.logger.Error("获取云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + // 构建要更新的字段map + metadata := make(map[string]interface{}) + + // 只添加非空字段 + if req.Environment != "" { + metadata["environment"] = req.Environment + } + if req.Description != "" { + metadata["description"] = req.Description + } + if req.Tags != nil { + metadata["tags"] = req.Tags + } + if req.Port > 0 { + metadata["port"] = req.Port + } + if req.Username != "" { + metadata["username"] = req.Username + } + if req.Password != "" { + // 加密SSH密码 + encrypted, err := treeUtils.EncryptPassword(req.Password) + if err != nil { + s.logger.Error("密码加密失败", zap.Error(err)) + return fmt.Errorf("密码加密失败: %w", err) + } + metadata["password"] = encrypted + } + if req.Key != "" { + metadata["key"] = req.Key + } + if req.AuthMode > 0 { + metadata["auth_mode"] = req.AuthMode + } + + // 如果没有字段需要更新 + if len(metadata) == 0 { + s.logger.Info("没有字段需要更新", zap.Int("id", req.ID)) + return nil + } + + // 更新元数据 + if err := s.dao.UpdateMetadata(ctx, req.ID, metadata); err != nil { + s.logger.Error("更新云资源元数据失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + // 记录变更日志 + // 获取资源实例ID用于日志 + resource, _ := s.dao.GetByID(ctx, req.ID) + instanceID := "" + if resource != nil { + instanceID = resource.InstanceID + } + + // 为每个更新的字段创建变更日志 + for fieldName, newValue := range metadata { + changeLog := &model.CloudResourceChangeLog{ + ResourceID: req.ID, + InstanceID: instanceID, + ChangeType: model.ChangeTypeUpdated, + FieldName: fieldName, + OldValue: "", + NewValue: fmt.Sprintf("%v", newValue), + ChangeSource: model.ChangeSourceManual, + OperatorID: req.OperatorID, + OperatorName: req.OperatorName, + ChangeTime: time.Now(), + } + // 异步记录,不影响主流程 + go func(log *model.CloudResourceChangeLog) { + if err := s.dao.CreateChangeLog(context.Background(), log); err != nil { + s.logger.Error("记录变更日志失败", zap.Error(err)) + } + }(changeLog) + } + + s.logger.Info("更新云资源元数据成功", zap.Int("id", req.ID), zap.Int("fields", len(metadata))) + return nil +} + +// DeleteTreeCloudResource 删除云资源 +func (s *treeCloudService) DeleteTreeCloudResource(ctx context.Context, req *model.DeleteTreeCloudResourceReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云资源ID: %w", err) + } + + // 获取资源信息用于日志记录 + cloud, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云资源不存在") + } + s.logger.Error("获取云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + // 记录删除日志 + s.recordChangeLog(ctx, cloud, nil, model.ChangeSourceManual, req.OperatorID, req.OperatorName) + + if err := s.dao.Delete(ctx, req.ID); err != nil { + s.logger.Error("删除云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + s.logger.Info("从平台删除云资源成功", + zap.Int("id", req.ID), + zap.String("instanceID", cloud.InstanceID), + zap.String("name", cloud.Name)) + return nil +} + +// BindTreeCloudResource 绑定云资源到树节点 +func (s *treeCloudService) BindTreeCloudResource(ctx context.Context, req *model.BindTreeCloudResourceReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云资源ID: %w", err) + } + + if err := s.dao.BindTreeNodes(ctx, req.ID, req.TreeNodeIDs); err != nil { + s.logger.Error("绑定云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + return nil +} + +// UnBindTreeCloudResource 解绑云资源与树节点 +func (s *treeCloudService) UnBindTreeCloudResource(ctx context.Context, req *model.UnBindTreeCloudResourceReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云资源ID: %w", err) + } + + if err := s.dao.UnBindTreeNodes(ctx, req.ID, req.TreeNodeIDs); err != nil { + s.logger.Error("解绑云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + return nil +} + +// GetTreeNodeCloudResources 获取树节点下的云资源 +func (s *treeCloudService) GetTreeNodeCloudResources(ctx context.Context, req *model.GetTreeNodeCloudResourcesReq) ([]*model.TreeCloudResource, error) { + if err := treeUtils.ValidateID(req.NodeID); err != nil { + return nil, fmt.Errorf("无效的节点ID: %w", err) + } + + clouds, err := s.dao.GetByNodeID(ctx, req.NodeID, req) + if err != nil { + s.logger.Error("获取树节点云资源失败", zap.Int("nodeID", req.NodeID), zap.Error(err)) + return nil, err + } + + return clouds, nil +} + +// UpdateCloudResourceStatus 更新云资源状态 +func (s *treeCloudService) UpdateCloudResourceStatus(ctx context.Context, req *model.UpdateCloudResourceStatusReq) error { + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的云资源ID: %w", err) + } + + // 检查云资源是否存在 + _, err := s.dao.GetByID(ctx, req.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("云资源不存在") + } + s.logger.Error("获取云资源失败", zap.Int("id", req.ID), zap.Error(err)) + return err + } + + if err := s.dao.UpdateStatus(ctx, req.ID, req.Status); err != nil { + s.logger.Error("更新云资源状态失败", zap.Int("id", req.ID), zap.Int8("status", int8(req.Status)), zap.Error(err)) + return err + } + + return nil +} + +// SyncTreeCloudResource 从云厂商同步资源 +func (s *treeCloudService) SyncTreeCloudResource(ctx context.Context, req *model.SyncTreeCloudResourceReq) (*model.SyncCloudResourceResp, error) { + startTime := time.Now() + + // 设置默认的同步模式 + if req.SyncMode == "" { + req.SyncMode = model.SyncModeIncremental + } + + // 初始化同步响应 + resp := &model.SyncCloudResourceResp{ + SyncTime: startTime, + FailedInstances: []string{}, + } + + // 创建同步历史记录 + syncHistory := &model.CloudResourceSyncHistory{ + CloudAccountID: req.CloudAccountID, + SyncMode: req.SyncMode, + StartTime: startTime, + SyncStatus: "running", + } + + // 获取云账户信息 + account, err := s.cloudAccountDAO.GetByID(ctx, req.CloudAccountID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("云账户不存在") + } + s.logger.Error("获取云账户失败", zap.Int("cloudAccountID", req.CloudAccountID), zap.Error(err)) + return nil, err + } + + // 检查云账户状态 + if account.Status != model.CloudAccountEnabled { + return nil, errors.New("云账户已禁用,无法同步资源") + } + + // 解密密钥 + accessKey, err := treeUtils.DecryptPassword(account.AccessKey) + if err != nil { + s.logger.Error("解密AccessKey失败", zap.Error(err)) + syncHistory.SyncStatus = "failed" + syncHistory.ErrorMessage = fmt.Sprintf("解密AccessKey失败: %v", err) + s.saveSyncHistory(ctx, syncHistory) + return nil, fmt.Errorf("解密AccessKey失败: %w", err) + } + + secretKey, err := treeUtils.DecryptPassword(account.SecretKey) + if err != nil { + s.logger.Error("解密SecretKey失败", zap.Error(err)) + syncHistory.SyncStatus = "failed" + syncHistory.ErrorMessage = fmt.Sprintf("解密SecretKey失败: %v", err) + s.saveSyncHistory(ctx, syncHistory) + return nil, fmt.Errorf("解密SecretKey失败: %w", err) + } + + s.logger.Info("开始同步云资源", + zap.Int("cloudAccountID", req.CloudAccountID), + zap.Int8("provider", int8(account.Provider)), + zap.String("region", account.Region), + zap.String("syncMode", string(req.SyncMode))) + + // 根据不同的云厂商调用对应的同步逻辑 + var syncErr error + switch account.Provider { + case model.ProviderAliyun: + syncErr = s.syncAliyunResourcesWithStats(ctx, account, accessKey, secretKey, req, resp) + case model.ProviderTencent: + syncErr = errors.New("腾讯云资源同步功能暂未实现") + case model.ProviderAWS: + syncErr = errors.New("AWS资源同步功能暂未实现") + case model.ProviderHuawei: + syncErr = errors.New("华为云资源同步功能暂未实现") + case model.ProviderAzure: + syncErr = errors.New("Azure资源同步功能暂未实现") + case model.ProviderGCP: + syncErr = errors.New("GCP资源同步功能暂未实现") + default: + syncErr = fmt.Errorf("不支持的云厂商: %d", account.Provider) + } + + // 更新同步历史记录 + endTime := time.Now() + syncHistory.EndTime = endTime + syncHistory.Duration = int(endTime.Sub(startTime).Seconds()) + syncHistory.TotalCount = resp.TotalCount + syncHistory.NewCount = resp.NewCount + syncHistory.UpdateCount = resp.UpdateCount + syncHistory.DeleteCount = resp.DeleteCount + syncHistory.FailedCount = resp.FailedCount + + if len(resp.FailedInstances) > 0 { + // 将失败的实例ID列表转为JSON字符串 + failedJSON, _ := json.Marshal(resp.FailedInstances) + syncHistory.FailedInstances = string(failedJSON) + } + + if syncErr != nil { + syncHistory.SyncStatus = "failed" + syncHistory.ErrorMessage = syncErr.Error() + s.saveSyncHistory(ctx, syncHistory) + return resp, syncErr + } + + if resp.FailedCount > 0 { + syncHistory.SyncStatus = "partial" + } else { + syncHistory.SyncStatus = "success" + } + + s.saveSyncHistory(ctx, syncHistory) + + s.logger.Info("云资源同步完成", + zap.Int("total", resp.TotalCount), + zap.Int("new", resp.NewCount), + zap.Int("update", resp.UpdateCount), + zap.Int("delete", resp.DeleteCount), + zap.Int("failed", resp.FailedCount), + zap.Duration("duration", endTime.Sub(startTime))) + + return resp, nil +} + +// syncAliyunResourcesWithStats 同步阿里云资源并返回统计信息 +func (s *treeCloudService) syncAliyunResourcesWithStats(ctx context.Context, account *model.CloudAccount, accessKey, secretKey string, req *model.SyncTreeCloudResourceReq, resp *model.SyncCloudResourceResp) error { + // 构建同步配置 + config := &treeUtils.AliyunSyncConfig{ + AccessKey: accessKey, + SecretKey: secretKey, + Region: account.Region, + CloudAccountID: account.ID, + ResourceType: 0, // 暂时只同步ECS + InstanceIDs: req.InstanceIDs, + SyncMode: req.SyncMode, + } + + // 从阿里云获取资源列表 + resources, err := treeUtils.SyncAliyunResources(ctx, config, s.logger) + if err != nil { + return err + } + + // 根据同步模式处理资源 + if req.SyncMode == model.SyncModeFull { + // 全量同步:先删除该云账户下的所有ECS资源,再重新创建 + return s.fullSyncResources(ctx, account.ID, resources, resp, req.AutoBind, req.BindNodeID, req.OperatorID, req.OperatorName) + } + + // 增量同步:更新已存在的资源,创建不存在的资源 + return s.incrementalSyncResources(ctx, account.ID, resources, resp, req.AutoBind, req.BindNodeID, req.OperatorID, req.OperatorName) +} + +// fullSyncResources 全量同步资源 +func (s *treeCloudService) fullSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource, resp *model.SyncCloudResourceResp, autoBind bool, bindNodeID int, operatorID int, operatorName string) error { + // 获取该云账户下的所有ECS资源 + req := &model.GetTreeCloudResourceListReq{ + ListReq: model.ListReq{ + Page: 1, + Size: 10000, // 获取所有资源 + }, + CloudAccountID: cloudAccountID, + ResourceType: model.ResourceTypeECS, + } + existingResources, _, err := s.dao.GetList(ctx, req) + if err != nil { + s.logger.Error("获取现有资源失败", zap.Error(err)) + return err + } + + // 删除不在新资源列表中的资源 + newInstanceIDSet := make(map[string]bool) + for _, resource := range resources { + newInstanceIDSet[resource.InstanceID] = true + } + + for _, existingResource := range existingResources { + if !newInstanceIDSet[existingResource.InstanceID] { + if err := s.dao.Delete(ctx, existingResource.ID); err != nil { + s.logger.Error("删除资源失败", zap.Int("id", existingResource.ID), zap.Error(err)) + resp.FailedCount++ + resp.FailedInstances = append(resp.FailedInstances, existingResource.InstanceID) + } else { + resp.DeleteCount++ + // 记录删除日志 + s.recordChangeLog(ctx, existingResource, nil, model.ChangeSourceSync, operatorID, operatorName) + } + } + } + + // 更新或创建资源 + return s.incrementalSyncResources(ctx, cloudAccountID, resources, resp, autoBind, bindNodeID, operatorID, operatorName) +} + +// incrementalSyncResources 增量同步资源 +func (s *treeCloudService) incrementalSyncResources(ctx context.Context, cloudAccountID int, resources []*model.TreeCloudResource, resp *model.SyncCloudResourceResp, autoBind bool, bindNodeID int, operatorID int, operatorName string) error { + for _, resource := range resources { + resp.TotalCount++ + + // 检查资源是否已存在 + existing, err := s.dao.GetByAccountAndInstanceID(ctx, cloudAccountID, resource.InstanceID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.logger.Error("查询资源失败", zap.String("instanceID", resource.InstanceID), zap.Error(err)) + resp.FailedCount++ + resp.FailedInstances = append(resp.FailedInstances, resource.InstanceID) + continue + } + + if existing != nil { + // 更新现有资源 + resource.ID = existing.ID + if err := s.dao.Update(ctx, resource); err != nil { + s.logger.Error("更新资源失败", zap.Int("id", existing.ID), zap.Error(err)) + resp.FailedCount++ + resp.FailedInstances = append(resp.FailedInstances, resource.InstanceID) + } else { + resp.UpdateCount++ + // 记录更新日志 + s.recordChangeLog(ctx, existing, resource, model.ChangeSourceSync, operatorID, operatorName) + } + } else { + // 创建新资源 + if err := s.dao.Create(ctx, resource); err != nil { + s.logger.Error("创建资源失败", zap.String("instanceID", resource.InstanceID), zap.Error(err)) + resp.FailedCount++ + resp.FailedInstances = append(resp.FailedInstances, resource.InstanceID) + } else { + resp.NewCount++ + // 记录创建日志 + s.recordChangeLog(ctx, nil, resource, model.ChangeSourceSync, operatorID, operatorName) + + // 如果启用自动绑定,则绑定到指定节点 + if autoBind && bindNodeID > 0 { + if err := s.dao.BindTreeNodes(ctx, resource.ID, []int{bindNodeID}); err != nil { + s.logger.Error("自动绑定资源到节点失败", + zap.Int("resourceID", resource.ID), + zap.Int("nodeID", bindNodeID), + zap.Error(err)) + } + } + } + } + } + + return nil +} + +// recordChangeLog 记录资源变更日志 +func (s *treeCloudService) recordChangeLog(ctx context.Context, oldResource, newResource *model.TreeCloudResource, source string, operatorID int, operatorName string) { + // 如果是删除操作 + if oldResource != nil && newResource == nil { + changeLog := &model.CloudResourceChangeLog{ + ResourceID: oldResource.ID, + InstanceID: oldResource.InstanceID, + ChangeType: model.ChangeTypeDeleted, + FieldName: "", + OldValue: oldResource.Name, + NewValue: "", + ChangeSource: source, + OperatorID: operatorID, + OperatorName: operatorName, + ChangeTime: time.Now(), + } + // 保存变更日志 + if err := s.dao.CreateChangeLog(ctx, changeLog); err != nil { + s.logger.Error("保存删除日志失败", zap.Error(err)) + } + return + } + + // 如果是创建操作 + if oldResource == nil && newResource != nil { + changeLog := &model.CloudResourceChangeLog{ + ResourceID: newResource.ID, + InstanceID: newResource.InstanceID, + ChangeType: model.ChangeTypeCreated, + FieldName: "", + OldValue: "", + NewValue: newResource.Name, + ChangeSource: source, + OperatorID: operatorID, + OperatorName: operatorName, + ChangeTime: time.Now(), + } + // 保存变更日志 + if err := s.dao.CreateChangeLog(ctx, changeLog); err != nil { + s.logger.Error("保存创建日志失败", zap.Error(err)) + } + return + } + + // 如果是更新操作,比较字段变化 + if oldResource != nil && newResource != nil { + // 比较状态 + if oldResource.Status != newResource.Status { + changeLog := &model.CloudResourceChangeLog{ + ResourceID: newResource.ID, + InstanceID: newResource.InstanceID, + ChangeType: model.ChangeTypeStatusChanged, + FieldName: "status", + OldValue: fmt.Sprintf("%d", oldResource.Status), + NewValue: fmt.Sprintf("%d", newResource.Status), + ChangeSource: source, + OperatorID: operatorID, + OperatorName: operatorName, + ChangeTime: time.Now(), + } + // 保存变更日志 + if err := s.dao.CreateChangeLog(ctx, changeLog); err != nil { + s.logger.Error("保存状态变更日志失败", zap.Error(err)) + } + } + // 可以继续比较其他字段... + } +} + +// saveSyncHistory 保存同步历史 +func (s *treeCloudService) saveSyncHistory(ctx context.Context, history *model.CloudResourceSyncHistory) { + if err := s.dao.CreateSyncHistory(ctx, history); err != nil { + s.logger.Error("保存同步历史失败", zap.Error(err)) + } +} + +// GetSyncHistory 获取同步历史 +func (s *treeCloudService) GetSyncHistory(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) (model.ListResp[*model.CloudResourceSyncHistory], error) { + // 兜底分页参数 + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) + + histories, total, err := s.dao.GetSyncHistoryList(ctx, req) + if err != nil { + s.logger.Error("获取同步历史失败", zap.Error(err)) + return model.ListResp[*model.CloudResourceSyncHistory]{}, err + } + + return model.ListResp[*model.CloudResourceSyncHistory]{ + Items: histories, + Total: total, + }, nil +} + +// GetChangeLog 获取资源变更日志 +func (s *treeCloudService) GetChangeLog(ctx context.Context, req *model.GetCloudResourceChangeLogReq) (model.ListResp[*model.CloudResourceChangeLog], error) { + // 兜底分页参数 + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) + + logs, total, err := s.dao.GetChangeLogList(ctx, req) + if err != nil { + s.logger.Error("获取变更日志失败", zap.Error(err)) + return model.ListResp[*model.CloudResourceChangeLog]{}, err + } + + return model.ListResp[*model.CloudResourceChangeLog]{ + Items: logs, + Total: total, + }, nil +} diff --git a/internal/tree/service/tree_local_service.go b/internal/tree/service/tree_local_service.go index 33bd47c6..aab94bfc 100644 --- a/internal/tree/service/tree_local_service.go +++ b/internal/tree/service/tree_local_service.go @@ -32,9 +32,8 @@ import ( "github.com/GoSimplicity/AI-CloudOps/internal/model" "github.com/GoSimplicity/AI-CloudOps/internal/tree/dao" - "github.com/GoSimplicity/AI-CloudOps/pkg/utils" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" "github.com/imdario/mergo" - "github.com/spf13/viper" "go.uber.org/zap" "gorm.io/gorm" ) @@ -65,12 +64,7 @@ func NewTreeLocalService(logger *zap.Logger, dao dao.TreeLocalDAO) TreeLocalServ // GetTreeLocalList 获取本地主机列表 func (s *treeLocalService) GetTreeLocalList(ctx context.Context, req *model.GetTreeLocalResourceListReq) (model.ListResp[*model.TreeLocalResource], error) { // 兜底分页参数,避免offset为负或size为0 - if req.Page <= 0 { - req.Page = 1 - } - if req.Size <= 0 { - req.Size = 10 - } + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) locals, total, err := s.dao.GetList(ctx, req) if err != nil { s.logger.Error("获取本地主机列表失败", zap.Error(err)) @@ -85,8 +79,8 @@ func (s *treeLocalService) GetTreeLocalList(ctx context.Context, req *model.GetT // GetTreeLocalDetail 获取本地主机详情 func (s *treeLocalService) GetTreeLocalDetail(ctx context.Context, req *model.GetTreeLocalResourceDetailReq) (*model.TreeLocalResource, error) { - if req.ID <= 0 { - return nil, errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return nil, fmt.Errorf("无效的主机ID: %w", err) } local, err := s.dao.GetByID(ctx, req.ID) @@ -103,8 +97,8 @@ func (s *treeLocalService) GetTreeLocalDetail(ctx context.Context, req *model.Ge // GetTreeLocalForConnection 获取用于连接的本地主机详情(包含解密后的密码) func (s *treeLocalService) GetTreeLocalForConnection(ctx context.Context, req *model.GetTreeLocalResourceDetailReq) (*model.TreeLocalResource, error) { - if req.ID <= 0 { - return nil, errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return nil, fmt.Errorf("无效的主机ID: %w", err) } local, err := s.dao.GetByID(ctx, req.ID) @@ -118,7 +112,7 @@ func (s *treeLocalService) GetTreeLocalForConnection(ctx context.Context, req *m // 解密密码以供连接使用 if local.AuthMode == model.AuthModePassword && local.Password != "" { - plainPassword, err := s.decryptPassword(local.Password) + plainPassword, err := treeUtils.DecryptPassword(local.Password) if err != nil { s.logger.Error("密码解密失败", zap.Int("id", req.ID), zap.Error(err)) return nil, fmt.Errorf("密码解密失败: %w", err) @@ -150,23 +144,17 @@ func (s *treeLocalService) CreateTreeLocal(ctx context.Context, req *model.Creat CreateUserName: req.CreateUserName, Key: req.Key, AuthMode: req.AuthMode, - OsType: req.OsType, + OSType: req.OSType, OSName: req.OSName, ImageName: req.ImageName, } // 设置默认值 - if local.Port == 0 { - local.Port = 22 - } - - if local.Username == "" { - local.Username = "root" - } + treeUtils.SetSSHDefaults(&local.Port, &local.Username) // 加密 if local.AuthMode == model.AuthModePassword && req.Password != "" { - encryptedPassword, err := s.encryptPassword(req.Password) + encryptedPassword, err := treeUtils.EncryptPassword(req.Password) if err != nil { s.logger.Error("密码加密失败", zap.Error(err)) return fmt.Errorf("密码加密失败: %w", err) @@ -183,8 +171,8 @@ func (s *treeLocalService) CreateTreeLocal(ctx context.Context, req *model.Creat } func (s *treeLocalService) UpdateTreeLocal(ctx context.Context, req *model.UpdateTreeLocalResourceReq) error { - if req.ID <= 0 { - return errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的主机ID: %w", err) } // 检查是否存在 @@ -216,7 +204,7 @@ func (s *treeLocalService) UpdateTreeLocal(ctx context.Context, req *model.Updat Status: model.STARTING, IpAddr: req.IpAddr, Port: req.Port, - OsType: req.OsType, + OSType: req.OSType, OSName: req.OSName, ImageName: req.ImageName, AuthMode: req.AuthMode, @@ -224,7 +212,7 @@ func (s *treeLocalService) UpdateTreeLocal(ctx context.Context, req *model.Updat // 加密密码 if req.AuthMode == model.AuthModePassword && req.Password != "" { - pwd, err := s.encryptPassword(req.Password) + pwd, err := treeUtils.EncryptPassword(req.Password) if err != nil { s.logger.Error("密码加密失败", zap.Error(err)) return fmt.Errorf("密码加密失败: %w", err) @@ -252,8 +240,8 @@ func (s *treeLocalService) UpdateTreeLocal(ctx context.Context, req *model.Updat // DeleteTreeLocal 删除本地主机 func (s *treeLocalService) DeleteTreeLocal(ctx context.Context, req *model.DeleteTreeLocalResourceReq) error { - if req.ID <= 0 { - return errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的主机ID: %w", err) } if err := s.dao.Delete(ctx, req.ID); err != nil { @@ -268,8 +256,8 @@ func (s *treeLocalService) DeleteTreeLocal(ctx context.Context, req *model.Delet } func (s *treeLocalService) BindTreeLocal(ctx context.Context, req *model.BindTreeLocalResourceReq) error { - if req.ID <= 0 { - return errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的主机ID: %w", err) } if err := s.dao.BindTreeNodes(ctx, req.ID, req.TreeNodeIDs); err != nil { @@ -281,8 +269,8 @@ func (s *treeLocalService) BindTreeLocal(ctx context.Context, req *model.BindTre } func (s *treeLocalService) UnBindLocalResource(ctx context.Context, req *model.UnBindTreeLocalResourceReq) error { - if req.ID <= 0 { - return errors.New("无效的主机ID") + if err := treeUtils.ValidateID(req.ID); err != nil { + return fmt.Errorf("无效的主机ID: %w", err) } if err := s.dao.UnBindTreeNodes(ctx, req.ID, req.TreeNodeIDs); err != nil { @@ -292,37 +280,3 @@ func (s *treeLocalService) UnBindLocalResource(ctx context.Context, req *model.U return nil } - -// encryptPassword 加密密码 -func (s *treeLocalService) encryptPassword(password string) (string, error) { - if password == "" { - return "", nil - } - - encryptionKey := viper.GetString("tree.password_encryption_key") - if encryptionKey == "" { - return "", errors.New("未配置密码加密密钥") - } - if len(encryptionKey) != 32 { - return "", errors.New("密码加密密钥长度必须为32字节") - } - - return utils.EncryptSecretKey(password, []byte(encryptionKey)) -} - -// decryptPassword 解密密码 -func (s *treeLocalService) decryptPassword(encryptedPassword string) (string, error) { - if encryptedPassword == "" { - return "", nil - } - - encryptionKey := viper.GetString("tree.password_encryption_key") - if encryptionKey == "" { - return "", errors.New("未配置密码加密密钥") - } - if len(encryptionKey) != 32 { - return "", errors.New("密码加密密钥长度必须为32字节") - } - - return utils.DecryptSecretKey(encryptedPassword, []byte(encryptionKey)) -} diff --git a/internal/tree/service/tree_node_service.go b/internal/tree/service/tree_node_service.go index 4078a428..4151c827 100644 --- a/internal/tree/service/tree_node_service.go +++ b/internal/tree/service/tree_node_service.go @@ -32,22 +32,11 @@ import ( "github.com/GoSimplicity/AI-CloudOps/internal/model" "github.com/GoSimplicity/AI-CloudOps/internal/tree/dao" + treeUtils "github.com/GoSimplicity/AI-CloudOps/internal/tree/utils" userDao "github.com/GoSimplicity/AI-CloudOps/internal/user/dao" "go.uber.org/zap" ) -const ( - NodeAdminRole = "admin" // 管理员角色 - NodeMemberRole = "member" // 普通成员角色 - NodeStatusActive = "active" // 活跃状态 - NodeStatusInactive = "inactive" // 非活跃状态 - NodeStatusDeleted = "deleted" // 删除状态 - - // 默认值 - DefaultLevel = 1 - DefaultStatus = NodeStatusActive -) - type TreeNodeService interface { // 树结构相关接口 GetTreeList(ctx context.Context, req *model.GetTreeNodeListReq) (model.ListResp[*model.TreeNode], error) @@ -91,6 +80,9 @@ func (s *treeService) GetTreeList(ctx context.Context, req *model.GetTreeNodeLis return model.ListResp[*model.TreeNode]{}, errors.New("层级不能为负数") } + // 设置分页默认值 + treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) + s.logger.Debug("获取树节点列表", zap.Int("level", req.Level), zap.Int("status", int(req.Status))) trees, total, err := s.dao.GetTreeList(ctx, req) @@ -118,8 +110,8 @@ func (s *treeService) GetNodeDetail(ctx context.Context, id int) (*model.TreeNod // GetChildNodes 获取直接子节点 func (s *treeService) GetChildNodes(ctx context.Context, parentID int) ([]*model.TreeNode, error) { - if parentID < 0 { - return nil, errors.New("父节点ID无效") + if err := treeUtils.ValidateParentID(parentID); err != nil { + return nil, err } return s.dao.GetChildNodes(ctx, parentID) } @@ -179,12 +171,12 @@ func (s *treeService) DeleteNode(ctx context.Context, id int) error { // MoveNode 移动节点 func (s *treeService) MoveNode(ctx context.Context, nodeId, newParentId int) error { - if newParentId < 0 { - return errors.New("新父节点ID不能为负数") + if err := treeUtils.ValidateParentID(newParentId); err != nil { + return err } - if nodeId == newParentId { - return errors.New("节点不能移动到自己") + if err := treeUtils.ValidateNodeMove(nodeId, newParentId); err != nil { + return err } s.logger.Info("移动节点", zap.Int("nodeId", nodeId), zap.Int("newParentId", newParentId)) @@ -209,8 +201,8 @@ func (s *treeService) MoveNode(ctx context.Context, nodeId, newParentId int) err // GetNodeMembers 获取节点成员列表 func (s *treeService) GetNodeMembers(ctx context.Context, nodeId int, memberType string) (model.ListResp[*model.User], error) { - if memberType != "" && memberType != NodeAdminRole && memberType != NodeMemberRole && memberType != "all" { - return model.ListResp[*model.User]{}, errors.New("成员类型只能是admin、member或all") + if err := treeUtils.ValidateMemberType(memberType); err != nil { + return model.ListResp[*model.User]{}, err } s.logger.Debug("获取节点成员", zap.Int("nodeId", nodeId), zap.String("memberType", memberType)) @@ -257,8 +249,8 @@ func (s *treeService) RemoveNodeMember(ctx context.Context, req *model.RemoveTre // BindResource 绑定资源到节点 func (s *treeService) BindResource(ctx context.Context, req *model.BindTreeNodeResourceReq) error { - if len(req.ResourceIDs) == 0 { - return errors.New("资源ID列表不能为空") + if err := treeUtils.ValidateResourceIDs(req.ResourceIDs); err != nil { + return err } return s.dao.BindResource(ctx, req.NodeID, req.ResourceIDs) @@ -266,8 +258,8 @@ func (s *treeService) BindResource(ctx context.Context, req *model.BindTreeNodeR // UnbindResource 解绑资源 func (s *treeService) UnbindResource(ctx context.Context, req *model.UnbindTreeNodeResourceReq) error { - if req.ResourceID <= 0 { - return errors.New("资源ID不能为空或小于等于0") + if err := treeUtils.ValidateID(req.ResourceID); err != nil { + return err } s.logger.Info("解绑资源", diff --git a/internal/tree/ssh/ssh.go b/internal/tree/ssh/ssh.go index 2a5f18c7..a7fcd424 100644 --- a/internal/tree/ssh/ssh.go +++ b/internal/tree/ssh/ssh.go @@ -24,13 +24,12 @@ type ecsSSH struct { Port int // SSH端口号,默认22 Username string // SSH用户名 Mode int8 // 认证方式:1:密码,2:密钥 - Password string // 密码(当Mode为password时使用) - Key string // SSH私钥内容(当Mode为key时使用) + Password string // 密码 + Key string // SSH私钥内容 Client *ssh.Client // SSH客户端连接 UserID int // 用户ID,用于区分不同用户的会话 Sessions map[int]*ssh.Session // 用户会话映射表,key为UserID,value为对应的SSH会话 sessionMu sync.RWMutex // 保护Sessions的读写锁 - Channel ssh.Channel // SSH通信通道(当前版本未使用) LastResult string // 最近一次执行命令的结果 logger *zap.Logger // 日志记录器 } @@ -71,7 +70,7 @@ func (s *ecsSSH) Connect(ip string, port int, username string, password string, s.Mode = mode s.UserID = userID - // 初始化Sessions映射(双重检查锁定模式) + // 初始化Sessions映射 if s.Sessions == nil { s.sessionMu.Lock() if s.Sessions == nil { @@ -90,8 +89,8 @@ func (s *ecsSSH) Connect(ip string, port int, username string, password string, // 配置SSH客户端 config := &ssh.ClientConfig{ User: s.Username, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 注意:生产环境应使用更安全的主机密钥验证 - Timeout: 10 * time.Second, // 连接超时时间 + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 10 * time.Second, } // 根据认证方式配置认证方法 @@ -186,7 +185,7 @@ func (s *ecsSSH) Run(command string) (string, error) { } } - // 为每个命令创建新的会话(避免会话状态污染) + // 为每个命令创建新的会话 session, err := s.Client.NewSession() if err != nil { s.logger.Error("创建命令执行会话失败", zap.Error(err)) @@ -200,7 +199,7 @@ func (s *ecsSSH) Run(command string) (string, error) { } }() - // 执行命令并获取输出(合并stdout和stderr) + // 执行命令并获取输出 s.logger.Debug("执行命令", zap.String("命令", command)) buf, err := session.CombinedOutput(command) s.LastResult = string(buf) @@ -284,7 +283,7 @@ func (r MyReader) Read(p []byte) (n int, err error) { // 将接收到的消息转换为命令字符串 cmdStr := string(message) - // 确保命令以换行符结尾(终端需要) + // 确保命令以换行符结尾 if !strings.HasSuffix(cmdStr, "\n") { cmdStr = cmdStr + "\n" } @@ -364,13 +363,12 @@ func (s *ecsSSH) Web2SSH(ws *websocket.Conn) { // 配置伪终端模式 modes := ssh.TerminalModes{ - ssh.ECHO: 0, // 禁用回显(避免重复显示用户输入) - ssh.TTY_OP_ISPEED: 14400, // 输入波特率 + ssh.ECHO: 0, // 禁用回显 + ssh.TTY_OP_ISPEED: 14400, ssh.TTY_OP_OSPEED: 14400, // 输出波特率 } - // 请求伪终端(PTY) - // xterm: 终端类型 + // 请求伪终端 // 25: 终端行数 // 80: 终端列数 if err := session.RequestPty("xterm", 25, 80, modes); err != nil { @@ -401,7 +399,7 @@ func (s *ecsSSH) Web2SSH(ws *websocket.Conn) { s.logger.Info("Web终端SSH会话已启动", zap.Int("用户ID", s.UserID)) - // 等待SSH会话结束(阻塞直到用户退出或连接断开) + // 等待SSH会话结束 if err := session.Wait(); err != nil { s.logger.Info("SSH会话结束", zap.Int("用户ID", s.UserID), zap.Error(err)) } else { diff --git a/internal/tree/utils/aliyun_util.go b/internal/tree/utils/aliyun_util.go new file mode 100644 index 00000000..41b942e6 --- /dev/null +++ b/internal/tree/utils/aliyun_util.go @@ -0,0 +1,242 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" + "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" + "go.uber.org/zap" +) + +// AliyunClient 阿里云客户端封装 +type AliyunClient struct { + client *ecs.Client + region string + logger *zap.Logger +} + +// NewAliyunClient 创建阿里云客户端 +func NewAliyunClient(accessKey, secretKey, region string, logger *zap.Logger) (*AliyunClient, error) { + client, err := ecs.NewClientWithAccessKey(region, accessKey, secretKey) + if err != nil { + return nil, fmt.Errorf("创建阿里云客户端失败: %w", err) + } + + return &AliyunClient{ + client: client, + region: region, + logger: logger, + }, nil +} + +// VerifyCredentials 验证阿里云凭证 +func (c *AliyunClient) VerifyCredentials(ctx context.Context) error { + request := ecs.CreateDescribeRegionsRequest() + request.Scheme = "https" + + _, err := c.client.DescribeRegions(request) + if err != nil { + return fmt.Errorf("阿里云凭证验证失败: %w", err) + } + + c.logger.Info("阿里云凭证验证成功") + return nil +} + +// ListECSInstances 获取ECS实例列表 +func (c *AliyunClient) ListECSInstances(ctx context.Context, instanceIDs []string) ([]*model.TreeCloudResource, error) { + var allResources []*model.TreeCloudResource + pageNumber := 1 + pageSize := 100 + + c.logger.Info("开始获取阿里云ECS实例", + zap.String("region", c.region), + zap.Int("specifiedInstanceCount", len(instanceIDs))) + + for { + request := ecs.CreateDescribeInstancesRequest() + request.Scheme = "https" + request.PageSize = requests.NewInteger(pageSize) + request.PageNumber = requests.NewInteger(pageNumber) + + // 如果指定了实例ID,则只获取指定的实例 + if len(instanceIDs) > 0 { + request.InstanceIds = fmt.Sprintf(`["%s"]`, strings.Join(instanceIDs, `","`)) + } + + c.logger.Debug("调用阿里云API", + zap.Int("pageNumber", pageNumber), + zap.Int("pageSize", pageSize)) + + response, err := c.client.DescribeInstances(request) + if err != nil { + c.logger.Error("阿里云API调用失败", + zap.Int("pageNumber", pageNumber), + zap.Error(err)) + return nil, fmt.Errorf("获取ECS实例列表失败(页码:%d): %w", pageNumber, err) + } + + c.logger.Info("阿里云API返回", + zap.Int("pageNumber", pageNumber), + zap.Int("totalCount", response.TotalCount), + zap.Int("currentPageCount", len(response.Instances.Instance)), + zap.Int("pageSize", response.PageSize)) + + // 转换实例数据 + for _, instance := range response.Instances.Instance { + resource := c.convertECSToResource(&instance) + allResources = append(allResources, resource) + } + + // 使用TotalCount来判断是否还有下一页 + // 如果已经获取的资源数量大于等于总数,停止分页 + if len(allResources) >= response.TotalCount { + c.logger.Info("已获取所有实例", + zap.Int("totalCount", response.TotalCount), + zap.Int("fetchedCount", len(allResources))) + break + } + + // 判断是否为最后一页 + if len(response.Instances.Instance) < pageSize { + c.logger.Info("到达最后一页", + zap.Int("currentPageCount", len(response.Instances.Instance)), + zap.Int("pageSize", pageSize)) + break + } + + // 如果指定了实例ID,不需要分页 + if len(instanceIDs) > 0 { + break + } + + pageNumber++ + } + + c.logger.Info("获取阿里云ECS实例成功", + zap.Int("count", len(allResources)), + zap.String("region", c.region)) + return allResources, nil +} + +// convertECSToResource 将阿里云ECS实例转换为内部资源模型 +func (c *AliyunClient) convertECSToResource(instance *ecs.Instance) *model.TreeCloudResource { + resource := &model.TreeCloudResource{ + Name: instance.InstanceName, + ResourceType: model.ResourceTypeECS, + InstanceID: instance.InstanceId, + InstanceType: instance.InstanceType, + Status: c.convertECSStatus(instance.Status), + Region: c.region, + ZoneID: instance.ZoneId, + VpcID: instance.VpcAttributes.VpcId, + OSType: instance.OSType, + OSName: instance.OSName, + ImageID: instance.ImageId, + Cpu: instance.Cpu, + Memory: instance.Memory / 1024, // 阿里云返回的是MB,转换为GB + } + + if len(instance.PublicIpAddress.IpAddress) > 0 { + resource.PublicIP = instance.PublicIpAddress.IpAddress[0] + } + + if len(instance.VpcAttributes.PrivateIpAddress.IpAddress) > 0 { + resource.PrivateIP = instance.VpcAttributes.PrivateIpAddress.IpAddress[0] + } + + // 设置镜像名称 + if instance.OSName != "" { + resource.ImageName = instance.OSName + } else { + resource.ImageName = instance.ImageId + } + + switch instance.InstanceChargeType { + case "PostPaid": + resource.ChargeType = model.ChargeTypePostPaid + case "PrePaid": + resource.ChargeType = model.ChargeTypePrePaid + if instance.ExpiredTime != "" { + expireTime, err := time.Parse("2006-01-02T15:04Z", instance.ExpiredTime) + if err == nil { + resource.ExpireTime = &expireTime + } + } + } + + resource.Currency = model.CurrencyCNY + + var tags model.KeyValueList + for _, tag := range instance.Tags.Tag { + tags = append(tags, model.KeyValue{ + Key: tag.TagKey, + Value: tag.TagValue, + }) + } + resource.Tags = tags + + resource.Port = 22 + resource.Username = "root" + + return resource +} + +// convertECSStatus 转换阿里云ECS状态到内部状态 +func (c *AliyunClient) convertECSStatus(status string) model.CloudResourceStatus { + switch status { + case "Running": + return model.CloudResourceRunning + case "Stopped": + return model.CloudResourceStopped + case "Starting": + return model.CloudResourceStarting + case "Stopping": + return model.CloudResourceStopping + default: + return model.CloudResourceUnknown + } +} + +// GetECSInstanceByID 根据实例ID获取单个ECS实例 +func (c *AliyunClient) GetECSInstanceByID(ctx context.Context, instanceID string) (*model.TreeCloudResource, error) { + resources, err := c.ListECSInstances(ctx, []string{instanceID}) + if err != nil { + return nil, err + } + + if len(resources) == 0 { + return nil, fmt.Errorf("未找到实例: %s", instanceID) + } + + return resources[0], nil +} diff --git a/internal/tree/utils/cloud_account_util.go b/internal/tree/utils/cloud_account_util.go new file mode 100644 index 00000000..bd03831a --- /dev/null +++ b/internal/tree/utils/cloud_account_util.go @@ -0,0 +1,80 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import ( + "context" + "fmt" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "go.uber.org/zap" +) + +// VerifyAliyunCredentials 验证阿里云凭证 +func VerifyAliyunCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + client, err := NewAliyunClient(req.AccessKey, req.SecretKey, req.Region, logger) + if err != nil { + logger.Error("创建阿里云客户端失败", zap.Error(err)) + return err + } + + if err := client.VerifyCredentials(ctx); err != nil { + logger.Error("验证阿里云凭证失败", zap.Error(err)) + return err + } + + return nil +} + +// VerifyTencentCredentials 验证腾讯云凭证 +func VerifyTencentCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + logger.Warn("腾讯云凭证验证功能暂未实现") + return fmt.Errorf("腾讯云凭证验证功能暂未实现") +} + +// VerifyAWSCredentials 验证AWS凭证 +func VerifyAWSCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + logger.Warn("AWS凭证验证功能暂未实现") + return fmt.Errorf("AWS凭证验证功能暂未实现") +} + +// VerifyHuaweiCredentials 验证华为云凭证 +func VerifyHuaweiCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + logger.Warn("华为云凭证验证功能暂未实现") + return fmt.Errorf("华为云凭证验证功能暂未实现") +} + +// VerifyAzureCredentials 验证Azure凭证 +func VerifyAzureCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + logger.Warn("Azure凭证验证功能暂未实现") + return fmt.Errorf("Azure凭证验证功能暂未实现") +} + +// VerifyGCPCredentials 验证GCP凭证 +func VerifyGCPCredentials(ctx context.Context, req *model.VerifyCloudCredentialsReq, logger *zap.Logger) error { + logger.Warn("GCP凭证验证功能暂未实现") + return fmt.Errorf("GCP凭证验证功能暂未实现") +} diff --git a/internal/tree/utils/common_util.go b/internal/tree/utils/common_util.go new file mode 100644 index 00000000..918230cb --- /dev/null +++ b/internal/tree/utils/common_util.go @@ -0,0 +1,100 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import ( + "errors" + + pkgUtils "github.com/GoSimplicity/AI-CloudOps/pkg/utils" + "github.com/spf13/viper" +) + +// ValidateAndSetPaginationDefaults 验证并设置分页参数的默认值 +func ValidateAndSetPaginationDefaults(page, size *int) { + if *page <= 0 { + *page = 1 + } + if *size <= 0 { + *size = 10 + } +} + +// ValidateID 验证ID是否有效 +func ValidateID(id int) error { + if id <= 0 { + return errors.New("无效的ID") + } + return nil +} + +// ValidateTreeNodeIDs 验证树节点ID列表 +func ValidateTreeNodeIDs(treeNodeIDs []int) bool { + return len(treeNodeIDs) > 0 +} + +// SetSSHDefaults 设置SSH连接的默认值 +func SetSSHDefaults(port *int, username *string) { + if *port == 0 { + *port = 22 + } + if *username == "" { + *username = "root" + } +} + +// EncryptPassword 加密密码 +func EncryptPassword(password string) (string, error) { + if password == "" { + return "", nil + } + + encryptionKey := viper.GetString("tree.password_encryption_key") + if encryptionKey == "" { + return "", errors.New("未配置密码加密密钥") + } + if len(encryptionKey) != 32 { + return "", errors.New("密码加密密钥长度必须为32字节") + } + + return pkgUtils.EncryptSecretKey(password, []byte(encryptionKey)) +} + +// DecryptPassword 解密密码 +func DecryptPassword(encryptedPassword string) (string, error) { + if encryptedPassword == "" { + return "", nil + } + + encryptionKey := viper.GetString("tree.password_encryption_key") + if encryptionKey == "" { + return "", errors.New("未配置密码加密密钥") + } + if len(encryptionKey) != 32 { + return "", errors.New("密码加密密钥长度必须为32字节") + } + + return pkgUtils.DecryptSecretKey(encryptedPassword, []byte(encryptionKey)) +} diff --git a/internal/tree/utils/tree_cloud_util.go b/internal/tree/utils/tree_cloud_util.go new file mode 100644 index 00000000..a750a7a0 --- /dev/null +++ b/internal/tree/utils/tree_cloud_util.go @@ -0,0 +1,116 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import ( + "context" + "fmt" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" + "go.uber.org/zap" +) + +// AliyunSyncConfig 阿里云同步配置 +type AliyunSyncConfig struct { + AccessKey string + SecretKey string + Region string + CloudAccountID int + ResourceType model.CloudResourceType + InstanceIDs []string + SyncMode model.SyncMode +} + +// SyncAliyunResources 同步阿里云资源 +func SyncAliyunResources(ctx context.Context, config *AliyunSyncConfig, logger *zap.Logger) ([]*model.TreeCloudResource, error) { + logger.Info("开始同步阿里云资源", + zap.Int("cloudAccountID", config.CloudAccountID), + zap.String("region", config.Region), + zap.String("syncMode", string(config.SyncMode)), + zap.Int("specifiedInstanceCount", len(config.InstanceIDs))) + + // 创建阿里云客户端 + client, err := NewAliyunClient(config.AccessKey, config.SecretKey, config.Region, logger) + if err != nil { + logger.Error("创建阿里云客户端失败", zap.Error(err)) + return nil, err + } + + // 目前只支持ECS资源类型的同步 + if config.ResourceType != 0 && config.ResourceType != model.ResourceTypeECS { + return nil, fmt.Errorf("暂不支持该资源类型的同步: %d", config.ResourceType) + } + + // 获取ECS实例列表 + resources, err := client.ListECSInstances(ctx, config.InstanceIDs) + if err != nil { + logger.Error("获取阿里云ECS实例失败", zap.Error(err)) + return nil, err + } + + logger.Info("阿里云API返回资源数量", zap.Int("count", len(resources))) + + // 为每个资源设置云账户ID + for _, resource := range resources { + resource.CloudAccountID = config.CloudAccountID + } + + logger.Info("同步阿里云资源成功", + zap.String("syncMode", string(config.SyncMode)), + zap.Int("count", len(resources))) + + return resources, nil +} + +// SyncTencentResources 同步腾讯云资源 +func SyncTencentResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + logger.Warn("腾讯云资源同步功能暂未实现") + return fmt.Errorf("腾讯云资源同步功能暂未实现") +} + +// SyncAWSResources 同步AWS资源 +func SyncAWSResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + logger.Warn("AWS资源同步功能暂未实现") + return fmt.Errorf("AWS资源同步功能暂未实现") +} + +// SyncHuaweiResources 同步华为云资源 +func SyncHuaweiResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + logger.Warn("华为云资源同步功能暂未实现") + return fmt.Errorf("华为云资源同步功能暂未实现") +} + +// SyncAzureResources 同步Azure资源 +func SyncAzureResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + logger.Warn("Azure资源同步功能暂未实现") + return fmt.Errorf("Azure资源同步功能暂未实现") +} + +// SyncGCPResources 同步GCP资源 +func SyncGCPResources(ctx context.Context, req *model.SyncTreeCloudResourceReq, logger *zap.Logger) error { + logger.Warn("GCP资源同步功能暂未实现") + return fmt.Errorf("GCP资源同步功能暂未实现") +} diff --git a/internal/tree/utils/tree_node_util.go b/internal/tree/utils/tree_node_util.go new file mode 100644 index 00000000..71a87b94 --- /dev/null +++ b/internal/tree/utils/tree_node_util.go @@ -0,0 +1,93 @@ +/* + * MIT License + * + * Copyright (c) 2024 Bamboo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +package utils + +import ( + "errors" + + "github.com/GoSimplicity/AI-CloudOps/internal/model" +) + +// ValidateParentID 验证父节点ID是否有效 +func ValidateParentID(parentID int) error { + if parentID < 0 { + return errors.New("父节点ID不能为负数") + } + return nil +} + +// ValidateNodeMove 验证节点移动操作 +func ValidateNodeMove(nodeID, newParentID int) error { + if nodeID == newParentID { + return errors.New("节点不能移动到自己") + } + return nil +} + +// ValidateMemberType 验证成员类型 +func ValidateMemberType(memberType string) error { + if memberType != "" && memberType != "admin" && memberType != "member" && memberType != "all" { + return errors.New("成员类型只能是admin、member或all") + } + return nil +} + +// ValidateResourceIDs 验证资源ID列表 +func ValidateResourceIDs(resourceIDs []int) error { + if len(resourceIDs) == 0 { + return errors.New("资源ID列表不能为空") + } + return nil +} + +// BuildTreeStructure 构建树形结构 +func BuildTreeStructure(nodes []*model.TreeNode) []*model.TreeNode { + // 创建节点映射表,用于快速查找节点 + nodeMap := make(map[int]*model.TreeNode) + var rootNodes []*model.TreeNode + + // 第一遍遍历:复制节点并初始化Children字段 + for _, node := range nodes { + nodeClone := *node + nodeClone.Children = make([]*model.TreeNode, 0) + nodeMap[node.ID] = &nodeClone + } + + // 第二遍遍历:建立父子关系 + for _, node := range nodes { + currentNode := nodeMap[node.ID] + // 如果节点没有父节点或父节点不存在,则为根节点 + if node.ParentID == 0 || nodeMap[node.ParentID] == nil { + rootNodes = append(rootNodes, currentNode) + } else { + // 将当前节点添加到其父节点的Children列表中 + parent := nodeMap[node.ParentID] + parent.Children = append(parent.Children, currentNode) + } + } + + return rootNodes +} diff --git a/pkg/di/init.go b/pkg/di/init.go index ffc29c15..42c90120 100644 --- a/pkg/di/init.go +++ b/pkg/di/init.go @@ -50,9 +50,11 @@ func InitTables(db *gorm.DB) error { // tree &model.TreeNode{}, &model.TreeLocalResource{}, - - // k8s + &model.TreeCloudResource{}, + &model.CloudAccount{}, &model.K8sCluster{}, + &model.CloudResourceSyncHistory{}, + &model.CloudResourceChangeLog{}, // prometheus &model.MonitorScrapePool{}, diff --git a/pkg/di/logger.go b/pkg/di/logger.go index d2711d3f..f7a0eb8d 100644 --- a/pkg/di/logger.go +++ b/pkg/di/logger.go @@ -65,10 +65,13 @@ func InitLogger() *zap.Logger { // 创建控制台输出 consoleWriter := zapcore.AddSync(os.Stdout) + // 从配置读取日志等级 + logLevel := getLogLevel(viper.GetString("log.level")) + // 创建 Core core := zapcore.NewTee( - zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), consoleWriter, zapcore.WarnLevel), // 控制台输出warn及以上级别 - zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), fileWriter, zapcore.WarnLevel), // 文件记录warn及以上级别 + zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), consoleWriter, logLevel), // 控制台输出 + zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), fileWriter, logLevel), // 文件记录 ) // 创建 logger @@ -76,3 +79,25 @@ func InitLogger() *zap.Logger { return logger } + +// getLogLevel 根据配置字符串返回对应的日志等级 +func getLogLevel(level string) zapcore.Level { + switch level { + case "debug": + return zapcore.DebugLevel + case "info": + return zapcore.InfoLevel + case "warn": + return zapcore.WarnLevel + case "error": + return zapcore.ErrorLevel + case "dpanic": + return zapcore.DPanicLevel + case "panic": + return zapcore.PanicLevel + case "fatal": + return zapcore.FatalLevel + default: + return zapcore.InfoLevel // 默认使用 Info 级别 + } +} diff --git a/pkg/di/web.go b/pkg/di/web.go index 52b09fd6..8fb743c7 100644 --- a/pkg/di/web.go +++ b/pkg/di/web.go @@ -82,6 +82,8 @@ func InitGinServer( instanceTimeLineHdl *workorderApi.InstanceTimeLineHandler, treeNodeHdl *resourceApi.TreeNodeHandler, treeLocalHdl *resourceApi.TreeLocalHandler, + treeCloudHdl *resourceApi.TreeCloudHandler, + cloudAccountHdl *resourceApi.CloudAccountHandler, notificationHdl *workorderApi.NotificationHandler, ingressHdl *k8sApi.K8sIngressHandler, k8sPodHdl *k8sApi.K8sPodHandler, @@ -133,6 +135,8 @@ func InitGinServer( categoryHdl.RegisterRouters(server) treeNodeHdl.RegisterRouters(server) treeLocalHdl.RegisterRouters(server) + treeCloudHdl.RegisterRouters(server) + cloudAccountHdl.RegisterRouters(server) notificationHdl.RegisterRouters(server) ingressHdl.RegisterRouters(server) k8sPodHdl.RegisterRouters(server) diff --git a/pkg/di/wire.go b/pkg/di/wire.go index 8609cc61..1e75ca4d 100644 --- a/pkg/di/wire.go +++ b/pkg/di/wire.go @@ -131,6 +131,8 @@ var HandlerSet = wire.NewSet( workorderHandler.NewNotificationHandler, treeHandler.NewTreeNodeHandler, treeHandler.NewTreeLocalHandler, + treeHandler.NewTreeCloudHandler, + treeHandler.NewCloudAccountHandler, terminal.NewTerminalHandler, cronApi.NewCronJobHandler, ) @@ -185,6 +187,8 @@ var ServiceSet = wire.NewSet( workorderService.NewWorkorderNotificationService, treeService.NewTreeNodeService, treeService.NewTreeLocalService, + treeService.NewTreeCloudService, + treeService.NewCloudAccountService, cronService.NewCronService, ) @@ -216,6 +220,8 @@ var DaoSet = wire.NewSet( workorderDao.NewNotificationDAO, treeDao.NewTreeNodeDAO, treeDao.NewTreeLocalDAO, + treeDao.NewTreeCloudDAO, + treeDao.NewCloudAccountDAO, cronDao.NewCronJobDAO, ) diff --git a/pkg/di/wire_gen.go b/pkg/di/wire_gen.go index 312e6f67..d28c9b15 100644 --- a/pkg/di/wire_gen.go +++ b/pkg/di/wire_gen.go @@ -203,6 +203,12 @@ func ProvideCmd() *Cmd { treeLocalService := service6.NewTreeLocalService(logger, treeLocalDAO) sshClient := ssh.NewClient(logger) treeLocalHandler := api7.NewTreeLocalHandler(treeLocalService, sshClient) + treeCloudDAO := dao5.NewTreeCloudDAO(db, logger) + cloudAccountDAO := dao5.NewCloudAccountDAO(db, logger) + treeCloudService := service6.NewTreeCloudService(logger, treeCloudDAO, cloudAccountDAO) + treeCloudHandler := api7.NewTreeCloudHandler(treeCloudService, sshClient) + cloudAccountService := service6.NewCloudAccountService(logger, cloudAccountDAO) + cloudAccountHandler := api7.NewCloudAccountHandler(cloudAccountService) notificationHandler := api6.NewNotificationHandler(workorderNotificationService) ingressManager := manager.NewIngressManager(k8sClient, logger) ingressService := service4.NewIngressService(ingressManager, logger) @@ -222,7 +228,7 @@ func ProvideCmd() *Cmd { cronScheduler := scheduler.NewCronScheduler(logger, cronJobDAO, asynqScheduler, asynqClient) cronService := service7.NewCronService(logger, cronJobDAO, userDAO, asynqClient, cronScheduler) cronJobHandler := api8.NewCronJobHandler(logger, cronService) - engine := InitGinServer(v, userHandler, apiHandler, roleHandler, systemHandler, notAuthHandler, k8sClusterHandler, k8sDeploymentHandler, k8sNamespaceHandler, k8sNodeHandler, k8sSvcHandler, k8sYamlTaskHandler, k8sYamlTemplateHandler, k8sDaemonSetHandler, k8sEventHandler, k8sStatefulSetHandler, k8sServiceAccountHandler, k8sRoleHandler, k8sClusterRoleHandler, k8sRoleBindingHandler, k8sClusterRoleBindingHandler, k8sConfigMapHandler, k8sSecretHandler, alertEventHandler, alertPoolHandler, alertRuleHandler, monitorConfigHandler, onDutyGroupHandler, recordRuleHandler, scrapePoolHandler, scrapeJobHandler, sendGroupHandler, auditHandler, formDesignHandler, workorderProcessHandler, templateHandler, instanceHandler, instanceFlowHandler, instanceCommentHandler, categoryGroupHandler, instanceTimeLineHandler, treeNodeHandler, treeLocalHandler, notificationHandler, k8sIngressHandler, k8sPodHandler, k8sPVHandler, k8sPVCHandler, cronJobHandler) + engine := InitGinServer(v, userHandler, apiHandler, roleHandler, systemHandler, notAuthHandler, k8sClusterHandler, k8sDeploymentHandler, k8sNamespaceHandler, k8sNodeHandler, k8sSvcHandler, k8sYamlTaskHandler, k8sYamlTemplateHandler, k8sDaemonSetHandler, k8sEventHandler, k8sStatefulSetHandler, k8sServiceAccountHandler, k8sRoleHandler, k8sClusterRoleHandler, k8sRoleBindingHandler, k8sClusterRoleBindingHandler, k8sConfigMapHandler, k8sSecretHandler, alertEventHandler, alertPoolHandler, alertRuleHandler, monitorConfigHandler, onDutyGroupHandler, recordRuleHandler, scrapePoolHandler, scrapeJobHandler, sendGroupHandler, auditHandler, formDesignHandler, workorderProcessHandler, templateHandler, instanceHandler, instanceFlowHandler, instanceCommentHandler, categoryGroupHandler, instanceTimeLineHandler, treeNodeHandler, treeLocalHandler, treeCloudHandler, cloudAccountHandler, notificationHandler, k8sIngressHandler, k8sPodHandler, k8sPVHandler, k8sPVCHandler, cronJobHandler) applicationBootstrap := startup.NewApplicationBootstrap(clusterManager, logger) builtinTaskManager := cron.NewBuiltinTaskManager(logger, cronJobDAO) cronManager := cron.NewUnifiedCronManager(logger, alertManagerOnDutyDAO, clusterDAO, k8sClient, clusterManager, monitorCache, cronScheduler, builtinTaskManager) @@ -252,11 +258,11 @@ type Cmd struct { CronHandlers *handler.CronHandlers } -var HandlerSet = wire.NewSet(api2.NewRoleHandler, api2.NewApiHandler, api2.NewAuditHandler, api2.NewSystemHandler, api.NewUserHandler, api3.NewNotAuthHandler, api4.NewK8sNodeHandler, api4.NewK8sClusterHandler, api4.NewK8sDeploymentHandler, api4.NewK8sNamespaceHandler, api4.NewK8sSvcHandler, api4.NewK8sYamlTaskHandler, api4.NewK8sYamlTemplateHandler, api4.NewK8sDaemonSetHandler, api4.NewK8sEventHandler, api4.NewK8sStatefulSetHandler, api4.NewK8sServiceAccountHandler, api4.NewK8sRoleHandler, api4.NewK8sClusterRoleHandler, api4.NewK8sRoleBindingHandler, api4.NewK8sClusterRoleBindingHandler, api4.NewK8sRBACHandler, api4.NewK8sIngressHandler, api4.NewK8sPodHandler, api4.NewK8sConfigMapHandler, api4.NewK8sSecretHandler, api4.NewK8sPVHandler, api4.NewK8sPVCHandler, api5.NewAlertPoolHandler, api5.NewMonitorConfigHandler, api5.NewOnDutyGroupHandler, api5.NewRecordRuleHandler, api5.NewAlertRuleHandler, api5.NewSendGroupHandler, api5.NewScrapeJobHandler, api5.NewScrapePoolHandler, api5.NewAlertEventHandler, api6.NewFormDesignHandler, api6.NewInstanceHandler, api6.NewInstanceFlowHandler, api6.NewInstanceCommentHandler, api6.NewInstanceTimeLineHandler, api6.NewTemplateHandler, api6.NewWorkorderProcessHandler, api6.NewCategoryGroupHandler, api6.NewNotificationHandler, api7.NewTreeNodeHandler, api7.NewTreeLocalHandler, terminal.NewTerminalHandler, api8.NewCronJobHandler) +var HandlerSet = wire.NewSet(api2.NewRoleHandler, api2.NewApiHandler, api2.NewAuditHandler, api2.NewSystemHandler, api.NewUserHandler, api3.NewNotAuthHandler, api4.NewK8sNodeHandler, api4.NewK8sClusterHandler, api4.NewK8sDeploymentHandler, api4.NewK8sNamespaceHandler, api4.NewK8sSvcHandler, api4.NewK8sYamlTaskHandler, api4.NewK8sYamlTemplateHandler, api4.NewK8sDaemonSetHandler, api4.NewK8sEventHandler, api4.NewK8sStatefulSetHandler, api4.NewK8sServiceAccountHandler, api4.NewK8sRoleHandler, api4.NewK8sClusterRoleHandler, api4.NewK8sRoleBindingHandler, api4.NewK8sClusterRoleBindingHandler, api4.NewK8sRBACHandler, api4.NewK8sIngressHandler, api4.NewK8sPodHandler, api4.NewK8sConfigMapHandler, api4.NewK8sSecretHandler, api4.NewK8sPVHandler, api4.NewK8sPVCHandler, api5.NewAlertPoolHandler, api5.NewMonitorConfigHandler, api5.NewOnDutyGroupHandler, api5.NewRecordRuleHandler, api5.NewAlertRuleHandler, api5.NewSendGroupHandler, api5.NewScrapeJobHandler, api5.NewScrapePoolHandler, api5.NewAlertEventHandler, api6.NewFormDesignHandler, api6.NewInstanceHandler, api6.NewInstanceFlowHandler, api6.NewInstanceCommentHandler, api6.NewInstanceTimeLineHandler, api6.NewTemplateHandler, api6.NewWorkorderProcessHandler, api6.NewCategoryGroupHandler, api6.NewNotificationHandler, api7.NewTreeNodeHandler, api7.NewTreeLocalHandler, api7.NewTreeCloudHandler, api7.NewCloudAccountHandler, terminal.NewTerminalHandler, api8.NewCronJobHandler) -var ServiceSet = wire.NewSet(service4.NewClusterService, service4.NewDeploymentService, service4.NewNamespaceService, service4.NewSvcService, service4.NewNodeService, service4.NewTaintService, service4.NewYamlTaskService, service4.NewYamlTemplateService, service4.NewDaemonSetService, service4.NewEventService, service4.NewStatefulSetService, service4.NewServiceAccountService, service4.NewRoleService, service4.NewClusterRoleService, service4.NewRoleBindingService, service4.NewClusterRoleBindingService, service4.NewRBACService, service4.NewIngressService, service4.NewPodService, service4.NewConfigMapService, service4.NewSecretService, service4.NewPVService, service4.NewPVCService, service2.NewUserService, service.NewApiService, service.NewRoleService, service.NewAuditService, service.NewSystemService, alert2.NewAlertManagerEventService, alert2.NewAlertManagerOnDutyService, alert2.NewAlertManagerPoolService, alert2.NewAlertManagerRecordService, alert2.NewAlertManagerRuleService, alert2.NewAlertManagerSendService, scrape2.NewPrometheusScrapeService, scrape2.NewPrometheusPoolService, config2.NewMonitorConfigService, service3.NewNotAuthService, service5.NewFormDesignService, service5.NewInstanceService, service5.NewInstanceFlowService, service5.NewInstanceCommentService, service5.NewWorkorderInstanceTimeLineService, service5.NewWorkorderTemplateService, service5.NewWorkorderProcessService, service5.NewCategoryGroupService, service5.NewWorkorderNotificationService, service6.NewTreeNodeService, service6.NewTreeLocalService, service7.NewCronService) +var ServiceSet = wire.NewSet(service4.NewClusterService, service4.NewDeploymentService, service4.NewNamespaceService, service4.NewSvcService, service4.NewNodeService, service4.NewTaintService, service4.NewYamlTaskService, service4.NewYamlTemplateService, service4.NewDaemonSetService, service4.NewEventService, service4.NewStatefulSetService, service4.NewServiceAccountService, service4.NewRoleService, service4.NewClusterRoleService, service4.NewRoleBindingService, service4.NewClusterRoleBindingService, service4.NewRBACService, service4.NewIngressService, service4.NewPodService, service4.NewConfigMapService, service4.NewSecretService, service4.NewPVService, service4.NewPVCService, service2.NewUserService, service.NewApiService, service.NewRoleService, service.NewAuditService, service.NewSystemService, alert2.NewAlertManagerEventService, alert2.NewAlertManagerOnDutyService, alert2.NewAlertManagerPoolService, alert2.NewAlertManagerRecordService, alert2.NewAlertManagerRuleService, alert2.NewAlertManagerSendService, scrape2.NewPrometheusScrapeService, scrape2.NewPrometheusPoolService, config2.NewMonitorConfigService, service3.NewNotAuthService, service5.NewFormDesignService, service5.NewInstanceService, service5.NewInstanceFlowService, service5.NewInstanceCommentService, service5.NewWorkorderInstanceTimeLineService, service5.NewWorkorderTemplateService, service5.NewWorkorderProcessService, service5.NewCategoryGroupService, service5.NewWorkorderNotificationService, service6.NewTreeNodeService, service6.NewTreeLocalService, service6.NewTreeCloudService, service6.NewCloudAccountService, service7.NewCronService) -var DaoSet = wire.NewSet(alert.NewAlertManagerEventDAO, alert.NewAlertManagerOnDutyDAO, alert.NewAlertManagerPoolDAO, alert.NewAlertManagerRecordDAO, alert.NewAlertManagerRuleDAO, alert.NewAlertManagerSendDAO, scrape.NewScrapeJobDAO, scrape.NewScrapePoolDAO, config.NewMonitorConfigDAO, dao2.NewUserDAO, dao.NewRoleDAO, dao.NewApiDAO, dao.NewAuditDAO, dao3.NewClusterDAO, dao3.NewYamlTaskDAO, dao3.NewYamlTemplateDAO, dao4.NewWorkorderFormDesignDAO, dao4.NewTemplateDAO, dao4.NewWorkorderInstanceDAO, dao4.NewProcessDAO, dao4.NewWorkorderCategoryDAO, dao4.NewWorkorderInstanceCommentDAO, dao4.NewInstanceFlowDAO, dao4.NewInstanceTimeLineDAO, dao4.NewNotificationDAO, dao5.NewTreeNodeDAO, dao5.NewTreeLocalDAO, dao6.NewCronJobDAO) +var DaoSet = wire.NewSet(alert.NewAlertManagerEventDAO, alert.NewAlertManagerOnDutyDAO, alert.NewAlertManagerPoolDAO, alert.NewAlertManagerRecordDAO, alert.NewAlertManagerRuleDAO, alert.NewAlertManagerSendDAO, scrape.NewScrapeJobDAO, scrape.NewScrapePoolDAO, config.NewMonitorConfigDAO, dao2.NewUserDAO, dao.NewRoleDAO, dao.NewApiDAO, dao.NewAuditDAO, dao3.NewClusterDAO, dao3.NewYamlTaskDAO, dao3.NewYamlTemplateDAO, dao4.NewWorkorderFormDesignDAO, dao4.NewTemplateDAO, dao4.NewWorkorderInstanceDAO, dao4.NewProcessDAO, dao4.NewWorkorderCategoryDAO, dao4.NewWorkorderInstanceCommentDAO, dao4.NewInstanceFlowDAO, dao4.NewInstanceTimeLineDAO, dao4.NewNotificationDAO, dao5.NewTreeNodeDAO, dao5.NewTreeLocalDAO, dao5.NewTreeCloudDAO, dao5.NewCloudAccountDAO, dao6.NewCronJobDAO) var SSHSet = wire.NewSet(ssh.NewClient)