瀏覽代碼

feat(wip): node management #70

0xJacky 2 年之前
父節點
當前提交
47ea62adb2
共有 41 個文件被更改,包括 1931 次插入482 次删除
  1. 2 2
      .air.toml
  2. 2 2
      dev.Dockerfile
  3. 3 0
      frontend/components.d.ts
  4. 5 0
      frontend/src/api/environment.ts
  5. 0 0
      frontend/src/assets/svg/cpu.svg
  6. 1 0
      frontend/src/assets/svg/memory.svg
  7. 1 0
      frontend/src/assets/svg/pulse.svg
  8. 86 0
      frontend/src/components/EnvIndicator/EnvIndicator.vue
  9. 8 3
      frontend/src/components/StdDataDisplay/StdCurd.vue
  10. 8 3
      frontend/src/layouts/SideBar.vue
  11. 20 1
      frontend/src/lib/helper/index.ts
  12. 5 2
      frontend/src/lib/http/index.ts
  13. 5 3
      frontend/src/lib/websocket/index.ts
  14. 9 1
      frontend/src/pinia/moudule/settings.ts
  15. 17 8
      frontend/src/routes/index.ts
  16. 1 1
      frontend/src/version.json
  17. 5 320
      frontend/src/views/dashboard/DashBoard.vue
  18. 102 0
      frontend/src/views/dashboard/Environments.vue
  19. 330 0
      frontend/src/views/dashboard/ServerAnalytic.vue
  20. 73 0
      frontend/src/views/environment/Environment.vue
  21. 1 1
      frontend/version.json
  22. 1 1
      frontend/vite.config.ts
  23. 58 42
      go.mod
  24. 222 0
      go.sum
  25. 111 0
      server/api/environment.go
  26. 55 54
      server/api/install.go
  27. 35 0
      server/api/node.go
  28. 1 0
      server/internal/environment/environment.go
  29. 6 6
      server/internal/logger/logger.go
  30. 8 0
      server/model/environment.go
  31. 1 0
      server/model/model.go
  32. 5 0
      server/query/certs.gen.go
  33. 378 0
      server/query/environments.gen.go
  34. 14 2
      server/query/gen.go
  35. 30 15
      server/router/middleware.go
  36. 92 0
      server/router/proxy.go
  37. 90 0
      server/router/proxy_ws.go
  38. 32 15
      server/router/routers.go
  39. 13 0
      server/server.go
  40. 94 0
      server/service/environment.go
  41. 1 0
      server/settings/settings.go

+ 2 - 2
.air.toml

@@ -13,9 +13,9 @@ bin = "tmp/main"
 # Customize binary.
 full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
 # Watch these filename extensions.
-include_ext = ["go", "tpl", "tmpl", "html", "conf", "ini", "toml"]
+include_ext = ["go", "tpl", "tmpl", "html", "toml"]
 # Ignore these filename extensions or directories.
-exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "upload", "docs", "resources"]
+exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "upload", "docs", "resources", "frontend/src"]
 # Watch these directories if you specified.
 include_dir = []
 # Exclude files.

+ 2 - 2
dev.Dockerfile

@@ -11,8 +11,8 @@ RUN set -x \
     && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 101 nginx \
     && apt update && apt install -y wget nginx gcc curl
 
-RUN wget https://go.dev/dl/go1.20.1.linux-arm64.tar.gz && \
-    rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.1.linux-arm64.tar.gz && rm -f go1.20.1.linux-arm64.tar.gz
+RUN wget https://go.dev/dl/go1.20.4.linux-arm64.tar.gz && \
+    rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.4.linux-arm64.tar.gz && rm -f go1.20.4.linux-arm64.tar.gz
 
 ENV PATH="${PATH}:/usr/local/go/bin"
 

+ 3 - 0
frontend/components.d.ts

