Skip to content

Commit

Permalink
Merge pull request kubectyl#3 from kubectyl:patch-2
Browse files Browse the repository at this point in the history
Updates
  • Loading branch information
andrei0465 authored Jun 12, 2023
2 parents 18593e1 + 9bde87c commit 506a12b
Show file tree
Hide file tree
Showing 26 changed files with 756 additions and 582 deletions.
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

0 comments on commit 506a12b

Please sign in to comment.