0c86b0a0d73bafd33be5c5c9085f8df097e8c2b0

Author
Ayman Bagabas <ayman.bagabas@gmail.com>
Committer
Ayman Bagabas <ayman.bagabas@gmail.com>
Date

Message

feat(cmd): migrate config command

Diff

This diff is truncated to protect this page.

  1diff --git a/cmd/soft/migrate_config.go b/cmd/soft/migrate_config.go
  2new file mode 100644
  3index 0000000000000000000000000000000000000000..9c4f23e3857a3177e6bb610cc3301a47d33e42e0
  4--- /dev/null
  5+++ b/cmd/soft/migrate_config.go
  6@@ -0,0 +1,431 @@
  7+package main
  8+
  9+import (
 10+	"encoding/json"
 11+	"fmt"
 12+	"io"
 13+	"os"
 14+	"path/filepath"
 15+	"strings"
 16+
 17+	"github.com/charmbracelet/log"
 18+	"github.com/charmbracelet/soft-serve/git"
 19+	"github.com/charmbracelet/soft-serve/server/backend"
 20+	"github.com/charmbracelet/soft-serve/server/backend/sqlite"
 21+	"github.com/charmbracelet/soft-serve/server/config"
 22+	"github.com/charmbracelet/soft-serve/server/utils"
 23+	"github.com/spf13/cobra"
 24+	"golang.org/x/crypto/ssh"
 25+	"gopkg.in/yaml.v3"
 26+)
 27+
 28+var (
 29+	migrateConfig = &cobra.Command{
 30+		Use:   "migrate-config",
 31+		Short: "Migrate config to new format",
 32+		RunE: func(cmd *cobra.Command, args []string) error {
 33+			keyPath := os.Getenv("SOFT_SERVE_KEY_PATH")
 34+			reposPath := os.Getenv("SOFT_SERVE_REPO_PATH")
 35+			bindAddr := os.Getenv("SOFT_SERVE_BIND_ADDRESS")
 36+			cfg := config.DefaultConfig()
 37+			sb, err := sqlite.NewSqliteBackend(cfg.DataPath)
 38+			if err != nil {
 39+				return fmt.Errorf("failed to create sqlite backend: %w", err)
 40+			}
 41+
 42+			cfg = cfg.WithBackend(sb)
 43+
 44+			// Set SSH listen address
 45+			log.Info("Setting SSH listen address...")
 46+			if bindAddr != "" {
 47+				cfg.SSH.ListenAddr = bindAddr
 48+			}
 49+
 50+			// Copy SSH host key
 51+			log.Info("Copying SSH host key...")
 52+			if keyPath != "" {
 53+				if err := os.MkdirAll(filepath.Join(cfg.DataPath, "ssh"), 0700); err != nil {
 54+					return fmt.Errorf("failed to create ssh directory: %w", err)
 55+				}
 56+
 57+				if err := copyFile(keyPath, filepath.Join(cfg.DataPath, "ssh", filepath.Base(keyPath))); err != nil {
 58+					return fmt.Errorf("failed to copy ssh key: %w", err)
 59+				}
 60+
 61+				cfg.SSH.KeyPath = filepath.Join("ssh", filepath.Base(keyPath))
 62+			}
 63+
 64+			// Read config
 65+			log.Info("Reading config repository...")
 66+			r, err := git.Open(filepath.Join(reposPath, "config"))
 67+			if err != nil {
 68+				return fmt.Errorf("failed to open config repo: %w", err)
 69+			}
 70+
 71+			head, err := r.HEAD()
 72+			if err != nil {
 73+				return fmt.Errorf("failed to get head: %w", err)
 74+			}
 75+
 76+			tree, err := r.TreePath(head, "")
 77+			if err != nil {
 78+				return fmt.Errorf("failed to get tree: %w", err)
 79+			}
 80+
 81+			isJson := false
 82+			te, err := tree.TreeEntry("config.yaml")
 83+			if err != nil {
 84+				te, err = tree.TreeEntry("config.json")
 85+				if err != nil {
 86+					return fmt.Errorf("failed to get config file: %w", err)
 87+				}
 88+				isJson = true
 89+			}
 90+
 91+			cc, err := te.Contents()
 92+			if err != nil {
 93+				return fmt.Errorf("failed to get config contents: %w", err)
 94+			}
 95+
 96+			var ocfg Config
 97+			if isJson {
 98+				if err := json.Unmarshal(cc, &ocfg); err != nil {
 99+					return fmt.Errorf("failed to unmarshal config: %w", err)
100+				}
101+			} else {
102+				if err := yaml.Unmarshal(cc, &ocfg); err != nil {
103+					return fmt.Errorf("failed to unmarshal config: %w", err)
104+				}
105+			}
106diff --git a/cmd/soft/root.go b/cmd/soft/root.go
107index fe7cb6540a8028bda78d65df29b1a1bf46b02071..5fc9e4588e6dcc9fb57169b6ea1a857c1d899c44 100644
108--- a/cmd/soft/root.go
109+++ b/cmd/soft/root.go
110@@ -32,6 +32,7 @@ func init() {
111 		serveCmd,
112 		manCmd,
113 		hookCmd,
114+		migrateConfig,
115 	)
116 	rootCmd.CompletionOptions.HiddenDefaultCmd = true
117 
118diff --git a/server/backend/backend.go b/server/backend/backend.go
119index e95e6b74a88108fdb09d8b2055cf8e06f7f01c17..ae243d1a1e6232c09f048b5d8519f9bd3268b801 100644
120--- a/server/backend/backend.go
121+++ b/server/backend/backend.go
122@@ -3,7 +3,8 @@ package backend
123 import (
124 	"bytes"
125 
126-	"golang.org/x/crypto/ssh"
127+	"github.com/charmbracelet/ssh"
128+	gossh "golang.org/x/crypto/ssh"
129 )
130 
131 // Backend is an interface that handles repositories management and any
132@@ -18,8 +19,8 @@ type Backend interface {
133 }
134 
135 // ParseAuthorizedKey parses an authorized key string into a public key.
136-func ParseAuthorizedKey(ak string) (ssh.PublicKey, string, error) {
137-	pk, c, _, _, err := ssh.ParseAuthorizedKey([]byte(ak))
138+func ParseAuthorizedKey(ak string) (gossh.PublicKey, string, error) {
139+	pk, c, _, _, err := gossh.ParseAuthorizedKey([]byte(ak))
140 	return pk, c, err
141 }
142 
143@@ -28,9 +29,14 @@ func ParseAuthorizedKey(ak string) (ssh.PublicKey, string, error) {
144 // This is the inverse of ParseAuthorizedKey.
145 // This function is a copy of ssh.MarshalAuthorizedKey, but without the trailing newline.
146 // It returns an empty string if pk is nil.
147-func MarshalAuthorizedKey(pk ssh.PublicKey) string {
148+func MarshalAuthorizedKey(pk gossh.PublicKey) string {
149 	if pk == nil {
150 		return ""
151 	}
152-	return string(bytes.TrimSuffix(ssh.MarshalAuthorizedKey(pk), []byte("\n")))
153+	return string(bytes.TrimSuffix(gossh.MarshalAuthorizedKey(pk), []byte("\n")))
154+}
155+
156+// KeysEqual returns whether the two public keys are equal.
157+func KeysEqual(a, b gossh.PublicKey) bool {
158+	return ssh.KeysEqual(a, b)
159 }
160diff --git a/server/backend/sqlite/db.go b/server/backend/sqlite/db.go
161index f3ed49d74cf459a155cb558023d682ca2b541f02..a8fe46bb499183946fbea1827cfbe51d05094c54 100644
162--- a/server/backend/sqlite/db.go
163+++ b/server/backend/sqlite/db.go
164@@ -8,7 +8,6 @@ import (
165 
166 	"github.com/charmbracelet/soft-serve/server/backend"
167 	"github.com/jmoiron/sqlx"
168-	"golang.org/x/crypto/bcrypt"
169 	"modernc.org/sqlite"
170 	sqlite3 "modernc.org/sqlite/lib"
171 )
172@@ -96,20 +95,3 @@ func rollback(tx *sqlx.Tx, err error) error {
173 
174 	return err
175 }
176-
177-func hashPassword(password string) (string, error) {
178-	hash, err := bcrypt.GenerateFromPassword([]byte(password+"soft-serve-v1"), 14)
179-	if err != nil {
180-		return "", fmt.Errorf("failed to hash password: %w", err)
181-	}
182-
183-	return string(hash), nil
184-}
185-
186-func checkPassword(hash, password string) error {
187-	if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+"soft-serve-v1")); err != nil {
188-		return fmt.Errorf("failed to check password: %w", err)
189-	}
190-
191-	return nil
192-}
193diff --git a/server/backend/sqlite/sqlite.go b/server/backend/sqlite/sqlite.go
194index 263885b492c9cb28a2e699de2d4cd0493011a7cb..eb7c3413cefdb7c443300e90d0403af17aca07af 100644
195--- a/server/backend/sqlite/sqlite.go
196+++ b/server/backend/sqlite/sqlite.go
197@@ -38,6 +38,10 @@ func (d *SqliteBackend) reposPath() string {
198 
199 // NewSqliteBackend creates a new SqliteBackend.
200 func NewSqliteBackend(dataPath string) (*SqliteBackend, error) {
201+	if err := os.MkdirAll(dataPath, 0755); err != nil {
202+		return nil, err
203+	}
204+
205 	db, err := sqlx.Connect("sqlite", filepath.Join(dataPath, "soft-serve.db"+
206 		"?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)"))
207 	if err != nil {
208@@ -164,6 +168,7 @@ func (d *SqliteBackend) ImportRepository(name string, remote string, opts backen
209 
210 	copts := git.CloneOptions{
211 		Mirror: opts.Mirror,
212+		Quiet:  true,
213 	}
214 	if err := git.Clone(remote, rp, copts); err != nil {
215 		logger.Debug("failed to clone repository", "err", err, "mirror", opts.Mirror, "remote", remote, "path", rp)
216diff --git a/server/backend/sqlite/user.go b/server/backend/sqlite/user.go
217index d03410277ae45f60457eb48521198ff34f3646a5..068c85a3a2b39251b069696c3505eaca4b2659a8 100644
218--- a/server/backend/sqlite/user.go
219+++ b/server/backend/sqlite/user.go
220@@ -168,13 +168,13 @@ func (d *SqliteBackend) CreateUser(username string, opts backend.UserOptions) (b
221 
222 	var user *User
223 	if err := wrapTx(d.db, context.Background(), func(tx *sqlx.Tx) error {
224-		into := "INSERT INTO user (username, admin"
225-		values := "VALUES (?, ?"
226-		args := []interface{}{username, opts.Admin}
227-		into += ", updated_at)"
228-		values += ", CURRENT_TIMESTAMP)"
229+		stmt, err := tx.Prepare("INSERT INTO user (username, admin, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP);")
230+		if err != nil {
231+			return err
232+		}
233 
234-		r, err := tx.Exec(into+" "+values, args...)
235+		defer stmt.Close() // nolint: errcheck
236+		r, err := stmt.Exec(username, opts.Admin)
237 		if err != nil {
238 			return err
239 		}
240@@ -182,12 +182,19 @@ func (d *SqliteBackend) CreateUser(username string, opts backend.UserOptions) (b
241 		if len(opts.PublicKeys) > 0 {
242 			userID, err := r.LastInsertId()
243 			if err != nil {
244+				logger.Error("error getting last insert id")
245 				return err
246 			}
247 
248 			for _, pk := range opts.PublicKeys {
249-				if _, err := tx.Exec(`INSERT INTO public_key (user_id, public_key, updated_at)
250-					VALUES (?, ?, CURRENT_TIMESTAMP);`, userID, backend.MarshalAuthorizedKey(pk)); err != nil {
251+				stmt, err := tx.Prepare(`INSERT INTO public_key (user_id, public_key, updated_at)
252+					VALUES (?, ?, CURRENT_TIMESTAMP);`)
253+				if err != nil {
254+					return err
255+				}
256+
257+				defer stmt.Close() // nolint: errcheck
258+				if _, err := stmt.Exec(userID, backend.MarshalAuthorizedKey(pk)); err != nil {
259 					return err
260 				}
261 			}