@@ -10,6 +10,7 @@ export {}
 declare module '@vue/runtime-core' {
   export interface GlobalComponents {
     AAlert: typeof import('ant-design-vue/es')['Alert']
+    AAvatar: typeof import('ant-design-vue/es')['Avatar']
     ABadge: typeof import('ant-design-vue/es')['Badge']
     ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
     ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
@@ -37,6 +38,7 @@ declare module '@vue/runtime-core' {
     ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
     AList: typeof import('ant-design-vue/es')['List']
     AListItem: typeof import('ant-design-vue/es')['ListItem']
+    AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
     AMenu: typeof import('ant-design-vue/es')['Menu']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
     AModal: typeof import('ant-design-vue/es')['Modal']
@@ -64,6 +66,7 @@ declare module '@vue/runtime-core' {
     ChartRadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default']
     ChatGPTChatGPT: typeof import('./src/components/ChatGPT/ChatGPT.vue')['default']
     CodeEditorCodeEditor: typeof import('./src/components/CodeEditor/CodeEditor.vue')['default']
+    EnvIndicatorEnvIndicator: typeof import('./src/components/EnvIndicator/EnvIndicator.vue')['default']
     FooterToolbarFooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
     LogoLogo: typeof import('./src/components/Logo/Logo.vue')['default']
     NginxControlNginxControl: typeof import('./src/components/NginxControl/NginxControl.vue')['default']

+ 5 - 0
frontend/src/api/environment.ts

@@ -0,0 +1,5 @@
+import Curd from '@/api/curd'
+
+const environment = new Curd('/environment')
+
+export default environment

File diff suppressed because it is too large
+ 0 - 0
frontend/src/assets/svg/cpu.svg


+ 1 - 0
frontend/src/assets/svg/memory.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683972914887" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5222" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M922.688 810.624h-149.312a37.376 37.376 0 0 1-37.312-37.376L736 736h-49.792v74.688H611.584V736h-49.792v74.688H462.208V736h-49.728v74.688H337.792V736H288l-0.256 37.248a37.312 37.312 0 0 1-37.312 37.376h-149.12A37.312 37.312 0 0 1 64 773.248V250.752c0-20.672 16.704-37.376 37.312-37.376h821.312a37.312 37.312 0 0 1 37.376 37.376v522.496a37.312 37.312 0 0 1-37.312 37.376z m-37.376-177.344h-39.168a37.376 37.376 0 0 1 0-74.752h39.168V288.128H138.688v270.4h37.952a37.376 37.376 0 0 1 0 74.752h-37.952v102.528H213.12l0.256-49.6c0-20.672 4.288-24.896 24.896-24.896h547.584c20.608 0 24.896 4.224 24.896 24.896v49.6h74.624V633.28zM736 337.792h74.688V512H736V337.792z m-174.208 0h74.688V512H561.792V337.792z m-174.208 0h74.688V512H387.584V337.792z m-174.272 0H288V512l-73.472-0.128-1.216-174.08z" p-id="5223"></path></svg>

+ 1 - 0
frontend/src/assets/svg/pulse.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683971747666" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8583" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M448 938.666667a21.333333 21.333333 0 0 1-20.38-15.06L272.6 419.833333l-61.52 123.04A21.333333 21.333333 0 0 1 192 554.666667H21.333333a21.333333 21.333333 0 0 1 0-42.666667h157.48l79.44-158.873333a21.333333 21.333333 0 0 1 39.466667 3.266666L444.666667 834 597.84 144.706667a21.333333 21.333333 0 0 1 41.213333-1.646667l155.013334 503.773333 61.52-123.04A21.333333 21.333333 0 0 1 874.666667 512h128a21.333333 21.333333 0 0 1 0 42.666667h-114.813334l-79.44 158.873333a21.333333 21.333333 0 0 1-39.466666-3.266667L622 232.666667 468.826667 921.96a21.333333 21.333333 0 0 1-20 16.666667z" fill="#5C5C66" p-id="8584"></path></svg>

+ 86 - 0
frontend/src/components/EnvIndicator/EnvIndicator.vue

@@ -0,0 +1,86 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+import {CloseOutlined, DashboardOutlined, DatabaseOutlined} from '@ant-design/icons-vue'
+import {useSettingsStore} from '@/pinia'
+import {storeToRefs} from 'pinia'
+import {useRouter} from 'vue-router'
+import {computed, watch} from 'vue'
+
+const {$gettext} = useGettext()
+const settingsStore = useSettingsStore()
+
+const {environment} = storeToRefs(settingsStore)
+const router = useRouter()
+
+function clear_env() {
+    router.push('/dashboard')
+    location.reload()
+    settingsStore.clear_environment()
+}
+
+const is_local = computed(() => {
+    return environment.value.id === 0
+})
+
+const node_id = computed(() => environment.value.id)
+
+watch(node_id, () => {
+    router.push('/dashboard')
+    location.reload()
+})
+
+</script>
+
+<template>
+    <div class="indicator">
+        <div class="container">
+            <database-outlined/>
+            <span class="env-name" v-if="is_local">
+                 {{ $gettext('Local') }}
+            </span>
+            <span class="env-name" v-else>
+                 {{ environment.name }}
+            </span>
+            <a-tag @click="clear_env">
+                <dashboard-outlined v-if="is_local"/>
+                <close-outlined v-else/>
+            </a-tag>
+        </div>
+    </div>
+</template>
+
+<style scoped lang="less">
+.ant-layout-sider-collapsed {
+    .ant-tag, .env-name {
+        display: none;
+    }
+
+    .indicator {
+        .container {
+            justify-content: center;
+        }
+    }
+}
+
+.indicator {
+    padding: 20px;
+
+    .container {
+        border-radius: 16px;
+        border: 1px solid #91d5ff;
+        background: #e6f7ff;
+        padding: 5px 15px;
+        color: #096dd9;
+
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+
+        .ant-tag {
+            cursor: pointer;
+            margin-right: 0;
+            padding: 0 5px;
+        }
+    }
+}
+</style>

+ 8 - 3
frontend/src/components/StdDataDisplay/StdCurd.vue

@@ -86,8 +86,14 @@ function add() {
     visible.value = true
 }
 
+function get_list() {
+    const t: Table = table.value!
+    t!.get_list()
+}
+
 defineExpose({
     add,
+    get_list,
     data
 })
 
@@ -109,8 +115,7 @@ const ok = async () => {
     props.api!.save(data.id, data).then((r: any) => {
         message.success($gettext('Save Successfully'))
         Object.assign(data, r)
-        const t: Table = table.value!
-        t!.get_list()
+        get_list()
 
     }).catch((e: any) => {
         message.error($gettext(e?.message ?? 'Server error'), 5)
@@ -144,7 +149,7 @@ const selectedRowKeys = ref([])
     <div class="std-curd">
         <a-card :title="title||$gettext('Table')">
             <template v-if="!disable_add" #extra>
-                <a @click="add" v-translate>Add</a>
+                <a @click="add">{{ $gettext('Add') }}</a>
             </template>
 
             <std-table

+ 8 - 3
frontend/src/layouts/SideBar.vue

@@ -2,7 +2,8 @@
 import Logo from '@/components/Logo/Logo.vue'
 import {routes} from '@/routes'
 import {useRoute} from 'vue-router'
-import {computed, ref, watch} from 'vue'
+import {computed, ComputedRef, ref, watch} from 'vue'
+import EnvIndicator from '@/components/EnvIndicator/EnvIndicator.vue'
 
 const route = useRoute()
 
@@ -30,6 +31,7 @@ const sidebars = computed(() => {
 interface meta {
     icon: any
     hiddenInSidebar: boolean
+    hideChildren: boolean
 }
 
 interface sidebar {
@@ -39,7 +41,7 @@ interface sidebar {
     children: sidebar[]
 }
 
-const visible = computed(() => {
+const visible: ComputedRef<sidebar[]> = computed(() => {
 
     const res: sidebar[] = [];
 
@@ -71,6 +73,9 @@ const visible = computed(() => {
 <template>
     <div class="sidebar">
         <logo/>
+
+        <env-indicator/>
+
         <a-menu
             :openKeys="openKeys"
             mode="inline"
@@ -78,7 +83,7 @@ const visible = computed(() => {
             v-model:selectedKeys="selectedKey"
         >
             <template v-for="sidebar in visible">
-                <a-menu-item v-if="sidebar.children.length===0 || sidebar.meta.hideChildren === true"
+                <a-menu-item v-if="sidebar.children.length===0 || sidebar.meta.hideChildren"
                              :key="sidebar.name"
                              @click="$router.push('/'+sidebar.path).catch(() => {})">
                     <component :is="sidebar.meta.icon"/>

+ 20 - 1
frontend/src/lib/helper/index.ts

@@ -1,3 +1,6 @@
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+
 function bytesToSize(bytes: number) {
     if (bytes === 0) return '0 B'
 
@@ -60,9 +63,25 @@ function createEnum(definition: any) {
     }
 }
 
+function fromNow(t: string) {
+    dayjs.extend(relativeTime)
+    return dayjs(t).fromNow()
+}
+
+function formatDate(t: string) {
+    return dayjs(t).format('YYYY.MM.DD')
+}
+
+function formatDateTime(t: string) {
+    return dayjs(t).format('YYYY-MM-DD HH:mm:ss')
+}
+
 export {
     bytesToSize,
     downloadCsv,
     urlJoin,
-    createEnum
+    createEnum,
+    fromNow,
+    formatDate,
+    formatDateTime
 }

+ 5 - 2
frontend/src/lib/http/index.ts

@@ -1,5 +1,5 @@
 import axios, {AxiosRequestConfig} from 'axios'
-import {useUserStore} from '@/pinia'
+import {useSettingsStore, useUserStore} from '@/pinia'
 import {storeToRefs} from 'pinia'
 import NProgress from 'nprogress'
 import 'nprogress/nprogress.css'
@@ -7,7 +7,7 @@ import 'nprogress/nprogress.css'
 import router from '@/routes'
 
 const user = useUserStore()
-
+const settings = useSettingsStore()
 const {token} = storeToRefs(user)
 
 let instance = axios.create({
@@ -31,6 +31,9 @@ instance.interceptors.request.use(
         if (token) {
             (config.headers as any).Authorization = token.value
         }
+        if (settings.environment.id) {
+            (config.headers as any)['X-Node-ID'] = settings.environment.id
+        }
         return config
     },
     err => {

+ 5 - 3
frontend/src/lib/websocket/index.ts

@@ -1,24 +1,26 @@
 import ReconnectingWebSocket from 'reconnecting-websocket'
-import {useUserStore} from '@/pinia'
+import {useSettingsStore, useUserStore} from '@/pinia'
 import {storeToRefs} from 'pinia'
 import {urlJoin} from '@/lib/helper'
 
 
 function ws(url: string, reconnect: boolean = true): ReconnectingWebSocket | WebSocket {
     const user = useUserStore()
+    const settings = useSettingsStore()
     const {token} = storeToRefs(user)
 
     const protocol = location.protocol === 'https:' ? 'wss://' : 'ws://'
 
+    const node_id = (settings.environment.id > 0) ? ('&x_node_id=' + settings.environment.id) : ''
+
     const _url = urlJoin(protocol + window.location.host, window.location.pathname,
-        url, '?token=' + btoa(token.value))
+        url, '?token=' + btoa(token.value), node_id)
 
     if (reconnect) {
         return new ReconnectingWebSocket(_url)
     }
 
     return new WebSocket(_url)
-
 }
 
 export default ws

+ 9 - 1
frontend/src/pinia/moudule/settings.ts

@@ -4,7 +4,11 @@ export const useSettingsStore = defineStore('settings', {
     state: () => ({
         language: '',
         theme: 'light',
-        preference_theme: 'auto'
+        preference_theme: 'auto',
+        environment: {
+            id: 0,
+            name: 'Local'
+        }
     }),
     getters: {},
     actions: {
@@ -16,6 +20,10 @@ export const useSettingsStore = defineStore('settings', {
         },
         set_preference_theme(t: string) {
             this.preference_theme = t
+        },
+        clear_environment() {
+            this.environment.id = 0
+            this.environment.name = 'Local'
         }
     },
     persist: true

+ 17 - 8
frontend/src/routes/index.ts

@@ -5,6 +5,7 @@ import {useUserStore} from '@/pinia'
 import {
     CloudOutlined,
     CodeOutlined,
+    DatabaseOutlined,
     FileOutlined,
     FileTextOutlined,
     HomeOutlined,
@@ -34,14 +35,6 @@ export const routes = [
                     icon: HomeOutlined
                 }
             },
-            {
-                path: 'user',
-                name: () => $gettext('Manage Users'),
-                component: () => import('@/views/user/User.vue'),
-                meta: {
-                    icon: UserOutlined
-                }
-            },
             {
                 path: 'domain',
                 name: () => $gettext('Manage Sites'),
@@ -135,6 +128,22 @@ export const routes = [
                     }
                 }]
             },
+            {
+                path: 'environment',
+                name: () => $gettext('Environment'),
+                component: () => import('@/views/environment/environment.vue'),
+                meta: {
+                    icon: DatabaseOutlined
+                }
+            },
+            {
+                path: 'user',
+                name: () => $gettext('Manage Users'),
+                component: () => import('@/views/user/User.vue'),
+                meta: {
+                    icon: UserOutlined
+                }
+            },
             {
                 path: 'preference',
                 name: () => $gettext('Preference'),

+ 1 - 1
frontend/src/version.json

@@ -1 +1 @@
-{"version":"1.9.9","build_id":109,"total_build":179}
+{"version":"1.9.9","build_id":113,"total_build":183}

+ 5 - 320
frontend/src/views/dashboard/DashBoard.vue

@@ -1,330 +1,15 @@
 <script setup lang="ts">
-import AreaChart from '@/components/Chart/AreaChart.vue'
-
-import RadialBarChart from '@/components/Chart/RadialBarChart.vue'
-import {useGettext} from 'vue3-gettext'
-import {onMounted, onUnmounted, reactive, ref} from 'vue'
-import analytic from '@/api/analytic'
-import ws from '@/lib/websocket'
-import {bytesToSize} from '@/lib/helper'
-import ReconnectingWebSocket from 'reconnecting-websocket'
-
-const {$gettext} = useGettext()
-
-let websocket: ReconnectingWebSocket | WebSocket
-
-const host = reactive({})
-const cpu = ref('0.0')
-const cpu_info = reactive([])
-const cpu_analytic_series = reactive([{name: 'User', data: <any>[]}, {name: 'Total', data: <any>[]}])
-const net_analytic = reactive([{name: $gettext('Receive'), data: <any>[]},
-    {name: $gettext('Send'), data: <any>[]}])
-const disk_io_analytic = reactive([{name: $gettext('Writes'), data: <any>[]},
-    {name: $gettext('Reads'), data: <any>[]}])
-const memory = reactive({})
-const disk = reactive({})
-const disk_io = reactive({writes: 0, reads: 0})
-const uptime = ref('')
-const loadavg = reactive({})
-const net = reactive({recv: 0, sent: 0, last_recv: 0, last_sent: 0})
-
-const net_formatter = (bytes: number) => {
-    return bytesToSize(bytes) + '/s'
-}
-
-interface Usage {
-    x: number
-    y: number
-}
-
-onMounted(() => {
-    analytic.init().then(r => {
-        Object.assign(host, r.host)
-        Object.assign(cpu_info, r.cpu.info)
-        Object.assign(memory, r.memory)
-        Object.assign(disk, r.disk)
-
-        // uptime
-        handle_uptime(r.host?.uptime)
-        // load_avg
-        Object.assign(loadavg, r.loadavg)
-
-        net.last_recv = r.network.init.bytesRecv
-        net.last_sent = r.network.init.bytesSent
-        r.cpu.user.forEach((u: Usage) => {
-            cpu_analytic_series[0].data.push([u.x, u.y.toFixed(2)])
-        })
-        r.cpu.total.forEach((u: Usage) => {
-            cpu_analytic_series[1].data.push([u.x, u.y.toFixed(2)])
-        })
-        r.network.bytesRecv.forEach((u: Usage) => {
-            net_analytic[0].data.push([u.x, u.y.toFixed(2)])
-        })
-        r.network.bytesSent.forEach((u: Usage) => {
-            net_analytic[1].data.push([u.x, u.y.toFixed(2)])
-        })
-        disk_io_analytic[0].data = disk_io_analytic[0].data.concat(r.disk_io.writes)
-        disk_io_analytic[1].data = disk_io_analytic[1].data.concat(r.disk_io.reads)
-
-        websocket = ws('/api/analytic')
-        websocket.onmessage = wsOnMessage
-
-    })
-})
-
-onUnmounted(() => {
-    websocket.close()
-})
-
-function handle_uptime(t: number) {
-    // uptime
-    let _uptime = Math.floor(t)
-    let uptime_days = Math.floor(_uptime / 86400)
-    _uptime -= uptime_days * 86400
-    let uptime_hours = Math.floor(_uptime / 3600)
-    _uptime -= uptime_hours * 3600
-    uptime.value = uptime_days + 'd ' + uptime_hours + 'h ' + Math.floor(_uptime / 60) + 'm'
-}
-
-function wsOnMessage(m: { data: any }) {
-    const r = JSON.parse(m.data)
-
-    const cpu_usage = r.cpu.system + r.cpu.user
-    cpu.value = cpu_usage.toFixed(2)
-
-    const time = new Date().getTime()
-
-    cpu_analytic_series[0].data.push([time, r.cpu.user.toFixed(2)])
-    cpu_analytic_series[1].data.push([time, cpu.value])
-
-    if (cpu_analytic_series[0].data.length > 100) {
-        cpu_analytic_series[0].data.shift()
-        cpu_analytic_series[1].data.shift()
-    }
-
-    // mem
-    Object.assign(memory, r.memory)
-
-    // disk
-    Object.assign(disk, r.disk)
-    disk_io.writes = r.disk.writes.y
-    disk_io.reads = r.disk.reads.y
-
-    // uptime
-    handle_uptime(r.uptime)
-
-    // loadavg
-    Object.assign(loadavg, r.loadavg)
-
-    // network
-    Object.assign(net, r.network)
-    net.recv = r.network.bytesRecv - net.last_recv
-    net.sent = r.network.bytesSent - net.last_sent
-    net.last_recv = r.network.bytesRecv
-    net.last_sent = r.network.bytesSent
-
-    net_analytic[0].data.push([time, net.recv])
-    net_analytic[1].data.push([time, net.sent])
-
-    if (net_analytic[0].data.length > 100) {
-        net_analytic[0].data.shift()
-        net_analytic[1].data.shift()
-    }
-
-    disk_io_analytic[0].data.push(r.disk.writes)
-    disk_io_analytic[1].data.push(r.disk.reads)
-
-    if (disk_io_analytic[0].data.length > 100) {
-        disk_io_analytic[0].data.shift()
-        disk_io_analytic[1].data.shift()
-    }
-}
+import ServerAnalytic from '@/views/dashboard/ServerAnalytic.vue'
+import Environments from '@/views/dashboard/Environments.vue'
 </script>
 
 <template>
     <div>
-        <a-row :gutter="[{xs: 0, sm: 16}, 16]" class="first-row">
-            <a-col :xl="7" :lg="24" :md="24" :xs="24">
-                <a-card :title="$gettext('Server Info')" :bordered="false">
-                    <p>
-                        <translate>Uptime:</translate>
-                        {{ uptime }}
-                    </p>
-                    <p>
-                        <translate>Load Averages:</translate>
-                        <span class="load-avg-describe"> 1min:</span>{{ ' ' + loadavg?.load1?.toFixed(2) }}
-                        <span class="load-avg-describe"> | 5min:</span>{{ loadavg?.load5?.toFixed(2) }}
-                        <span class="load-avg-describe"> | 15min:</span>{{ loadavg?.load15?.toFixed(2) }}
-                    </p>
-                    <p>
-                        <translate>OS:</translate>
-                        <span class="os-platform">{{ ' ' + host.platform }}</span> {{ host.platformVersion }}
-                        <span class="os-info">({{ host.os }} {{ host.kernelVersion }}
-                        {{ host.kernelArch }})</span>
-                    </p>
-                    <p v-if="cpu_info">
-                        {{ $gettext('CPU:') + ' ' }}
-                        <span class="cpu-model">{{ cpu_info[0]?.modelName || 'core' }}</span>
-                        <span class="cpu-mhz">{{ (cpu_info[0]?.mhz / 1000).toFixed(2) + 'GHz' }}</span>
-                        * {{ cpu_info.length }}
-                    </p>
-                </a-card>
-            </a-col>
-            <a-col :xl="10" :lg="16" :md="24" :xs="24" class="chart_dashboard">
-                <a-card :title="$gettext('Memory and Storage')" :bordered="false">
-                    <a-row :gutter="[0,16]">
-                        <a-col :xs="24" :sm="24" :md="8">
-                            <radial-bar-chart :name="$gettext('Memory')" :series="[memory.pressure]"
-                                              :centerText="memory.used" :bottom-text="memory.total" colors="#36a3eb"/>
-                        </a-col>
-                        <a-col :xs="24" :sm="12" :md="8">
-                            <radial-bar-chart :name="$gettext('Swap')" :series="[memory.swap_percent]"
-                                              :centerText="memory.swap_used"
-                                              :bottom-text="memory.swap_total" colors="#ff6385"/>
-                        </a-col>
-                        <a-col :xs="24" :sm="12" :md="8">
-                            <radial-bar-chart :name="$gettext('Storage')" :series="[disk.percentage]"
-                                              :centerText="disk.used" :bottom-text="disk.total" colors="#87d068"/>
-                        </a-col>
-                    </a-row>
-                </a-card>
-            </a-col>
-            <a-col :xl="7" :lg="8" :sm="24" :xs="24" class="chart_dashboard network-total">
-                <a-card :title="$gettext('Network Statistics')" :bordered="false">
-                    <a-row :gutter="16">
-                        <a-col :span="12">
-                            <a-statistic :value="bytesToSize(net.last_recv)"
-                                         :title="$gettext('Network Total Receive')"/>
-                        </a-col>
-                        <a-col :span="12">
-                            <a-statistic :value="bytesToSize(net.last_sent)"
-                                         :title="$gettext('Network Total Send')"/>
-                        </a-col>
-                    </a-row>
-                </a-card>
-            </a-col>
-        </a-row>
-        <a-row :gutter="[{xs: 0, sm: 16}, 16]" class="row-two">
-            <a-col :xl="8" :lg="24" :md="24" :sm="24" :xs="24">
-                <a-card :title="$gettext('CPU Status')" :bordered="false">
-                    <a-statistic :value="cpu" title="CPU">
-                        <template v-slot:suffix>
-                            <span>%</span>
-                        </template>
-                    </a-statistic>
-                    <area-chart :series="cpu_analytic_series" :max="100"/>
-                </a-card>
-            </a-col>
-            <a-col :xl="8" :lg="12" :md="24" :sm="24" :xs="24">
-                <a-card :title="$gettext('Network')" :bordered="false">
-                    <a-row :gutter="16">
-                        <a-col :span="12">
-                            <a-statistic :value="bytesToSize(net.recv)"
-                                         :title="$gettext('Receive')">
-                                <template v-slot:suffix>
-                                    <span>/s</span>
-                                </template>
-                            </a-statistic>
-                        </a-col>
-                        <a-col :span="12">
-                            <a-statistic :value="bytesToSize(net.sent)" :title="$gettext('Send')">
-                                <template v-slot:suffix>
-                                    <span>/s</span>
-                                </template>
-                            </a-statistic>
-                        </a-col>
-                    </a-row>
-                    <area-chart :series="net_analytic" :y_formatter="net_formatter"/>
-                </a-card>
-            </a-col>
-            <a-col :xl="8" :lg="12" :md="24" :sm="24" :xs="24">
-                <a-card :title="$gettext('Disk IO')" :bordered="false">
-                    <a-row :gutter="16">
-                        <a-col :span="12">
-                            <a-statistic :value="disk_io.writes"
-                                         :title="$gettext('Writes')">
-                                <template v-slot:suffix>
-                                    <span>/s</span>
-                                </template>
-                            </a-statistic>
-                        </a-col>
-                        <a-col :span="12">
-                            <a-statistic :value="disk_io.reads" :title="$gettext('Reads')">
-                                <template v-slot:suffix>
-                                    <span>/s</span>
-                                </template>
-                            </a-statistic>
-                        </a-col>
-                    </a-row>
-                    <area-chart :series="disk_io_analytic"/>
-                </a-card>
-            </a-col>
-        </a-row>
+        <server-analytic/>
+        <environments/>
     </div>
 </template>
 
-<style lang="less" scoped>
-.first-row {
-    .ant-card {
-        min-height: 227px;
-
-        p {
-            margin-bottom: 8px;
-        }
-    }
-
-    margin-bottom: 20px;
-}
+<style scoped lang="less">
 
-.ant-card {
-    .ant-statistic {
-        margin: 0 0 10px 10px
-    }
-
-    .chart {
-        max-width: 800px;
-        max-height: 350px;
-    }
-
-    .chart_dashboard {
-        padding: 60px;
-
-        .description {
-            width: 120px;
-            text-align: center
-        }
-    }
-
-    @media (max-width: 512px) {
-        margin: 10px 0;
-        .chart_dashboard {
-            padding: 20px;
-        }
-    }
-}
-
-.load-avg-describe {
-    @media (max-width: 1600px) and (min-width: 1200px) {
-        display: none;
-    }
-}
-
-.os-info {
-    @media (max-width: 1600px) and (min-width: 1200px) {
-        display: none;
-    }
-}
-
-.cpu-model {
-    @media (max-width: 1790px) and (min-width: 1200px) {
-        display: none;
-    }
-}
-
-.cpu-mhz {
-    @media (min-width: 1790px) or (max-width: 1200px) {
-        display: none;
-    }
-}
 </style>
-

+ 102 - 0
frontend/src/views/dashboard/Environments.vue

@@ -0,0 +1,102 @@
+<script setup lang="ts">
+import {useSettingsStore} from '@/pinia'
+import {useGettext} from 'vue3-gettext'
+import {computed, ref} from 'vue'
+import environment from '@/api/environment'
+import Icon, {LinkOutlined, SendOutlined, ThunderboltOutlined} from '@ant-design/icons-vue'
+import logo from '@/assets/img/logo.png'
+import pulse from '@/assets/svg/pulse.svg'
+import cpu from '@/assets/svg/cpu.svg'
+import memory from '@/assets/svg/memory.svg'
+import {formatDateTime} from '@/lib/helper'
+
+const settingsStore = useSettingsStore()
+const {$gettext} = useGettext()
+
+const data = ref([])
+
+environment.get_list().then(r => {
+    data.value = r.data
+})
+
+export interface Node {
+    id: number
+    name: string
+    token: string
+}
+
+const {environment: env} = settingsStore
+
+function link_start(node: Node) {
+    env.id = node.id
+    env.name = node.name
+}
+
+const visible = computed(() => {
+    if (env.id > 0) {
+        return false
+    } else {
+        return data.value?.length
+    }
+})
+</script>
+
+<template>
+    <a-card class="env-list-card" :title="$gettext('Environments')" v-if="visible">
+        <a-list item-layout="horizontal" :data-source="data">
+            <template #renderItem="{ item }">
+                <a-list-item>
+                    <template #actions>
+                        <a-button type="primary" @click="link_start(item)" :disabled="env.id===item.id" ghost>
+                            <send-outlined/>
+                            {{ env.id !== item.id ? $gettext('Link Start') : $gettext('Connected') }}
+                        </a-button>
+                    </template>
+                    <a-list-item-meta>
+                        <template #title>
+                            {{ item.name }}
+                            <a-tag color="blue" v-if="item.status">{{ $gettext('Online') }}</a-tag>
+                            <a-tag color="error" v-else>{{ $gettext('Offline') }}</a-tag>
+                            <div class="runtime-meta">
+                                <span><Icon :component="pulse"/> {{ formatDateTime(item.response_at) }}</span>
+                                <span><thunderbolt-outlined/>{{ item.version }}</span>
+                                <span><link-outlined/>{{ item.url }}</span>
+                            </div>
+                        </template>
+                        <template #avatar>
+                            <a-avatar :src="logo"/>
+                        </template>
+                        <template #description>
+                            <div class="runtime-meta">
+                                <span><Icon :component="cpu"/> {{ item.cpu_num }} CPU</span>
+                                <span><Icon :component="memory"/> {{ item.memory_total }}</span>
+                            </div>
+                        </template>
+                    </a-list-item-meta>
+                </a-list-item>
+            </template>
+        </a-list>
+    </a-card>
+</template>
+
+<style scoped lang="less">
+.env-list-card {
+    margin-top: 16px;
+
+    .runtime-meta {
+        display: inline-flex;
+        margin-left: 8px;
+
+        span {
+            font-weight: 400;
+            font-size: 13px;
+            margin-right: 16px;
+            color: #9b9b9b;
+
+            &.anticon {
+                margin-right: 4px;
+            }
+        }
+    }
+}
+</style>

+ 330 - 0
frontend/src/views/dashboard/ServerAnalytic.vue

@@ -0,0 +1,330 @@
+<script setup lang="ts">
+import AreaChart from '@/components/Chart/AreaChart.vue'
+
+import RadialBarChart from '@/components/Chart/RadialBarChart.vue'
+import {useGettext} from 'vue3-gettext'
+import {onMounted, onUnmounted, reactive, ref} from 'vue'
+import analytic from '@/api/analytic'
+import ws from '@/lib/websocket'
+import {bytesToSize} from '@/lib/helper'
+import ReconnectingWebSocket from 'reconnecting-websocket'
+
+const {$gettext} = useGettext()
+
+let websocket: ReconnectingWebSocket | WebSocket
+
+const host = reactive({})
+const cpu = ref('0.0')
+const cpu_info = reactive([])
+const cpu_analytic_series = reactive([{name: 'User', data: <any>[]}, {name: 'Total', data: <any>[]}])
+const net_analytic = reactive([{name: $gettext('Receive'), data: <any>[]},
+    {name: $gettext('Send'), data: <any>[]}])
+const disk_io_analytic = reactive([{name: $gettext('Writes'), data: <any>[]},
+    {name: $gettext('Reads'), data: <any>[]}])
+const memory = reactive({})
+const disk = reactive({})
+const disk_io = reactive({writes: 0, reads: 0})
+const uptime = ref('')
+const loadavg = reactive({})
+const net = reactive({recv: 0, sent: 0, last_recv: 0, last_sent: 0})
+
+const net_formatter = (bytes: number) => {
+    return bytesToSize(bytes) + '/s'
+}
+
+interface Usage {
+    x: number
+    y: number
+}
+
+onMounted(() => {
+    analytic.init().then(r => {
+        Object.assign(host, r.host)
+        Object.assign(cpu_info, r.cpu.info)
+        Object.assign(memory, r.memory)
+        Object.assign(disk, r.disk)
+
+        // uptime
+        handle_uptime(r.host?.uptime)
+        // load_avg
+        Object.assign(loadavg, r.loadavg)
+
+        net.last_recv = r.network.init.bytesRecv
+        net.last_sent = r.network.init.bytesSent
+        r.cpu.user.forEach((u: Usage) => {
+            cpu_analytic_series[0].data.push([u.x, u.y.toFixed(2)])
+        })
+        r.cpu.total.forEach((u: Usage) => {
+            cpu_analytic_series[1].data.push([u.x, u.y.toFixed(2)])
+        })
+        r.network.bytesRecv.forEach((u: Usage) => {
+            net_analytic[0].data.push([u.x, u.y.toFixed(2)])
+        })
+        r.network.bytesSent.forEach((u: Usage) => {
+            net_analytic[1].data.push([u.x, u.y.toFixed(2)])
+        })
+        disk_io_analytic[0].data = disk_io_analytic[0].data.concat(r.disk_io.writes)
+        disk_io_analytic[1].data = disk_io_analytic[1].data.concat(r.disk_io.reads)
+
+        websocket = ws('/api/analytic')
+        websocket.onmessage = wsOnMessage
+
+    })
+})
+
+onUnmounted(() => {
+    websocket.close()
+})
+
+function handle_uptime(t: number) {
+    // uptime
+    let _uptime = Math.floor(t)
+    let uptime_days = Math.floor(_uptime / 86400)
+    _uptime -= uptime_days * 86400
+    let uptime_hours = Math.floor(_uptime / 3600)
+    _uptime -= uptime_hours * 3600
+    uptime.value = uptime_days + 'd ' + uptime_hours + 'h ' + Math.floor(_uptime / 60) + 'm'
+}
+
+function wsOnMessage(m: { data: any }) {
+    const r = JSON.parse(m.data)
+
+    const cpu_usage = r.cpu.system + r.cpu.user
+    cpu.value = cpu_usage.toFixed(2)
+
+    const time = new Date().getTime()
+
+    cpu_analytic_series[0].data.push([time, r.cpu.user.toFixed(2)])
+    cpu_analytic_series[1].data.push([time, cpu.value])
+
+    if (cpu_analytic_series[0].data.length > 100) {
+        cpu_analytic_series[0].data.shift()
+        cpu_analytic_series[1].data.shift()
+    }
+
+    // mem
+    Object.assign(memory, r.memory)
+
+    // disk
+    Object.assign(disk, r.disk)
+    disk_io.writes = r.disk.writes.y
+    disk_io.reads = r.disk.reads.y
+
+    // uptime
+    handle_uptime(r.uptime)
+
+    // loadavg
+    Object.assign(loadavg, r.loadavg)
+
+    // network
+    Object.assign(net, r.network)
+    net.recv = r.network.bytesRecv - net.last_recv
+    net.sent = r.network.bytesSent - net.last_sent
+    net.last_recv = r.network.bytesRecv
+    net.last_sent = r.network.bytesSent
+
+    net_analytic[0].data.push([time, net.recv])
+    net_analytic[1].data.push([time, net.sent])
+
+    if (net_analytic[0].data.length > 100) {
+        net_analytic[0].data.shift()
+        net_analytic[1].data.shift()
+    }
+
+    disk_io_analytic[0].data.push(r.disk.writes)
+    disk_io_analytic[1].data.push(r.disk.reads)
+
+    if (disk_io_analytic[0].data.length > 100) {
+        disk_io_analytic[0].data.shift()
+        disk_io_analytic[1].data.shift()
+    }
+}
+</script>
+
+<template>
+    <div>
+        <a-row :gutter="[{xs: 0, sm: 16}, 16]" class="first-row">
+            <a-col :xl="7" :lg="24" :md="24" :xs="24">
+                <a-card :title="$gettext('Server Info')" :bordered="false">
+                    <p>
+                        <translate>Uptime:</translate>
+                        {{ uptime }}
+                    </p>
+                    <p>
+                        <translate>Load Averages:</translate>
+                        <span class="load-avg-describe"> 1min:</span>{{ ' ' + loadavg?.load1?.toFixed(2) }}
+                        <span class="load-avg-describe"> | 5min:</span>{{ loadavg?.load5?.toFixed(2) }}
+                        <span class="load-avg-describe"> | 15min:</span>{{ loadavg?.load15?.toFixed(2) }}
+                    </p>
+                    <p>
+                        <translate>OS:</translate>
+                        <span class="os-platform">{{ ' ' + host.platform }}</span> {{ host.platformVersion }}
+                        <span class="os-info">({{ host.os }} {{ host.kernelVersion }}
+                        {{ host.kernelArch }})</span>
+                    </p>
+                    <p v-if="cpu_info">
+                        {{ $gettext('CPU:') + ' ' }}
+                        <span class="cpu-model">{{ cpu_info[0]?.modelName || 'core' }}</span>
+                        <span class="cpu-mhz">{{ (cpu_info[0]?.mhz / 1000).toFixed(2) + 'GHz' }}</span>
+                        * {{ cpu_info.length }}
+                    </p>
+                </a-card>
+            </a-col>
+            <a-col :xl="10" :lg="16" :md="24" :xs="24" class="chart_dashboard">
+                <a-card :title="$gettext('Memory and Storage')" :bordered="false">
+                    <a-row :gutter="[0,16]">
+                        <a-col :xs="24" :sm="24" :md="8">
+                            <radial-bar-chart :name="$gettext('Memory')" :series="[memory.pressure]"
+                                              :centerText="memory.used" :bottom-text="memory.total" colors="#36a3eb"/>
+                        </a-col>
+                        <a-col :xs="24" :sm="12" :md="8">
+                            <radial-bar-chart :name="$gettext('Swap')" :series="[memory.swap_percent]"
+                                              :centerText="memory.swap_used"
+                                              :bottom-text="memory.swap_total" colors="#ff6385"/>
+                        </a-col>
+                        <a-col :xs="24" :sm="12" :md="8">
+                            <radial-bar-chart :name="$gettext('Storage')" :series="[disk.percentage]"
+                                              :centerText="disk.used" :bottom-text="disk.total" colors="#87d068"/>
+                        </a-col>
+                    </a-row>
+                </a-card>
+            </a-col>
+            <a-col :xl="7" :lg="8" :sm="24" :xs="24" class="chart_dashboard network-total">
+                <a-card :title="$gettext('Network Statistics')" :bordered="false">
+                    <a-row :gutter="16">
+                        <a-col :span="12">
+                            <a-statistic :value="bytesToSize(net.last_recv)"
+                                         :title="$gettext('Network Total Receive')"/>
+                        </a-col>
+                        <a-col :span="12">
+                            <a-statistic :value="bytesToSize(net.last_sent)"
+                                         :title="$gettext('Network Total Send')"/>
+                        </a-col>
+                    </a-row>
+                </a-card>
+            </a-col>
+        </a-row>
+        <a-row :gutter="[{xs: 0, sm: 16}, 16]" class="row-two">
+            <a-col :xl="8" :lg="24" :md="24" :sm="24" :xs="24">
+                <a-card :title="$gettext('CPU Status')" :bordered="false">
+                    <a-statistic :value="cpu" title="CPU">
+                        <template v-slot:suffix>
+                            <span>%</span>
+                        </template>
+                    </a-statistic>
+                    <area-chart :series="cpu_analytic_series" :max="100"/>
+                </a-card>
+            </a-col>
+            <a-col :xl="8" :lg="12" :md="24" :sm="24" :xs="24">
+                <a-card :title="$gettext('Network')" :bordered="false">
+                    <a-row :gutter="16">
+                        <a-col :span="12">
+                            <a-statistic :value="bytesToSize(net.recv)"
+                                         :title="$gettext('Receive')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
+                        </a-col>
+                        <a-col :span="12">
+                            <a-statistic :value="bytesToSize(net.sent)" :title="$gettext('Send')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
+                        </a-col>
+                    </a-row>
+                    <area-chart :series="net_analytic" :y_formatter="net_formatter"/>
+                </a-card>
+            </a-col>
+            <a-col :xl="8" :lg="12" :md="24" :sm="24" :xs="24">
+                <a-card :title="$gettext('Disk IO')" :bordered="false">
+                    <a-row :gutter="16">
+                        <a-col :span="12">
+                            <a-statistic :value="disk_io.writes"
+                                         :title="$gettext('Writes')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
+                        </a-col>
+                        <a-col :span="12">
+                            <a-statistic :value="disk_io.reads" :title="$gettext('Reads')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
+                        </a-col>
+                    </a-row>
+                    <area-chart :series="disk_io_analytic"/>
+                </a-card>
+            </a-col>
+        </a-row>
+    </div>
+</template>
+
+<style lang="less" scoped>
+.first-row {
+    .ant-card {
+        min-height: 227px;
+
+        p {
+            margin-bottom: 8px;
+        }
+    }
+
+    margin-bottom: 20px;
+}
+
+.ant-card {
+    .ant-statistic {
+        margin: 0 0 10px 10px
+    }
+
+    .chart {
+        max-width: 800px;
+        max-height: 350px;
+    }
+
+    .chart_dashboard {
+        padding: 60px;
+
+        .description {
+            width: 120px;
+            text-align: center
+        }
+    }
+
+    @media (max-width: 512px) {
+        margin: 10px 0;
+        .chart_dashboard {
+            padding: 20px;
+        }
+    }
+}
+
+.load-avg-describe {
+    @media (max-width: 1600px) and (min-width: 1200px) {
+        display: none;
+    }
+}
+
+.os-info {
+    @media (max-width: 1600px) and (min-width: 1200px) {
+        display: none;
+    }
+}
+
+.cpu-model {
+    @media (max-width: 1790px) and (min-width: 1200px) {
+        display: none;
+    }
+}
+
+.cpu-mhz {
+    @media (min-width: 1790px) or (max-width: 1200px) {
+        display: none;
+    }
+}
+</style>
+

+ 73 - 0
frontend/src/views/environment/Environment.vue

@@ -0,0 +1,73 @@
+<script setup lang="tsx">
+import {useGettext} from 'vue3-gettext'
+import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+import environment from '@/api/environment'
+import StdCurd from '@/components/StdDataDisplay/StdCurd.vue'
+import {input} from '@/components/StdDataEntry'
+import {h} from 'vue'
+import {Badge} from 'ant-design-vue'
+
+const {$gettext, interpolate} = useGettext()
+
+const columns = [{
+    title: () => $gettext('Name'),
+    dataIndex: 'name',
+    sorter: true,
+    pithy: true,
+    edit: {
+        type: input
+    }
+}, {
+    title: () => $gettext('URL'),
+    dataIndex: 'url',
+    sorter: true,
+    pithy: true,
+    edit: {
+        type: input,
+        placeholder: () => 'https://10.0.0.1:9000'
+    }
+}, {
+    title: () => $gettext('Token'),
+    dataIndex: 'token',
+    sorter: true,
+    display: false,
+    edit: {
+        type: input
+    }
+}, {
+    title: () => $gettext('Status'),
+    dataIndex: 'status',
+    customRender: (args: customRender) => {
+        const template: any = []
+        const {text} = args
+        if (text === true || text > 0) {
+            template.push(<Badge status="success"/>)
+            template.push($gettext('Online'))
+        } else {
+            template.push(<Badge status="error"/>)
+            template.push($gettext('Offline'))
+        }
+        return h('div', template)
+    },
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Updated at'),
+    dataIndex: 'updated_at',
+    customRender: datetime,
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Action'),
+    dataIndex: 'action'
+}]
+
+</script>
+
+<template>
+    <std-curd :title="$gettext('Environment')" :api="environment" :columns="columns"/>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 1 - 1
frontend/version.json

@@ -1 +1 @@
-{"version":"1.9.9","build_id":109,"total_build":179}
+{"version":"1.9.9","build_id":113,"total_build":183}

+ 1 - 1
frontend/vite.config.ts

@@ -75,6 +75,6 @@ export default defineConfig({
         }
     },
     build: {
-        chunkSizeWarningLimit: 600
+        chunkSizeWarningLimit: 1000
     }
 })

+ 58 - 42
go.mod

@@ -6,39 +6,39 @@ require (
 	github.com/BurntSushi/toml v1.2.1
 	github.com/creack/pty v1.1.18
 	github.com/dustin/go-humanize v1.0.1
-	github.com/fatih/color v1.13.0
+	github.com/fatih/color v1.15.0
 	github.com/gin-contrib/static v0.0.1
 	github.com/gin-gonic/gin v1.9.0
-	github.com/go-acme/lego/v4 v4.10.2
-	github.com/go-co-op/gocron v1.20.1
-	github.com/go-playground/validator/v10 v10.12.0
+	github.com/go-acme/lego/v4 v4.11.0
+	github.com/go-co-op/gocron v1.27.0
+	github.com/go-playground/validator/v10 v10.13.0
 	github.com/golang-jwt/jwt v3.2.2+incompatible
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/websocket v1.5.0
 	github.com/hpcloud/tail v1.0.0
 	github.com/jpillora/overseer v1.1.6
-	github.com/lib/pq v1.10.7
+	github.com/lib/pq v1.10.9
 	github.com/pkg/errors v0.9.1
-	github.com/sashabaranov/go-openai v1.7.0
-	github.com/shirou/gopsutil/v3 v3.23.3
+	github.com/sashabaranov/go-openai v1.9.4
+	github.com/shirou/gopsutil/v3 v3.23.4
 	github.com/spf13/cast v1.5.0
-	github.com/tufanbarisyildirim/gonginx v0.0.0-20230508112508-44b3f58122c3
+	github.com/tufanbarisyildirim/gonginx v0.0.0-20230508164033-d7b72d6cd0d5
 	github.com/unknwon/com v1.0.1
 	go.uber.org/zap v1.24.0
-	golang.org/x/crypto v0.8.0
+	golang.org/x/crypto v0.9.0
 	gopkg.in/ini.v1 v1.67.0
 	gorm.io/driver/sqlite v1.5.0
-	gorm.io/gen v0.3.21
-	gorm.io/gorm v1.25.0
+	gorm.io/gen v0.3.22
+	gorm.io/gorm v1.25.1
 	gorm.io/plugin/dbresolver v1.4.1
 )
 
 require (
-	cloud.google.com/go/compute v1.19.1 // indirect
+	cloud.google.com/go/compute v1.19.2 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
 	github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
-	github.com/Azure/go-autorest/autorest v0.11.28 // indirect
+	github.com/Azure/go-autorest/autorest v0.11.29 // indirect
 	github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
 	github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
@@ -50,16 +50,16 @@ require (
 	github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
 	github.com/StackExchange/wmi v1.2.1 // indirect
 	github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
-	github.com/aliyun/alibaba-cloud-sdk-go v1.62.281 // indirect
+	github.com/aliyun/alibaba-cloud-sdk-go v1.62.318 // indirect
 	github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
 	github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
-	github.com/aws/aws-sdk-go v1.44.242 // indirect
+	github.com/aws/aws-sdk-go v1.44.262 // indirect
 	github.com/boombuler/barcode v1.0.1 // indirect
-	github.com/bytedance/sonic v1.8.7 // indirect
-	github.com/cenkalti/backoff/v4 v4.2.0 // indirect
+	github.com/bytedance/sonic v1.8.8 // indirect
+	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
-	github.com/civo/civogo v0.3.29 // indirect
-	github.com/cloudflare/cloudflare-go v0.49.0 // indirect
+	github.com/civo/civogo v0.3.34 // indirect
+	github.com/cloudflare/cloudflare-go v0.67.0 // indirect
 	github.com/cpu/goacmedns v0.1.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/deepmap/oapi-codegen v1.12.4 // indirect
@@ -72,25 +72,29 @@ require (
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-errors/errors v1.4.2 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
+	github.com/go-logr/logr v1.2.4 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-resty/resty/v2 v2.7.0 // indirect
-	github.com/go-sql-driver/mysql v1.7.0 // indirect
+	github.com/go-sql-driver/mysql v1.7.1 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
-	github.com/google/s2a-go v0.1.1 // indirect
+	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/google/s2a-go v0.1.3 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
 	github.com/googleapis/gax-go/v2 v2.8.0 // indirect
 	github.com/gophercloud/gophercloud v1.3.0 // indirect
-	github.com/gophercloud/utils v0.0.0-20230330070308-5bd5e1d608f8 // indirect
+	github.com/gophercloud/utils v0.0.0-20230418172808-6eab72e966e1 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
+	github.com/hashicorp/go-uuid v1.0.3 // indirect
 	github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
 	github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
@@ -103,8 +107,8 @@ require (
 	github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
 	github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
 	github.com/labbsr0x/goh v1.0.1 // indirect
-	github.com/leodido/go-urn v1.2.3 // indirect
-	github.com/linode/linodego v1.16.1 // indirect
+	github.com/leodido/go-urn v1.2.4 // indirect
+	github.com/linode/linodego v1.16.2 // indirect
 	github.com/liquidweb/go-lwApi v0.0.5 // indirect
 	github.com/liquidweb/liquidweb-cli v0.6.10 // indirect
 	github.com/liquidweb/liquidweb-go v1.6.3 // indirect
@@ -112,7 +116,7 @@ require (
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.18 // indirect
 	github.com/mattn/go-sqlite3 v1.14.16 // indirect
-	github.com/miekg/dns v1.1.53 // indirect
+	github.com/miekg/dns v1.1.54 // indirect
 	github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -125,6 +129,7 @@ require (
 	github.com/nrdcg/freemyip v0.2.0 // indirect
 	github.com/nrdcg/goinwx v0.8.2 // indirect
 	github.com/nrdcg/namesilo v0.2.1 // indirect
+	github.com/nrdcg/nodion v0.1.0 // indirect
 	github.com/nrdcg/porkbun v0.2.0 // indirect
 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
 	github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect
@@ -134,31 +139,33 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
 	github.com/pquerna/otp v1.4.0 // indirect
+	github.com/pretty66/websocketproxy v0.0.0-20220507015215-930b3a686308 // indirect
 	github.com/robfig/cron/v3 v3.0.1 // indirect
 	github.com/sacloud/api-client-go v0.2.7 // indirect
 	github.com/sacloud/go-http v0.1.5 // indirect
-	github.com/sacloud/iaas-api-go v1.9.2 // indirect
+	github.com/sacloud/iaas-api-go v1.10.0 // indirect
 	github.com/sacloud/packages-go v0.0.8 // indirect
-	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.15 // indirect
-	github.com/shoenig/go-m1cpu v0.1.5 // indirect
+	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.16 // indirect
+	github.com/shoenig/go-m1cpu v0.1.6 // indirect
+	github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 // indirect
 	github.com/sirupsen/logrus v1.9.0 // indirect
 	github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
 	github.com/softlayer/softlayer-go v1.1.2 // indirect
 	github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
 	github.com/stretchr/objx v0.5.0 // indirect
 	github.com/stretchr/testify v1.8.2 // indirect
-	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.637 // indirect
-	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.637 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.655 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.655 // indirect
 	github.com/tklauser/go-sysconf v0.3.11 // indirect
 	github.com/tklauser/numcpus v0.6.0 // indirect
 	github.com/transip/gotransip/v6 v6.20.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.11 // indirect
-	github.com/ultradns/ultradns-go-sdk v1.4.1-20230224143201-0d8b0f6 // indirect
+	github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c // indirect
 	github.com/vinyldns/go-vinyldns v0.9.16 // indirect
 	github.com/vultr/govultr/v2 v2.17.2 // indirect
-	github.com/yandex-cloud/go-genproto v0.0.0-20230410092700-15216dc82345 // indirect
-	github.com/yandex-cloud/go-sdk v0.0.0-20230403093608-cc5174142a48 // indirect
+	github.com/yandex-cloud/go-genproto v0.0.0-20230511103421-ecb0cd1514ab // indirect
+	github.com/yandex-cloud/go-sdk v0.0.0-20230511104317-0ccfef4d3a91 // indirect
 	github.com/yusufpapurcu/wmi v1.2.2 // indirect
 	go.opencensus.io v0.24.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
@@ -166,24 +173,33 @@ require (
 	go.uber.org/ratelimit v0.2.0 // indirect
 	golang.org/x/arch v0.3.0 // indirect
 	golang.org/x/mod v0.10.0 // indirect
-	golang.org/x/net v0.9.0 // indirect
-	golang.org/x/oauth2 v0.7.0 // indirect
-	golang.org/x/sync v0.1.0 // indirect
-	golang.org/x/sys v0.7.0 // indirect
+	golang.org/x/net v0.10.0 // indirect
+	golang.org/x/oauth2 v0.8.0 // indirect
+	golang.org/x/sync v0.2.0 // indirect
+	golang.org/x/sys v0.8.0 // indirect
 	golang.org/x/text v0.9.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
-	golang.org/x/tools v0.8.0 // indirect
-	google.golang.org/api v0.118.0 // indirect
+	golang.org/x/tools v0.9.1 // indirect
+	google.golang.org/api v0.122.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
-	google.golang.org/grpc v1.54.0 // indirect
+	google.golang.org/grpc v1.55.0 // indirect
 	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/fsnotify.v1 v1.4.7 // indirect
-	gopkg.in/ns1/ns1-go.v2 v2.7.5 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	gopkg.in/ns1/ns1-go.v2 v2.7.6 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gorm.io/datatypes v1.2.0 // indirect
 	gorm.io/driver/mysql v1.5.0 // indirect
-	gorm.io/hints v1.1.1 // indirect
+	gorm.io/hints v1.1.2 // indirect
+	k8s.io/api v0.27.1 // indirect
+	k8s.io/apimachinery v0.27.1 // indirect
+	k8s.io/klog/v2 v2.100.1 // indirect
+	k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect
+	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
 )
+
+replace github.com/cloudflare/cloudflare-go v0.67.0 => github.com/cloudflare/cloudflare-go v0.49.0

+ 222 - 0
go.sum

@@ -9,6 +9,8 @@ cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
 cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
+cloud.google.com/go/compute v1.19.2 h1:GbJtPo8OKVHbVep8jvM57KidbYHxeE68LOVqouNLrDY=
+cloud.google.com/go/compute v1.19.2/go.mod h1:5f5a+iC1IriXYauaQ0EyQmEAEq9CGRnV5xJSQSlTV08=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
@@ -24,7 +26,10 @@ github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSW
 github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
 github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM=
 github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA=
+github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw=
+github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs=
 github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
+github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=
 github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8=
 github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c=
 github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk=
@@ -49,6 +54,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
 github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24=
 github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
@@ -62,6 +68,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/aliyun/alibaba-cloud-sdk-go v1.62.281 h1:sN94THxWQA+nPMDZD0esg1PGy6pmkTh7SCVc1MQaKzA=
 github.com/aliyun/alibaba-cloud-sdk-go v1.62.281/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
+github.com/aliyun/alibaba-cloud-sdk-go v1.62.318 h1:1ntKWopst53IVWKlEVrgutJpEgQN+FyNZXO+h6ePgXw=
+github.com/aliyun/alibaba-cloud-sdk-go v1.62.318/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
 github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI=
 github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -70,8 +78,12 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/aws/aws-sdk-go v1.44.242 h1:bb6Rqd7dxh1gTUoVXLJTNC2c+zNaHpLRlNKk0kGN3fc=
 github.com/aws/aws-sdk-go v1.44.242/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.262 h1:gyXpcJptWoNkK+DiAiaBltlreoWKQXjAIh6FRh60F+I=
+github.com/aws/aws-sdk-go v1.44.262/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -85,10 +97,14 @@ github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
 github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q=
+github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
 github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
 github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
 github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -100,9 +116,13 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/civo/civogo v0.3.29 h1:N87pFQxEpiqaDiFkeDMxvd6YLHHTmSC2/+9v5l93bO0=
 github.com/civo/civogo v0.3.29/go.mod h1:SbS06e0JPgIF27r1sLC97gjU1xWmONQeHgzF1hfLpak=
+github.com/civo/civogo v0.3.34 h1:E78ExdJLtrLvpMqJb3kq5Y/p200m6S2l/2piFDNqzPQ=
+github.com/civo/civogo v0.3.34/go.mod h1:ovGwXtszFiTsVq1OgKG9CtE8q8TPm+4bwE13KuJBr9E=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cloudflare/cloudflare-go v0.49.0 h1:KqJYk/YQ5ZhmyYz1oa4kGDskfF1gVuZfqesaJ/XDLto=
 github.com/cloudflare/cloudflare-go v0.49.0/go.mod h1:h0QgcIZ3qEXwFiwfBO8sQxjVdYsLX+PfD7NFEnANaKg=
+github.com/cloudflare/cloudflare-go v0.67.0 h1:cTwYOAlycKb7cx4+ObSNd9iW8Om4ydqNhwcWEnVyRaE=
+github.com/cloudflare/cloudflare-go v0.67.0/go.mod h1:FfMEQEtz3ZciNqer63pwAnQTpAn0pijqov0WxnOQ7rM=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
@@ -132,8 +152,10 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi
 github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
 github.com/dnsimple/dnsimple-go v1.2.0 h1:ddTGyLVKly5HKb5L65AkLqFqwZlWo3WnR0BlFZlIddM=
 github.com/dnsimple/dnsimple-go v1.2.0/go.mod h1:z/cs26v/eiRvUyXsHQBLd8lWF8+cD6GbmkPH84plM4U=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -141,11 +163,14 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/exoscale/egoscale v0.90.0 h1:DZBXVU3iHqu5Ju5lQ5jWVlPo0IpI98SUo8Aa1UQVrmo=
 github.com/exoscale/egoscale v0.90.0/go.mod h1:wyXE5zrnFynMXA0jMhwQqSe24CfUhmBk2WI5wFZcq6Y=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
+github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
 github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
@@ -165,9 +190,13 @@ github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
 github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
 github.com/go-acme/lego/v4 v4.10.2 h1:5eW3qmda5v/LP21v1Hj70edKY1jeFZQwO617tdkwp6Q=
 github.com/go-acme/lego/v4 v4.10.2/go.mod h1:EMbf0Jmqwv94nJ5WL9qWnSXIBZnvsS9gNypansHGc6U=
+github.com/go-acme/lego/v4 v4.11.0 h1:oIPoU7zBJoTfoVrbqk62+/2NsGCSgCVK1JtZSZZ28SU=
+github.com/go-acme/lego/v4 v4.11.0/go.mod h1:dENL0J3/WughN2NLy0T35otK5k1EWCmXTwCw0+X5ZaE=
 github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
 github.com/go-co-op/gocron v1.20.1 h1:wCGabII3xf/NrrYeOzJ4voLBBtA5k7Rb99+7l/iiu+g=
 github.com/go-co-op/gocron v1.20.1/go.mod h1:UqVyvM90I1q/R1qGEX6cBORI6WArLuEgYlbncLMvzRM=
+github.com/go-co-op/gocron v1.27.0 h1:GbP9A0wauIeGbMCUzdGb2IAi1JHzNHT/H/lLW2ODwLE=
+github.com/go-co-op/gocron v1.27.0/go.mod h1:39f6KNSGVOU1LO/ZOoZfcSxwlsJDQOKSu8erN0SH48Y=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
 github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
@@ -177,10 +206,19 @@ github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxF
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
 github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
 github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@@ -192,11 +230,15 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
 github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
 github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
 github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
+github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ=
+github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4=
 github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
 github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b h1:/vQ+oYKu+JoyaMPDsv5FzwuL2wwWBgBbtj/YLCi4LuA=
@@ -205,6 +247,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
 github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+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 v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
 github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
@@ -241,6 +285,7 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -251,6 +296,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
@@ -258,13 +304,19 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/s2a-go v0.1.1 h1:XJQvZvUdPzOGdf4ZMQc78wYt9XrdIZOl//n03i8P68Q=
 github.com/google/s2a-go v0.1.1/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM=
+github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE=
+github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@@ -279,6 +331,8 @@ github.com/gophercloud/gophercloud v1.3.0 h1:RUKyCMiZoQR3VlVR5E3K7PK1AC3/qppsWYo
 github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
 github.com/gophercloud/utils v0.0.0-20230330070308-5bd5e1d608f8 h1:K9r5WEeAiaEgFZsuOP0OYjE4TtyFcCLG1nI08t9AP6A=
 github.com/gophercloud/utils v0.0.0-20230330070308-5bd5e1d608f8/go.mod h1:VSalo4adEk+3sNkmVJLnhHoOyOYYS8sTWLG4mv5BKto=
+github.com/gophercloud/utils v0.0.0-20230418172808-6eab72e966e1 h1:vJyXd9+MB5vAKxpOo4z/PDSiPgKmEyJwHIDOdV4Y0KY=
+github.com/gophercloud/utils v0.0.0-20230418172808-6eab72e966e1/go.mod h1:VSalo4adEk+3sNkmVJLnhHoOyOYYS8sTWLG4mv5BKto=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -314,6 +368,7 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
 github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
 github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -347,6 +402,7 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/jpillora/overseer v1.1.6 h1:3ygYfNcR3FfOr22miu3vR1iQcXKMHbmULBh98rbkIyo=
 github.com/jpillora/overseer v1.1.6/go.mod h1:aPXQtxuVb9PVWRWTXpo+LdnC/YXQ0IBLNXqKMJmgk88=
 github.com/jpillora/s3 v1.1.4 h1:YCCKDWzb/Ye9EBNd83ATRF/8wPEy0xd43Rezb6u6fzc=
@@ -366,6 +422,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
 github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
 github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
@@ -376,7 +433,10 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -388,10 +448,16 @@ github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
 github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
 github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
 github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/linode/linodego v1.16.1 h1:5otq57M4PdHycPERRfSFZ0s1yz1ETVWGjCp3hh7+F9w=
 github.com/linode/linodego v1.16.1/go.mod h1:aESRAbpLY9R6IA1WGAWHikRI9DU9Lhesapv1MhKmPHM=
+github.com/linode/linodego v1.16.2 h1:LwtCQxEvXu57zn1c+0kH5jh/xUnv3YBYYFhHWyohlbk=
+github.com/linode/linodego v1.16.2/go.mod h1:n4KKMzPcmZ18qOdYzixRXTPmBv3r4KkVu4pNT6pIN5M=
 github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
 github.com/liquidweb/go-lwApi v0.0.5 h1:CT4cdXzJXmo0bon298kS7NeSk+Gt8/UHpWBBol1NGCA=
 github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
@@ -405,6 +471,7 @@ github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0g
 github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -433,6 +500,8 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N
 github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
 github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
 github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
+github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI=
+github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
 github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34=
 github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
@@ -448,6 +517,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
 github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -455,7 +525,9 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g=
 github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
@@ -472,6 +544,8 @@ github.com/nrdcg/goinwx v0.8.2 h1:RmjiHlEA+lzi3toXyPSaE6hWnBQ0+G+1u7w8C6Fpp4g=
 github.com/nrdcg/goinwx v0.8.2/go.mod h1:mnMSTi7CXBu2io4DzdOBoGFA1XclD0sEPWJaDhNgkA4=
 github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
 github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
+github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw=
+github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms=
 github.com/nrdcg/porkbun v0.2.0 h1:ghaqPtIKcffba99epWFkK3VWf6TKJT9WMXMgaTqv95Y=
 github.com/nrdcg/porkbun v0.2.0/go.mod h1:i0uLMn9ItFsLsSQIAeEu1wQ9/+6EvX1eQw15hulMMRw=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -484,12 +558,31 @@ github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
 github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
 github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
+github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
+github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
+github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0=
+github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo=
+github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw=
+github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo=
+github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc=
+github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk=
+github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
 github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
 github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
+github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
+github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc=
+github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM=
+github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
+github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM=
+github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
+github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw=
+github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw=
+github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
+github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
 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/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr97jXRSSN5MxTgG/zU=
@@ -503,6 +596,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
 github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
 github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -517,6 +611,8 @@ github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3g
 github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
 github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
 github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
+github.com/pretty66/websocketproxy v0.0.0-20220507015215-930b3a686308 h1:JfSau4YABtkm5gRtFWuRWHT2Lsw4ZbyB4F/qORwf+BA=
+github.com/pretty66/websocketproxy v0.0.0-20220507015215-930b3a686308/go.mod h1:hxhFuMswfNko9fAxYeqBapfUdJHAgDafBs/MzOZh0X8=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -538,7 +634,11 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
+github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sacloud/api-client-go v0.2.7 h1:u8e8UdvYtpLiqTsmbJ6fLXceTievQ104ZKZb7VQ5wq8=
@@ -547,21 +647,33 @@ github.com/sacloud/go-http v0.1.5 h1:Ov1Vr4Olf0P+FG2okmpSaftCQnyHoCKZtbCC6RlNZUI
 github.com/sacloud/go-http v0.1.5/go.mod h1:jlBMvkz4PuAelewTMOVzNQVuI2EyBYJGHS7nub79Yh0=
 github.com/sacloud/iaas-api-go v1.9.2 h1:j+8E3RESsCp5hMzeDbldOv2/lmDKMvUoZOz3L90/0iY=
 github.com/sacloud/iaas-api-go v1.9.2/go.mod h1:A3shH+pHq9V1ZXw15KScArLs8BstYsdyrBQkxFM5Bcs=
+github.com/sacloud/iaas-api-go v1.10.0 h1:dBXqyUr3bQR0hQppIesFD4MbFernmqG+5wl6RWUNoxg=
+github.com/sacloud/iaas-api-go v1.10.0/go.mod h1:JRu6N3Dh2MNcznmYfC9OjM/4zVHWua2flIRWp7d5DBw=
 github.com/sacloud/packages-go v0.0.8 h1:9l1XrukLdLPd6l/y2et9foQK2Z00ZQEGCInZMRGivAA=
 github.com/sacloud/packages-go v0.0.8/go.mod h1:btPji+wtZ+Pk7MeCy+zo61o5IziBoLdHIrdGiYq9Kb8=
 github.com/sashabaranov/go-openai v1.7.0 h1:D1dBXoZhtf/aKNu6WFf0c7Ah2NM30PZ/3Mqly6cZ7fk=
 github.com/sashabaranov/go-openai v1.7.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
+github.com/sashabaranov/go-openai v1.9.4 h1:KanoCEoowAI45jVXlenMCckutSRr39qOmSi9MyPBfZM=
+github.com/sashabaranov/go-openai v1.9.4/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.15 h1:Y7xOFbD+3jaPw+VN7lkakNJ/pa+ZSQVFp1ONtJaBxns=
 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.15/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.16 h1:Ted1/3BGV1d0c7J+69N+brveAgJNWZlWnI8iYP3dZMs=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.16/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
 github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
+github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
+github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
 github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
 github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
 github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
+github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
+github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
 github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
 github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 h1:ZTzdx88+AcnjqUfJwnz89UBrMSBQ1NEysg9u5d+dU9c=
+github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04/go.mod h1:5KS21fpch8TIMyAUv/qQqTa3GZfBDYgjaZbd2KXKYfg=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
@@ -584,6 +696,7 @@ github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVd
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@@ -597,6 +710,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
+github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -616,8 +730,12 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.637 h1:qFqiFFxUQUixUn5op2w8CnBCWC7gAsvyx3/m8LsHx60=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.637/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.655 h1:wXBlXLfBbqTBpsiKBBULW63KvMy3wsu3/CD25cR9NEA=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.655/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.637 h1:9r85LEYF4CcKDbQQhJ5b3hYh5vj1WNvjsHrWHAV3c60=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.637/go.mod h1:5z3RG36i3UQvMr3aHVjPfrEzLdmk+sTiLgip3aFvKBo=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.655 h1:LgLA3nzvsBggdt1NRDNi6KVk9HRHLwBUltxXupdRMeM=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.655/go.mod h1:v8wyOnL22mqDNeBqsasAQzP6eQI0Lpa+cAxFtVThHTk=
 github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
 github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
 github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
@@ -627,6 +745,8 @@ github.com/transip/gotransip/v6 v6.20.0 h1:AuvwyOZ51f2brzMbTqlRy/wmaM3kF7Vx5Wds8
 github.com/transip/gotransip/v6 v6.20.0/go.mod h1:nzv9eN2tdsUrm5nG5ZX6AugYIU4qgsMwIn2c0EZLk8c=
 github.com/tufanbarisyildirim/gonginx v0.0.0-20230508112508-44b3f58122c3 h1:JtqbmHUcarxOxHSmTPKBHiLQWp66tGf4D4xOzHFRQCI=
 github.com/tufanbarisyildirim/gonginx v0.0.0-20230508112508-44b3f58122c3/go.mod h1:4fTjBxMoWGOIVnGFSTS9GAZ0yMyiGzTdATQS0krQv18=
+github.com/tufanbarisyildirim/gonginx v0.0.0-20230508164033-d7b72d6cd0d5 h1:FkmR72IA+ZHvCbMG4Mbl5+9oovKUfHsayzBM90GP1LE=
+github.com/tufanbarisyildirim/gonginx v0.0.0-20230508164033-d7b72d6cd0d5/go.mod h1:4fTjBxMoWGOIVnGFSTS9GAZ0yMyiGzTdATQS0krQv18=
 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=
@@ -639,6 +759,8 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
 github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/ultradns/ultradns-go-sdk v1.4.1-20230224143201-0d8b0f6 h1:QLAl5WjtTCho+BEY7jfYjF6I9eK2jSfvMxjeRjP34PQ=
 github.com/ultradns/ultradns-go-sdk v1.4.1-20230224143201-0d8b0f6/go.mod h1:F4UyVEmq4/m5lAmx+GccrxyRCXmnBjzUL09JLTQFp94=
+github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c h1:mKnW6IGLw7uXu6DL6RitufZWcXS6hCnauXRUFof7rKM=
+github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c/go.mod h1:F4UyVEmq4/m5lAmx+GccrxyRCXmnBjzUL09JLTQFp94=
 github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
 github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
 github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
@@ -652,10 +774,16 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
 github.com/yandex-cloud/go-genproto v0.0.0-20230403093326-123923969dc6/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
 github.com/yandex-cloud/go-genproto v0.0.0-20230410092700-15216dc82345 h1:GpSlltaiDKRYF1JgqQO+cSLMNuADuSfKqRNaxgmXV+Q=
 github.com/yandex-cloud/go-genproto v0.0.0-20230410092700-15216dc82345/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
+github.com/yandex-cloud/go-genproto v0.0.0-20230511103421-ecb0cd1514ab h1:Y9sWstUXfHwHufw95mI58ZEvZ720KWyR+niLQbd2q1k=
+github.com/yandex-cloud/go-genproto v0.0.0-20230511103421-ecb0cd1514ab/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
 github.com/yandex-cloud/go-sdk v0.0.0-20230403093608-cc5174142a48 h1:C3yjOqP3gGxwiW3bXDAGI8tS+eKjxySJ9Ix7lpdtKZw=
 github.com/yandex-cloud/go-sdk v0.0.0-20230403093608-cc5174142a48/go.mod h1:+bvtdW+7bn1Yc7xUCbITnEalQ+hwkAAbUFHpeIY2wUQ=
+github.com/yandex-cloud/go-sdk v0.0.0-20230511104317-0ccfef4d3a91 h1:bYY90Y33XH7xJh8Qa5ZIgmjyWDp2S6sixTRxYbHCQLU=
+github.com/yandex-cloud/go-sdk v0.0.0-20230511104317-0ccfef4d3a91/go.mod h1:QOnqdE3DjwgoKvhw4Scx6HTCfAlYHZMoUzyaC8kcdzk=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
 github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
@@ -670,6 +798,7 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 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 v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
 go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -694,11 +823,15 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
 golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
 golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/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=
@@ -717,9 +850,15 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
 golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
+golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
 golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -738,6 +877,7 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -750,17 +890,28 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
 golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
+golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
+golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -772,6 +923,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
+golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -813,6 +966,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -821,15 +976,24 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
+golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -838,12 +1002,17 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -865,15 +1034,26 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
+golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
 golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
 golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
+golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
+golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 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=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 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=
@@ -881,6 +1061,8 @@ google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEn
 google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
 google.golang.org/api v0.118.0 h1:FNfHq9Z2GKULxu7cEhCaB0wWQHg43UpomrrN+24ZRdE=
 google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjYK+5E=
+google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es=
+google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -898,6 +1080,7 @@ google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBr
 google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
@@ -915,6 +1098,8 @@ google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzI
 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
 google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
 google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
+google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
+google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -923,10 +1108,13 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
 google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@@ -935,11 +1123,14 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
 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=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
 gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -948,6 +1139,8 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ns1/ns1-go.v2 v2.7.5 h1:tE4SLOAFx2YXawh6MPv57lmlaBFUTyxSYOWKOGDgkM4=
 gopkg.in/ns1/ns1-go.v2 v2.7.5/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk=
+gopkg.in/ns1/ns1-go.v2 v2.7.6 h1:mCPl7q0jbIGACXvGBljAuuApmKZo3rRi4tlRIEbMvjA=
+gopkg.in/ns1/ns1-go.v2 v2.7.6/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@@ -961,6 +1154,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-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
@@ -976,14 +1170,20 @@ gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k
 gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
 gorm.io/gen v0.3.21 h1:t8329wT4tW1ZZWOm7vn4LV6OIrz8a5zCg+p78ezt+rA=
 gorm.io/gen v0.3.21/go.mod h1:aWgvoKdG9f8Des4TegSa0N5a+gwhGsFo0JJMaLwokvk=
+gorm.io/gen v0.3.22 h1:K7u5tCyaZfe1cbQFD8N2xrTqUuqximNFSRl7zOFPq+M=
+gorm.io/gen v0.3.22/go.mod h1:dQcELeF/7Kf82M6AQF+O/rKT5r1sjv49TlGz0cerPn4=
 gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
 gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
 gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
 gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
 gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
+gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 gorm.io/hints v1.1.1 h1:NPampLxQujY+277452rt4yqtg6JmzNZ1jA2olk0eFXw=
 gorm.io/hints v1.1.1/go.mod h1:zdwzfFqvBWGbpuKiAhLFOSGSpeD3/VsRgkXR9Y7Z3cs=
+gorm.io/hints v1.1.2 h1:b5j0kwk5p4+3BtDtYqqfY+ATSxjj+6ptPgVveuynn9o=
+gorm.io/hints v1.1.2/go.mod h1:/ARdpUHAtyEMCh5NNi3tI7FsGh+Cj/MIUlvNxCNCFWg=
 gorm.io/plugin/dbresolver v1.4.1 h1:Ug4LcoPhrvqq71UhxtF346f+skTYoCa/nEsdjvHwEzk=
 gorm.io/plugin/dbresolver v1.4.1/go.mod h1:CTbCtMWhsjXSiJqiW2R8POvJ2cq18RVOl4WGyT5nhNc=
 gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
@@ -992,5 +1192,27 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+k8s.io/api v0.27.1 h1:Z6zUGQ1Vd10tJ+gHcNNNgkV5emCyW+v2XTmn+CLjSd0=
+k8s.io/api v0.27.1/go.mod h1:z5g/BpAiD+f6AArpqNjkY+cji8ueZDU/WV1jcj5Jk4E=
+k8s.io/apimachinery v0.27.1 h1:EGuZiLI95UQQcClhanryclaQE6xjg1Bts6/L3cD7zyc=
+k8s.io/apimachinery v0.27.1/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM=
+k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
+k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=
+k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU=
+k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
+sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

+ 111 - 0
server/api/environment.go

@@ -0,0 +1,111 @@
+package api
+
+import (
+	"github.com/0xJacky/Nginx-UI/server/model"
+	"github.com/0xJacky/Nginx-UI/server/query"
+	"github.com/0xJacky/Nginx-UI/server/service"
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/cast"
+	"net/http"
+)
+
+func GetEnvironment(c *gin.Context) {
+	id := cast.ToInt(c.Param("id"))
+
+	envQuery := query.Environment
+
+	environment, err := envQuery.FirstByID(id)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, environment)
+}
+
+func GetEnvironmentList(c *gin.Context) {
+	data, err := service.RetrieveEnvironmentList()
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"data": data,
+	})
+}
+
+type EnvironmentManageJson struct {
+	Name  string `json:"name" binding:"required"`
+	URL   string `json:"url" binding:"required"`
+	Token string `json:"token"  binding:"required"`
+}
+
+func AddEnvironment(c *gin.Context) {
+	var json EnvironmentManageJson
+	if !BindAndValid(c, &json) {
+		return
+	}
+
+	environment := model.Environment{
+		Name:  json.Name,
+		URL:   json.URL,
+		Token: json.Token,
+	}
+
+	envQuery := query.Environment
+
+	err := envQuery.Create(&environment)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, environment)
+}
+
+func EditEnvironment(c *gin.Context) {
+	id := cast.ToInt(c.Param("id"))
+
+	var json EnvironmentManageJson
+	if !BindAndValid(c, &json) {
+		return
+	}
+
+	envQuery := query.Environment
+
+	environment, err := envQuery.FirstByID(id)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	_, err = envQuery.Where(envQuery.ID.Eq(environment.ID)).Updates(&model.Environment{
+		Name:  json.Name,
+		URL:   json.URL,
+		Token: json.Token,
+	})
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	GetEnvironment(c)
+}
+
+func DeleteEnvironment(c *gin.Context) {
+	id := cast.ToInt(c.Param("id"))
+	envQuery := query.Environment
+
+	environment, err := envQuery.FirstByID(id)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	err = envQuery.DeleteByID(environment.ID)
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusNoContent, nil)
+}

+ 55 - 54
server/api/install.go

@@ -1,76 +1,77 @@
 package api
 
 import (
-    "github.com/0xJacky/Nginx-UI/server/model"
-    "github.com/0xJacky/Nginx-UI/server/query"
-    "github.com/0xJacky/Nginx-UI/server/settings"
-    "github.com/gin-gonic/gin"
-    "github.com/google/uuid"
-    "golang.org/x/crypto/bcrypt"
-    "net/http"
+	"github.com/0xJacky/Nginx-UI/server/model"
+	"github.com/0xJacky/Nginx-UI/server/query"
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
+	"golang.org/x/crypto/bcrypt"
+	"net/http"
 )
 
 func installLockStatus() bool {
-    return "" != settings.ServerSettings.JwtSecret
+	return "" != settings.ServerSettings.JwtSecret
 }
 
 func InstallLockCheck(c *gin.Context) {
-    c.JSON(http.StatusOK, gin.H{
-        "lock": installLockStatus(),
-    })
+	c.JSON(http.StatusOK, gin.H{
+		"lock": installLockStatus(),
+	})
 }
 
 type InstallJson struct {
-    Email    string `json:"email" binding:"required,email"`
-    Username string `json:"username" binding:"required,max=255"`
-    Password string `json:"password" binding:"required,max=255"`
-    Database string `json:"database"`
+	Email    string `json:"email" binding:"required,email"`
+	Username string `json:"username" binding:"required,max=255"`
+	Password string `json:"password" binding:"required,max=255"`
+	Database string `json:"database"`
 }
 
 func InstallNginxUI(c *gin.Context) {
-    // Visit this api after installed is forbidden
-    if installLockStatus() {
-        c.JSON(http.StatusForbidden, gin.H{
-            "error": "installed",
-        })
-        return
-    }
-    var json InstallJson
-    ok := BindAndValid(c, &json)
-    if !ok {
-        return
-    }
+	// Visit this api after installed is forbidden
+	if installLockStatus() {
+		c.JSON(http.StatusForbidden, gin.H{
+			"error": "installed",
+		})
+		return
+	}
+	var json InstallJson
+	ok := BindAndValid(c, &json)
+	if !ok {
+		return
+	}
 
-    settings.ServerSettings.JwtSecret = uuid.New().String()
-    settings.ServerSettings.Email = json.Email
-    if "" != json.Database {
-        settings.ServerSettings.Database = json.Database
-    }
-    settings.ReflectFrom()
+	settings.ServerSettings.JwtSecret = uuid.New().String()
+	settings.ServerSettings.NodeSecret = uuid.New().String()
+	settings.ServerSettings.Email = json.Email
+	if "" != json.Database {
+		settings.ServerSettings.Database = json.Database
+	}
+	settings.ReflectFrom()
 
-    err := settings.Save()
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
+	err := settings.Save()
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
 
-    // Init model
-    db := model.Init()
-    query.Init(db)
+	// Init model
+	db := model.Init()
+	query.Init(db)
 
-    pwd, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
+	pwd, _ := bcrypt.GenerateFromPassword([]byte(json.Password), bcrypt.DefaultCost)
 
-    u := query.Auth
-    err = u.Create(&model.Auth{
-        Name:     json.Username,
-        Password: string(pwd),
-    })
+	u := query.Auth
+	err = u.Create(&model.Auth{
+		Name:     json.Username,
+		Password: string(pwd),
+	})
 
-    if err != nil {
-        ErrHandler(c, err)
-        return
-    }
-    c.JSON(http.StatusOK, gin.H{
-        "message": "ok",
-    })
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
 }

+ 35 - 0
server/api/node.go

@@ -0,0 +1,35 @@
+package api
+
+import (
+	"github.com/0xJacky/Nginx-UI/server/service"
+	"github.com/gin-gonic/gin"
+	"github.com/shirou/gopsutil/v3/cpu"
+	"net/http"
+)
+
+func GetCurrentNode(c *gin.Context) {
+	if _, ok := c.Get("NodeSecret"); !ok {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "node secret not exist",
+		})
+		return
+	}
+
+	runtimeInfo, err := service.GetRuntimeInfo()
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	cpuInfo, _ := cpu.Info()
+	memory, _ := getMemoryStat()
+	ver, _ := service.GetCurrentVersion()
+
+	c.JSON(http.StatusOK, gin.H{
+		"request_node_secret": c.MustGet("NodeSecret"),
+		"node_runtime_info":   runtimeInfo,
+		"cpu_num":             len(cpuInfo),
+		"memory_total":        memory.Total,
+		"version":             ver.Version,
+	})
+}

+ 1 - 0
server/internal/environment/environment.go

@@ -0,0 +1 @@
+package environment

+ 6 - 6
server/internal/logger/logger.go

@@ -1,11 +1,11 @@
 package logger
 
 import (
-    "github.com/0xJacky/Nginx-UI/server/settings"
-    "github.com/gin-gonic/gin"
-    "go.uber.org/zap"
-    "go.uber.org/zap/zapcore"
-    "os"
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-gonic/gin"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+	"os"
 )
 
 var logger *zap.SugaredLogger
@@ -31,7 +31,7 @@ func init() {
 	consoleErrors := zapcore.Lock(os.Stderr)
 	encoderConfig := zap.NewDevelopmentEncoderConfig()
 	encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")
-	encoderConfig.ConsoleSeparator = " "
+	encoderConfig.ConsoleSeparator = "\t"
 	encoderConfig.EncodeLevel = colorLevelEncoder
 	consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
 

+ 8 - 0
server/model/environment.go

@@ -0,0 +1,8 @@
+package model
+
+type Environment struct {
+	Model
+	Name  string `json:"name"`
+	URL   string `json:"url"`
+	Token string `json:"token"`
+}

+ 1 - 0
server/model/model.go

@@ -32,6 +32,7 @@ func GenerateAllModel() []any {
 		ChatGPTLog{},
 		Site{},
 		DnsCredential{},
+		Environment{},
 	}
 }
 

+ 5 - 0
server/query/certs.gen.go

@@ -166,6 +166,11 @@ func (a certBelongsToDnsCredential) WithContext(ctx context.Context) *certBelong
 	return &a
 }
 
+func (a certBelongsToDnsCredential) Session(session *gorm.Session) *certBelongsToDnsCredential {
+	a.db = a.db.Session(session)
+	return &a
+}
+
 func (a certBelongsToDnsCredential) Model(m *model.Cert) *certBelongsToDnsCredentialTx {
 	return &certBelongsToDnsCredentialTx{a.db.Model(m).Association(a.Name())}
 }

+ 378 - 0
server/query/environments.gen.go

@@ -0,0 +1,378 @@
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+// Code generated by gorm.io/gen. DO NOT EDIT.
+
+package query
+
+import (
+	"context"
+	"strings"
+
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"gorm.io/gorm/schema"
+
+	"gorm.io/gen"
+	"gorm.io/gen/field"
+
+	"gorm.io/plugin/dbresolver"
+
+	"github.com/0xJacky/Nginx-UI/server/model"
+)
+
+func newEnvironment(db *gorm.DB, opts ...gen.DOOption) environment {
+	_environment := environment{}
+
+	_environment.environmentDo.UseDB(db, opts...)
+	_environment.environmentDo.UseModel(&model.Environment{})
+
+	tableName := _environment.environmentDo.TableName()
+	_environment.ALL = field.NewAsterisk(tableName)
+	_environment.ID = field.NewInt(tableName, "id")
+	_environment.CreatedAt = field.NewTime(tableName, "created_at")
+	_environment.UpdatedAt = field.NewTime(tableName, "updated_at")
+	_environment.DeletedAt = field.NewField(tableName, "deleted_at")
+	_environment.Name = field.NewString(tableName, "name")
+	_environment.URL = field.NewString(tableName, "url")
+	_environment.Token = field.NewString(tableName, "token")
+
+	_environment.fillFieldMap()
+
+	return _environment
+}
+
+type environment struct {
+	environmentDo
+
+	ALL       field.Asterisk
+	ID        field.Int
+	CreatedAt field.Time
+	UpdatedAt field.Time
+	DeletedAt field.Field
+	Name      field.String
+	URL       field.String
+	Token     field.String
+
+	fieldMap map[string]field.Expr
+}
+
+func (e environment) Table(newTableName string) *environment {
+	e.environmentDo.UseTable(newTableName)
+	return e.updateTableName(newTableName)
+}
+
+func (e environment) As(alias string) *environment {
+	e.environmentDo.DO = *(e.environmentDo.As(alias).(*gen.DO))
+	return e.updateTableName(alias)
+}
+
+func (e *environment) updateTableName(table string) *environment {
+	e.ALL = field.NewAsterisk(table)
+	e.ID = field.NewInt(table, "id")
+	e.CreatedAt = field.NewTime(table, "created_at")
+	e.UpdatedAt = field.NewTime(table, "updated_at")
+	e.DeletedAt = field.NewField(table, "deleted_at")
+	e.Name = field.NewString(table, "name")
+	e.URL = field.NewString(table, "url")
+	e.Token = field.NewString(table, "token")
+
+	e.fillFieldMap()
+
+	return e
+}
+
+func (e *environment) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
+	_f, ok := e.fieldMap[fieldName]
+	if !ok || _f == nil {
+		return nil, false
+	}
+	_oe, ok := _f.(field.OrderExpr)
+	return _oe, ok
+}
+
+func (e *environment) fillFieldMap() {
+	e.fieldMap = make(map[string]field.Expr, 7)
+	e.fieldMap["id"] = e.ID
+	e.fieldMap["created_at"] = e.CreatedAt
+	e.fieldMap["updated_at"] = e.UpdatedAt
+	e.fieldMap["deleted_at"] = e.DeletedAt
+	e.fieldMap["name"] = e.Name
+	e.fieldMap["url"] = e.URL
+	e.fieldMap["token"] = e.Token
+}
+
+func (e environment) clone(db *gorm.DB) environment {
+	e.environmentDo.ReplaceConnPool(db.Statement.ConnPool)
+	return e
+}
+
+func (e environment) replaceDB(db *gorm.DB) environment {
+	e.environmentDo.ReplaceDB(db)
+	return e
+}
+
+type environmentDo struct{ gen.DO }
+
+// FirstByID Where("id=@id")
+func (e environmentDo) FirstByID(id int) (result *model.Environment, err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = e.UnderlyingDB().Where(generateSQL.String(), params...).Take(&result) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+// DeleteByID update @@table set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=@id
+func (e environmentDo) DeleteByID(id int) (err error) {
+	var params []interface{}
+
+	var generateSQL strings.Builder
+	params = append(params, id)
+	generateSQL.WriteString("update environments set deleted_at=strftime('%Y-%m-%d %H:%M:%S','now') where id=? ")
+
+	var executeSQL *gorm.DB
+	executeSQL = e.UnderlyingDB().Exec(generateSQL.String(), params...) // ignore_security_alert
+	err = executeSQL.Error
+
+	return
+}
+
+func (e environmentDo) Debug() *environmentDo {
+	return e.withDO(e.DO.Debug())
+}
+
+func (e environmentDo) WithContext(ctx context.Context) *environmentDo {
+	return e.withDO(e.DO.WithContext(ctx))
+}
+
+func (e environmentDo) ReadDB() *environmentDo {
+	return e.Clauses(dbresolver.Read)
+}
+
+func (e environmentDo) WriteDB() *environmentDo {
+	return e.Clauses(dbresolver.Write)
+}
+
+func (e environmentDo) Session(config *gorm.Session) *environmentDo {
+	return e.withDO(e.DO.Session(config))
+}
+
+func (e environmentDo) Clauses(conds ...clause.Expression) *environmentDo {
+	return e.withDO(e.DO.Clauses(conds...))
+}
+
+func (e environmentDo) Returning(value interface{}, columns ...string) *environmentDo {
+	return e.withDO(e.DO.Returning(value, columns...))
+}
+
+func (e environmentDo) Not(conds ...gen.Condition) *environmentDo {
+	return e.withDO(e.DO.Not(conds...))
+}
+
+func (e environmentDo) Or(conds ...gen.Condition) *environmentDo {
+	return e.withDO(e.DO.Or(conds...))
+}
+
+func (e environmentDo) Select(conds ...field.Expr) *environmentDo {
+	return e.withDO(e.DO.Select(conds...))
+}
+
+func (e environmentDo) Where(conds ...gen.Condition) *environmentDo {
+	return e.withDO(e.DO.Where(conds...))
+}
+
+func (e environmentDo) Exists(subquery interface{ UnderlyingDB() *gorm.DB }) *environmentDo {
+	return e.Where(field.CompareSubQuery(field.ExistsOp, nil, subquery.UnderlyingDB()))
+}
+
+func (e environmentDo) Order(conds ...field.Expr) *environmentDo {
+	return e.withDO(e.DO.Order(conds...))
+}
+
+func (e environmentDo) Distinct(cols ...field.Expr) *environmentDo {
+	return e.withDO(e.DO.Distinct(cols...))
+}
+
+func (e environmentDo) Omit(cols ...field.Expr) *environmentDo {
+	return e.withDO(e.DO.Omit(cols...))
+}
+
+func (e environmentDo) Join(table schema.Tabler, on ...field.Expr) *environmentDo {
+	return e.withDO(e.DO.Join(table, on...))
+}
+
+func (e environmentDo) LeftJoin(table schema.Tabler, on ...field.Expr) *environmentDo {
+	return e.withDO(e.DO.LeftJoin(table, on...))
+}
+
+func (e environmentDo) RightJoin(table schema.Tabler, on ...field.Expr) *environmentDo {
+	return e.withDO(e.DO.RightJoin(table, on...))
+}
+
+func (e environmentDo) Group(cols ...field.Expr) *environmentDo {
+	return e.withDO(e.DO.Group(cols...))
+}
+
+func (e environmentDo) Having(conds ...gen.Condition) *environmentDo {
+	return e.withDO(e.DO.Having(conds...))
+}
+
+func (e environmentDo) Limit(limit int) *environmentDo {
+	return e.withDO(e.DO.Limit(limit))
+}
+
+func (e environmentDo) Offset(offset int) *environmentDo {
+	return e.withDO(e.DO.Offset(offset))
+}
+
+func (e environmentDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *environmentDo {
+	return e.withDO(e.DO.Scopes(funcs...))
+}
+
+func (e environmentDo) Unscoped() *environmentDo {
+	return e.withDO(e.DO.Unscoped())
+}
+
+func (e environmentDo) Create(values ...*model.Environment) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return e.DO.Create(values)
+}
+
+func (e environmentDo) CreateInBatches(values []*model.Environment, batchSize int) error {
+	return e.DO.CreateInBatches(values, batchSize)
+}
+
+// Save : !!! underlying implementation is different with GORM
+// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
+func (e environmentDo) Save(values ...*model.Environment) error {
+	if len(values) == 0 {
+		return nil
+	}
+	return e.DO.Save(values)
+}
+
+func (e environmentDo) First() (*model.Environment, error) {
+	if result, err := e.DO.First(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Environment), nil
+	}
+}
+
+func (e environmentDo) Take() (*model.Environment, error) {
+	if result, err := e.DO.Take(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Environment), nil
+	}
+}
+
+func (e environmentDo) Last() (*model.Environment, error) {
+	if result, err := e.DO.Last(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Environment), nil
+	}
+}
+
+func (e environmentDo) Find() ([]*model.Environment, error) {
+	result, err := e.DO.Find()
+	return result.([]*model.Environment), err
+}
+
+func (e environmentDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Environment, err error) {
+	buf := make([]*model.Environment, 0, batchSize)
+	err = e.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
+		defer func() { results = append(results, buf...) }()
+		return fc(tx, batch)
+	})
+	return results, err
+}
+
+func (e environmentDo) FindInBatches(result *[]*model.Environment, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+	return e.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (e environmentDo) Attrs(attrs ...field.AssignExpr) *environmentDo {
+	return e.withDO(e.DO.Attrs(attrs...))
+}
+
+func (e environmentDo) Assign(attrs ...field.AssignExpr) *environmentDo {
+	return e.withDO(e.DO.Assign(attrs...))
+}
+
+func (e environmentDo) Joins(fields ...field.RelationField) *environmentDo {
+	for _, _f := range fields {
+		e = *e.withDO(e.DO.Joins(_f))
+	}
+	return &e
+}
+
+func (e environmentDo) Preload(fields ...field.RelationField) *environmentDo {
+	for _, _f := range fields {
+		e = *e.withDO(e.DO.Preload(_f))
+	}
+	return &e
+}
+
+func (e environmentDo) FirstOrInit() (*model.Environment, error) {
+	if result, err := e.DO.FirstOrInit(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Environment), nil
+	}
+}
+
+func (e environmentDo) FirstOrCreate() (*model.Environment, error) {
+	if result, err := e.DO.FirstOrCreate(); err != nil {
+		return nil, err
+	} else {
+		return result.(*model.Environment), nil
+	}
+}
+
+func (e environmentDo) FindByPage(offset int, limit int) (result []*model.Environment, count int64, err error) {
+	result, err = e.Offset(offset).Limit(limit).Find()
+	if err != nil {
+		return
+	}
+
+	if size := len(result); 0 < limit && 0 < size && size < limit {
+		count = int64(size + offset)
+		return
+	}
+
+	count, err = e.Offset(-1).Limit(-1).Count()
+	return
+}
+
+func (e environmentDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
+	count, err = e.Count()
+	if err != nil {
+		return
+	}
+
+	err = e.Offset(offset).Limit(limit).Scan(result)
+	return
+}
+
+func (e environmentDo) Scan(result interface{}) (err error) {
+	return e.DO.Scan(result)
+}
+
+func (e environmentDo) Delete(models ...*model.Environment) (result gen.ResultInfo, err error) {
+	return e.DO.Delete(models)
+}
+
+func (e *environmentDo) withDO(do gen.Dao) *environmentDo {
+	e.DO = *do.(*gen.DO)
+	return e
+}

+ 14 - 2
server/query/gen.go

@@ -23,6 +23,7 @@ var (
 	ChatGPTLog    *chatGPTLog
 	ConfigBackup  *configBackup
 	DnsCredential *dnsCredential
+	Environment   *environment
 	Site          *site
 )
 
@@ -34,6 +35,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	ChatGPTLog = &Q.ChatGPTLog
 	ConfigBackup = &Q.ConfigBackup
 	DnsCredential = &Q.DnsCredential
+	Environment = &Q.Environment
 	Site = &Q.Site
 }
 
@@ -46,6 +48,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 		ChatGPTLog:    newChatGPTLog(db, opts...),
 		ConfigBackup:  newConfigBackup(db, opts...),
 		DnsCredential: newDnsCredential(db, opts...),
+		Environment:   newEnvironment(db, opts...),
 		Site:          newSite(db, opts...),
 	}
 }
@@ -59,6 +62,7 @@ type Query struct {
 	ChatGPTLog    chatGPTLog
 	ConfigBackup  configBackup
 	DnsCredential dnsCredential
+	Environment   environment
 	Site          site
 }
 
@@ -73,6 +77,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
 		ChatGPTLog:    q.ChatGPTLog.clone(db),
 		ConfigBackup:  q.ConfigBackup.clone(db),
 		DnsCredential: q.DnsCredential.clone(db),
+		Environment:   q.Environment.clone(db),
 		Site:          q.Site.clone(db),
 	}
 }
@@ -94,6 +99,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
 		ChatGPTLog:    q.ChatGPTLog.replaceDB(db),
 		ConfigBackup:  q.ConfigBackup.replaceDB(db),
 		DnsCredential: q.DnsCredential.replaceDB(db),
+		Environment:   q.Environment.replaceDB(db),
 		Site:          q.Site.replaceDB(db),
 	}
 }
@@ -105,6 +111,7 @@ type queryCtx struct {
 	ChatGPTLog    *chatGPTLogDo
 	ConfigBackup  *configBackupDo
 	DnsCredential *dnsCredentialDo
+	Environment   *environmentDo
 	Site          *siteDo
 }
 
@@ -116,6 +123,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
 		ChatGPTLog:    q.ChatGPTLog.WithContext(ctx),
 		ConfigBackup:  q.ConfigBackup.WithContext(ctx),
 		DnsCredential: q.DnsCredential.WithContext(ctx),
+		Environment:   q.Environment.WithContext(ctx),
 		Site:          q.Site.WithContext(ctx),
 	}
 }
@@ -125,10 +133,14 @@ func (q *Query) Transaction(fc func(tx *Query) error, opts ...*sql.TxOptions) er
 }
 
 func (q *Query) Begin(opts ...*sql.TxOptions) *QueryTx {
-	return &QueryTx{q.clone(q.db.Begin(opts...))}
+	tx := q.db.Begin(opts...)
+	return &QueryTx{Query: q.clone(tx), Error: tx.Error}
 }
 
-type QueryTx struct{ *Query }
+type QueryTx struct {
+	*Query
+	Error error
+}
 
 func (q *QueryTx) Commit() error {
 	return q.db.Commit().Error

+ 30 - 15
server/router/middleware.go

@@ -40,28 +40,38 @@ func recovery() gin.HandlerFunc {
 
 func authRequired() gin.HandlerFunc {
 	return func(c *gin.Context) {
+		abortWithAuthFailure := func() {
+			c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
+				"message": "Authorization failed",
+			})
+		}
+
 		token := c.GetHeader("Authorization")
 		if token == "" {
-			tmp, _ := base64.StdEncoding.DecodeString(c.Query("token"))
-			token = string(tmp)
-			if token == "" {
-				c.JSON(http.StatusForbidden, gin.H{
-					"message": "Authorization failed",
-				})
-				c.Abort()
+			if token = c.GetHeader("X-Node-Secret"); token != "" && token == settings.ServerSettings.NodeSecret {
+				c.Set("NodeSecret", token)
+				c.Next()
 				return
+			} else {
+				c.Set("ProxyNodeID", c.Query("x_node_id"))
+				tokenBytes, _ := base64.StdEncoding.DecodeString(c.Query("token"))
+				token = string(tokenBytes)
+				if token == "" {
+					abortWithAuthFailure()
+					return
+				}
 			}
 		}
 
-		n := model.CheckToken(token)
-
-		if n < 1 {
-			c.JSON(http.StatusForbidden, gin.H{
-				"message": "Authorization failed",
-			})
-			c.Abort()
+		if model.CheckToken(token) < 1 {
+			abortWithAuthFailure()
 			return
 		}
+
+		if nodeID := c.GetHeader("X-Node-ID"); nodeID != "" {
+			c.Set("ProxyNodeID", nodeID)
+		}
+
 		c.Next()
 	}
 }
@@ -73,7 +83,12 @@ type serverFileSystemType struct {
 func (f serverFileSystemType) Exists(prefix string, _path string) bool {
 	file, err := f.Open(path.Join(prefix, _path))
 	if file != nil {
-		defer file.Close()
+		defer func(file http.File) {
+			err = file.Close()
+			if err != nil {
+				logger.Error("file not found", err)
+			}
+		}(file)
 	}
 	return err == nil
 }

+ 92 - 0
server/router/proxy.go

@@ -0,0 +1,92 @@
+package router
+
+import (
+	"crypto/tls"
+	"github.com/0xJacky/Nginx-UI/server/internal/logger"
+	"github.com/0xJacky/Nginx-UI/server/query"
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/cast"
+	"io"
+	"net/http"
+	"net/url"
+)
+
+func proxy() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		nodeID, ok := c.Get("ProxyNodeID")
+		if !ok {
+			c.Next()
+			return
+		}
+		id := cast.ToInt(nodeID)
+		if id == 0 {
+			c.Next()
+			return
+		}
+
+		defer c.Abort()
+
+		env := query.Environment
+		environment, err := env.Where(env.ID.Eq(id)).First()
+
+		if err != nil {
+			logger.Error(err)
+			c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
+				"message": err.Error(),
+			})
+			return
+		}
+
+		u, err := url.JoinPath(environment.URL, c.Request.RequestURI)
+
+		if err != nil {
+			logger.Error(err)
+			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+				"message": err.Error(),
+			})
+			return
+		}
+
+		decodedUri, err := url.QueryUnescape(u)
+
+		if err != nil {
+			logger.Error(err)
+			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+				"message": err.Error(),
+			})
+			return
+		}
+
+		logger.Debug("Proxy request", decodedUri)
+		client := http.Client{
+			Transport: &http.Transport{
+				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+			},
+		}
+
+		req, err := http.NewRequest(c.Request.Method, decodedUri, c.Request.Body)
+		req.Header.Set("X-Node-Secret", environment.Token)
+
+		resp, err := client.Do(req)
+
+		if err != nil {
+			logger.Error(err)
+			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+				"message": err.Error(),
+			})
+			return
+		}
+
+		defer resp.Body.Close()
+
+		c.Writer.WriteHeader(resp.StatusCode)
+
+		c.Writer.Header().Add("Content-Type", resp.Header.Get("Content-Type"))
+
+		_, err = io.Copy(c.Writer, resp.Body)
+		if err != nil {
+			logger.Error(err)
+			return
+		}
+	}
+}

