detached 7fb924c

fix: lint — use presentation helpers, extract functions to reduce complexity

commit
7fb924c6c58a29514f2799351825dcf9c1db83f1
author
brice · · 2026-03-19 05:54 UTC
parent
950bfdc
signature
unsigned
2 files · +147 -111
M internal/adapters/in/cli/preview.go +124 -91
@@ -1,8 +1,9 @@
11 package cli
22
33 import (
4- "encoding/json"
4+ "context"
55 "fmt"
6+ "io"
67 "os"
78 "os/exec"
89 "strings"
@@ -11,7 +12,7 @@ import (
1112
1213 "github.com/spf13/cobra"
1314
14- "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"
15+ "github.com/bnema/gordon/internal/domain"
1516 "github.com/bnema/gordon/internal/usecase/auto/preview"
1617 )
1718
@@ -56,20 +57,16 @@ func newPreviewCreateCmd() *cobra.Command {
5657 out := cmd.OutOrStdout()
5758
5859 // Resolve preview name from arg or git branch.
59- name := ""
60- if len(args) > 0 {
61- name = args[0]
62- } else {
63- branch, err := detectGitBranch()
64- if err != nil {
65- return fmt.Errorf("no preview name provided and could not detect git branch: %w", err)
66- }
67- name = preview.SanitizeBranchName(branch)
60+ name, err := resolvePreviewName(args)
61+ if err != nil {
62+ return err
6863 }
6964
7065 previewTag := "preview-" + name
7166
72- fmt.Fprintf(out, "Creating preview: %s\n", styles.Theme.Bold.Render(name))
67+ if err := cliWriteLine(out, cliRenderTitle("Preview: "+name)); err != nil {
68+ return err
69+ }
7370
7471 // Resolve control plane (remote or local) — same as push.
7572 handle, err := resolveControlPlane(configPath)
@@ -78,61 +75,16 @@ func newPreviewCreateCmd() *cobra.Command {
7875 }
7976 defer handle.close()
8077
81- // Detect image name and registry using the same logic as push.
82- dockerfile := "Dockerfile"
83- imageName, err := detectImageName(dockerfile)
78+ previewRef, err := resolvePreviewImageRef(ctx, handle.plane, out, previewTag)
8479 if err != nil {
8580 return err
8681 }
8782
88- fmt.Fprintf(out, "Image: %s\n", styles.Theme.Bold.Render(imageName))
89-
90- // Resolve registry from routes.
91- registry, _, _, err := resolveRoute(ctx, handle.plane, "", "", dockerfile)
92- if err != nil {
93- return fmt.Errorf("failed to resolve registry: %w", err)
94- }
95-
96- // Build the image ref with the preview tag.
97- previewRef := fmt.Sprintf("%s/%s:%s", registry, imageName, previewTag)
98- fmt.Fprintf(out, "Tag: %s\n", styles.Theme.Bold.Render(previewRef))
99-
100- // Build and push.
101- imageOps, err := newImageOpsFn()
102- if err != nil {
83+ if err := buildAndPushPreview(ctx, out, previewRef, previewTag, platform, noBuild); err != nil {
10384 return err
10485 }
10586
106- if !noBuild {
107- if _, statErr := os.Stat(dockerfile); os.IsNotExist(statErr) {
108- return fmt.Errorf("dockerfile not found: %s", dockerfile)
109- }
110-
111- buildPlatform := platform
112- buildArgs := buildImageArgs(ctx, previewTag, buildPlatform, dockerfile, nil, previewRef, previewRef)
113- fmt.Fprintln(out, "\nBuilding image...")
114- if err := imageOps.Build(ctx, buildArgs); err != nil {
115- return err
116- }
117- }
118-
119- fmt.Fprintln(out, "Pushing...")
120- if err := imageOps.Push(ctx, previewRef); err != nil {
121- return fmt.Errorf("failed to push %s: %w", previewRef, err)
122- }
123-
124- fmt.Fprintln(out, styles.RenderSuccess("Push complete"))
125- fmt.Fprintf(out, "Preview %s will be created by the server's autopreview handler.\n",
126- styles.Theme.Bold.Render(name))
127-
128- if ttl != "" {
129- fmt.Fprintf(out, "Requested TTL: %s (server config controls actual TTL)\n", ttl)
130- }
131- if noData {
132- fmt.Fprintf(out, "Data copy: skipped (--no-data)\n")
133- }
134-
135- return nil
87+ return printPreviewCreateSummary(out, name, ttl, noData)
13688 },
13789 }
13890
@@ -163,38 +115,14 @@ func newPreviewListCmd() *cobra.Command {
163115 }
164116
165117 if jsonOutput {
166- enc := json.NewEncoder(out)
167- enc.SetIndent("", " ")
168- return enc.Encode(previews)
118+ return writeJSON(out, previews)
169119 }
170120
171121 if len(previews) == 0 {
172- fmt.Fprintln(out, "No active previews")
173- return nil
122+ return cliWriteLine(out, cliRenderEmptyState("No active previews"))
174123 }
175124
176- w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
177- if _, err := fmt.Fprintln(w, "NAME\tDOMAIN\tIMAGE\tCREATED\tEXPIRES"); err != nil {
178- return err
179- }
180- now := time.Now()
181- for _, p := range previews {
182- remaining := time.Until(p.ExpiresAt).Truncate(time.Minute)
183- expiresStr := remaining.String()
184- if p.IsExpired(now) {
185- expiresStr = "expired"
186- }
187- if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
188- p.Name,
189- p.Domain,
190- truncateImage(p.Image, 40),
191- p.CreatedAt.Format(time.DateTime),
192- expiresStr,
193- ); err != nil {
194- return err
195- }
196- }
197- return w.Flush()
125+ return printPreviewTable(out, previews)
198126 },
199127 }
200128 cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output as JSON")
@@ -220,8 +148,7 @@ func newPreviewDeleteCmd() *cobra.Command {
220148 return fmt.Errorf("failed to delete preview %q: %w", name, err)
221149 }
222150
223- fmt.Fprintln(out, styles.RenderSuccess(fmt.Sprintf("Preview %q deleted", name)))
224- return nil
151+ return cliWriteLine(out, cliRenderSuccess(fmt.Sprintf("Preview %q deleted", name)))
225152 },
226153 }
227154 }
@@ -246,14 +173,120 @@ func newPreviewExtendCmd() *cobra.Command {
246173 return fmt.Errorf("failed to extend preview %q: %w", name, err)
247174 }
248175
249- fmt.Fprintln(out, styles.RenderSuccess(fmt.Sprintf("Preview %q extended by %s", name, ttl)))
250- return nil
176+ return cliWriteLine(out, cliRenderSuccess(fmt.Sprintf("Preview %q extended by %s", name, ttl)))
251177 },
252178 }
253179 cmd.Flags().StringVar(&ttl, "ttl", "24h", "Additional TTL duration")
254180 return cmd
255181 }
256182
183+func printPreviewTable(out io.Writer, previews []domain.PreviewRoute) error {
184+ w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
185+ if _, err := fmt.Fprintln(w, "NAME\tDOMAIN\tIMAGE\tCREATED\tEXPIRES"); err != nil {
186+ return err
187+ }
188+ now := time.Now()
189+ for _, p := range previews {
190+ remaining := time.Until(p.ExpiresAt).Truncate(time.Minute)
191+ expiresStr := remaining.String()
192+ if p.IsExpired(now) {
193+ expiresStr = "expired"
194+ }
195+ if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
196+ p.Name,
197+ p.Domain,
198+ truncateImage(p.Image, 40),
199+ p.CreatedAt.Format(time.DateTime),
200+ expiresStr,
201+ ); err != nil {
202+ return err
203+ }
204+ }
205+ return w.Flush()
206+}
207+
208+func resolvePreviewName(args []string) (string, error) {
209+ if len(args) > 0 {
210+ return args[0], nil
211+ }
212+ branch, err := detectGitBranch()
213+ if err != nil {
214+ return "", fmt.Errorf("no preview name provided and could not detect git branch: %w", err)
215+ }
216+ return preview.SanitizeBranchName(branch), nil
217+}
218+
219+func resolvePreviewImageRef(ctx context.Context, cp ControlPlane, out io.Writer, previewTag string) (string, error) {
220+ dockerfile := "Dockerfile"
221+ imageName, err := detectImageName(dockerfile)
222+ if err != nil {
223+ return "", err
224+ }
225+ if err := cliWriteLine(out, cliRenderMeta("Image:", imageName)); err != nil {
226+ return "", err
227+ }
228+
229+ registry, _, _, err := resolveRoute(ctx, cp, "", "", dockerfile)
230+ if err != nil {
231+ return "", fmt.Errorf("failed to resolve registry: %w", err)
232+ }
233+
234+ previewRef := fmt.Sprintf("%s/%s:%s", registry, imageName, previewTag)
235+ if err := cliWriteLine(out, cliRenderMeta("Tag:", previewRef)); err != nil {
236+ return "", err
237+ }
238+ return previewRef, nil
239+}
240+
241+func buildAndPushPreview(ctx context.Context, out io.Writer, previewRef, previewTag, platform string, noBuild bool) error {
242+ imageOps, err := newImageOpsFn()
243+ if err != nil {
244+ return err
245+ }
246+
247+ if !noBuild {
248+ dockerfile := "Dockerfile"
249+ if _, statErr := os.Stat(dockerfile); os.IsNotExist(statErr) {
250+ return fmt.Errorf("dockerfile not found: %s", dockerfile)
251+ }
252+ buildArgs := buildImageArgs(ctx, previewTag, platform, dockerfile, nil, previewRef, previewRef)
253+ if err := cliWriteLine(out, "\nBuilding image..."); err != nil {
254+ return err
255+ }
256+ if err := imageOps.Build(ctx, buildArgs); err != nil {
257+ return err
258+ }
259+ }
260+
261+ if err := cliWriteLine(out, "Pushing..."); err != nil {
262+ return err
263+ }
264+ if err := imageOps.Push(ctx, previewRef); err != nil {
265+ return fmt.Errorf("failed to push %s: %w", previewRef, err)
266+ }
267+ return nil
268+}
269+
270+func printPreviewCreateSummary(out io.Writer, name, ttl string, noData bool) error {
271+ if err := cliWriteLine(out, cliRenderSuccess("Push complete")); err != nil {
272+ return err
273+ }
274+ if err := cliWritef(out, "Preview %s will be created by the server's autopreview handler.\n", name); err != nil {
275+ return err
276+ }
277+ if ttl != "" {
278+ if err := cliWritef(out, "Requested TTL: %s (server config controls actual TTL)\n", ttl); err != nil {
279+ return err
280+ }
281+ }
282+ if noData {
283+ if err := cliWriteLine(out, cliRenderInfo("Data copy: skipped (--no-data)")); err != nil {
284+ return err
285+ }
286+ }
287+ return nil
288+}
289+
257290 func detectGitBranch() (string, error) {
258291 out, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output() // #nosec G204
259292 if err != nil {
@@ -1,8 +1,9 @@
1 package cli1 package cli
2 2
3 import (3 import (
4- "encoding/json"4+ "context"
5 "fmt"5 "fmt"
6+ "io"
6 "os"7 "os"
7 "os/exec"8 "os/exec"
8 "strings"9 "strings"
@@ -11,7 +12,7 @@ import (
11 12
12 "github.com/spf13/cobra"13 "github.com/spf13/cobra"
13 14
14- "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles"15+ "github.com/bnema/gordon/internal/domain"
15 "github.com/bnema/gordon/internal/usecase/auto/preview"16 "github.com/bnema/gordon/internal/usecase/auto/preview"
16 )17 )
17 18
@@ -56,20 +57,16 @@ func newPreviewCreateCmd() *cobra.Command {
56 out := cmd.OutOrStdout()57 out := cmd.OutOrStdout()
57 58
58 // Resolve preview name from arg or git branch.59 // Resolve preview name from arg or git branch.
59- name := ""60+ name, err := resolvePreviewName(args)
60- if len(args) > 0 {61+ if err != nil {
61- name = args[0]62+ return err
62- } else {
63- branch, err := detectGitBranch()
64- if err != nil {
65- return fmt.Errorf("no preview name provided and could not detect git branch: %w", err)
66- }
67- name = preview.SanitizeBranchName(branch)
68 }63 }
69 64
70 previewTag := "preview-" + name65 previewTag := "preview-" + name
71 66
72- fmt.Fprintf(out, "Creating preview: %s\n", styles.Theme.Bold.Render(name))67+ if err := cliWriteLine(out, cliRenderTitle("Preview: "+name)); err != nil {
68+ return err
69+ }
73 70
74 // Resolve control plane (remote or local) — same as push.71 // Resolve control plane (remote or local) — same as push.
75 handle, err := resolveControlPlane(configPath)72 handle, err := resolveControlPlane(configPath)
@@ -78,61 +75,16 @@ func newPreviewCreateCmd() *cobra.Command {
78 }75 }
79 defer handle.close()76 defer handle.close()
80 77
81- // Detect image name and registry using the same logic as push.78+ previewRef, err := resolvePreviewImageRef(ctx, handle.plane, out, previewTag)
82- dockerfile := "Dockerfile"
83- imageName, err := detectImageName(dockerfile)
84 if err != nil {79 if err != nil {
85 return err80 return err
86 }81 }
87 82
88- fmt.Fprintf(out, "Image: %s\n", styles.Theme.Bold.Render(imageName))83+ if err := buildAndPushPreview(ctx, out, previewRef, previewTag, platform, noBuild); err != nil {
89-
90- // Resolve registry from routes.
91- registry, _, _, err := resolveRoute(ctx, handle.plane, "", "", dockerfile)
92- if err != nil {
93- return fmt.Errorf("failed to resolve registry: %w", err)
94- }
95-
96- // Build the image ref with the preview tag.
97- previewRef := fmt.Sprintf("%s/%s:%s", registry, imageName, previewTag)
98- fmt.Fprintf(out, "Tag: %s\n", styles.Theme.Bold.Render(previewRef))
99-
100- // Build and push.
101- imageOps, err := newImageOpsFn()
102- if err != nil {
103 return err84 return err
104 }85 }
105 86
106- if !noBuild {87+ return printPreviewCreateSummary(out, name, ttl, noData)
107- if _, statErr := os.Stat(dockerfile); os.IsNotExist(statErr) {
108- return fmt.Errorf("dockerfile not found: %s", dockerfile)
109- }
110-
111- buildPlatform := platform
112- buildArgs := buildImageArgs(ctx, previewTag, buildPlatform, dockerfile, nil, previewRef, previewRef)
113- fmt.Fprintln(out, "\nBuilding image...")
114- if err := imageOps.Build(ctx, buildArgs); err != nil {
115- return err
116- }
117- }
118-
119- fmt.Fprintln(out, "Pushing...")
120- if err := imageOps.Push(ctx, previewRef); err != nil {
121- return fmt.Errorf("failed to push %s: %w", previewRef, err)
122- }
123-
124- fmt.Fprintln(out, styles.RenderSuccess("Push complete"))
125- fmt.Fprintf(out, "Preview %s will be created by the server's autopreview handler.\n",
126- styles.Theme.Bold.Render(name))
127-
128- if ttl != "" {
129- fmt.Fprintf(out, "Requested TTL: %s (server config controls actual TTL)\n", ttl)
130- }
131- if noData {
132- fmt.Fprintf(out, "Data copy: skipped (--no-data)\n")
133- }
134-
135- return nil
136 },88 },
137 }89 }
138 90
@@ -163,38 +115,14 @@ func newPreviewListCmd() *cobra.Command {
163 }115 }
164 116
165 if jsonOutput {117 if jsonOutput {
166- enc := json.NewEncoder(out)118+ return writeJSON(out, previews)
167- enc.SetIndent("", " ")
168- return enc.Encode(previews)
169 }119 }
170 120
171 if len(previews) == 0 {121 if len(previews) == 0 {
172- fmt.Fprintln(out, "No active previews")122+ return cliWriteLine(out, cliRenderEmptyState("No active previews"))
173- return nil
174 }123 }
175 124
176- w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)125+ return printPreviewTable(out, previews)
177- if _, err := fmt.Fprintln(w, "NAME\tDOMAIN\tIMAGE\tCREATED\tEXPIRES"); err != nil {
178- return err
179- }
180- now := time.Now()
181- for _, p := range previews {
182- remaining := time.Until(p.ExpiresAt).Truncate(time.Minute)
183- expiresStr := remaining.String()
184- if p.IsExpired(now) {
185- expiresStr = "expired"
186- }
187- if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
188- p.Name,
189- p.Domain,
190- truncateImage(p.Image, 40),
191- p.CreatedAt.Format(time.DateTime),
192- expiresStr,
193- ); err != nil {
194- return err
195- }
196- }
197- return w.Flush()
198 },126 },
199 }127 }
200 cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output as JSON")128 cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output as JSON")
@@ -220,8 +148,7 @@ func newPreviewDeleteCmd() *cobra.Command {
220 return fmt.Errorf("failed to delete preview %q: %w", name, err)148 return fmt.Errorf("failed to delete preview %q: %w", name, err)
221 }149 }
222 150
223- fmt.Fprintln(out, styles.RenderSuccess(fmt.Sprintf("Preview %q deleted", name)))151+ return cliWriteLine(out, cliRenderSuccess(fmt.Sprintf("Preview %q deleted", name)))
224- return nil
225 },152 },
226 }153 }
227 }154 }
@@ -246,14 +173,120 @@ func newPreviewExtendCmd() *cobra.Command {
246 return fmt.Errorf("failed to extend preview %q: %w", name, err)173 return fmt.Errorf("failed to extend preview %q: %w", name, err)
247 }174 }
248 175
249- fmt.Fprintln(out, styles.RenderSuccess(fmt.Sprintf("Preview %q extended by %s", name, ttl)))176+ return cliWriteLine(out, cliRenderSuccess(fmt.Sprintf("Preview %q extended by %s", name, ttl)))
250- return nil
251 },177 },
252 }178 }
253 cmd.Flags().StringVar(&ttl, "ttl", "24h", "Additional TTL duration")179 cmd.Flags().StringVar(&ttl, "ttl", "24h", "Additional TTL duration")
254 return cmd180 return cmd
255 }181 }
256 182
183+func printPreviewTable(out io.Writer, previews []domain.PreviewRoute) error {
184+ w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
185+ if _, err := fmt.Fprintln(w, "NAME\tDOMAIN\tIMAGE\tCREATED\tEXPIRES"); err != nil {
186+ return err
187+ }
188+ now := time.Now()
189+ for _, p := range previews {
190+ remaining := time.Until(p.ExpiresAt).Truncate(time.Minute)
191+ expiresStr := remaining.String()
192+ if p.IsExpired(now) {
193+ expiresStr = "expired"
194+ }
195+ if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
196+ p.Name,
197+ p.Domain,
198+ truncateImage(p.Image, 40),
199+ p.CreatedAt.Format(time.DateTime),
200+ expiresStr,
201+ ); err != nil {
202+ return err
203+ }
204+ }
205+ return w.Flush()
206+}
207+
208+func resolvePreviewName(args []string) (string, error) {
209+ if len(args) > 0 {
210+ return args[0], nil
211+ }
212+ branch, err := detectGitBranch()
213+ if err != nil {
214+ return "", fmt.Errorf("no preview name provided and could not detect git branch: %w", err)
215+ }
216+ return preview.SanitizeBranchName(branch), nil
217+}
218+
219+func resolvePreviewImageRef(ctx context.Context, cp ControlPlane, out io.Writer, previewTag string) (string, error) {
220+ dockerfile := "Dockerfile"
221+ imageName, err := detectImageName(dockerfile)
222+ if err != nil {
223+ return "", err
224+ }
225+ if err := cliWriteLine(out, cliRenderMeta("Image:", imageName)); err != nil {
226+ return "", err
227+ }
228+
229+ registry, _, _, err := resolveRoute(ctx, cp, "", "", dockerfile)
230+ if err != nil {
231+ return "", fmt.Errorf("failed to resolve registry: %w", err)
232+ }
233+
234+ previewRef := fmt.Sprintf("%s/%s:%s", registry, imageName, previewTag)
235+ if err := cliWriteLine(out, cliRenderMeta("Tag:", previewRef)); err != nil {
236+ return "", err
237+ }
238+ return previewRef, nil
239+}
240+
241+func buildAndPushPreview(ctx context.Context, out io.Writer, previewRef, previewTag, platform string, noBuild bool) error {
242+ imageOps, err := newImageOpsFn()
243+ if err != nil {
244+ return err
245+ }
246+
247+ if !noBuild {
248+ dockerfile := "Dockerfile"
249+ if _, statErr := os.Stat(dockerfile); os.IsNotExist(statErr) {
250+ return fmt.Errorf("dockerfile not found: %s", dockerfile)
251+ }
252+ buildArgs := buildImageArgs(ctx, previewTag, platform, dockerfile, nil, previewRef, previewRef)
253+ if err := cliWriteLine(out, "\nBuilding image..."); err != nil {
254+ return err
255+ }
256+ if err := imageOps.Build(ctx, buildArgs); err != nil {
257+ return err
258+ }
259+ }
260+
261+ if err := cliWriteLine(out, "Pushing..."); err != nil {
262+ return err
263+ }
264+ if err := imageOps.Push(ctx, previewRef); err != nil {
265+ return fmt.Errorf("failed to push %s: %w", previewRef, err)
266+ }
267+ return nil
268+}
269+
270+func printPreviewCreateSummary(out io.Writer, name, ttl string, noData bool) error {
271+ if err := cliWriteLine(out, cliRenderSuccess("Push complete")); err != nil {
272+ return err
273+ }
274+ if err := cliWritef(out, "Preview %s will be created by the server's autopreview handler.\n", name); err != nil {
275+ return err
276+ }
277+ if ttl != "" {
278+ if err := cliWritef(out, "Requested TTL: %s (server config controls actual TTL)\n", ttl); err != nil {
279+ return err
280+ }
281+ }
282+ if noData {
283+ if err := cliWriteLine(out, cliRenderInfo("Data copy: skipped (--no-data)")); err != nil {
284+ return err
285+ }
286+ }
287+ return nil
288+}
289+
257 func detectGitBranch() (string, error) {290 func detectGitBranch() (string, error) {
258 out, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output() // #nosec G204291 out, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output() // #nosec G204
259 if err != nil {292 if err != nil {
M internal/app/run.go +23 -20
@@ -545,26 +545,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
545545 // Create log service for accessing logs via admin API
546546 svc.logSvc = logs.NewService(resolveLogFilePath(cfg), cfg.Logging.File.Enabled, svc.containerSvc, svc.runtime, log)
547547
548- // Preview store and service
549- previewStorePath := filepath.Join(resolveDataDir(cfg.Server.DataDir), "previews.json")
550- svc.previewStore = filesystem.NewPreviewStore(previewStorePath)
551- previewConfig := svc.configSvc.GetPreviewConfig()
552- svc.previewService = preview.NewService(svc.previewStore, previewConfig.TTL)
553- if err := svc.previewService.Load(ctx); err != nil {
554- log.Warn().Err(err).Msg("failed to load previews")
555- }
556-
557- // Start TTL ticker to expire and clean up preview environments
558- svc.previewService.StartTicker(ctx, 60*time.Second, func(ctx context.Context, p domain.PreviewRoute) {
559- for _, containerName := range p.Containers {
560- _ = svc.runtime.StopContainer(ctx, containerName)
561- _ = svc.runtime.RemoveContainer(ctx, containerName, true)
562- }
563- for _, volName := range p.Volumes {
564- _ = svc.runtime.RemoveVolume(ctx, volName, true)
565- }
566- svc.proxySvc.InvalidateTarget(ctx, p.Domain)
567- })
548+ initPreviewService(ctx, cfg, svc, log)
568549
569550 // Create admin handler for admin API
570551 svc.adminHandler = admin.NewHandler(admin.HandlerDeps{
@@ -586,6 +567,28 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
586567 return svc, nil
587568 }
588569
570+// initPreviewService sets up the preview store, service, and TTL ticker.
571+func initPreviewService(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger) {
572+ previewStorePath := filepath.Join(resolveDataDir(cfg.Server.DataDir), "previews.json")
573+ svc.previewStore = filesystem.NewPreviewStore(previewStorePath)
574+ previewConfig := svc.configSvc.GetPreviewConfig()
575+ svc.previewService = preview.NewService(svc.previewStore, previewConfig.TTL)
576+ if err := svc.previewService.Load(ctx); err != nil {
577+ log.Warn().Err(err).Msg("failed to load previews")
578+ }
579+
580+ svc.previewService.StartTicker(ctx, 60*time.Second, func(ctx context.Context, p domain.PreviewRoute) {
581+ for _, containerName := range p.Containers {
582+ _ = svc.runtime.StopContainer(ctx, containerName)
583+ _ = svc.runtime.RemoveContainer(ctx, containerName, true)
584+ }
585+ for _, volName := range p.Volumes {
586+ _ = svc.runtime.RemoveVolume(ctx, volName, true)
587+ }
588+ svc.proxySvc.InvalidateTarget(ctx, p.Domain)
589+ })
590+}
591+
589592 // injectTelemetryMetrics creates and injects OTel metrics into services when
590593 // telemetry is enabled. Skipped otherwise to avoid unnecessary allocations.
591594 func injectTelemetryMetrics(cfg Config, svc *services, log zerowrap.Logger) {
@@ -545,26 +545,7 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
545 // Create log service for accessing logs via admin API545 // Create log service for accessing logs via admin API
546 svc.logSvc = logs.NewService(resolveLogFilePath(cfg), cfg.Logging.File.Enabled, svc.containerSvc, svc.runtime, log)546 svc.logSvc = logs.NewService(resolveLogFilePath(cfg), cfg.Logging.File.Enabled, svc.containerSvc, svc.runtime, log)
547 547
548- // Preview store and service548+ initPreviewService(ctx, cfg, svc, log)
549- previewStorePath := filepath.Join(resolveDataDir(cfg.Server.DataDir), "previews.json")
550- svc.previewStore = filesystem.NewPreviewStore(previewStorePath)
551- previewConfig := svc.configSvc.GetPreviewConfig()
552- svc.previewService = preview.NewService(svc.previewStore, previewConfig.TTL)
553- if err := svc.previewService.Load(ctx); err != nil {
554- log.Warn().Err(err).Msg("failed to load previews")
555- }
556-
557- // Start TTL ticker to expire and clean up preview environments
558- svc.previewService.StartTicker(ctx, 60*time.Second, func(ctx context.Context, p domain.PreviewRoute) {
559- for _, containerName := range p.Containers {
560- _ = svc.runtime.StopContainer(ctx, containerName)
561- _ = svc.runtime.RemoveContainer(ctx, containerName, true)
562- }
563- for _, volName := range p.Volumes {
564- _ = svc.runtime.RemoveVolume(ctx, volName, true)
565- }
566- svc.proxySvc.InvalidateTarget(ctx, p.Domain)
567- })
568 549
569 // Create admin handler for admin API550 // Create admin handler for admin API
570 svc.adminHandler = admin.NewHandler(admin.HandlerDeps{551 svc.adminHandler = admin.NewHandler(admin.HandlerDeps{
@@ -586,6 +567,28 @@ func createServices(ctx context.Context, v *viper.Viper, cfg Config, log zerowra
586 return svc, nil567 return svc, nil
587 }568 }
588 569
570+// initPreviewService sets up the preview store, service, and TTL ticker.
571+func initPreviewService(ctx context.Context, cfg Config, svc *services, log zerowrap.Logger) {
572+ previewStorePath := filepath.Join(resolveDataDir(cfg.Server.DataDir), "previews.json")
573+ svc.previewStore = filesystem.NewPreviewStore(previewStorePath)
574+ previewConfig := svc.configSvc.GetPreviewConfig()
575+ svc.previewService = preview.NewService(svc.previewStore, previewConfig.TTL)
576+ if err := svc.previewService.Load(ctx); err != nil {
577+ log.Warn().Err(err).Msg("failed to load previews")
578+ }
579+
580+ svc.previewService.StartTicker(ctx, 60*time.Second, func(ctx context.Context, p domain.PreviewRoute) {
581+ for _, containerName := range p.Containers {
582+ _ = svc.runtime.StopContainer(ctx, containerName)
583+ _ = svc.runtime.RemoveContainer(ctx, containerName, true)
584+ }
585+ for _, volName := range p.Volumes {
586+ _ = svc.runtime.RemoveVolume(ctx, volName, true)
587+ }
588+ svc.proxySvc.InvalidateTarget(ctx, p.Domain)
589+ })
590+}
591+
589 // injectTelemetryMetrics creates and injects OTel metrics into services when592 // injectTelemetryMetrics creates and injects OTel metrics into services when
590 // telemetry is enabled. Skipped otherwise to avoid unnecessary allocations.593 // telemetry is enabled. Skipped otherwise to avoid unnecessary allocations.
591 func injectTelemetryMetrics(cfg Config, svc *services, log zerowrap.Logger) {594 func injectTelemetryMetrics(cfg Config, svc *services, log zerowrap.Logger) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close