diff --git a/.gitignore b/.gitignore index f57d7fb..7ce527c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ x-* dist .trae .vscode -.idea \ No newline at end of file +.idea +forge \ No newline at end of file diff --git a/internal/cmd/install.go b/internal/cmd/install.go index b23312e..accd496 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -1,9 +1,8 @@ package cmd import ( - "fmt" - "github.com/spf13/cobra" + "yizhisec.com/hsv2/forge/internal/cmd/installcmd" ) func installCmd() *cobra.Command { @@ -11,15 +10,12 @@ func installCmd() *cobra.Command { Use: "install", Short: "Install the project", Long: `Install the built project to the specified location.`, - RunE: func(cmd *cobra.Command, args []string) error { - return runInstall(args) - }, } + _cmd.AddCommand( + installcmd.Check(), + installcmd.Prepare(), + ) + return _cmd } - -func runInstall(args []string) error { - fmt.Println("Running install command...") - return nil -} diff --git a/internal/cmd/installcmd/check.go b/internal/cmd/installcmd/check.go new file mode 100644 index 0000000..23977f2 --- /dev/null +++ b/internal/cmd/installcmd/check.go @@ -0,0 +1,39 @@ +package installcmd + +import ( + "github.com/spf13/cobra" + "yizhisec.com/hsv2/forge/internal/controller/installer" +) + +func Check() *cobra.Command { + var ( + workdir string + target string + ignoreDisk bool + ignoreMemory bool + ignoreCPU bool + ) + + _cmd := &cobra.Command{ + Use: "check", + Short: "Check system requirements", + Long: `Check system requirements for the project.`, + RunE: func(cmd *cobra.Command, args []string) error { + _installer := installer.NewInstaller(workdir, target) + return _installer.Check( + cmd.Context(), + installer.WithIgnoreDiskCheck(ignoreDisk), + installer.WithIgnoreMemoryCheck(ignoreMemory), + installer.WithIgnoreCPUCheck(ignoreCPU), + ) + }, + } + + _cmd.Flags().StringVar(&workdir, "workdir", "/root/hs-installation", "Working directory") + _cmd.Flags().StringVar(&target, "target", "self", "Target") + _cmd.Flags().BoolVar(&ignoreDisk, "ignore-check-disk", false, "ignore disk requirement check result") + _cmd.Flags().BoolVar(&ignoreMemory, "ignore-check-memory", false, "ignore memory requirement check result") + _cmd.Flags().BoolVar(&ignoreCPU, "ignore-check-cpu", false, "ignore cpu requirement check result") + + return _cmd +} diff --git a/internal/cmd/installcmd/k0s.go b/internal/cmd/installcmd/k0s.go new file mode 100644 index 0000000..46cb8bb --- /dev/null +++ b/internal/cmd/installcmd/k0s.go @@ -0,0 +1,29 @@ +package installcmd + +import ( + "github.com/spf13/cobra" + "yizhisec.com/hsv2/forge/internal/controller/installer" +) + +func K0s() *cobra.Command { + + var ( + workdir string + target string + ) + + _cmd := &cobra.Command{ + Use: "k0s", + Short: "Install k0s", + Long: "Install k0s", + RunE: func(cmd *cobra.Command, args []string) error { + _installer := installer.NewInstaller(workdir, target) + return _installer.K0s(cmd.Context()) + }, + } + + _cmd.PersistentFlags().StringVar(&workdir, "workdir", "/root/hs-installation", "working directory") + _cmd.PersistentFlags().StringVar(&target, "target", "self", "target directory") + + return _cmd +} diff --git a/internal/cmd/installcmd/prepare.go b/internal/cmd/installcmd/prepare.go new file mode 100644 index 0000000..3dd35ef --- /dev/null +++ b/internal/cmd/installcmd/prepare.go @@ -0,0 +1,30 @@ +package installcmd + +import ( + "github.com/spf13/cobra" + "yizhisec.com/hsv2/forge/internal/controller/installer" +) + +func Prepare() *cobra.Command { + + var ( + workdir string + target string + ) + + _cmd := &cobra.Command{ + Use: "prepare", + Short: "Prepare for installation", + Long: "Prepare for installation", + RunE: func(cmd *cobra.Command, args []string) error { + _installer := installer.NewInstaller(workdir, target) + _installer.Prepare(cmd.Context()) + return nil + }, + } + + _cmd.Flags().StringVar(&workdir, "workdir", "/root/hs-installation", "Working directory") + _cmd.Flags().StringVar(&target, "target", "self", "Target") + + return _cmd +} diff --git a/internal/controller/installer/installer.check.go b/internal/controller/installer/installer.check.go index 1fc05b5..b7fa36b 100644 --- a/internal/controller/installer/installer.check.go +++ b/internal/controller/installer/installer.check.go @@ -1,15 +1,140 @@ package installer -import "context" +import ( + "context" + "fmt" -func (i *installer) Check(ctx context.Context) error { + "gitea.loveuer.com/yizhisec/pkg3/logger" + "yizhisec.com/hsv2/forge/pkg/syscheck" +) + +type CheckOption func(*checkOpt) +type checkOpt struct { + ignoreDisk bool + ignoreMemory bool + ignoreCPU bool +} + +func WithIgnoreDiskCheck(ignore bool) CheckOption { + return func(o *checkOpt) { + o.ignoreDisk = ignore + } +} + +func WithIgnoreMemoryCheck(ignore bool) CheckOption { + return func(o *checkOpt) { + o.ignoreMemory = ignore + } +} + +func WithIgnoreCPUCheck(ignore bool) CheckOption { + return func(o *checkOpt) { + o.ignoreCPU = ignore + } +} + +func (i *installer) Check(ctx context.Context, opts ...CheckOption) error { var ( err error + o = &checkOpt{} ) + for _, fn := range opts { + fn(o) + } + + logger.Info("☑️ installer.Check: Starting system checks...") + if err = i.targetOK(ctx); err != nil { return err } + // 1. Check disk space: >= 500GB + logger.Info("☑️ installer.Check: Checking disk space...") + diskSpaceResult, err := syscheck.CheckDiskSpace(ctx, i, 500) + if err != nil { + logger.Debug("❌ installer.Check: Failed to check disk space: %v", err) + return fmt.Errorf("failed to check disk space: %w", err) + } + + if !diskSpaceResult.Passed { + logger.Error("❌ %s: %s (Expected: %s, Actual: %s)", diskSpaceResult.Name, diskSpaceResult.Message, diskSpaceResult.Expected, diskSpaceResult.Actual) + if !o.ignoreDisk { + return fmt.Errorf("disk space check failed: %s", diskSpaceResult.Message) + } + } + + logger.Info("✅ %s: %s", diskSpaceResult.Name, diskSpaceResult.Actual) + + // 2. Check disk performance: write >= 500MB/s, read >= 500MB/s + logger.Info("☑️ installer.Check: Checking disk performance...") + diskPerfResult, err := syscheck.CheckDiskPerformance(ctx, i, 500, 500) + if err != nil { + logger.Debug("❌ installer.Check: Failed to check disk performance: %v", err) + return fmt.Errorf("failed to check disk performance: %w", err) + } + + if !diskPerfResult.Passed { + logger.Error("❌ %s: %s (Expected: %s, Actual: %s)", diskPerfResult.Name, diskPerfResult.Message, diskPerfResult.Expected, diskPerfResult.Actual) + if !o.ignoreDisk { + return fmt.Errorf("disk performance check failed: %s", diskPerfResult.Message) + } + } + + logger.Info("✅ %s: %s", diskPerfResult.Name, diskPerfResult.Actual) + + // 3. Check memory size: >= 15.5GB + logger.Info("☑️ installer.Check: Checking memory size...") + memResult, err := syscheck.CheckMemory(ctx, i, 15.5) + if err != nil { + logger.Debug("❌ installer.Check: Failed to check memory: %v", err) + return fmt.Errorf("failed to check memory: %w", err) + } + + if !memResult.Passed { + logger.Error("❌ %s: %s (Expected: %s, Actual: %s)", memResult.Name, memResult.Message, memResult.Expected, memResult.Actual) + if !o.ignoreMemory { + return fmt.Errorf("memory check failed: %s", memResult.Message) + } + } + + logger.Info("✅ %s: %s", memResult.Name, memResult.Actual) + + // 4. Check CPU cores: >= 8 + logger.Info("☑️ installer.Check: Checking CPU cores...") + cpuCoresResult, err := syscheck.CheckCPUCores(ctx, i, 8) + if err != nil { + logger.Debug("❌ installer.Check: Failed to check CPU cores: %v", err) + return fmt.Errorf("failed to check CPU cores: %w", err) + } + + if !cpuCoresResult.Passed { + logger.Error("❌ %s: %s (Expected: %s, Actual: %s)", cpuCoresResult.Name, cpuCoresResult.Message, cpuCoresResult.Expected, cpuCoresResult.Actual) + if !o.ignoreCPU { + return fmt.Errorf("CPU cores check failed: %s", cpuCoresResult.Message) + } + } + + logger.Info("✅ %s: %s", cpuCoresResult.Name, cpuCoresResult.Actual) + + // 5. Check CPU frequency: >= 2GHz + logger.Info("☑️ installer.Check: Checking CPU frequency...") + cpuFreqResult, err := syscheck.CheckCPUFrequency(ctx, i, 2.0) + if err != nil { + logger.Debug("❌ installer.Check: Failed to check CPU frequency: %v", err) + return fmt.Errorf("failed to check CPU frequency: %w", err) + } + + if !cpuFreqResult.Passed { + logger.Error("❌ %s: %s (Expected: %s, Actual: %s)", cpuFreqResult.Name, cpuFreqResult.Message, cpuFreqResult.Expected, cpuFreqResult.Actual) + if !o.ignoreCPU { + return fmt.Errorf("CPU frequency check failed: %s", cpuFreqResult.Message) + } + } + + logger.Info("✅ %s: %s", cpuFreqResult.Name, cpuFreqResult.Actual) + + logger.Info("✅ installer.Check: All system checks passed successfully!") + return nil } diff --git a/internal/controller/installer/installer.go b/internal/controller/installer/installer.go index 54993e2..7616543 100644 --- a/internal/controller/installer/installer.go +++ b/internal/controller/installer/installer.go @@ -3,7 +3,11 @@ package installer import ( "context" "errors" + "fmt" + "io" + "os" "os/exec" + "path/filepath" "gitea.loveuer.com/yizhisec/pkg3/logger" ) @@ -13,14 +17,118 @@ type installer struct { target string } -func (i *installer) targetOK(ctx context.Context) error { - if i.target == "" { - logger.Debug("🎯 installer.targetOK: target = self") +func (i *installer) buildCommand(ctx context.Context, cmds ...string) *exec.Cmd { + if len(cmds) == 0 { return nil } - // run ssh , check if it's reachable, and it's root user - cmd := exec.CommandContext(ctx, "ssh", i.target, "whoami") + if i.target == "self" { + return exec.CommandContext(ctx, cmds[0], cmds[1:]...) + } + + sshArgs := append([]string{i.target}, cmds...) + return exec.CommandContext(ctx, "ssh", sshArgs...) +} + +// ExecuteCommand implements syscheck.CommandExecutor interface +func (i *installer) ExecuteCommand(ctx context.Context, cmds ...string) (string, error) { + cmd := i.buildCommand(ctx, cmds...) + if cmd == nil { + return "", fmt.Errorf("failed to build command") + } + + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("command failed: %w, output: %s", err, string(output)) + } + + return string(output), nil +} + +type CopyFileOption func(*copyFileOptions) +type copyFileOptions struct { + addExecutable bool +} + +func withCopyFileExecutable() func(*copyFileOptions) { + return func(o *copyFileOptions) { + o.addExecutable = true + } +} + +func (i *installer) copyFile(ctx context.Context, src, dst string, opts ...CopyFileOption) error { + logger.Debug("☑️ installer.copyFile: Copying file from %s to %s (target: %s)", src, dst, i.target) + + var ( + err error + o = ©FileOptions{} + srcFile, dstFile *os.File + srcInfo os.FileInfo + ) + + for _, fn := range opts { + fn(o) + } + + if i.target == "self" { + // Simply copy file locally + logger.Debug("Copying file locally: %s -> %s", src, dst) + + // Open source file + if srcFile, err = os.Open(src); err != nil { + logger.Error("❌ Failed to open source file %s: %v", src, err) + return fmt.Errorf("failed to open source file: %w", err) + } + defer srcFile.Close() + + // Create destination directory if needed + dstDir := filepath.Dir(dst) + if err = os.MkdirAll(dstDir, 0755); err != nil { + logger.Error("❌ Failed to create destination directory %s: %v", dstDir, err) + return fmt.Errorf("failed to create destination directory: %w", err) + } + + // Create destination file + if dstFile, err = os.Create(dst); err != nil { + logger.Error("❌ Failed to create destination file %s: %v", dst, err) + return fmt.Errorf("failed to create destination file: %w", err) + } + defer dstFile.Close() + + // Copy file content + if _, err = io.Copy(dstFile, srcFile); err != nil { + logger.Error("❌ Failed to copy file content: %v", err) + return fmt.Errorf("failed to copy file content: %w", err) + } + + // Get source file permissions + if srcInfo, err = os.Stat(src); err == nil { + if err = os.Chmod(dst, srcInfo.Mode()); err != nil { + logger.Debug("⚠️ Failed to set file permissions: %v", err) + } + } + + logger.Info("✅ File copied locally: %s -> %s", src, dst) + return nil + } + + // Copy file via scp to remote target + logger.Debug("Copying file via scp: %s -> %s:%s", src, i.target, dst) + + // Format: scp : + cmd := exec.CommandContext(ctx, "scp", src, fmt.Sprintf("%s:%s", i.target, dst)) + output, err := cmd.CombinedOutput() + if err != nil { + logger.Error("❌ Failed to copy file via scp: %v, output: %s", err, string(output)) + return fmt.Errorf("failed to copy file via scp: %w, output: %s", err, string(output)) + } + + logger.Info("✅ File copied via scp: %s -> %s:%s", src, i.target, dst) + return nil +} + +func (i *installer) targetOK(ctx context.Context) error { + cmd := i.buildCommand(ctx, "whoami") output, err := cmd.CombinedOutput() if err != nil { logger.Debug("❌ installer.targetOK: check target %s failed, err = %v", i.target, err) @@ -36,5 +144,10 @@ func (i *installer) targetOK(ctx context.Context) error { } func NewInstaller(workdir, target string) *installer { + if target == "" { + logger.Warn("🎯 NewInstaller: target empty, set to default(self)") + target = "self" + } + return &installer{workdir: workdir, target: target} } diff --git a/internal/controller/installer/installer.k0s.go b/internal/controller/installer/installer.k0s.go index 53653ee..81cafdd 100644 --- a/internal/controller/installer/installer.k0s.go +++ b/internal/controller/installer/installer.k0s.go @@ -2,15 +2,19 @@ package installer import ( "context" + "fmt" + "os" + "path/filepath" + "gitea.loveuer.com/yizhisec/pkg3/logger" "github.com/samber/lo" ) type K0sOpt func(*k0sOpt) type k0sOpt struct { - Type string // controller, worker - DisableWorker bool - WorkerTokenFile string + Type string // controller, worker + controllerAsWorker bool + WorkerTokenFile string } func WithK0sType(t string) K0sOpt { @@ -22,9 +26,9 @@ func WithK0sType(t string) K0sOpt { } } -func WithoutK0sWorker() K0sOpt { +func WithK0sControllerAsWorker() K0sOpt { return func(o *k0sOpt) { - o.DisableWorker = true + o.controllerAsWorker = true } } @@ -40,9 +44,9 @@ func (i *installer) K0s(ctx context.Context, opts ...K0sOpt) error { var ( err error o = &k0sOpt{ - Type: "controller", - DisableWorker: false, - WorkerTokenFile: "/etc/k0s/worker.token", + Type: "controller", + controllerAsWorker: false, + WorkerTokenFile: "/etc/k0s/worker.token", } ) @@ -54,5 +58,66 @@ func (i *installer) K0s(ctx context.Context, opts ...K0sOpt) error { fn(o) } + binaries := []string{ + "dependency/bin/k0s", + "dependency/bin/k9s", "dependency/bin/kubectl", "dependency/bin/helm"} + if err = i.checkFiles(binaries...); err != nil { + return err + } + + // check image tar files: + images := []string{ + "dependency/image/k0s.apiserver-network-proxy-agent.tar", + "dependency/image/k0s.cni-node.tar", + "dependency/image/k0s.coredns.tar", + "dependency/image/k0s.kube-proxy.tar", + "dependency/image/k0s.kube-router.tar", + "dependency/image/k0s.metrics-server.tar", + "dependency/image/k0s.pause.tar", + } + + if err = i.checkFiles(images...); err != nil { + return err + } + + // copy binaries to /usr/local/bin and add executable permissions + if err = i.copyFile(ctx, "dependency/bin/k0s", "/usr/local/bin/k0s", withCopyFileExecutable()); err != nil { + return err + } + if err = i.copyFile(ctx, "dependency/bin/k9s", "/usr/local/bin/k9s", withCopyFileExecutable()); err != nil { + return err + } + if err = i.copyFile(ctx, "dependency/bin/kubectl", "/usr/local/bin/kubectl", withCopyFileExecutable()); err != nil { + return err + } + if err = i.copyFile(ctx, "dependency/bin/helm", "/usr/local/bin/helm", withCopyFileExecutable()); err != nil { + return err + } + + i.ExecuteCommand(ctx, "k0s", "") + + return nil +} + +// checkBinaryFiles checks if the required binary files exist in the dependency/bin directory +func (i *installer) checkFiles(fileBaseName ...string) error { + logger.Info("☑️ installer.checkFiles: Checking files in %s...", i.workdir) + + for _, file := range fileBaseName { + filename := filepath.Join(i.workdir, file) + logger.Debug("Checking file: %s", filename) + + if _, err := os.Stat(filename); os.IsNotExist(err) { + logger.Error("❌ File not found: %s", filename) + return fmt.Errorf("file not found: %s", filename) + } else if err != nil { + logger.Error("❌ Failed to check file %s: %v", filename, err) + return fmt.Errorf("failed to check file %s: %w", filename, err) + } + + logger.Info("✅ File found: %s", file) + } + + logger.Info("✅ installer.checkBinaryFiles: All binary files verified successfully!") return nil } diff --git a/internal/controller/installer/installer.prepare.go b/internal/controller/installer/installer.prepare.go index e9cc8ab..5c0876a 100644 --- a/internal/controller/installer/installer.prepare.go +++ b/internal/controller/installer/installer.prepare.go @@ -1,15 +1,184 @@ package installer -import "context" +import ( + "context" + "fmt" + + "gitea.loveuer.com/yizhisec/pkg3/logger" +) func (i *installer) Prepare(ctx context.Context) error { var ( err error ) + logger.Info("☑️ installer.Prepare: Starting system preparation...") + if err = i.targetOK(ctx); err != nil { return err } + // 1. Set timezone to Asia/Shanghai + logger.Info("☑️ installer.Prepare: Setting timezone to Asia/Shanghai...") + if err = i.setTimezone(ctx); err != nil { + logger.Debug("❌ installer.Prepare: Failed to set timezone: %v", err) + return fmt.Errorf("failed to set timezone: %w", err) + } + logger.Info("✅ installer.Prepare: Timezone set successfully") + + // 2. Disable swap + logger.Info("☑️ installer.Prepare: Disabling swap...") + if err = i.disableSwap(ctx); err != nil { + logger.Debug("❌ installer.Prepare: Failed to disable swap: %v", err) + return fmt.Errorf("failed to disable swap: %w", err) + } + logger.Info("✅ installer.Prepare: Swap disabled successfully") + + // 3. Load module: iscsi_tcp + logger.Info("☑️ installer.Prepare: Loading kernel module iscsi_tcp...") + if err = i.loadKernelModule(ctx, "iscsi_tcp"); err != nil { + logger.Debug("❌ installer.Prepare: Failed to load iscsi_tcp module: %v", err) + return fmt.Errorf("failed to load iscsi_tcp module: %w", err) + } + logger.Info("✅ installer.Prepare: iscsi_tcp module loaded successfully") + + // 4. Load module: br_netfilter + logger.Info("☑️ installer.Prepare: Loading kernel module br_netfilter...") + if err = i.loadKernelModule(ctx, "br_netfilter"); err != nil { + logger.Debug("❌ installer.Prepare: Failed to load br_netfilter module: %v", err) + return fmt.Errorf("failed to load br_netfilter module: %w", err) + } + logger.Info("✅ installer.Prepare: br_netfilter module loaded successfully") + + // 5. Apply sysctl settings + logger.Info("☑️ installer.Prepare: Applying sysctl settings...") + if err = i.applySysctlSettings(ctx); err != nil { + logger.Debug("❌ installer.Prepare: Failed to apply sysctl settings: %v", err) + return fmt.Errorf("failed to apply sysctl settings: %w", err) + } + logger.Info("✅ installer.Prepare: Sysctl settings applied successfully") + + logger.Info("✅ installer.Prepare: System preparation completed successfully!") + + return nil +} + +// setTimezone sets the system timezone to Asia/Shanghai +func (i *installer) setTimezone(ctx context.Context) error { + // Check if timezone file exists + cmd := i.buildCommand(ctx, "test", "-f", "/usr/share/zoneinfo/Asia/Shanghai") + if cmd == nil { + return fmt.Errorf("failed to build command") + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("timezone file /usr/share/zoneinfo/Asia/Shanghai not found") + } + + // Remove old localtime link/file + cmd = i.buildCommand(ctx, "rm", "-f", "/etc/localtime") + if cmd == nil { + return fmt.Errorf("failed to build command") + } + if err := cmd.Run(); err != nil { + logger.Debug("Failed to remove /etc/localtime: %v", err) + } + + // Create symlink + cmd = i.buildCommand(ctx, "ln", "-s", "/usr/share/zoneinfo/Asia/Shanghai", "/etc/localtime") + if cmd == nil { + return fmt.Errorf("failed to build command") + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to create symlink: %w", err) + } + + return nil +} + +// disableSwap disables all swap partitions and removes swap entries from /etc/fstab +func (i *installer) disableSwap(ctx context.Context) error { + // Turn off all swap + cmd := i.buildCommand(ctx, "swapoff", "-a") + if cmd == nil { + return fmt.Errorf("failed to build command") + } + if err := cmd.Run(); err != nil { + logger.Debug("Failed to swapoff: %v (may be already off)", err) + } + + // Comment out swap entries in /etc/fstab to make it persistent + cmd = i.buildCommand(ctx, "sed", "-i", "/swap/s/^/#/", "/etc/fstab") + if cmd == nil { + return fmt.Errorf("failed to build command") + } + if err := cmd.Run(); err != nil { + logger.Debug("Failed to comment swap in /etc/fstab: %v", err) + } + + return nil +} + +// loadKernelModule loads a kernel module and ensures it's loaded on boot +func (i *installer) loadKernelModule(ctx context.Context, moduleName string) error { + // Load the module immediately + cmd := i.buildCommand(ctx, "modprobe", moduleName) + if cmd == nil { + return fmt.Errorf("failed to build command") + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to load module %s: %w", moduleName, err) + } + + // Add to /etc/modules-load.d/ to load on boot + filePath := fmt.Sprintf("/etc/modules-load.d/%s.conf", moduleName) + cmd = i.buildCommand(ctx, "bash", "-c", fmt.Sprintf("echo '%s' > %s", moduleName, filePath)) + if cmd == nil { + return fmt.Errorf("failed to build command") + } + if err := cmd.Run(); err != nil { + logger.Debug("Failed to add module to modules-load.d: %v", err) + } + + return nil +} + +// applySysctlSettings applies required sysctl settings for Kubernetes +func (i *installer) applySysctlSettings(ctx context.Context) error { + const sysctlConfig = `# Kubernetes required settings +net.bridge.bridge-nf-call-iptables = 1 +net.bridge.bridge-nf-call-ip6tables = 1 +net.ipv4.ip_forward = 1 +net.ipv4.conf.all.forwarding = 1 +net.ipv6.conf.all.forwarding = 1 +vm.swappiness = 0 +vm.overcommit_memory = 1 +vm.panic_on_oom = 0 +fs.file-max = 1000000 +fs.inotify.max_user_watches = 2099999999 +fs.inotify.max_user_instances = 2099999999 +fs.inotify.max_queued_events = 2099999999 +net.ipv4.neigh.default.gc_thresh1 = 1024 +net.ipv4.neigh.default.gc_thresh2 = 4096 +net.ipv4.neigh.default.gc_thresh3 = 8192 +` + + // Write sysctl config file + cmd := i.buildCommand(ctx, "bash", "-c", fmt.Sprintf("cat > /etc/sysctl.d/99-kubernetes.conf << 'EOF'\n%sEOF", sysctlConfig)) + if cmd == nil { + return fmt.Errorf("failed to build command") + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to write sysctl config: %w", err) + } + + // Apply sysctl settings + cmd = i.buildCommand(ctx, "sysctl", "--system") + if cmd == nil { + return fmt.Errorf("failed to build command") + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to apply sysctl settings: %w", err) + } + return nil } diff --git a/internal/controller/maker/configmap.go b/internal/controller/maker/configmap.go index 8e9afe9..b78a234 100644 --- a/internal/controller/maker/configmap.go +++ b/internal/controller/maker/configmap.go @@ -100,6 +100,16 @@ kubectl create configmap ssl-client-key --namespace hsv2 --from-file=client.key= kubectl create configmap ssl-client-ca-crt --namespace hsv2 --from-file=client.ca.crt=./ssl_client_ca.crt --dry-run=client -o yaml | kubectl apply -f - kubectl create configmap ssl-client-ca-key --namespace hsv2 --from-file=client.ca.key=./ssl_client_ca.key --dry-run=client -o yaml | kubectl apply -f - kubectl create configmap ssl-web-crt --namespace hsv2 --from-file=web.server.crt=./ssl_web.crt --dry-run=client -o yaml | kubectl apply -f - +` + _version_yaml = ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-version + namespace: hsv2 +data: + version.txt: | + __version__ ` ) @@ -208,6 +218,12 @@ kubectl create configmap ssl-web-crt --namespace hsv2 --from-file=web.server.crt } logger.Debug("✅ maker.ConfigMap: 写入 ssl_client_ca.key 文件: %s 成功", filepath.Join(dir, "ssl_client_ca.key")) + if err = os.WriteFile(filepath.Join(dir, "version.yaml"), []byte(_version_yaml), 0644); err != nil { + logger.Debug("❌ maker.ConfigMap: 写入 version.yaml 文件: %s 失败, 错误: %v", filepath.Join(dir, "version.yaml"), err) + return err + } + logger.Debug("✅ maker.ConfigMap: 写入 version.yaml 文件: %s 成功", filepath.Join(dir, "version.yaml")) + // upsert configmap logger.Debug("☑️ maker.ConfigMap: 执行 upsert 脚本: %s", filepath.Join(dir, "upsert.sh")) if err = os.WriteFile(filepath.Join(dir, "upsert.sh"), []byte(upsert), 0755); err != nil { diff --git a/internal/controller/maker/proxy.go b/internal/controller/maker/proxy.go index 514eb46..f7933de 100644 --- a/internal/controller/maker/proxy.go +++ b/internal/controller/maker/proxy.go @@ -2,31 +2,18 @@ package maker import ( "context" + "encoding/json" "os" "path/filepath" "gitea.loveuer.com/yizhisec/pkg3/logger" "yizhisec.com/hsv2/forge/pkg/downloader" + "yizhisec.com/hsv2/forge/pkg/model" ) func (m *maker) Proxy(ctx context.Context) error { const ( - binURL = "https://artifactory.yizhisec.com:443/artifactory/filestore/hsv2/bin/caddy" - caddyfileTpl = `{ - layer4 { - :8443 { - route { - proxy __UPSTREAMS_8443__ - } - } - - :443 { - route { - proxy __UPSTREAMS_443__ - } - } - } -}` + binURL = "https://artifactory.yizhisec.com:443/artifactory/filestore/hsv2/bin/caddy" systemdSvc = `[Unit] Description=YiZhiSec Caddy Reverse Proxy After=network.target @@ -34,7 +21,7 @@ After=network.target [Service] Type=simple User=root -ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile +ExecStart=/usr/local/bin/caddy run --config /etc/caddy/caddy.json StandardOutput=journal StandardError=journal Nice=-20 @@ -68,12 +55,85 @@ WantedBy=multi-user.target` } logger.Debug("✅ maker.Proxy: 下载 caddy 成功, url = %s", binURL) - logger.Debug("☑️ maker.Proxy: 写入 Caddyfile 文件..., dest = %s", filepath.Join(location, "Caddyfile")) - if err := os.WriteFile(filepath.Join(location, "Caddyfile"), []byte(caddyfileTpl), 0644); err != nil { - logger.Debug("❌ maker.Proxy: 写入 Caddyfile 失败, dest = %s, err = %v", filepath.Join(location, "Caddyfile"), err) + logger.Debug("☑️ maker.Proxy: 写入 caddy.json 文件..., dest = %s", filepath.Join(location, "caddy.json")) + caddyConfig := model.CaddyConfig{ + "apps": &model.CaddyApp{ + Layer4: &model.CaddyLayer4{ + Servers: map[string]*model.CaddyServer{ + "proxy_8443": { + Listen: []string{":8443"}, + Routes: []*model.CaddyRoute{ + { + Handle: []*model.CaddyHandle{ + { + Handler: "proxy", + Upstreams: []*model.CaddyUpstream{ + {Dial: []string{"__ip_1__:32443"}}, + {Dial: []string{"__ip_2__:32443"}}, + }, + HealthChecks: &model.CaddyHealthCheck{ + Active: &model.CaddyActive{ + Interval: "10s", + Timeout: "2s", + Port: 32443, + }, + Passive: &model.CaddyPassive{ + FailDuration: "30s", + MaxFails: 2, + }, + }, + LoadBalancing: &model.CaddyLoadBalancing{ + Selection: &model.CaddySelection{ + Policy: "round_robin", + }, + }, + }, + }, + }, + }, + }, + "proxy_443": { + Listen: []string{":443"}, + Routes: []*model.CaddyRoute{ + { + Handle: []*model.CaddyHandle{ + { + Handler: "proxy", + Upstreams: []*model.CaddyUpstream{ + {Dial: []string{"__ip_1__:31443"}}, + {Dial: []string{"__ip_2__:31443"}}, + }, + HealthChecks: &model.CaddyHealthCheck{ + Active: &model.CaddyActive{ + Interval: "10s", + Timeout: "2s", + Port: 31443, + }, + Passive: &model.CaddyPassive{ + FailDuration: "30s", + MaxFails: 2, + }, + }, + LoadBalancing: &model.CaddyLoadBalancing{ + Selection: &model.CaddySelection{ + Policy: "round_robin", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + bs, _ := json.MarshalIndent(caddyConfig, "", " ") + if err := os.WriteFile(filepath.Join(location, "caddy.json"), []byte(bs), 0644); err != nil { + logger.Debug("❌ maker.Proxy: 写入 Caddyfile 失败, dest = %s, err = %v", filepath.Join(location, "caddy.json"), err) return err } - logger.Debug("✅ maker.Proxy: 写入 Caddyfile 文件成功, dest = %s", filepath.Join(location, "Caddyfile")) + logger.Debug("✅ maker.Proxy: 写入 Caddyfile 文件成功, dest = %s", filepath.Join(location, "caddy.json")) logger.Debug("☑️ maker.Proxy: 写入 caddy.service 文件..., dest = %s", filepath.Join(location, "caddy.service")) if err := os.WriteFile(filepath.Join(location, "caddy.service"), []byte(systemdSvc), 0644); err != nil { diff --git a/pkg/model/caddy.go b/pkg/model/caddy.go new file mode 100644 index 0000000..434a66f --- /dev/null +++ b/pkg/model/caddy.go @@ -0,0 +1,55 @@ +package model + +type CaddyUpstream struct { + Dial []string `json:"dial"` +} + +type CaddyActive struct { + Interval string `json:"interval"` + Timeout string `json:"timeout"` + Port int `json:"port"` +} + +type CaddyPassive struct { + MaxFails int `json:"max_fails"` + FailDuration string `json:"fail_duration"` +} + +type CaddyHealthCheck struct { + Active *CaddyActive `json:"active"` + Passive *CaddyPassive `json:"passive"` +} + +type CaddySelection struct { + Policy string `json:"policy"` +} + +type CaddyLoadBalancing struct { + Selection *CaddySelection `json:"selection"` +} + +type CaddyHandle struct { + Handler string `json:"handler"` + Upstreams []*CaddyUpstream `json:"upstreams"` + HealthChecks *CaddyHealthCheck `json:"health_checks"` + LoadBalancing *CaddyLoadBalancing `json:"load_balancing"` +} + +type CaddyRoute struct { + Handle []*CaddyHandle `json:"handle"` +} + +type CaddyServer struct { + Listen []string `json:"listen"` + Routes []*CaddyRoute `json:"routes"` +} + +type CaddyLayer4 struct { + Servers map[string]*CaddyServer `json:"servers"` +} + +type CaddyApp struct { + Layer4 *CaddyLayer4 `json:"layer4"` +} + +type CaddyConfig map[string]*CaddyApp diff --git a/pkg/resource/nginx/caddy.json b/pkg/resource/nginx/caddy.json new file mode 100644 index 0000000..793e529 --- /dev/null +++ b/pkg/resource/nginx/caddy.json @@ -0,0 +1,73 @@ +{ + "apps": { + "layer4": { + "servers": { + "proxy_8443_tcp_backends": { + "listen": [":8443"], + "routes": [ + { + "handle": [ + { + "handler": "proxy", + "upstreams": [ + {"dial": ["10.118.2.11:32443"]}, + {"dial": ["10.118.2.12:32443"]} + ], + "health_checks": { + "active": { + "interval": "5s", + "timeout": "2s", + "port": 32443 + }, + "passive": { + "max_fails": 1, + "fail_duration": "30s" + } + }, + "load_balancing": { + "selection": { + "policy": "round_robin" + } + } + } + ] + } + ] + }, + "proxy_443_tcp_backends": { + "listen": [":443"], + "routes": [ + { + "handle": [ + { + "handler": "proxy", + "upstreams": [ + {"dial": ["10.118.2.11:31443"]}, + {"dial": ["10.118.2.12:31443"]} + ], + "health_checks": { + "active": { + "interval": "5s", + "timeout": "2s", + "port": 31443 + }, + "passive": { + "max_fails": 1, + "fail_duration": "30s" + } + }, + "load_balancing": { + "selection": { + "policy": "round_robin" + } + } + } + ] + } + ] + } + } + } + } +} + diff --git a/pkg/resource/nginx/client.conf b/pkg/resource/nginx/client.conf index 9cb206b..b03d151 100644 --- a/pkg/resource/nginx/client.conf +++ b/pkg/resource/nginx/client.conf @@ -37,6 +37,10 @@ server { proxy_pass http://u-api-service/api/v2_2/client/download/check; } + location /api/v1/version { + proxy_pass http://u-api-service/api/v2_2/client/version; + } + location /api/ { proxy_pass http://hs-client-server; proxy_http_version 1.1; @@ -140,10 +144,6 @@ server { client_max_body_size 50M; - # location /api/v1/pkg/config/setup { - # proxy_pass http://u-api-service/api/v2_2/client/download/version; - # } - location /api/v1/pkg/archive { proxy_pass http://u-api-service/api/v2_2/client/download/check; } @@ -152,18 +152,22 @@ server { proxy_pass http://u-api-service/api/v2_2/client/download/version; } + location /api/v1/version { + proxy_pass http://u-api-service/api/v2_2/client/version; + } + location /static/config/rc.json { proxy_pass http://u-api-service/api/v2_2/client/rc/json?os=win; } - location = /api/v1/version { - proxy_pass http://hs-client-without-auth-server; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $proxy_protocol_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_read_timeout 300s; - } + # location = /api/v1/version { + # proxy_pass http://hs-client-without-auth-server; + # proxy_http_version 1.1; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $proxy_protocol_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_read_timeout 300s; + # } location /api/v1/pkg { proxy_pass http://hs-client-without-auth-server; diff --git a/pkg/syscheck/syscheck.go b/pkg/syscheck/syscheck.go new file mode 100644 index 0000000..2f79dd1 --- /dev/null +++ b/pkg/syscheck/syscheck.go @@ -0,0 +1,318 @@ +package syscheck + +import ( + "context" + "fmt" + "strconv" + "strings" +) + +// CheckResult represents the result of a system check +type CheckResult struct { + Name string + Passed bool + Actual string + Expected string + Message string +} + +// DiskInfo represents disk information +type DiskInfo struct { + AvailableGB float64 + WriteSpeed float64 // MB/s + ReadSpeed float64 // MB/s +} + +// MemInfo represents memory information +type MemInfo struct { + TotalGB float64 +} + +// CPUInfo represents CPU information +type CPUInfo struct { + Cores int + FrequencyMHz float64 +} + +// CommandExecutor defines interface for executing commands +type CommandExecutor interface { + ExecuteCommand(ctx context.Context, cmds ...string) (string, error) +} + +// CheckDiskSpace checks if disk space meets minimum requirements +func CheckDiskSpace(ctx context.Context, executor CommandExecutor, minGB float64) (*CheckResult, error) { + // Use df to check available disk space on root partition + output, err := executor.ExecuteCommand(ctx, "df", "-BG", "/") + if err != nil { + return nil, fmt.Errorf("failed to check disk space: %w", err) + } + + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) < 2 { + return nil, fmt.Errorf("unexpected df output format") + } + + fields := strings.Fields(lines[1]) + if len(fields) < 4 { + return nil, fmt.Errorf("unexpected df fields count") + } + + // Parse available space (4th field, format: "500G") + availableStr := strings.TrimSuffix(fields[3], "G") + available, err := strconv.ParseFloat(availableStr, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse available disk space: %w", err) + } + + result := &CheckResult{ + Name: "Disk Space", + Passed: available >= minGB, + Actual: fmt.Sprintf("%.2f GB", available), + Expected: fmt.Sprintf(">= %.2f GB", minGB), + } + + if !result.Passed { + result.Message = fmt.Sprintf("Insufficient disk space: %.2f GB available, %.2f GB required", available, minGB) + } + + return result, nil +} + +// CheckDiskPerformance checks disk read/write performance +func CheckDiskPerformance(ctx context.Context, executor CommandExecutor, minWriteMBps, minReadMBps float64) (*CheckResult, error) { + // Use dd to test write performance + writeCmd := "dd if=/dev/zero of=/tmp/test_write bs=1M count=1024 oflag=direct 2>&1 | tail -1" + writeOutput, err := executor.ExecuteCommand(ctx, "bash", "-c", writeCmd) + if err != nil { + return nil, fmt.Errorf("failed to check disk write performance: %w", err) + } + + // Parse write speed from dd output (format: "... copied, X.XX s, XXX MB/s") + writeSpeed, err := parseDDSpeed(writeOutput) + if err != nil { + return nil, fmt.Errorf("failed to parse write speed: %w, output: %s", err, writeOutput) + } + + // Test read performance and clean up test file + readCmd := "dd if=/tmp/test_write of=/dev/null bs=1M count=1024 iflag=direct 2>&1 | tail -1; rm -f /tmp/test_write" + readOutput, err := executor.ExecuteCommand(ctx, "bash", "-c", readCmd) + if err != nil { + return nil, fmt.Errorf("failed to check disk read performance: %w", err) + } + + // Parse read speed from dd output + readSpeed, err := parseDDSpeed(readOutput) + if err != nil { + return nil, fmt.Errorf("failed to parse read speed: %w, output: %s", err, readOutput) + } + + passed := writeSpeed >= minWriteMBps && readSpeed >= minReadMBps + result := &CheckResult{ + Name: "Disk Performance", + Passed: passed, + Actual: fmt.Sprintf("Write: %.2f MB/s, Read: %.2f MB/s", writeSpeed, readSpeed), + Expected: fmt.Sprintf("Write: >= %.2f MB/s, Read: >= %.2f MB/s", minWriteMBps, minReadMBps), + } + + if !passed { + result.Message = fmt.Sprintf("Insufficient disk performance") + } + + return result, nil +} + +// parseDDSpeed parses the speed from dd command output +// Expected format: "104857600 bytes (105 MB, 100 MiB) copied, 0.125749 s, 834 MB/s" +func parseDDSpeed(output string) (float64, error) { + output = strings.TrimSpace(output) + if output == "" { + return 0, fmt.Errorf("empty output") + } + + // Find the last occurrence of "MB/s" or "GB/s" + var speed float64 + var unit string + + // Try to match "XXX MB/s" or "XXX GB/s" pattern + if idx := strings.LastIndex(output, " MB/s"); idx != -1 { + // Extract the number before " MB/s" + fields := strings.Fields(output[:idx]) + if len(fields) == 0 { + return 0, fmt.Errorf("no speed value found") + } + speedStr := fields[len(fields)-1] + var err error + speed, err = strconv.ParseFloat(speedStr, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse speed value '%s': %w", speedStr, err) + } + unit = "MB/s" + } else if idx := strings.LastIndex(output, " GB/s"); idx != -1 { + // Extract the number before " GB/s" + fields := strings.Fields(output[:idx]) + if len(fields) == 0 { + return 0, fmt.Errorf("no speed value found") + } + speedStr := fields[len(fields)-1] + var err error + speed, err = strconv.ParseFloat(speedStr, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse speed value '%s': %w", speedStr, err) + } + unit = "GB/s" + speed *= 1024 // Convert GB/s to MB/s + } else { + return 0, fmt.Errorf("no MB/s or GB/s found in output") + } + + if unit == "MB/s" || unit == "GB/s" { + return speed, nil + } + + return 0, fmt.Errorf("unexpected unit: %s", unit) +} + +// CheckMemory checks if system memory meets minimum requirements +func CheckMemory(ctx context.Context, executor CommandExecutor, minGB float64) (*CheckResult, error) { + // Use free -m to check memory in MB for better precision + output, err := executor.ExecuteCommand(ctx, "free", "-m") + if err != nil { + return nil, fmt.Errorf("failed to check memory: %w", err) + } + + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) < 2 { + return nil, fmt.Errorf("unexpected free output format") + } + + fields := strings.Fields(lines[1]) + if len(fields) < 2 { + return nil, fmt.Errorf("unexpected free fields count") + } + + // Parse total memory in MB + totalMB, err := strconv.ParseFloat(fields[1], 64) + if err != nil { + return nil, fmt.Errorf("failed to parse memory size: %w", err) + } + + // Convert MB to GB (1 GB = 1024 MB) + totalGB := totalMB / 1024.0 + + result := &CheckResult{ + Name: "Memory Size", + Passed: totalGB >= minGB, + Actual: fmt.Sprintf("%.2f GB", totalGB), + Expected: fmt.Sprintf(">= %.2f GB", minGB), + } + + if !result.Passed { + result.Message = fmt.Sprintf("Insufficient memory: %.2f GB available, %.2f GB required", totalGB, minGB) + } + + return result, nil +} + +// CheckCPUCores checks if CPU core count meets minimum requirements +func CheckCPUCores(ctx context.Context, executor CommandExecutor, minCores int) (*CheckResult, error) { + // Read /proc/cpuinfo to get CPU core count (more universal than nproc) + output, err := executor.ExecuteCommand(ctx, "cat", "/proc/cpuinfo") + if err != nil { + return nil, fmt.Errorf("failed to check CPU cores: %w", err) + } + + // Count the number of "processor" lines + cores := 0 + lines := strings.Split(output, "\n") + for _, line := range lines { + if strings.HasPrefix(strings.TrimSpace(line), "processor") { + cores++ + } + } + + if cores == 0 { + return nil, fmt.Errorf("failed to parse CPU cores from /proc/cpuinfo") + } + + result := &CheckResult{ + Name: "CPU Cores", + Passed: cores >= minCores, + Actual: fmt.Sprintf("%d cores", cores), + Expected: fmt.Sprintf(">= %d cores", minCores), + } + + if !result.Passed { + result.Message = fmt.Sprintf("Insufficient CPU cores: %d cores available, %d cores required", cores, minCores) + } + + return result, nil +} + +// CheckCPUFrequency checks if CPU frequency meets minimum requirements +func CheckCPUFrequency(ctx context.Context, executor CommandExecutor, minGHz float64) (*CheckResult, error) { + // Read /proc/cpuinfo to get CPU frequency (more universal than lscpu) + output, err := executor.ExecuteCommand(ctx, "cat", "/proc/cpuinfo") + if err != nil { + return nil, fmt.Errorf("failed to check CPU frequency: %w", err) + } + + var maxFreqMHz float64 + lines := strings.Split(output, "\n") + + // Try to parse from "cpu MHz" field (runtime frequency) + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "cpu MHz") { + fields := strings.Split(line, ":") + if len(fields) >= 2 { + freqStr := strings.TrimSpace(fields[1]) + freq, err := strconv.ParseFloat(freqStr, 64) + if err == nil && freq > maxFreqMHz { + maxFreqMHz = freq + } + } + } + } + + // If not found, try to parse from "model name" field (base frequency) + if maxFreqMHz == 0 { + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "model name") { + // Look for pattern like "@ 2.60GHz" + if idx := strings.Index(line, "@"); idx != -1 { + freqPart := line[idx+1:] + // Extract GHz value + if ghzIdx := strings.Index(freqPart, "GHz"); ghzIdx != -1 { + freqStr := strings.TrimSpace(freqPart[:ghzIdx]) + freqGHz, err := strconv.ParseFloat(freqStr, 64) + if err == nil { + maxFreqMHz = freqGHz * 1000.0 + break + } + } + } + } + } + } + + if maxFreqMHz == 0 { + return nil, fmt.Errorf("failed to parse CPU frequency from /proc/cpuinfo") + } + + freqGHz := maxFreqMHz / 1000.0 + minMHz := minGHz * 1000.0 + + result := &CheckResult{ + Name: "CPU Frequency", + Passed: maxFreqMHz >= minMHz, + Actual: fmt.Sprintf("%.2f GHz", freqGHz), + Expected: fmt.Sprintf(">= %.2f GHz", minGHz), + } + + if !result.Passed { + result.Message = fmt.Sprintf("Insufficient CPU frequency: %.2f GHz available, %.2f GHz required", freqGHz, minGHz) + } + + return result, nil +}