+ 90 - 0
server/router/proxy_ws.go

@@ -0,0 +1,90 @@
+package router
+
+import (
+    "github.com/0xJacky/Nginx-UI/server/internal/logger"
+    "github.com/0xJacky/Nginx-UI/server/query"
+    "github.com/gin-gonic/gin"
+    "github.com/pretty66/websocketproxy"
+    "github.com/spf13/cast"
+    "net/http"
+    "net/url"
+    "strings"
+)
+
+func proxyWs() gin.HandlerFunc {
+    return func(c *gin.Context) {
+        nodeID, ok := c.Get("ProxyNodeID")
+        if !ok {
+            c.Next()
+            return
+        }
+        id := cast.ToInt(nodeID)
+        if id == 0 {
+            c.Next()
+            return
+        }
+
+        defer c.Abort()
+
+        env := query.Environment
+        environment, err := env.Where(env.ID.Eq(id)).First()
+
+        if err != nil {
+            logger.Error(err)
+            return
+        }
+
+        baseUrl, err := url.Parse(environment.URL)
+        if err != nil {
+            logger.Error(err)
+            return
+        }
+
+        logger.Debug(baseUrl.Port())
+        defaultPort := ""
+        if baseUrl.Port() == "" {
+            switch baseUrl.Scheme {
+            default:
+                fallthrough
+            case "http":
+                defaultPort = "80"
+            case "https":
+                defaultPort = "443"
+            }
+
+            baseUrl.Host = baseUrl.Hostname() + ":" + defaultPort
+        }
+        logger.Debug(baseUrl.String())
+
+        u, err := url.JoinPath(baseUrl.String(), c.Request.RequestURI)
+
+        if err != nil {
+            logger.Error(err)
+            return
+        }
+
+        decodedUri, err := url.QueryUnescape(u)
+
+        if err != nil {
+            logger.Error(err)
+            return
+        }
+
+        // http will be replaced with ws, https will be replaced with wss
+        decodedUri = strings.ReplaceAll(decodedUri, "http", "ws")
+
+        logger.Debug("Proxy request", decodedUri)
+
+        wp, err := websocketproxy.NewProxy(decodedUri, func(r *http.Request) error {
+            r.Header.Set("X-Node-Secret", environment.Token)
+            return nil
+        })
+
+        if err != nil {
+            logger.Error(err)
+            return
+        }
+
+        wp.Proxy(c.Writer, c.Request)
+    }
+}

