repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
f23dc2b
parent
0c5fc9c
author
Eric Bower
date
2024-04-13 14:24:07 +0000 UTC
feat(imgs): ssh cli with ls and rm commands
7 files changed,  +441, -140
M Makefile
+8, -0
 1@@ -140,3 +140,11 @@ restore:
 2 	$(DOCKER_CMD) exec -it $(DB_CONTAINER) /bin/bash
 3 	# psql postgres -U postgres -d pico < /backup.sql
 4 .PHONY: restore
 5+
 6+registry-clean:
 7+	# https://github.com/distribution/distribution/issues/3200#issuecomment-671062638
 8+	# NOTICE: if using s3 you need an empty file inside:
 9+	#   - `imgs/docker/registry/v2/repositories` and
10+	#   - `imgs/docker/registry/v2/blobs`
11+	docker compose exec registry bin/registry garbage-collect /etc/docker/registry/config.yml --delete-untagged
12+.PHONY: registry-clean
M cmd/scripts/clean-object-store/clean.go
+1, -1
1@@ -102,7 +102,7 @@ func main() {
2 		}
3 	}
4 
5-	session := &pgs.CmdSessionLogger{
6+	session := &shared.CmdSessionLogger{
7 		Log: logger,
8 	}
9 
M docker-compose.yml
+2, -0
1@@ -23,6 +23,8 @@ services:
2     environment:
3       REGISTRY_STORAGE_S3_ACCESSKEY: ${MINIO_ROOT_USER}
4       REGISTRY_STORAGE_S3_SECRETKEY: ${MINIO_ROOT_PASSWORD}
5+    links:
6+      - minio
7   imgproxy:
8     image: darthsim/imgproxy:latest
9     restart: always
M imgs/ssh.go
+129, -107
  1@@ -24,6 +24,7 @@ import (
  2 	"github.com/picosh/pico/db"
  3 	"github.com/picosh/pico/db/postgres"
  4 	"github.com/picosh/pico/shared"
  5+	"github.com/picosh/pico/shared/storage"
  6 	"github.com/picosh/ptun"
  7 )
  8 
  9@@ -72,152 +73,154 @@ func (e *ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 10 	http.Error(w, e.Err.Error(), http.StatusInternalServerError)
 11 }
 12 
 13-func serveMux(ctx ssh.Context) http.Handler {
 14-	router := http.NewServeMux()
 15+func createServeMux(handler *CliHandler) func(ctx ssh.Context) http.Handler {
 16+	return func(ctx ssh.Context) http.Handler {
 17+		router := http.NewServeMux()
 18 
 19-	slug := ""
 20-	user, err := getUserCtx(ctx)
 21-	if err == nil && user != nil {
 22-		slug = user.Name
 23-	}
 24-
 25-	proxy := httputil.NewSingleHostReverseProxy(&url.URL{
 26-		Scheme: "http",
 27-		Host:   "registry:5000",
 28-	})
 29-
 30-	oldDirector := proxy.Director
 31-
 32-	proxy.Director = func(r *http.Request) {
 33-		log.Printf("%+v", r)
 34-		oldDirector(r)
 35-
 36-		if strings.HasSuffix(r.URL.Path, "_catalog") || r.URL.Path == "/v2" || r.URL.Path == "/v2/" {
 37-			return
 38+		slug := ""
 39+		user, err := getUserCtx(ctx)
 40+		if err == nil && user != nil {
 41+			slug = user.Name
 42 		}
 43 
 44-		fullPath := strings.TrimPrefix(r.URL.Path, "/v2")
 45+		proxy := httputil.NewSingleHostReverseProxy(&url.URL{
 46+			Scheme: "http",
 47+			Host:   handler.RegistryUrl,
 48+		})
 49 
 50-		newPath, err := url.JoinPath("/v2", slug, fullPath)
 51-		if err != nil {
 52-			return
 53-		}
 54+		oldDirector := proxy.Director
 55 
 56-		r.URL.Path = newPath
 57+		proxy.Director = func(r *http.Request) {
 58+			log.Printf("%+v", r)
 59+			oldDirector(r)
 60 
 61-		query := r.URL.Query()
 62+			if strings.HasSuffix(r.URL.Path, "_catalog") || r.URL.Path == "/v2" || r.URL.Path == "/v2/" {
 63+				return
 64+			}
 65 
 66-		if query.Has("from") {
 67-			joinedFrom, err := url.JoinPath(slug, query.Get("from"))
 68+			fullPath := strings.TrimPrefix(r.URL.Path, "/v2")
 69+
 70+			newPath, err := url.JoinPath("/v2", slug, fullPath)
 71 			if err != nil {
 72 				return
 73 			}
 74-			query.Set("from", joinedFrom)
 75 
 76-			r.URL.RawQuery = query.Encode()
 77-		}
 78+			r.URL.Path = newPath
 79 
 80-		log.Printf("%+v", r)
 81-	}
 82+			query := r.URL.Query()
 83 
 84-	proxy.ModifyResponse = func(r *http.Response) error {
 85-		log.Printf("%+v", r)
 86-		shared.CorsHeaders(r.Header)
 87+			if query.Has("from") {
 88+				joinedFrom, err := url.JoinPath(slug, query.Get("from"))
 89+				if err != nil {
 90+					return
 91+				}
 92+				query.Set("from", joinedFrom)
 93 
 94-		if slug != "" && r.Request.Method == http.MethodGet && strings.HasSuffix(r.Request.URL.Path, "_catalog") {
 95-			b, err := io.ReadAll(r.Body)
 96-			if err != nil {
 97-				return err
 98+				r.URL.RawQuery = query.Encode()
 99 			}
100 
101-			err = r.Body.Close()
102-			if err != nil {
103-				return err
104-			}
105+			log.Printf("%+v", r)
106+		}
107 
108-			var data map[string]any
109-			err = json.Unmarshal(b, &data)
110-			if err != nil {
111-				return err
112-			}
113+		proxy.ModifyResponse = func(r *http.Response) error {
114+			log.Printf("%+v", r)
115+			shared.CorsHeaders(r.Header)
116 
117-			newRepos := []string{}
118+			if r.Request.Method == http.MethodGet && strings.HasSuffix(r.Request.URL.Path, "_catalog") {
119+				b, err := io.ReadAll(r.Body)
120+				if err != nil {
121+					return err
122+				}
123 
124-			if repos, ok := data["repositories"].([]any); ok {
125-				for _, repo := range repos {
126-					if repoStr, ok := repo.(string); ok && strings.HasPrefix(repoStr, slug) {
127-						newRepos = append(newRepos, strings.Replace(repoStr, fmt.Sprintf("%s/", slug), "", 1))
128+				err = r.Body.Close()
129+				if err != nil {
130+					return err
131+				}
132+
133+				var data map[string]any
134+				err = json.Unmarshal(b, &data)
135+				if err != nil {
136+					return err
137+				}
138+
139+				newRepos := []string{}
140+
141+				if repos, ok := data["repositories"].([]any); ok {
142+					for _, repo := range repos {
143+						if repoStr, ok := repo.(string); ok && strings.HasPrefix(repoStr, slug) {
144+							newRepos = append(newRepos, strings.Replace(repoStr, fmt.Sprintf("%s/", slug), "", 1))
145+						}
146 					}
147 				}
148-			}
149 
150-			data["repositories"] = newRepos
151+				data["repositories"] = newRepos
152 
153-			newB, err := json.Marshal(data)
154-			if err != nil {
155-				return err
156-			}
157+				newB, err := json.Marshal(data)
158+				if err != nil {
159+					return err
160+				}
161 
162-			jsonBuf := bytes.NewBuffer(newB)
163+				jsonBuf := bytes.NewBuffer(newB)
164 
165-			r.ContentLength = int64(jsonBuf.Len())
166-			r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
167-			r.Body = io.NopCloser(jsonBuf)
168-		}
169+				r.ContentLength = int64(jsonBuf.Len())
170+				r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
171+				r.Body = io.NopCloser(jsonBuf)
172+			}
173 
174-		if slug != "" && r.Request.Method == http.MethodGet && (strings.Contains(r.Request.URL.Path, "/tags/") || strings.Contains(r.Request.URL.Path, "/manifests/")) {
175-			splitPath := strings.Split(r.Request.URL.Path, "/")
176+			if r.Request.Method == http.MethodGet && (strings.Contains(r.Request.URL.Path, "/tags/") || strings.Contains(r.Request.URL.Path, "/manifests/")) {
177+				splitPath := strings.Split(r.Request.URL.Path, "/")
178 
179-			if len(splitPath) > 1 {
180-				ele := splitPath[len(splitPath)-2]
181-				if ele == "tags" || ele == "manifests" {
182-					b, err := io.ReadAll(r.Body)
183-					if err != nil {
184-						return err
185-					}
186+				if len(splitPath) > 1 {
187+					ele := splitPath[len(splitPath)-2]
188+					if ele == "tags" || ele == "manifests" {
189+						b, err := io.ReadAll(r.Body)
190+						if err != nil {
191+							return err
192+						}
193 
194-					err = r.Body.Close()
195-					if err != nil {
196-						return err
197-					}
198+						err = r.Body.Close()
199+						if err != nil {
200+							return err
201+						}
202 
203-					var data map[string]any
204-					err = json.Unmarshal(b, &data)
205-					if err != nil {
206-						return err
207-					}
208+						var data map[string]any
209+						err = json.Unmarshal(b, &data)
210+						if err != nil {
211+							return err
212+						}
213 
214-					if name, ok := data["name"].(string); ok {
215-						if strings.HasPrefix(name, slug) {
216-							data["name"] = strings.Replace(name, fmt.Sprintf("%s/", slug), "", 1)
217+						if name, ok := data["name"].(string); ok {
218+							if strings.HasPrefix(name, slug) {
219+								data["name"] = strings.Replace(name, fmt.Sprintf("%s/", slug), "", 1)
220+							}
221 						}
222-					}
223 
224-					newB, err := json.Marshal(data)
225-					if err != nil {
226-						return err
227-					}
228+						newB, err := json.Marshal(data)
229+						if err != nil {
230+							return err
231+						}
232 
233-					jsonBuf := bytes.NewBuffer(newB)
234+						jsonBuf := bytes.NewBuffer(newB)
235 
236-					r.ContentLength = int64(jsonBuf.Len())
237-					r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
238-					r.Body = io.NopCloser(jsonBuf)
239+						r.ContentLength = int64(jsonBuf.Len())
240+						r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
241+						r.Body = io.NopCloser(jsonBuf)
242+					}
243 				}
244 			}
245-		}
246 
247-		locationHeader := r.Header.Get("location")
248-		if slug != "" && strings.Contains(locationHeader, fmt.Sprintf("/v2/%s", slug)) {
249-			r.Header.Set("location", strings.ReplaceAll(locationHeader, fmt.Sprintf("/v2/%s", slug), "/v2"))
250-		}
251+			locationHeader := r.Header.Get("location")
252+			if strings.Contains(locationHeader, fmt.Sprintf("/v2/%s", slug)) {
253+				r.Header.Set("location", strings.ReplaceAll(locationHeader, fmt.Sprintf("/v2/%s", slug), "/v2"))
254+			}
255 
256-		return nil
257-	}
258+			return nil
259+		}
260 
261-	router.HandleFunc("/", proxy.ServeHTTP)
262+		router.HandleFunc("/", proxy.ServeHTTP)
263 
264-	return router
265+		return router
266+	}
267 }
268 
269 func StartSshServer() {
270@@ -230,14 +233,33 @@ func StartSshServer() {
271 		port = "2222"
272 	}
273 	dbUrl := os.Getenv("DATABASE_URL")
274+	registryUrl := shared.GetEnv("REGISTRY_URL", "registry:5000")
275+	minioUrl := shared.GetEnv("MINIO_URL", "")
276+	minioUser := shared.GetEnv("MINIO_ROOT_USER", "")
277+	minioPass := shared.GetEnv("MINIO_ROOT_PASSWORD", "")
278+
279 	logger := slog.Default()
280 	dbh := postgres.NewDB(dbUrl, logger)
281+	st, err := storage.NewStorageMinio(minioUrl, minioUser, minioPass)
282+	if err != nil {
283+		panic(err)
284+	}
285+
286+	handler := &CliHandler{
287+		Logger:      logger,
288+		DBPool:      dbh,
289+		Storage:     st,
290+		RegistryUrl: registryUrl,
291+	}
292 
293 	s, err := wish.NewServer(
294 		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
295 		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
296 		wish.WithPublicKeyAuth(AuthHandler(dbh, logger)),
297-		ptun.WithWebTunnel(ptun.NewWebTunnelHandler(serveMux, logger)),
298+		wish.WithMiddleware(WishMiddleware(handler)),
299+		ptun.WithWebTunnel(
300+			ptun.NewWebTunnelHandler(createServeMux(handler), logger),
301+		),
302 	)
303 
304 	if err != nil {
A imgs/wish.go
+263, -0
  1@@ -0,0 +1,263 @@
  2+package imgs
  3+
  4+import (
  5+	"encoding/json"
  6+	"flag"
  7+	"fmt"
  8+	"io"
  9+	"log/slog"
 10+	"net/http"
 11+	"path/filepath"
 12+	"strings"
 13+
 14+	"github.com/charmbracelet/ssh"
 15+	"github.com/charmbracelet/wish"
 16+	"github.com/picosh/pico/db"
 17+	"github.com/picosh/pico/shared"
 18+	"github.com/picosh/pico/shared/storage"
 19+	"github.com/picosh/pico/tui/common"
 20+	sst "github.com/picosh/pobj/storage"
 21+	"github.com/picosh/send/send/utils"
 22+)
 23+
 24+func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
 25+	var err error
 26+	key, err := shared.KeyText(s)
 27+	if err != nil {
 28+		return nil, fmt.Errorf("key not found")
 29+	}
 30+
 31+	user, err := dbpool.FindUserForKey(s.User(), key)
 32+	if err != nil {
 33+		return nil, err
 34+	}
 35+
 36+	if user.Name == "" {
 37+		return nil, fmt.Errorf("must have username set")
 38+	}
 39+
 40+	return user, nil
 41+}
 42+
 43+func flagSet(cmdName string, sesh ssh.Session) (*flag.FlagSet, *bool) {
 44+	cmd := flag.NewFlagSet(cmdName, flag.ContinueOnError)
 45+	cmd.SetOutput(sesh)
 46+	write := cmd.Bool("write", false, "apply changes")
 47+	return cmd, write
 48+}
 49+
 50+func flagCheck(cmd *flag.FlagSet, posArg string, cmdArgs []string) bool {
 51+	_ = cmd.Parse(cmdArgs)
 52+
 53+	if posArg == "-h" || posArg == "--help" || posArg == "-help" {
 54+		cmd.Usage()
 55+		return false
 56+	}
 57+	return true
 58+}
 59+
 60+type Cmd struct {
 61+	User        *db.User
 62+	Session     shared.CmdSession
 63+	Log         *slog.Logger
 64+	Dbpool      db.DB
 65+	Write       bool
 66+	Styles      common.Styles
 67+	Storage     sst.ObjectStorage
 68+	RegistryUrl string
 69+}
 70+
 71+func (c *Cmd) output(out string) {
 72+	_, _ = c.Session.Write([]byte(out + "\r\n"))
 73+}
 74+
 75+func (c *Cmd) error(err error) {
 76+	_, _ = fmt.Fprint(c.Session.Stderr(), err, "\r\n")
 77+	_ = c.Session.Exit(1)
 78+	_ = c.Session.Close()
 79+}
 80+
 81+func (c *Cmd) bail(err error) {
 82+	if err == nil {
 83+		return
 84+	}
 85+	c.Log.Error(err.Error())
 86+	c.error(err)
 87+}
 88+
 89+func (c *Cmd) notice() {
 90+	if !c.Write {
 91+		c.output("\nNOTICE: changes not commited, use `--write` to save operation")
 92+	}
 93+}
 94+
 95+func (c *Cmd) help() {
 96+	helpStr := "Commands: [help, ls, rm]\n"
 97+	helpStr += "NOTICE: *must* append with `--write` for the changes to persist.\n"
 98+	c.output(helpStr)
 99+}
100+
101+func (c *Cmd) rm(repo string) error {
102+	bucket, err := c.Storage.GetBucket("imgs")
103+	if err != nil {
104+		return err
105+	}
106+
107+	fp := filepath.Join("docker/registry/v2/repositories", c.User.Name, repo)
108+
109+	fileList, err := c.Storage.ListObjects(bucket, fp, true)
110+	if err != nil {
111+		return err
112+	}
113+
114+	if len(fileList) == 0 {
115+		c.output(fmt.Sprintf("repo not found (%s)", repo))
116+		return nil
117+	}
118+	c.output(fmt.Sprintf("found (%d) objects for repo (%s), removing", len(fileList), repo))
119+
120+	for _, obj := range fileList {
121+		fname := filepath.Join(fp, obj.Name())
122+		intent := fmt.Sprintf("deleted (%s)", obj.Name())
123+		c.Log.Info(
124+			"attempting to delete file",
125+			"user", c.User.Name,
126+			"bucket", bucket.Name,
127+			"repo", repo,
128+			"filename", fname,
129+		)
130+		if c.Write {
131+			err := c.Storage.DeleteObject(bucket, fname)
132+			if err != nil {
133+				return err
134+			}
135+		}
136+		c.output(intent)
137+	}
138+
139+	return nil
140+}
141+
142+type RegistryCatalog struct {
143+	Repos []string `json:"repositories"`
144+}
145+
146+func (c *Cmd) ls() error {
147+	res, err := http.Get(
148+		fmt.Sprintf("http://%s/v2/_catalog", c.RegistryUrl),
149+	)
150+	if err != nil {
151+		return err
152+	}
153+
154+	body, err := io.ReadAll(res.Body)
155+	if err != nil {
156+		return err
157+	}
158+
159+	var data RegistryCatalog
160+	err = json.Unmarshal(body, &data)
161+
162+	if err != nil {
163+		return err
164+	}
165+
166+	if len(data.Repos) == 0 {
167+		c.output("You don't have any repos on imgs.sh")
168+		return nil
169+	}
170+
171+	user := c.User.Name
172+	out := "repos\n"
173+	out += "-----\n"
174+	for _, repo := range data.Repos {
175+		if !strings.HasPrefix(repo, user+"/") {
176+			continue
177+		}
178+		rr := strings.TrimPrefix(repo, user+"/")
179+		out += fmt.Sprintf("%s\n", rr)
180+	}
181+	c.output(out)
182+	return nil
183+}
184+
185+type CliHandler struct {
186+	DBPool      db.DB
187+	Logger      *slog.Logger
188+	Storage     storage.StorageServe
189+	RegistryUrl string
190+}
191+
192+func WishMiddleware(handler *CliHandler) wish.Middleware {
193+	dbpool := handler.DBPool
194+	log := handler.Logger
195+	st := handler.Storage
196+
197+	return func(next ssh.Handler) ssh.Handler {
198+		return func(sesh ssh.Session) {
199+			user, err := getUser(sesh, dbpool)
200+			if err != nil {
201+				utils.ErrorHandler(sesh, err)
202+				return
203+			}
204+
205+			args := sesh.Command()
206+
207+			opts := Cmd{
208+				Session:     sesh,
209+				User:        user,
210+				Log:         log,
211+				Dbpool:      dbpool,
212+				Write:       false,
213+				Storage:     st,
214+				RegistryUrl: handler.RegistryUrl,
215+			}
216+
217+			if len(args) == 0 {
218+				next(sesh)
219+				return
220+			}
221+
222+			cmd := strings.TrimSpace(args[0])
223+			if len(args) == 1 {
224+				if cmd == "help" {
225+					opts.help()
226+					return
227+				} else if cmd == "ls" {
228+					err := opts.ls()
229+					opts.bail(err)
230+					return
231+				} else {
232+					next(sesh)
233+					return
234+				}
235+			}
236+
237+			repoName := strings.TrimSpace(args[1])
238+			cmdArgs := args[2:]
239+			log.Info(
240+				"imgs middleware detected command",
241+				"args", args,
242+				"cmd", cmd,
243+				"repoName", repoName,
244+				"cmdArgs", cmdArgs,
245+			)
246+
247+			if cmd == "rm" {
248+				rmCmd, write := flagSet("rm", sesh)
249+				if !flagCheck(rmCmd, repoName, cmdArgs) {
250+					return
251+				}
252+				opts.Write = *write
253+
254+				err := opts.rm(repoName)
255+				opts.notice()
256+				opts.bail(err)
257+				return
258+			} else {
259+				next(sesh)
260+				return
261+			}
262+		}
263+	}
264+}
M pgs/cli.go
+1, -32
 1@@ -3,9 +3,7 @@ package pgs
 2 import (
 3 	"errors"
 4 	"fmt"
 5-	"io"
 6 	"log/slog"
 7-	"os"
 8 	"path/filepath"
 9 	"strings"
10 
11@@ -107,38 +105,9 @@ func getHelpText(styles common.Styles, userName string) string {
12 	return helpStr
13 }
14 
15-type CmdSessionLogger struct {
16-	Log *slog.Logger
17-}
18-
19-func (c *CmdSessionLogger) Write(out []byte) (int, error) {
20-	c.Log.Info(string(out))
21-	return 0, nil
22-}
23-
24-func (c *CmdSessionLogger) Exit(code int) error {
25-	os.Exit(code)
26-	return fmt.Errorf("panic %d", code)
27-}
28-
29-func (c *CmdSessionLogger) Close() error {
30-	return fmt.Errorf("closing")
31-}
32-
33-func (c *CmdSessionLogger) Stderr() io.ReadWriter {
34-	return nil
35-}
36-
37-type CmdSession interface {
38-	Write([]byte) (int, error)
39-	Exit(code int) error
40-	Close() error
41-	Stderr() io.ReadWriter
42-}
43-
44 type Cmd struct {
45 	User    *db.User
46-	Session CmdSession
47+	Session shared.CmdSession
48 	Log     *slog.Logger
49 	Store   storage.StorageServe
50 	Dbpool  db.DB
A shared/cmd.go
+37, -0
 1@@ -0,0 +1,37 @@
 2+package shared
 3+
 4+import (
 5+	"fmt"
 6+	"io"
 7+	"log/slog"
 8+	"os"
 9+)
10+
11+type CmdSessionLogger struct {
12+	Log *slog.Logger
13+}
14+
15+func (c *CmdSessionLogger) Write(out []byte) (int, error) {
16+	c.Log.Info(string(out))
17+	return 0, nil
18+}
19+
20+func (c *CmdSessionLogger) Exit(code int) error {
21+	os.Exit(code)
22+	return fmt.Errorf("panic %d", code)
23+}
24+
25+func (c *CmdSessionLogger) Close() error {
26+	return fmt.Errorf("closing")
27+}
28+
29+func (c *CmdSessionLogger) Stderr() io.ReadWriter {
30+	return nil
31+}
32+
33+type CmdSession interface {
34+	Write([]byte) (int, error)
35+	Exit(code int) error
36+	Close() error
37+	Stderr() io.ReadWriter
38+}