package cmd import ( "context" "os" "os/signal" "syscall" "time" "github.com/Neur0toxine/sshpoke/internal/api/plugin" "github.com/Neur0toxine/sshpoke/internal/api/rest" "github.com/Neur0toxine/sshpoke/internal/config" "github.com/Neur0toxine/sshpoke/internal/docker" "github.com/Neur0toxine/sshpoke/internal/logger" "github.com/Neur0toxine/sshpoke/internal/server" "github.com/Neur0toxine/sshpoke/pkg/dto" plugin2 "github.com/Neur0toxine/sshpoke/pkg/plugin" "github.com/fsnotify/fsnotify" "github.com/go-playground/validator/v10" "github.com/spf13/cobra" "github.com/spf13/viper" ) var ( cfgFile string Version string ) var rootCmd = &cobra.Command{ Use: "sshpoke", Version: Version, Short: "Expose your Docker services to the Internet via SSH.", Long: `sshpoke is a CLI application that listens to the docker socket and automatically exposes relevant services to the Internet.`, Run: func(cmd *cobra.Command, args []string) { config.Rehash() viper.WatchConfig() cancel, shutdown := initApp() viper.OnConfigChange(func(in fsnotify.Event) { if !in.Op.Has(fsnotify.Write) { return } initConfig() if !config.HasBeenUpdated() { return } config.Rehash() logger.Default.Info("configuration has been updated, restarting the app...") cancel() go server.DefaultManager.WaitForShutdown() ctx, innerCancel := context.WithTimeout(context.Background(), 2*time.Second) defer innerCancel() select { case <-ctx.Done(): } initApp() }) linuxSig := make(chan os.Signal, 1) signal.Notify(linuxSig) for sig := range linuxSig { switch sig { case os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM: shutdown(sig) default: } } shutdown(syscall.SIGHUP) }, } func initApp() (func(), func(os.Signal)) { ctx, cancel := context.WithCancel(context.Background()) server.DefaultManager = server.NewManager(ctx, config.Default.Servers, config.Default.DefaultServer) runPluginServer(ctx) runRestServer(ctx) runDockerEventListener(ctx) return cancel, makeShutdownFunc(cancel) } func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.yml", "Configuration file (default is config.yml)") } func initConfig() { if cfgFile != "" { viper.SetConfigFile(cfgFile) } else { workingDir, err := os.Getwd() cobra.CheckErr(err) viper.AddConfigPath(workingDir) viper.SetConfigType("yml") viper.SetConfigName("config") } log := logger.New(os.Getenv("SSHPOKE_DEBUG") == "true").Sugar() viper.SetEnvPrefix("SSHPOKE") viper.AutomaticEnv() if err := config.BindStructEnv(&config.Default); err != nil { log.Fatalf("cannot bind configuration keys: %s", err) } if err := viper.ReadInConfig(); err == nil { log.Debugf("using config file: %s", viper.ConfigFileUsed()) } if err := viper.Unmarshal(&config.Default); err != nil { log.Fatalf("cannot load configuration: %s", err) } if err := validator.New().Struct(config.Default); err != nil { log.Fatalf("invalid configuration: %s", err) } if config.Default.API.Rest.Port == 0 { config.Default.API.Rest.Port = rest.DefaultPort } if config.Default.API.Plugin.Port == 0 { config.Default.API.Plugin.Port = plugin2.DefaultPort } logger.Initialize() logger.Sugar.Debugw("configuration loaded", "config", config.Default) } func runPluginServer(ctx context.Context) { port := config.Default.API.Plugin.Port if port == 0 { port = plugin2.DefaultPort } go plugin.StartServer(ctx, port, logger.Sugar.With("component", "pluginServer")) } func runRestServer(ctx context.Context) { port := config.Default.API.Rest.Port if port == 0 { port = rest.DefaultPort } go rest.StartServer(ctx, config.Default.API.Rest.Token, port, logger.Sugar.With("component", "webServer"), config.Default.Debug) } func runDockerEventListener(ctx context.Context) { var err error docker.Default, err = docker.New(ctx, config.Default.Services, config.Default.DefaultServer) if err != nil { logger.Sugar.Fatalf("cannot connect to docker daemon: %s", err) } for id, item := range docker.Default.Containers() { err := server.DefaultManager.ProcessEvent(dto.Event{ Type: dto.EventStart, Container: item, }) if err != nil { logger.Sugar.Errorw("cannot expose container", "id", id, "error", err) } } events, err := docker.Default.Listen() if err != nil { logger.Sugar.Fatalf("cannot listen to docker events: %s", err) } go func() { logger.Sugar.Debug("listening for docker events...") for event := range events { err := server.DefaultManager.ProcessEvent(event) if err != nil { logger.Sugar.Errorw("cannot expose container", "id", event.Container.ID, "error", err) } } }() } func makeShutdownFunc(cancel func()) func(os.Signal) { return func(sig os.Signal) { cancel() server.DefaultManager.WaitForShutdown() logger.Sugar.Infof("received %s, exiting...", sig) os.Exit(0) } }