Skip to content
This repository was archived by the owner on Apr 27, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 68 additions & 40 deletions cmd/diagnostics.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package cmd

import (
"bytes"
"errors"
"fmt"
"io"
"math/rand"
"mime/multipart"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"strconv"
Expand All @@ -28,15 +30,15 @@ import (
)

const (
DefaultHastebinUrl = "https://ptero.co"
DefaultPastebinUrl = "https://pb.kubectyl.org"
DefaultLogLines = 200
)

var diagnosticsArgs struct {
IncludeEndpoints bool
IncludeLogs bool
ReviewBeforeUpload bool
HastebinURL string
PastebinURL string
LogLines int
}

Expand All @@ -51,7 +53,7 @@ func newDiagnosticsCommand() *cobra.Command {
Run: diagnosticsCmdRun,
}

command.Flags().StringVar(&diagnosticsArgs.HastebinURL, "hastebin-url", DefaultHastebinUrl, "the url of the hastebin instance to use")
command.Flags().StringVar(&diagnosticsArgs.PastebinURL, "hastebin-url", DefaultPastebinUrl, "the url of the hastebin instance to use")
command.Flags().IntVar(&diagnosticsArgs.LogLines, "log-lines", DefaultLogLines, "the number of log lines to include in the report")

return command
Expand All @@ -77,7 +79,7 @@ func diagnosticsCmdRun(*cobra.Command, []string) {
{
Name: "ReviewBeforeUpload",
Prompt: &survey.Confirm{
Message: "Do you want to review the collected data before uploading to " + diagnosticsArgs.HastebinURL + "?",
Message: "Do you want to review the collected data before uploading to " + diagnosticsArgs.PastebinURL + "?",
Help: "The data, especially the logs, might contain sensitive information, so you should review it. You will be asked again if you want to upload.",
Default: true,
},
Expand Down Expand Up @@ -112,27 +114,17 @@ func diagnosticsCmdRun(*cobra.Command, []string) {
{"Logs Directory", cfg.System.LogDirectory},
{"Data Directory", cfg.System.Data},
{"Archive Directory", cfg.System.ArchiveDirectory},
{"Backup Directory", cfg.System.BackupDirectory},

{"Username", cfg.System.Username},
{"Server Time", time.Now().Format(time.RFC1123Z)},
{"Debug Mode", fmt.Sprintf("%t", cfg.Debug)},
}

table := tablewriter.NewWriter(os.Stdout)
table := tablewriter.NewWriter(output)
table.SetHeader([]string{"Variable", "Value"})
table.SetRowLine(true)
table.AppendBulk(data)
table.Render()

printHeader(output, "Docker: Running Containers")
c := exec.Command("docker", "ps")
if co, err := c.Output(); err == nil {
output.Write(co)
} else {
fmt.Fprint(output, "Couldn't list containers: ", err)
}

printHeader(output, "Latest Kuber Logs")
if diagnosticsArgs.IncludeLogs {
p := "/var/log/kubectyl/kuber.log"
Expand Down Expand Up @@ -162,16 +154,38 @@ func diagnosticsCmdRun(*cobra.Command, []string) {
fmt.Println(output.String())
fmt.Print("--------------- end of report ---------------\n\n")

// upload := !diagnosticsArgs.ReviewBeforeUpload
// if !upload {
// survey.AskOne(&survey.Confirm{Message: "Upload to " + diagnosticsArgs.HastebinURL + "?", Default: false}, &upload)
// }
// if upload {
// u, err := uploadToHastebin(diagnosticsArgs.HastebinURL, output.String())
// if err == nil {
// fmt.Println("Your report is available here: ", u)
// }
// }
upload := !diagnosticsArgs.ReviewBeforeUpload
if !upload {
survey.AskOne(&survey.Confirm{Message: "Upload to " + diagnosticsArgs.PastebinURL + "?", Default: false}, &upload)
}
if upload {
passwordFunc := func(length int) string {
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
password := make([]byte, length)

for i := 0; i < length; i++ {
password[i] = charset[rand.Intn(len(charset))]
}

return string(password)
}
rand.Seed(time.Now().UnixNano())

password := passwordFunc(8)
result, err := uploadToPastebin(diagnosticsArgs.PastebinURL, output.String(), password)
if err == nil {
seconds, err := strconv.Atoi(fmt.Sprintf("%v", result["expire"]))
if err != nil {
return
}

expireTime := fmt.Sprintf("%d hours, %d minutes, %d seconds", seconds/3600, (seconds%3600)/60, seconds%60)

fmt.Println("Your report is available here:", result["url"])
fmt.Println("Will expire in", expireTime)
fmt.Printf("You can edit your pastebin here: %s\n", result["admin"])
}
}
}

// func getDockerInfo() (types.Version, types.Info, error) {
Expand All @@ -190,31 +204,45 @@ func diagnosticsCmdRun(*cobra.Command, []string) {
// return dockerVersion, dockerInfo, nil
// }

func uploadToHastebin(hbUrl, content string) (string, error) {
r := strings.NewReader(content)
u, err := url.Parse(hbUrl)
func uploadToPastebin(pbURL, content, password string) (map[string]interface{}, error) {
payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload)
writer.WriteField("c", content)
writer.WriteField("e", "300")
writer.WriteField("s", password)
writer.Close()

u, err := url.Parse(pbURL)
if err != nil {
return "", err
return nil, err
}
u.Path = path.Join(u.Path, "documents")
res, err := http.Post(u.String(), "plain/text", r)

req, err := http.NewRequest("POST", u.String(), payload)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", writer.FormDataContentType())

client := &http.Client{}
res, err := client.Do(req)
if err != nil || res.StatusCode != 200 {
fmt.Println("Failed to upload report to ", u.String(), err)
return "", err
fmt.Println("Failed to upload report to", u.String(), err)
return nil, err
}

pres := make(map[string]interface{})
body, err := io.ReadAll(res.Body)
if err != nil {
fmt.Println("Failed to parse response.", err)
return "", err
return nil, err
}
json.Unmarshal(body, &pres)
if key, ok := pres["key"].(string); ok {
u, _ := url.Parse(hbUrl)
u.Path = path.Join(u.Path, key)
return u.String(), nil
if key, ok := pres["url"].(string); ok {
u.Path = key
return pres, nil
}
return "", errors.New("failed to find key in response")

return nil, errors.New("failed to find key in response")
}

func redact(s string) string {
Expand Down
18 changes: 4 additions & 14 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,6 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
if err := environment.CreateSftpSecret(); err != nil {
log.WithField("error", err).Fatal("failed to create sftp secret")
}
if err := config.EnsureKubectylUser(); err != nil {
log.WithField("error", err).Fatal("failed to create kubectyl system user")
}
log.WithFields(log.Fields{
"username": config.Get().System.Username,
"uid": config.Get().System.User.Uid,
"gid": config.Get().System.User.Gid,
}).Info("configured system user successfully")
if err := config.EnableLogRotation(); err != nil {
log.WithField("error", err).Fatal("failed to configure log rotation on the system")
return
Expand Down Expand Up @@ -256,7 +248,10 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {

// we use goroutine to avoid blocking whole process
go func() {
if err := s.Environment.CreateSFTP(s.Context()); err != nil {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

if err := s.Environment.CreateSFTP(ctx, cancel); err != nil {
log.WithField("error", err).Warn("failed to create server SFTP pod")
}
}()
Expand Down Expand Up @@ -303,11 +298,6 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
log.WithField("error", err).Error("failed to create archive directory")
}

// Ensure the backup directory exists.
if err := os.MkdirAll(sys.BackupDirectory, 0o755); err != nil {
log.WithField("error", err).Error("failed to create backup directory")
}

autotls, _ := cmd.Flags().GetBool("auto-tls")
tlshostname, _ := cmd.Flags().GetString("tls-hostname")
if autotls && tlshostname == "" {
Expand Down
110 changes: 1 addition & 109 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ import (
"fmt"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"text/template"
"time"
Expand All @@ -21,8 +19,6 @@ import (
"github.com/creasty/defaults"
"github.com/gbrlsnchs/jwt/v3"
"gopkg.in/yaml.v2"

"github.com/kubectyl/kuber/system"
)

const DefaultLocation = "/etc/kubectyl/config.yml"
Expand Down Expand Up @@ -134,44 +130,17 @@ type SystemConfiguration struct {
// Directory where server archives for transferring will be stored.
ArchiveDirectory string `default:"/var/lib/kubectyl/archives" yaml:"archive_directory"`

// Directory where local backups will be stored on the machine.
BackupDirectory string `default:"/var/lib/kubectyl/backups" yaml:"backup_directory"`

// TmpDirectory specifies where temporary files for Kubectyl installation processes
// should be created. This supports environments running docker-in-docker.
TmpDirectory string `default:"/tmp/kubectyl" yaml:"tmp_directory"`

// The user that should own all of the server files, and be used for containers.
Username string `default:"kubectyl" yaml:"username"`

// The timezone for this Kuber instance. This is detected by Kuber automatically if possible,
// and falls back to UTC if not able to be detected. If you need to set this manually, that
// can also be done.
//
// This timezone value is passed into all containers created by Kuber.
Timezone string `yaml:"timezone"`

// Definitions for the user that gets created to ensure that we can quickly access
// this information without constantly having to do a system lookup.
User struct {
// Rootless controls settings related to rootless container daemons.
Rootless struct {
// Enabled controls whether rootless containers are enabled.
Enabled bool `yaml:"enabled" default:"false"`
// ContainerUID controls the UID of the user inside the container.
// This should likely be set to 0 so the container runs as the user
// running Kuber.
ContainerUID int `yaml:"container_uid" default:"0"`
// ContainerGID controls the GID of the user inside the container.
// This should likely be set to 0 so the container runs as the user
// running Kuber.
ContainerGID int `yaml:"container_gid" default:"0"`
} `yaml:"rootless"`

Uid int `yaml:"uid"`
Gid int `yaml:"gid"`
} `yaml:"user"`

// The amount of time in seconds that can elapse before a server's disk space calculation is
// considered stale and a re-check should occur. DANGER: setting this value too low can seriously
// impact system performance and cause massive I/O bottlenecks and high CPU usage for the Kuber
Expand Down Expand Up @@ -202,7 +171,7 @@ type SystemConfiguration struct {
EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"`

// The number of lines to send when a server connects to the websocket.
WebsocketLogCount int `default:"150" yaml:"websocket_log_count"`
WebsocketLogCount int64 `default:"150" yaml:"websocket_log_count"`

Sftp SftpConfiguration `yaml:"sftp"`

Expand Down Expand Up @@ -422,78 +391,6 @@ func WriteToDisk(c *Configuration) error {
return nil
}

// EnsureKubectylUser ensures that the Kubectyl core user exists on the
// system. This user will be the owner of all data in the root data directory
// and is used as the user within containers. If files are not owned by this
// user there will be issues with permissions on Docker mount points.
//
// This function IS NOT thread safe and should only be called in the main thread
// when the application is booting.
func EnsureKubectylUser() error {
sysName, err := getSystemName()
if err != nil {
return err
}

// Our way of detecting if kuber is running inside of Docker.
if sysName == "distroless" {
_config.System.Username = system.FirstNotEmpty(os.Getenv("KUBER_USERNAME"), "kubectyl")
_config.System.User.Uid = system.MustInt(system.FirstNotEmpty(os.Getenv("KUBER_UID"), "988"))
_config.System.User.Gid = system.MustInt(system.FirstNotEmpty(os.Getenv("KUBER_GID"), "988"))
return nil
}

if _config.System.User.Rootless.Enabled {
log.Info("rootless mode is enabled, skipping user creation...")
u, err := user.Current()
if err != nil {
return err
}
_config.System.Username = u.Username
_config.System.User.Uid = system.MustInt(u.Uid)
_config.System.User.Gid = system.MustInt(u.Gid)
return nil
}

log.WithField("username", _config.System.Username).Info("checking for kubectyl system user")
u, err := user.Lookup(_config.System.Username)
// If an error is returned but it isn't the unknown user error just abort
// the process entirely. If we did find a user, return it immediately.
if err != nil {
if _, ok := err.(user.UnknownUserError); !ok {
return err
}
} else {
_config.System.User.Uid = system.MustInt(u.Uid)
_config.System.User.Gid = system.MustInt(u.Gid)
return nil
}

command := fmt.Sprintf("useradd --system --no-create-home --shell /usr/sbin/nologin %s", _config.System.Username)
// Alpine Linux is the only OS we currently support that doesn't work with the useradd
// command, so in those cases we just modify the command a bit to work as expected.
if strings.HasPrefix(sysName, "alpine") {
command = fmt.Sprintf("adduser -S -D -H -G %[1]s -s /sbin/nologin %[1]s", _config.System.Username)
// We have to create the group first on Alpine, so do that here before continuing on
// to the user creation process.
if _, err := exec.Command("addgroup", "-S", _config.System.Username).Output(); err != nil {
return err
}
}

split := strings.Split(command, " ")
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
return err
}
u, err = user.Lookup(_config.System.Username)
if err != nil {
return err
}
_config.System.User.Uid = system.MustInt(u.Uid)
_config.System.User.Gid = system.MustInt(u.Gid)
return nil
}

// FromFile reads the configuration from the provided file and stores it in the
// global singleton for this instance.
func FromFile(path string) error {
Expand Down Expand Up @@ -553,11 +450,6 @@ func ConfigureDirectories() error {
return err
}

log.WithField("path", _config.System.BackupDirectory).Debug("ensuring backup data directory exists")
if err := os.MkdirAll(_config.System.BackupDirectory, 0o700); err != nil {
return err
}

return nil
}

Expand Down
Loading