diff --git a/cmd/dashboard.go b/cmd/dashboard.go new file mode 100644 index 00000000..cb29d5a5 --- /dev/null +++ b/cmd/dashboard.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "path/filepath" + "strconv" + + "github.com/cnrancher/autok3s/pkg/common" + "github.com/cnrancher/autok3s/pkg/settings" + k3dutil "github.com/k3d-io/k3d/v5/cmd/util" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "k8s.io/client-go/tools/clientcmd" +) + +var ( + dashboardCmd = &cobra.Command{ + Use: "helm-dashboard", + Short: "Enable helm-dashboard for K3s cluster", + Example: "autok3s helm-dashboard", + } + + dashboardPort = 0 +) + +func init() { + dashboardCmd.Flags().IntVarP(&dashboardPort, "port", "", dashboardPort, "Set http port for helm-dashboard") +} + +// DashboardCommand will start a helm-dashboard server for specified K3s cluster +func DashboardCommand() *cobra.Command { + dashboardCmd.PreRunE = func(cmd *cobra.Command, args []string) error { + cfg, err := clientcmd.LoadFromFile(filepath.Join(common.CfgPath, common.KubeCfgFile)) + if err != nil { + return err + } + if len(cfg.Contexts) == 0 { + logrus.Fatalln("cannot enable helm dashboard without K3s cluster") + } + return nil + } + dashboardCmd.Run = func(cmd *cobra.Command, args []string) { + if err := common.CheckCommandExist(common.HelmDashboardCommand); err != nil { + logrus.Fatalln(err) + } + if dashboardPort == 0 { + port, err := k3dutil.GetFreePort() + if err != nil { + logrus.Fatalf("failed to get free port for kube-explorer: %v", err) + } + dashboardPort = port + } + if err := settings.HelmDashboardPort.Set(strconv.Itoa(dashboardPort)); err != nil { + logrus.Fatalln(err) + } + err := common.StartHelmDashboard(dashboardCmd.Context(), strconv.Itoa(dashboardPort)) + if err != nil { + logrus.Fatalln(err) + } + logrus.Infof("Helm dashboard started with 127.0.0.1:%d", dashboardPort) + } + + return dashboardCmd +} diff --git a/cmd/serve.go b/cmd/serve.go index 395e6df8..823fe1f8 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -39,6 +39,10 @@ func ServeCommand() *cobra.Command { go func(ctx context.Context) { common.InitExplorer(ctx) }(serveCmd.Context()) + // start helm-dashboard server + go func(ctx context.Context) { + common.InitDashboard(ctx) + }(serveCmd.Context()) stopChan := make(chan struct{}) go func(c chan struct{}) { diff --git a/main.go b/main.go index 3bfbd62f..8c6687e9 100644 --- a/main.go +++ b/main.go @@ -44,7 +44,7 @@ func main() { rootCmd.AddCommand(cmd.CompletionCommand(), cmd.VersionCommand(gitVersion, gitCommit, gitTreeState, buildDate), cmd.ListCommand(), cmd.CreateCommand(), cmd.JoinCommand(), cmd.KubectlCommand(), cmd.DeleteCommand(), cmd.SSHCommand(), cmd.DescribeCommand(), cmd.ServeCommand(), cmd.ExplorerCommand(), cmd.UpgradeCommand(), - cmd.TelemetryCommand(), airgap.Command(), sshkey.Command()) + cmd.TelemetryCommand(), airgap.Command(), sshkey.Command(), cmd.DashboardCommand()) rootCmd.PersistentPreRun = func(c *cobra.Command, args []string) { common.InitLogger(logrus.StandardLogger()) diff --git a/pkg/common/common.go b/pkg/common/common.go index fb2f51b8..1aba6588 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -61,8 +61,9 @@ var ( Steps: 20, } // DefaultDB default database store. - DefaultDB *Store - ExplorerWatchers map[string]context.CancelFunc + DefaultDB *Store + ExplorerWatchers map[string]context.CancelFunc + DashboardCanceled context.CancelFunc FileManager *ConfigFileManager ) diff --git a/pkg/common/dashboard.go b/pkg/common/dashboard.go new file mode 100644 index 00000000..3bf79637 --- /dev/null +++ b/pkg/common/dashboard.go @@ -0,0 +1,120 @@ +package common + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/cnrancher/autok3s/pkg/settings" + k3dutil "github.com/k3d-io/k3d/v5/cmd/util" + "github.com/sirupsen/logrus" + + "k8s.io/client-go/tools/clientcmd" +) + +const HelmDashboardCommand = "helm-dashboard" + +func SwitchDashboard(ctx context.Context, enabled string) error { + if enabled == "true" { + // check if cluster list is empty + clusters, err := DefaultDB.ListCluster("") + if err != nil { + return err + } + if len(clusters) == 0 { + return errors.New("cannot enable helm-dashboard with empty cluster list") + } + } + if err := CheckCommandExist(HelmDashboardCommand); err != nil { + return err + } + + // command execution validate + if err := checkDashboardCmd(); err != nil { + return err + } + + isEnabled := settings.HelmDashboardEnabled.Get() + if !strings.EqualFold(isEnabled, enabled) { + if err := settings.HelmDashboardEnabled.Set(enabled); err != nil { + return err + } + if enabled == "true" { + enableDashboard(ctx) + } else { + if err := settings.HelmDashboardEnabled.Set("false"); err != nil { + return err + } + logrus.Info("Shutting down helm-dashboard...") + DashboardCanceled() + } + } + + return nil +} + +func InitDashboard(ctx context.Context) { + isEnabled := settings.HelmDashboardEnabled.Get() + if isEnabled == "true" { + enableDashboard(ctx) + } +} + +func StartHelmDashboard(ctx context.Context, port string) error { + _ = os.Setenv(clientcmd.RecommendedConfigPathEnvVar, filepath.Join(CfgPath, KubeCfgFile)) + dashboard := exec.CommandContext(ctx, HelmDashboardCommand, "-b", fmt.Sprintf("--port=%s", port)) + dashboard.Stdout = os.Stdout + dashboard.Stderr = os.Stderr + if err := dashboard.Start(); err != nil { + logrus.Errorf("fail to start helm-dashboard: %v", err) + } + logrus.Infof("helm-dashboard will listen on 127.0.0.1:%s ...", port) + return dashboard.Wait() +} + +func checkDashboardCmd() error { + explorerVersion := exec.Command(HelmDashboardCommand, "--version") + return explorerVersion.Run() +} + +func enableDashboard(ctx context.Context) { + clusters, err := DefaultDB.ListCluster("") + if err != nil { + logrus.Errorf("failed to list clusters: %v", err) + return + } + // disable helm-dashboard if there's no cluster context + if len(clusters) == 0 { + if err := settings.HelmDashboardEnabled.Set("false"); err != nil { + logrus.Errorf("failed to disable helm-dashboard due to empty cluster list: %v", err) + return + } + logrus.Warn("disabled helm-dashboard with empty cluster list") + return + } + dashboardPort := settings.HelmDashboardPort.Get() + if dashboardPort == "" { + freePort, err := k3dutil.GetFreePort() + if err != nil { + logrus.Errorf("failed to get free port for helm-dashboard: %v", err) + return + } + dashboardPort = strconv.Itoa(freePort) + err = settings.HelmDashboardPort.Set(dashboardPort) + if err != nil { + logrus.Errorf("failed to save helm-dashboard port to settings: %v", err) + return + } + } + logrus.Info("Enable helm-dashboard server...") + dashboardCtx, cancel := context.WithCancel(ctx) + DashboardCanceled = cancel + go func(ctx context.Context, port string) { + StartHelmDashboard(ctx, port) + }(dashboardCtx, dashboardPort) +} diff --git a/pkg/server/store/settings/store.go b/pkg/server/store/settings/store.go index 6b2fa1ee..6eb7d50e 100644 --- a/pkg/server/store/settings/store.go +++ b/pkg/server/store/settings/store.go @@ -1,9 +1,11 @@ package settings import ( + "context" "fmt" "github.com/cnrancher/autok3s/pkg/common" + "github.com/cnrancher/autok3s/pkg/settings" "github.com/rancher/apiserver/pkg/apierror" "github.com/rancher/apiserver/pkg/store/empty" @@ -24,10 +26,17 @@ func (s *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data ty if err != nil { return types.APIObject{}, err } - err = common.DefaultDB.SaveSetting(setting) - if err != nil { - return types.APIObject{}, err + if id == settings.HelmDashboardEnabled.Name { + if err := common.SwitchDashboard(context.TODO(), setting.Value); err != nil { + return types.APIObject{}, err + } + } else { + err = common.DefaultDB.SaveSetting(setting) + if err != nil { + return types.APIObject{}, err + } } + return s.ByID(apiOp, schema, setting.Name) } diff --git a/pkg/settings/setting.go b/pkg/settings/setting.go index c436d947..9ae4df59 100644 --- a/pkg/settings/setting.go +++ b/pkg/settings/setting.go @@ -31,6 +31,9 @@ var ( InstallScript = newSetting("install-script", "", "The k3s offline install script with base64 encode") ScriptUpdateSource = newSetting("install-script-source-repo", "https://rancher-mirror.rancher.cn/k3s/k3s-install.sh", "The install script auto update source, github or aliyun oss") PackageDownloadSource = newSetting("package-download-source", "github", "The airgap package download source, github and aliyunoss are validated.") + + HelmDashboardEnabled = newSetting("helm-dashboard-enabled", "false", "The helm-dashboard is enabled or not") + HelmDashboardPort = newSetting("helm-dashboard-port", "", "The helm-dashboard server port after enabled") ) func newSetting(