+ 32 - 15
server/router/routers.go

@@ -18,7 +18,9 @@ func InitRouter() *gin.Engine {
 	r.Use(static.Serve("/", mustFS("")))
 
 	r.NoRoute(func(c *gin.Context) {
-		c.Redirect(http.StatusMovedPermanently, "/")
+		c.JSON(http.StatusNotFound, gin.H{
+			"message": "not found",
+		})
 	})
 
 	root := r.Group("/api")
@@ -29,9 +31,19 @@ func InitRouter() *gin.Engine {
 		root.POST("/login", api.Login)
 		root.DELETE("/logout", api.Logout)
 
-		g := root.Group("/", authRequired())
+		w := root.Group("/", authRequired(), proxyWs())
 		{
-			g.GET("analytic", api.Analytic)
+			// Analytic
+			w.GET("analytic", api.Analytic)
+			// pty
+			w.GET("pty", api.Pty)
+			// Nginx log
+			w.GET("nginx_log", api.NginxLog)
+		}
+
+		g := root.Group("/", authRequired(), proxy())
+		{
+
 			g.GET("analytic/init", api.GetAnalyticInit)
 
 			g.GET("users", api.GetUsers)
@@ -52,13 +64,10 @@ func InitRouter() *gin.Engine {
 			g.POST("ngx/tokenize_config", api.TokenizeNginxConfig)
 			// Format nginx configuration code
 			g.POST("ngx/format_code", api.FormatNginxConfig)
-			// nginx reload
+
 			g.POST("nginx/reload", api.ReloadNginx)
-			// nginx restart
 			g.POST("nginx/restart", api.RestartNginx)
-			// nginx test
 			g.POST("nginx/test", api.TestNginx)
-			// nginx status
 			g.GET("nginx/status", api.NginxStatus)
 
 			g.POST("domain/:name/enable", api.EnableDomain)
@@ -66,7 +75,7 @@ func InitRouter() *gin.Engine {
 			g.POST("domain/:name/advance", api.DomainEditByAdvancedMode)
 
 			g.DELETE("domain/:name", api.DeleteDomain)
-			// duplicate site
+
 			g.POST("domain/:name/duplicate", api.DuplicateSite)
 			g.GET("domain/:name/cert", api.IssueCert)
 
@@ -104,11 +113,6 @@ func InitRouter() *gin.Engine {
 			g.POST("dns_credential/:id", api.EditDnsCredential)
 			g.DELETE("dns_credential/:id", api.DeleteDnsCredential)
 
-			// pty
-			g.GET("pty", api.Pty)
-
-			// Nginx log
-			g.GET("nginx_log", api.NginxLog)
 			g.POST("nginx_log", api.GetNginxLogPage)
 
 			// Settings
@@ -121,8 +125,21 @@ func InitRouter() *gin.Engine {
 			g.GET("upgrade/perform", api.PerformCoreUpgrade)
 
 			// ChatGPT
-			g.POST("/chat_gpt", api.MakeChatCompletionRequest)
-			g.POST("/chat_gpt_record", api.StoreChatGPTRecord)
+			g.POST("chat_gpt", api.MakeChatCompletionRequest)
+			g.POST("chat_gpt_record", api.StoreChatGPTRecord)
+
+			// Environment
+			g.GET("environments", api.GetEnvironmentList)
+			envGroup := g.Group("environment")
+			{
+				envGroup.GET("/:id", api.GetEnvironment)
+				envGroup.POST("", api.AddEnvironment)
+				envGroup.POST("/:id", api.EditEnvironment)
+				envGroup.DELETE("/:id", api.DeleteEnvironment)
+			}
+
+			// node
+			g.GET("node", api.GetCurrentNode)
 		}
 	}
 

+ 13 - 0
server/server.go

@@ -10,6 +10,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/server/router"
 	"github.com/0xJacky/Nginx-UI/server/settings"
 	"github.com/go-co-op/gocron"
+	"github.com/google/uuid"
 	"github.com/jpillora/overseer"
 	"log"
 	"mime"
@@ -30,6 +31,18 @@ func Program(state overseer.State) {
 		query.Init(db)
 	}
 
+	if "" == settings.ServerSettings.NodeSecret {
+		logger.Warn("NodeSecret is empty")
+		settings.ServerSettings.NodeSecret = uuid.New().String()
+		settings.ReflectFrom()
+
+		err := settings.Save()
+		if err != nil {
+			logger.Error("Error save settings")
+		}
+		logger.Warn("Generated NodeSecret: ", settings.ServerSettings.NodeSecret)
+	}
+
 	s := gocron.NewScheduler(time.UTC)
 	job, err := s.Every(30).Minute().SingletonMode().Do(cert.AutoObtain)
 

+ 94 - 0
server/service/environment.go

@@ -0,0 +1,94 @@
+package service
+
+import (
+	"crypto/tls"
+	"encoding/json"
+	"github.com/0xJacky/Nginx-UI/server/internal/logger"
+	"github.com/0xJacky/Nginx-UI/server/model"
+	"github.com/0xJacky/Nginx-UI/server/query"
+	"io"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+type Environment struct {
+	*model.Environment
+	Status bool `json:"status"`
+	NodeInfo
+}
+
+func RetrieveEnvironmentList() (envs []*Environment, err error) {
+	envQuery := query.Environment
+
+	data, err := envQuery.Find()
+	if err != nil {
+		return
+	}
+
+	for _, v := range data {
+		t := &Environment{
+			Environment: v,
+		}
+
+		node, status := t.GetNode()
+		t.Status = status
+		t.NodeInfo = node
+
+		envs = append(envs, t)
+	}
+
+	return
+}
+
+type NodeInfo struct {
+	RequestNodeSecret string      `json:"request_node_secret"`
+	NodeRuntimeInfo   RuntimeInfo `json:"node_runtime_info"`
+	Version           string      `json:"version"`
+	CPUNum            int         `json:"cpu_num"`
+	MemoryTotal       string      `json:"memory_total"`
+	ResponseAt        time.Time   `json:"response_at"`
+}
+
+func (env *Environment) GetNode() (node NodeInfo, status bool) {
+	u, err := url.JoinPath(env.URL, "/api/node")
+
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+	client := http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+	}
+	req, err := http.NewRequest("GET", u, nil)
+	req.Header.Set("X-Node-Secret", env.Token)
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	defer resp.Body.Close()
+	bytes, _ := io.ReadAll(resp.Body)
+
+	if resp.StatusCode != 200 {
+		logger.Error(string(bytes))
+		return
+	}
+
+	logger.Debug(string(bytes))
+	err = json.Unmarshal(bytes, &node)
+	if err != nil {
+		logger.Error(err)
+		return
+	}
+
+	node.ResponseAt = time.Now()
+	status = true
+
+	return
+}

+ 1 - 0
server/settings/settings.go

@@ -19,6 +19,7 @@ type Server struct {
 	HttpPort          string `json:"http_port"`
 	RunMode           string `json:"run_mode"`
 	JwtSecret         string `json:"jwt_secret"`
+	NodeSecret        string `json:"node_secret"`
 	HTTPChallengePort string `json:"http_challenge_port"`
 	Email             string `json:"email"`
 	Database          string `json:"database"`

Some files were not shown because too many files changed in this diff