detached b06de74

Refactor: Streamline container image deployment and push process via the CLI client

- Replace terminal UI with structured logging for deploy and push
commands
- Implement consistent JSON responses for server endpoints
- Enhance error handling and reporting throughout the codebase
- Simplify HTTP client setup and request handling
- Optimize Docker image export and import processes
- Remove unused MVU (Model-View-Update) code for progress bar and
spinner
- Consolidate deployment and push logic into separate functions
- Update version checking mechanism in web UI
- Fix building process
commit
b06de7490de623132247c5478c7ef6915825648b
author
bamen · · 2024-08-29 08:04 UTC
committer
bamen · · 2024-08-29 08:49 UTC
parent
fb6df0a
signature
unsigned
20 files · +403 -642
M .goreleaser.yaml +5 -5
@@ -1,3 +1,5 @@
1+version: 2
2+
13 before:
24 hooks:
35 - go mod tidy
@@ -27,13 +29,12 @@ dockers:
2729 build_flag_templates:
2830 - "--pull"
2931 - "--platform=linux/amd64"
32+ - "--build-arg=ARCH=amd64"
3033 - "--label=org.opencontainers.image.created={{.Date}}"
3134 - "--label=org.opencontainers.image.title={{.ProjectName}}"
3235 - "--label=org.opencontainers.image.revision={{.FullCommit}}"
3336 - "--label=org.opencontainers.image.version={{.Version}}"
3437 - "--label=org.opencontainers.image.source=https://github.com/bnema/gordon"
35- extra_files:
36- - .iscontainer
3738 - image_templates:
3839 - "ghcr.io/bnema/gordon:{{ .Tag }}-arm64"
3940 use: buildx
@@ -42,13 +43,12 @@ dockers:
4243 build_flag_templates:
4344 - "--pull"
4445 - "--platform=linux/arm64"
46+ - "--build-arg=ARCH=arm64"
4547 - "--label=org.opencontainers.image.created={{.Date}}"
4648 - "--label=org.opencontainers.image.title={{.ProjectName}}"
4749 - "--label=org.opencontainers.image.revision={{.FullCommit}}"
4850 - "--label=org.opencontainers.image.version={{.Version}}"
4951 - "--label=org.opencontainers.image.source=https://github.com/bnema/gordon"
50- extra_files:
51- - .iscontainer
5252
5353 archives:
5454 - format: zip
@@ -59,4 +59,4 @@ changelog:
5959 filters:
6060 exclude:
6161 - "^docs:"
62- - "^test:"⏎̸
62+ - "^test:"
@@ -1,3 +1,5 @@
1+version: 2
2+
1 before:3 before:
2 hooks:4 hooks:
3 - go mod tidy5 - go mod tidy
@@ -27,13 +29,12 @@ dockers:
27 build_flag_templates:29 build_flag_templates:
28 - "--pull"30 - "--pull"
29 - "--platform=linux/amd64"31 - "--platform=linux/amd64"
32+ - "--build-arg=ARCH=amd64"
30 - "--label=org.opencontainers.image.created={{.Date}}"33 - "--label=org.opencontainers.image.created={{.Date}}"
31 - "--label=org.opencontainers.image.title={{.ProjectName}}"34 - "--label=org.opencontainers.image.title={{.ProjectName}}"
32 - "--label=org.opencontainers.image.revision={{.FullCommit}}"35 - "--label=org.opencontainers.image.revision={{.FullCommit}}"
33 - "--label=org.opencontainers.image.version={{.Version}}"36 - "--label=org.opencontainers.image.version={{.Version}}"
34 - "--label=org.opencontainers.image.source=https://github.com/bnema/gordon"37 - "--label=org.opencontainers.image.source=https://github.com/bnema/gordon"
35- extra_files:
36- - .iscontainer
37 - image_templates:38 - image_templates:
38 - "ghcr.io/bnema/gordon:{{ .Tag }}-arm64"39 - "ghcr.io/bnema/gordon:{{ .Tag }}-arm64"
39 use: buildx40 use: buildx
@@ -42,13 +43,12 @@ dockers:
42 build_flag_templates:43 build_flag_templates:
43 - "--pull"44 - "--pull"
44 - "--platform=linux/arm64"45 - "--platform=linux/arm64"
46+ - "--build-arg=ARCH=arm64"
45 - "--label=org.opencontainers.image.created={{.Date}}"47 - "--label=org.opencontainers.image.created={{.Date}}"
46 - "--label=org.opencontainers.image.title={{.ProjectName}}"48 - "--label=org.opencontainers.image.title={{.ProjectName}}"
47 - "--label=org.opencontainers.image.revision={{.FullCommit}}"49 - "--label=org.opencontainers.image.revision={{.FullCommit}}"
48 - "--label=org.opencontainers.image.version={{.Version}}"50 - "--label=org.opencontainers.image.version={{.Version}}"
49 - "--label=org.opencontainers.image.source=https://github.com/bnema/gordon"51 - "--label=org.opencontainers.image.source=https://github.com/bnema/gordon"
50- extra_files:
51- - .iscontainer
52 52
53 archives:53 archives:
54 - format: zip54 - format: zip
@@ -59,4 +59,4 @@ changelog:
59 filters:59 filters:
60 exclude:60 exclude:
61 - "^docs:"61 - "^docs:"
62- - "^test:"⏎̸62+ - "^test:"
M Dockerfile +1 -8
@@ -1,15 +1,8 @@
1-# Start from scratch
21 FROM alpine:latest
32
4-ARG ARCH
3+COPY gordon /gordon
54
6-# Copy the pre-built binary for the specific architecture
7-COPY dist/gordon-linux-${ARCH} /gordon
8-# Create the .iscontainer file
95 RUN touch /.iscontainer
106
11-# Set the entrypoint
127 ENTRYPOINT ["/gordon"]
13-
14-# Default command
158 CMD ["serve"]
@@ -1,15 +1,8 @@
1-# Start from scratch
2 FROM alpine:latest1 FROM alpine:latest
3 2
4-ARG ARCH3+COPY gordon /gordon
5 4
6-# Copy the pre-built binary for the specific architecture
7-COPY dist/gordon-linux-${ARCH} /gordon
8-# Create the .iscontainer file
9 RUN touch /.iscontainer5 RUN touch /.iscontainer
10 6
11-# Set the entrypoint
12 ENTRYPOINT ["/gordon"]7 ENTRYPOINT ["/gordon"]
13-
14-# Default command
15 CMD ["serve"]8 CMD ["serve"]
M assets/vhs/demo.tape +11 -3
@@ -71,17 +71,25 @@ Space
7171 Type "deploy"
7272 Sleep 1
7373 Space
74-Type "bnema/go-helloworld-http:latest"
74+Type "bnema/go-helloworld:latest"
7575 Sleep 1
7676 Space
7777 Type "-p 8888:8080"
7878 Sleep 1
7979 Space
80-Type "-t hello.bamen.dev"
80+Type "-t hello.bamen.dev"
8181 Sleep 1
8282 Enter
8383
84-Sleep 15
84+Sleep 14
85+
86+Hide
87+Type "clear"
88+sleep 1
89+Enter
90+Show
91+
92+
8593 Enter
8694 Type "curl https://hello.bamen.dev"
8795 Sleep 1
@@ -71,17 +71,25 @@ Space
71 Type "deploy"71 Type "deploy"
72 Sleep 172 Sleep 1
73 Space73 Space
74-Type "bnema/go-helloworld-http:latest"74+Type "bnema/go-helloworld:latest"
75 Sleep 175 Sleep 1
76 Space76 Space
77 Type "-p 8888:8080"77 Type "-p 8888:8080"
78 Sleep 178 Sleep 1
79 Space79 Space
80-Type "-t hello.bamen.dev" 80+Type "-t hello.bamen.dev"
81 Sleep 181 Sleep 1
82 Enter82 Enter
83 83
84-Sleep 1584+Sleep 14
85+
86+Hide
87+Type "clear"
88+sleep 1
89+Enter
90+Show
91+
92+
85 Enter93 Enter
86 Type "curl https://hello.bamen.dev"94 Type "curl https://hello.bamen.dev"
87 Sleep 195 Sleep 1
M buildpush-dev.sh +39 -14
@@ -1,11 +1,20 @@
11 #!/bin/bash
2-
32 set -e
4-
3+ENGINE="podman"
54 REPO="ghcr.io/bnema/gordon"
65 TAG="dev"
76 DIST_DIR="./dist"
87
8+# Function to handle errors
9+handle_error() {
10+ echo "Error occurred. Cleaning up..."
11+ $ENGINE system prune -f
12+ exit 1
13+}
14+
15+# Set up error handling
16+trap 'handle_error' ERR
17+
918 # Ensure dist directory exists
1019 mkdir -p $DIST_DIR
1120
@@ -14,25 +23,41 @@ echo "Building Go binaries..."
1423 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $DIST_DIR/gordon-linux-amd64 ./cmd/cli
1524 CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $DIST_DIR/gordon-linux-arm64 ./cmd/cli
1625
26+# Clean up any dangling images before building
27+echo "Cleaning up dangling images..."
28+$ENGINE image prune -f
29+
1730 # Build Docker images for each architecture
1831 echo "Building Docker images..."
19-docker build -t $REPO:${TAG}-amd64 --build-arg ARCH=amd64 -f Dockerfile .
20-docker build -t $REPO:${TAG}-arm64v8 --build-arg ARCH=arm64 -f Dockerfile .
32+$ENGINE build -t $REPO:${TAG}-amd64 --build-arg ARCH=amd64 -f Dockerfile .
33+$ENGINE build -t $REPO:${TAG}-arm64v8 --build-arg ARCH=arm64 -f Dockerfile .
2134
2235 # Push images
2336 echo "Pushing Docker images..."
24-docker push $REPO:${TAG}-amd64
25-docker push $REPO:${TAG}-arm64v8
37+$ENGINE push $REPO:${TAG}-amd64
38+$ENGINE push $REPO:${TAG}-arm64v8
39+
40+# Remove existing manifest if it exists
41+echo "Removing existing manifest..."
42+$ENGINE manifest rm $REPO:$TAG || true
2643
27-# Create and push multi-arch manifest
28-echo "Creating and pushing multi-arch manifest..."
29-docker manifest create $REPO:$TAG \
44+# Create multi-arch manifest
45+echo "Creating multi-arch manifest..."
46+$ENGINE manifest create $REPO:$TAG \
3047 $REPO:${TAG}-amd64 \
31- $REPO:${TAG}-arm64v8 \
32- --amend
48+ $REPO:${TAG}-arm64
3349
3450 # Annotate the arm64 image with variant information
35-docker manifest annotate $REPO:$TAG \
36- $REPO:${TAG}-arm64v8 --arch arm64 --variant v8
51+echo "Annotating arm64 image..."
52+$ENGINE manifest annotate $REPO:$TAG \
53+ $REPO:${TAG}-arm64 --arch arm64 --variant v8
54+
55+# Debug: List manifests
56+echo "Listing manifests..."
57+$ENGINE manifest inspect $REPO:$TAG
58+
59+# Push multi-arch manifest
60+echo "Pushing multi-arch manifest..."
61+$ENGINE manifest push --all $REPO:$TAG
3762
38-docker manifest push $REPO:$TAG
63+echo "Script completed successfully."
@@ -1,11 +1,20 @@
1 #!/bin/bash1 #!/bin/bash
2-
3 set -e2 set -e
4-3+ENGINE="podman"
5 REPO="ghcr.io/bnema/gordon"4 REPO="ghcr.io/bnema/gordon"
6 TAG="dev"5 TAG="dev"
7 DIST_DIR="./dist"6 DIST_DIR="./dist"
8 7
8+# Function to handle errors
9+handle_error() {
10+ echo "Error occurred. Cleaning up..."
11+ $ENGINE system prune -f
12+ exit 1
13+}
14+
15+# Set up error handling
16+trap 'handle_error' ERR
17+
9 # Ensure dist directory exists18 # Ensure dist directory exists
10 mkdir -p $DIST_DIR19 mkdir -p $DIST_DIR
11 20
@@ -14,25 +23,41 @@ echo "Building Go binaries..."
14 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $DIST_DIR/gordon-linux-amd64 ./cmd/cli23 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $DIST_DIR/gordon-linux-amd64 ./cmd/cli
15 CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $DIST_DIR/gordon-linux-arm64 ./cmd/cli24 CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $DIST_DIR/gordon-linux-arm64 ./cmd/cli
16 25
26+# Clean up any dangling images before building
27+echo "Cleaning up dangling images..."
28+$ENGINE image prune -f
29+
17 # Build Docker images for each architecture30 # Build Docker images for each architecture
18 echo "Building Docker images..."31 echo "Building Docker images..."
19-docker build -t $REPO:${TAG}-amd64 --build-arg ARCH=amd64 -f Dockerfile .32+$ENGINE build -t $REPO:${TAG}-amd64 --build-arg ARCH=amd64 -f Dockerfile .
20-docker build -t $REPO:${TAG}-arm64v8 --build-arg ARCH=arm64 -f Dockerfile .33+$ENGINE build -t $REPO:${TAG}-arm64v8 --build-arg ARCH=arm64 -f Dockerfile .
21 34
22 # Push images35 # Push images
23 echo "Pushing Docker images..."36 echo "Pushing Docker images..."
24-docker push $REPO:${TAG}-amd6437+$ENGINE push $REPO:${TAG}-amd64
25-docker push $REPO:${TAG}-arm64v838+$ENGINE push $REPO:${TAG}-arm64v8
39+
40+# Remove existing manifest if it exists
41+echo "Removing existing manifest..."
42+$ENGINE manifest rm $REPO:$TAG || true
26 43
27-# Create and push multi-arch manifest44+# Create multi-arch manifest
28-echo "Creating and pushing multi-arch manifest..."45+echo "Creating multi-arch manifest..."
29-docker manifest create $REPO:$TAG \46+$ENGINE manifest create $REPO:$TAG \
30 $REPO:${TAG}-amd64 \47 $REPO:${TAG}-amd64 \
31- $REPO:${TAG}-arm64v8 \48+ $REPO:${TAG}-arm64
32- --amend
33 49
34 # Annotate the arm64 image with variant information50 # Annotate the arm64 image with variant information
35-docker manifest annotate $REPO:$TAG \51+echo "Annotating arm64 image..."
36- $REPO:${TAG}-arm64v8 --arch arm64 --variant v852+$ENGINE manifest annotate $REPO:$TAG \
53+ $REPO:${TAG}-arm64 --arch arm64 --variant v8
54+
55+# Debug: List manifests
56+echo "Listing manifests..."
57+$ENGINE manifest inspect $REPO:$TAG
58+
59+# Push multi-arch manifest
60+echo "Pushing multi-arch manifest..."
61+$ENGINE manifest push --all $REPO:$TAG
37 62
38-docker manifest push $REPO:$TAG63+echo "Script completed successfully."
M cmd/cli/main.go +1 -2
@@ -2,7 +2,6 @@ package main
22
33 import (
44 "fmt"
5- "log"
65 "regexp"
76
87 "github.com/bnema/gordon/internal/cli"
@@ -61,7 +60,7 @@ func main() {
6160 go func() {
6261 msg, err := common.CheckVersionPeriodically(&s.Config)
6362 if err != nil || msg != "" {
64- log.Println(msg)
63+ // log.Warnf("Error checking for new version: %s", err)
6564 }
6665 }()
6766
@@ -2,7 +2,6 @@ package main
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5- "log"
6 "regexp"5 "regexp"
7 6
8 "github.com/bnema/gordon/internal/cli"7 "github.com/bnema/gordon/internal/cli"
@@ -61,7 +60,7 @@ func main() {
61 go func() {60 go func() {
62 msg, err := common.CheckVersionPeriodically(&s.Config)61 msg, err := common.CheckVersionPeriodically(&s.Config)
63 if err != nil || msg != "" {62 if err != nil || msg != "" {
64- log.Println(msg)63+ // log.Warnf("Error checking for new version: %s", err)
65 }64 }
66 }()65 }()
67 66
M go.mod +4 -10
@@ -5,9 +5,7 @@ go 1.23
55 require (
66 github.com/AlecAivazis/survey/v2 v2.3.7
77 github.com/PuerkitoBio/goquery v1.9.2
8- github.com/charmbracelet/bubbles v0.19.0
9- github.com/charmbracelet/bubbletea v0.27.1
10- github.com/charmbracelet/lipgloss v0.13.0
8+ github.com/charmbracelet/log v0.4.0
119 github.com/docker/docker v27.1.2+incompatible
1210 github.com/docker/go-connections v0.5.0
1311 github.com/fatih/color v1.17.0
@@ -24,15 +22,14 @@ require (
2422
2523 require (
2624 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
27- github.com/charmbracelet/harmonica v0.2.0 // indirect
25+ github.com/charmbracelet/lipgloss v0.13.0 // indirect
2826 github.com/charmbracelet/x/ansi v0.2.3 // indirect
29- github.com/charmbracelet/x/term v0.2.0 // indirect
3027 github.com/containerd/log v0.1.0 // indirect
3128 github.com/creack/pty v1.1.18 // indirect
3229 github.com/distribution/reference v0.6.0 // indirect
3330 github.com/dustin/go-humanize v1.0.1 // indirect
34- github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
3531 github.com/felixge/httpsnoop v1.0.4 // indirect
32+ github.com/go-logfmt/logfmt v0.6.0 // indirect
3633 github.com/go-logr/logr v1.4.2 // indirect
3734 github.com/go-logr/stdr v1.2.2 // indirect
3835 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
@@ -40,12 +37,9 @@ require (
4037 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
4138 github.com/kr/pretty v0.3.1 // indirect
4239 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
43- github.com/mattn/go-localereader v0.0.1 // indirect
4440 github.com/mattn/go-runewidth v0.0.16 // indirect
4541 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
4642 github.com/moby/docker-image-spec v1.3.1 // indirect
47- github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
48- github.com/muesli/cancelreader v0.2.2 // indirect
4943 github.com/muesli/termenv v0.15.2 // indirect
5044 github.com/ncruces/go-strftime v0.1.9 // indirect
5145 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
@@ -57,7 +51,7 @@ require (
5751 go.opentelemetry.io/otel/metric v1.29.0 // indirect
5852 go.opentelemetry.io/otel/sdk v1.21.0 // indirect
5953 go.opentelemetry.io/otel/trace v1.29.0 // indirect
60- golang.org/x/sync v0.8.0 // indirect
54+ golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
6155 golang.org/x/term v0.23.0 // indirect
6256 google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
6357 modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
@@ -5,9 +5,7 @@ go 1.23
5 require (5 require (
6 github.com/AlecAivazis/survey/v2 v2.3.76 github.com/AlecAivazis/survey/v2 v2.3.7
7 github.com/PuerkitoBio/goquery v1.9.27 github.com/PuerkitoBio/goquery v1.9.2
8- github.com/charmbracelet/bubbles v0.19.08+ github.com/charmbracelet/log v0.4.0
9- github.com/charmbracelet/bubbletea v0.27.1
10- github.com/charmbracelet/lipgloss v0.13.0
11 github.com/docker/docker v27.1.2+incompatible9 github.com/docker/docker v27.1.2+incompatible
12 github.com/docker/go-connections v0.5.010 github.com/docker/go-connections v0.5.0
13 github.com/fatih/color v1.17.011 github.com/fatih/color v1.17.0
@@ -24,15 +22,14 @@ require (
24 22
25 require (23 require (
26 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect24 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
27- github.com/charmbracelet/harmonica v0.2.0 // indirect25+ github.com/charmbracelet/lipgloss v0.13.0 // indirect
28 github.com/charmbracelet/x/ansi v0.2.3 // indirect26 github.com/charmbracelet/x/ansi v0.2.3 // indirect
29- github.com/charmbracelet/x/term v0.2.0 // indirect
30 github.com/containerd/log v0.1.0 // indirect27 github.com/containerd/log v0.1.0 // indirect
31 github.com/creack/pty v1.1.18 // indirect28 github.com/creack/pty v1.1.18 // indirect
32 github.com/distribution/reference v0.6.0 // indirect29 github.com/distribution/reference v0.6.0 // indirect
33 github.com/dustin/go-humanize v1.0.1 // indirect30 github.com/dustin/go-humanize v1.0.1 // indirect
34- github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
35 github.com/felixge/httpsnoop v1.0.4 // indirect31 github.com/felixge/httpsnoop v1.0.4 // indirect
32+ github.com/go-logfmt/logfmt v0.6.0 // indirect
36 github.com/go-logr/logr v1.4.2 // indirect33 github.com/go-logr/logr v1.4.2 // indirect
37 github.com/go-logr/stdr v1.2.2 // indirect34 github.com/go-logr/stdr v1.2.2 // indirect
38 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect35 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
@@ -40,12 +37,9 @@ require (
40 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect37 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
41 github.com/kr/pretty v0.3.1 // indirect38 github.com/kr/pretty v0.3.1 // indirect
42 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect39 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
43- github.com/mattn/go-localereader v0.0.1 // indirect
44 github.com/mattn/go-runewidth v0.0.16 // indirect40 github.com/mattn/go-runewidth v0.0.16 // indirect
45 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect41 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
46 github.com/moby/docker-image-spec v1.3.1 // indirect42 github.com/moby/docker-image-spec v1.3.1 // indirect
47- github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
48- github.com/muesli/cancelreader v0.2.2 // indirect
49 github.com/muesli/termenv v0.15.2 // indirect43 github.com/muesli/termenv v0.15.2 // indirect
50 github.com/ncruces/go-strftime v0.1.9 // indirect44 github.com/ncruces/go-strftime v0.1.9 // indirect
51 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect45 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
@@ -57,7 +51,7 @@ require (
57 go.opentelemetry.io/otel/metric v1.29.0 // indirect51 go.opentelemetry.io/otel/metric v1.29.0 // indirect
58 go.opentelemetry.io/otel/sdk v1.21.0 // indirect52 go.opentelemetry.io/otel/sdk v1.21.0 // indirect
59 go.opentelemetry.io/otel/trace v1.29.0 // indirect53 go.opentelemetry.io/otel/trace v1.29.0 // indirect
60- golang.org/x/sync v0.8.0 // indirect54+ golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
61 golang.org/x/term v0.23.0 // indirect55 golang.org/x/term v0.23.0 // indirect
62 google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect56 google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
63 modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect57 modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
M go.sum +6 -17 generated file: collapsed
M internal/cli/cmd/deploy.go +105 -158
@@ -1,21 +1,21 @@
1+// gordon/internal/cli/cmd/deploy.go
2+
13 package cmd
24
35 import (
4- "crypto/tls"
6+ "encoding/json"
57 "fmt"
68 "io"
79 "net/http"
810 "os"
911 "strings"
10- "sync"
1112 "time"
1213
1314 "github.com/bnema/gordon/internal/cli"
1415 "github.com/bnema/gordon/internal/cli/handler"
15- "github.com/bnema/gordon/internal/cli/mvu"
1616 "github.com/bnema/gordon/internal/common"
1717 "github.com/bnema/gordon/pkg/docker"
18- "github.com/fatih/color"
18+ "github.com/charmbracelet/log"
1919 "github.com/spf13/cobra"
2020 )
2121
@@ -29,205 +29,152 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
2929 Args: cobra.ExactArgs(1),
3030 PreRun: func(cmd *cobra.Command, args []string) {
3131 if err := handler.FieldCheck(a); err != nil {
32- fmt.Println("Field check failed:", err)
32+ log.Error("Field check failed", "error", err)
3333 os.Exit(1)
3434 }
3535 },
3636 Run: func(cmd *cobra.Command, args []string) {
3737 imageName := args[0]
38- color.White("Pushing image: %s", imageName)
39-
40- // Validate the image name
41- if err := handler.ValidateImageName(imageName); err != nil {
42- fmt.Println(err)
43- return
44- }
45-
46- // Ensure the image has a tag
47- handler.EnsureImageTag(&imageName)
48-
49- // Validate the port mapping
50- if err := handler.ValidatePortMapping(port); err != nil {
51- fmt.Println(err)
52- return
53- }
38+ log.Info("Pushing image", "image", imageName)
5439
55- // Validate the target domain
56- if err := handler.ValidateTargetDomain(targetDomain); err != nil {
57- fmt.Println(err)
40+ if err := validateInputs(imageName, port, targetDomain); err != nil {
41+ log.Error("Validation failed", "error", err)
5842 return
5943 }
6044
61- // Export the image to a reader and return its true size
62- reader, actualSize, successMsg, err := exportDockerImage(imageName)
45+ reader, actualSize, err := exportDockerImage(imageName)
6346 if err != nil {
64- fmt.Println("Error exporting image:", err)
47+ log.Error("Error exporting image", "error", err)
6548 return
6649 }
67-
68- fmt.Println(successMsg)
69-
70- progressCh := make(chan mvu.ProgressMsg)
71- errCh := make(chan error, 1) // Buffer of 1 to prevent goroutine leak in case of non-blocking send
72-
73- // Create a progress function to update the progress bar
74- progressReader := &mvu.ProgressReader{
75- Reader: reader, // This is the actual reader from exportDockerImage
76- Total: actualSize, // This is the total size of the data to be read
77- ProgressCh: progressCh, // This is the channel used to send progress updates
78- }
79-
80- // Create a RequestPayload and populate it
81- reqPayload := common.RequestPayload{
82- Type: "deploy",
83- Payload: common.DeployPayload{
84- Ports: port,
85- TargetDomain: targetDomain,
86- ImageName: imageName,
87- Data: progressReader,
88- },
89- }
90-
91- var wg sync.WaitGroup
92- wg.Add(1)
93-
94- go func() {
95- defer wg.Done()
96- resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
97- if err != nil {
98- errCh <- fmt.Errorf("error sending HTTP request: %w", err)
99- return
100- }
101-
102- // Check the response
103- targetDomain := string(resp.Body)
104- targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
105- targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
106- // Close the progress channel after the upload is complete
107- // Determine if the target is HTTPS or HTTP
108- isHTTPS := strings.HasPrefix(targetDomain, "https://")
109-
110- // Initialize HTTP client
111- client := &http.Client{
112- Timeout: 60 * time.Second,
113- }
114-
115- // Only set TLS config if target is HTTPS
116- if isHTTPS {
117- client.Transport = &http.Transport{
118- TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
119- }
120- }
121-
122- // Run the TUI program
123- finalModel, err := mvu.RunDeploymentTUI(client, imageName, targetDomain, port)
124- if err != nil {
125- errCh <- fmt.Errorf("error running deployment TUI: %w", err)
126- return
50+ defer reader.Close()
51+
52+ log.Info("Image exported successfully", "image", imageName, "size", actualSize)
53+
54+ if err := deployImage(a, reader, imageName, port, targetDomain); err != nil {
55+ if deployErr, ok := err.(*common.DeploymentError); ok {
56+ log.Error("Deployment failed",
57+ "status_code", deployErr.StatusCode,
58+ "message", deployErr.Message,
59+ "raw_response", deployErr.RawResponse)
60+ } else {
61+ log.Error("Deployment failed", "error", err)
12762 }
128-
129- // Check if the deployment was successful
130- if !finalModel.DeploymentDone {
131- errCh <- fmt.Errorf("deployment failed")
132- return
133- }
134-
135- // Print the final message
136- color.Blue("Deployment successful!")
137- fmt.Println("Your application is now available at:", targetDomain)
138- close(progressCh)
139- }()
140- // Run the progress bar TUI
141- m, err := mvu.RunProgressBarTUI(progressCh)
142- if err != nil {
143- errCh <- fmt.Errorf("error running progress bar TUI: %w", err)
14463 return
14564 }
14665
147- // Wait for the deployment goroutine to complete
148- wg.Wait()
149-
150- done := false
151-
152- for !done {
153- select {
154- case err := <-errCh:
155- if err != nil {
156- fmt.Println(err)
157- return
158- }
159- case <-time.After(1 * time.Second):
160- // Check if the progress bar is done
161- if m.Done {
162- done = true
163- }
164- }
165- }
166-
167- // Check for errors from the deployment goroutine
168- select {
169- case err := <-errCh:
170- if err != nil {
171- fmt.Println(err)
172- return
173- }
174- default:
175- }
176-
177- // Check if the progress bar is done
178- if m.Done {
179- // Close the reader
180- err = progressReader.Close()
181- if err != nil {
182- fmt.Println("Error closing reader:", err)
183- return
184- }
185- } else {
186- fmt.Println("Deployment completed, but progress bar is not done.")
187- }
66+ log.Info("Deployment successful!")
18867 },
18968 }
19069
191- // Add flags
19270 deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container")
19371 deployCmd.Flags().StringVarP(&targetDomain, "target", "t", "", "Target domain for Traefik")
19472
19573 return deployCmd
19674 }
19775
198-// export the docker image to a reader and return its true size
199-func exportDockerImage(imageName string) (io.ReadCloser, int64, string, error) {
200- // Check if what the user submitted is a valid image ID
76+func validateInputs(imageName, port, targetDomain string) error {
77+ if err := handler.ValidateImageName(imageName); err != nil {
78+ return err
79+ }
80+ handler.EnsureImageTag(&imageName)
81+ if err := handler.ValidatePortMapping(port); err != nil {
82+ return err
83+ }
84+ return handler.ValidateTargetDomain(targetDomain)
85+}
86+
87+func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {
20188 exists, err := docker.CheckIfImageExists(imageName)
20289 if err != nil {
203- return nil, 0, "", fmt.Errorf("error while checking image existence: %w", err)
90+ return nil, 0, fmt.Errorf("error checking image existence: %w", err)
20491 }
20592
20693 var imageID string
20794 if exists {
208- // What the user submitted is a valid image ID
20995 imageID = imageName
21096 } else {
211- // What the user submitted is not a valid image ID, search by name
21297 imageID, err = docker.GetImageIDByName(imageName)
21398 if err != nil {
214- return nil, 0, "", fmt.Errorf("error while searching for image by name: %w", err)
99+ return nil, 0, fmt.Errorf("error searching for image by name: %w", err)
215100 }
216101 }
217102
218103 actualSize, err := docker.GetImageSizeFromReader(imageID)
219104 if err != nil {
220- return nil, 0, "", fmt.Errorf("error while retrieving image size: %w", err)
105+ return nil, 0, fmt.Errorf("error retrieving image size: %w", err)
221106 }
222107
223108 reader, err := docker.ExportImageFromEngine(imageID)
224109 if err != nil {
225- return nil, 0, "", fmt.Errorf("error while exporting image: %w", err)
110+ return nil, 0, fmt.Errorf("error exporting image: %w", err)
226111 }
227112
228- // Create a success message
229- successMsg := fmt.Sprintf("Image %s exported successfully", imageName)
113+ return reader, actualSize, nil
114+}
115+
116+func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error {
117+ reqPayload := common.RequestPayload{
118+ Type: "deploy",
119+ Payload: common.DeployPayload{
120+ Ports: port,
121+ TargetDomain: targetDomain,
122+ ImageName: imageName,
123+ Data: io.NopCloser(reader),
124+ },
125+ }
126+
127+ resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/deploy")
128+ if err != nil {
129+ return &common.DeploymentError{
130+ StatusCode: 0,
131+ Message: fmt.Sprintf("error sending HTTP request: %v", err),
132+ }
133+ }
134+
135+ var deployResponse common.DeployResponse
136+ if err := json.Unmarshal(resp.Body, &deployResponse); err != nil {
137+ return &common.DeploymentError{
138+ StatusCode: resp.StatusCode,
139+ Message: fmt.Sprintf("error parsing response: %v", err),
140+ RawResponse: string(resp.Body),
141+ }
142+ }
143+
144+ if !deployResponse.Success {
145+ return &common.DeploymentError{
146+ StatusCode: resp.StatusCode,
147+ Message: deployResponse.Message,
148+ RawResponse: string(resp.Body),
149+ }
150+ }
151+
152+ log.Info("Application deployed", "domain", deployResponse.Domain)
153+ return waitForDeployment(deployResponse.Domain)
154+}
155+
156+func waitForDeployment(domain string) error {
157+ client := &http.Client{Timeout: 10 * time.Second}
158+ maxRetries := 20
159+ retryInterval := time.Second
160+
161+ log.Info("Waiting for deployment to be reachable")
162+ for i := 0; i < maxRetries; i++ {
163+ resp, err := client.Get(domain)
164+ if err == nil {
165+ defer resp.Body.Close()
166+ if resp.StatusCode == http.StatusOK {
167+ return nil
168+ }
169+ // Check for error messages in the response body
170+ body, _ := io.ReadAll(resp.Body)
171+ if strings.Contains(string(body), "failed to create container:") {
172+ return fmt.Errorf("deployment failed: %s", string(body))
173+ }
174+ }
175+ log.Warn("Deployment not ready yet, retrying", "attempt", fmt.Sprintf("%d/20", i+1))
176+ time.Sleep(retryInterval)
177+ }
230178
231- // Return the reader, actual size, and success message
232- return reader, actualSize, successMsg, nil
179+ return fmt.Errorf("deployment not ready after %d attempts, giving up", maxRetries)
233180 }
@@ -1,21 +1,21 @@
1+// gordon/internal/cli/cmd/deploy.go
2+
1 package cmd3 package cmd
2 4
3 import (5 import (
4- "crypto/tls"6+ "encoding/json"
5 "fmt"7 "fmt"
6 "io"8 "io"
7 "net/http"9 "net/http"
8 "os"10 "os"
9 "strings"11 "strings"
10- "sync"
11 "time"12 "time"
12 13
13 "github.com/bnema/gordon/internal/cli"14 "github.com/bnema/gordon/internal/cli"
14 "github.com/bnema/gordon/internal/cli/handler"15 "github.com/bnema/gordon/internal/cli/handler"
15- "github.com/bnema/gordon/internal/cli/mvu"
16 "github.com/bnema/gordon/internal/common"16 "github.com/bnema/gordon/internal/common"
17 "github.com/bnema/gordon/pkg/docker"17 "github.com/bnema/gordon/pkg/docker"
18- "github.com/fatih/color"18+ "github.com/charmbracelet/log"
19 "github.com/spf13/cobra"19 "github.com/spf13/cobra"
20 )20 )
21 21
@@ -29,205 +29,152 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
29 Args: cobra.ExactArgs(1),29 Args: cobra.ExactArgs(1),
30 PreRun: func(cmd *cobra.Command, args []string) {30 PreRun: func(cmd *cobra.Command, args []string) {
31 if err := handler.FieldCheck(a); err != nil {31 if err := handler.FieldCheck(a); err != nil {
32- fmt.Println("Field check failed:", err)32+ log.Error("Field check failed", "error", err)
33 os.Exit(1)33 os.Exit(1)
34 }34 }
35 },35 },
36 Run: func(cmd *cobra.Command, args []string) {36 Run: func(cmd *cobra.Command, args []string) {
37 imageName := args[0]37 imageName := args[0]
38- color.White("Pushing image: %s", imageName)38+ log.Info("Pushing image", "image", imageName)
39-
40- // Validate the image name
41- if err := handler.ValidateImageName(imageName); err != nil {
42- fmt.Println(err)
43- return
44- }
45-
46- // Ensure the image has a tag
47- handler.EnsureImageTag(&imageName)
48-
49- // Validate the port mapping
50- if err := handler.ValidatePortMapping(port); err != nil {
51- fmt.Println(err)
52- return
53- }
54 39
55- // Validate the target domain40+ if err := validateInputs(imageName, port, targetDomain); err != nil {
56- if err := handler.ValidateTargetDomain(targetDomain); err != nil {41+ log.Error("Validation failed", "error", err)
57- fmt.Println(err)
58 return42 return
59 }43 }
60 44
61- // Export the image to a reader and return its true size45+ reader, actualSize, err := exportDockerImage(imageName)
62- reader, actualSize, successMsg, err := exportDockerImage(imageName)
63 if err != nil {46 if err != nil {
64- fmt.Println("Error exporting image:", err)47+ log.Error("Error exporting image", "error", err)
65 return48 return
66 }49 }
67-50+ defer reader.Close()
68- fmt.Println(successMsg)51+
69-52+ log.Info("Image exported successfully", "image", imageName, "size", actualSize)
70- progressCh := make(chan mvu.ProgressMsg)53+
71- errCh := make(chan error, 1) // Buffer of 1 to prevent goroutine leak in case of non-blocking send54+ if err := deployImage(a, reader, imageName, port, targetDomain); err != nil {
72-55+ if deployErr, ok := err.(*common.DeploymentError); ok {
73- // Create a progress function to update the progress bar56+ log.Error("Deployment failed",
74- progressReader := &mvu.ProgressReader{57+ "status_code", deployErr.StatusCode,
75- Reader: reader, // This is the actual reader from exportDockerImage58+ "message", deployErr.Message,
76- Total: actualSize, // This is the total size of the data to be read59+ "raw_response", deployErr.RawResponse)
77- ProgressCh: progressCh, // This is the channel used to send progress updates60+ } else {
78- }61+ log.Error("Deployment failed", "error", err)
79-
80- // Create a RequestPayload and populate it
81- reqPayload := common.RequestPayload{
82- Type: "deploy",
83- Payload: common.DeployPayload{
84- Ports: port,
85- TargetDomain: targetDomain,
86- ImageName: imageName,
87- Data: progressReader,
88- },
89- }
90-
91- var wg sync.WaitGroup
92- wg.Add(1)
93-
94- go func() {
95- defer wg.Done()
96- resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
97- if err != nil {
98- errCh <- fmt.Errorf("error sending HTTP request: %w", err)
99- return
100- }
101-
102- // Check the response
103- targetDomain := string(resp.Body)
104- targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
105- targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
106- // Close the progress channel after the upload is complete
107- // Determine if the target is HTTPS or HTTP
108- isHTTPS := strings.HasPrefix(targetDomain, "https://")
109-
110- // Initialize HTTP client
111- client := &http.Client{
112- Timeout: 60 * time.Second,
113- }
114-
115- // Only set TLS config if target is HTTPS
116- if isHTTPS {
117- client.Transport = &http.Transport{
118- TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
119- }
120- }
121-
122- // Run the TUI program
123- finalModel, err := mvu.RunDeploymentTUI(client, imageName, targetDomain, port)
124- if err != nil {
125- errCh <- fmt.Errorf("error running deployment TUI: %w", err)
126- return
127 }62 }
128-
129- // Check if the deployment was successful
130- if !finalModel.DeploymentDone {
131- errCh <- fmt.Errorf("deployment failed")
132- return
133- }
134-
135- // Print the final message
136- color.Blue("Deployment successful!")
137- fmt.Println("Your application is now available at:", targetDomain)
138- close(progressCh)
139- }()
140- // Run the progress bar TUI
141- m, err := mvu.RunProgressBarTUI(progressCh)
142- if err != nil {
143- errCh <- fmt.Errorf("error running progress bar TUI: %w", err)
144 return63 return
145 }64 }
146 65
147- // Wait for the deployment goroutine to complete66+ log.Info("Deployment successful!")
148- wg.Wait()
149-
150- done := false
151-
152- for !done {
153- select {
154- case err := <-errCh:
155- if err != nil {
156- fmt.Println(err)
157- return
158- }
159- case <-time.After(1 * time.Second):
160- // Check if the progress bar is done
161- if m.Done {
162- done = true
163- }
164- }
165- }
166-
167- // Check for errors from the deployment goroutine
168- select {
169- case err := <-errCh:
170- if err != nil {
171- fmt.Println(err)
172- return
173- }
174- default:
175- }
176-
177- // Check if the progress bar is done
178- if m.Done {
179- // Close the reader
180- err = progressReader.Close()
181- if err != nil {
182- fmt.Println("Error closing reader:", err)
183- return
184- }
185- } else {
186- fmt.Println("Deployment completed, but progress bar is not done.")
187- }
188 },67 },
189 }68 }
190 69
191- // Add flags
192 deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container")70 deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container")
193 deployCmd.Flags().StringVarP(&targetDomain, "target", "t", "", "Target domain for Traefik")71 deployCmd.Flags().StringVarP(&targetDomain, "target", "t", "", "Target domain for Traefik")
194 72
195 return deployCmd73 return deployCmd
196 }74 }
197 75
198-// export the docker image to a reader and return its true size76+func validateInputs(imageName, port, targetDomain string) error {
199-func exportDockerImage(imageName string) (io.ReadCloser, int64, string, error) {77+ if err := handler.ValidateImageName(imageName); err != nil {
200- // Check if what the user submitted is a valid image ID78+ return err
79+ }
80+ handler.EnsureImageTag(&imageName)
81+ if err := handler.ValidatePortMapping(port); err != nil {
82+ return err
83+ }
84+ return handler.ValidateTargetDomain(targetDomain)
85+}
86+
87+func exportDockerImage(imageName string) (io.ReadCloser, int64, error) {
201 exists, err := docker.CheckIfImageExists(imageName)88 exists, err := docker.CheckIfImageExists(imageName)
202 if err != nil {89 if err != nil {
203- return nil, 0, "", fmt.Errorf("error while checking image existence: %w", err)90+ return nil, 0, fmt.Errorf("error checking image existence: %w", err)
204 }91 }
205 92
206 var imageID string93 var imageID string
207 if exists {94 if exists {
208- // What the user submitted is a valid image ID
209 imageID = imageName95 imageID = imageName
210 } else {96 } else {
211- // What the user submitted is not a valid image ID, search by name
212 imageID, err = docker.GetImageIDByName(imageName)97 imageID, err = docker.GetImageIDByName(imageName)
213 if err != nil {98 if err != nil {
214- return nil, 0, "", fmt.Errorf("error while searching for image by name: %w", err)99+ return nil, 0, fmt.Errorf("error searching for image by name: %w", err)
215 }100 }
216 }101 }
217 102
218 actualSize, err := docker.GetImageSizeFromReader(imageID)103 actualSize, err := docker.GetImageSizeFromReader(imageID)
219 if err != nil {104 if err != nil {
220- return nil, 0, "", fmt.Errorf("error while retrieving image size: %w", err)105+ return nil, 0, fmt.Errorf("error retrieving image size: %w", err)
221 }106 }
222 107
223 reader, err := docker.ExportImageFromEngine(imageID)108 reader, err := docker.ExportImageFromEngine(imageID)
224 if err != nil {109 if err != nil {
225- return nil, 0, "", fmt.Errorf("error while exporting image: %w", err)110+ return nil, 0, fmt.Errorf("error exporting image: %w", err)
226 }111 }
227 112
228- // Create a success message113+ return reader, actualSize, nil
229- successMsg := fmt.Sprintf("Image %s exported successfully", imageName)114+}
115+
116+func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error {
117+ reqPayload := common.RequestPayload{
118+ Type: "deploy",
119+ Payload: common.DeployPayload{
120+ Ports: port,
121+ TargetDomain: targetDomain,
122+ ImageName: imageName,
123+ Data: io.NopCloser(reader),
124+ },
125+ }
126+
127+ resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/deploy")
128+ if err != nil {
129+ return &common.DeploymentError{
130+ StatusCode: 0,
131+ Message: fmt.Sprintf("error sending HTTP request: %v", err),
132+ }
133+ }
134+
135+ var deployResponse common.DeployResponse
136+ if err := json.Unmarshal(resp.Body, &deployResponse); err != nil {
137+ return &common.DeploymentError{
138+ StatusCode: resp.StatusCode,
139+ Message: fmt.Sprintf("error parsing response: %v", err),
140+ RawResponse: string(resp.Body),
141+ }
142+ }
143+
144+ if !deployResponse.Success {
145+ return &common.DeploymentError{
146+ StatusCode: resp.StatusCode,
147+ Message: deployResponse.Message,
148+ RawResponse: string(resp.Body),
149+ }
150+ }
151+
152+ log.Info("Application deployed", "domain", deployResponse.Domain)
153+ return waitForDeployment(deployResponse.Domain)
154+}
155+
156+func waitForDeployment(domain string) error {
157+ client := &http.Client{Timeout: 10 * time.Second}
158+ maxRetries := 20
159+ retryInterval := time.Second
160+
161+ log.Info("Waiting for deployment to be reachable")
162+ for i := 0; i < maxRetries; i++ {
163+ resp, err := client.Get(domain)
164+ if err == nil {
165+ defer resp.Body.Close()
166+ if resp.StatusCode == http.StatusOK {
167+ return nil
168+ }
169+ // Check for error messages in the response body
170+ body, _ := io.ReadAll(resp.Body)
171+ if strings.Contains(string(body), "failed to create container:") {
172+ return fmt.Errorf("deployment failed: %s", string(body))
173+ }
174+ }
175+ log.Warn("Deployment not ready yet, retrying", "attempt", fmt.Sprintf("%d/20", i+1))
176+ time.Sleep(retryInterval)
177+ }
230 178
231- // Return the reader, actual size, and success message179+ return fmt.Errorf("deployment not ready after %d attempts, giving up", maxRetries)
232- return reader, actualSize, successMsg, nil
233 }180 }
M internal/cli/cmd/push.go +53 -83
@@ -1,119 +1,89 @@
11 package cmd
22
33 import (
4- "bytes"
5- "crypto/tls"
4+ "encoding/json"
65 "fmt"
7- "io"
8- "net/http"
96 "os"
10- "strings"
11- "sync"
12- "time"
137
148 "github.com/bnema/gordon/internal/cli"
159 "github.com/bnema/gordon/internal/cli/handler"
1610 "github.com/bnema/gordon/internal/common"
17- "github.com/fatih/color"
11+ "github.com/charmbracelet/log"
1812 "github.com/spf13/cobra"
1913 )
2014
2115 func NewPushCommand(a *cli.App) *cobra.Command {
16+ var port string
17+
2218 pushCmd := &cobra.Command{
2319 Use: "push [image:tag]",
2420 Short: "Push an image to your remote Gordon instance",
2521 Args: cobra.ExactArgs(1),
2622 PreRun: func(cmd *cobra.Command, args []string) {
2723 if err := handler.FieldCheck(a); err != nil {
28- fmt.Println("Field check failed:", err)
24+ log.Error("Field check failed", "error", err)
2925 os.Exit(1)
3026 }
3127 },
3228 Run: func(cmd *cobra.Command, args []string) {
3329 imageName := args[0]
34- color.White("Pushing image: %s", imageName)
30+ log.Info("Pushing image", "image", imageName)
3531
36- // Validate the image name
37- if err := handler.ValidateImageName(imageName); err != nil {
38- fmt.Println(err)
39- return
32+ if err := pushImage(a, imageName, port); err != nil {
33+ log.Error("Push failed", "error", err)
34+ os.Exit(1)
4035 }
36+ },
37+ }
4138
42- // Ensure the image has a tag
43- handler.EnsureImageTag(&imageName)
39+ pushCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container (e.g., 8080:80/tcp)")
4440
45- // Export the image to a reader and return its true size
46- reader, _, successMsg, err := exportDockerImage(imageName)
47- if err != nil {
48- fmt.Println("Error exporting image:", err)
49- return
50- }
41+ return pushCmd
42+}
5143
52- fmt.Println(successMsg)
44+func pushImage(a *cli.App, imageName, port string) error {
45+ if err := handler.ValidateImageName(imageName); err != nil {
46+ return fmt.Errorf("invalid image name: %w", err)
47+ }
5348
54- // Create a progress bar
55- progressBar := cmd.OutOrStdout()
56- fmt.Fprintf(progressBar, "Uploading image... ")
49+ handler.EnsureImageTag(&imageName)
5750
58- // Create a RequestPayload and populate it
59- reqPayload := common.RequestPayload{
60- Type: "push",
61- Payload: common.PushPayload{
62- ImageName: imageName,
63- Data: reader,
64- },
65- }
51+ if port != "" {
52+ if err := handler.ValidatePortMapping(port); err != nil {
53+ return fmt.Errorf("invalid port mapping: %w", err)
54+ }
55+ }
6656
67- var wg sync.WaitGroup
68- wg.Add(1)
69-
70- go func() {
71- defer wg.Done()
72- resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
73- if err != nil {
74- fmt.Println("Error sending request:", err)
75- return
76- }
77-
78- if resp.StatusCode != http.StatusOK {
79- bodyBytes, _ := io.ReadAll(bytes.NewReader(resp.Body))
80- fmt.Fprintln(cmd.OutOrStderr(), "Server returned an error:", string(bodyBytes))
81- return
82- }
83-
84- // Check the response
85- targetDomain := string(resp.Body)
86- targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
87- targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
88- // Close the progress channel after the upload is complete
89- // Determine if the target is HTTPS or HTTP
90- isHTTPS := strings.HasPrefix(targetDomain, "https://")
91-
92- // Initialize HTTP client
93- client := &http.Client{
94- Timeout: 10 * time.Second,
95- }
96-
97- // Only set TLS config if target is HTTPS
98- if isHTTPS {
99- client.Transport = &http.Transport{
100- TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
101- }
102- }
103-
104- }()
105-
106- // Wait for the HTTP request to finish
107- wg.Wait()
108-
109- // Close the reader
110- err = reader.Close()
111- if err != nil {
112- fmt.Println("Error closing reader:", err)
113- return
114- }
57+ reader, actualSize, err := exportDockerImage(imageName)
58+ if err != nil {
59+ return fmt.Errorf("error exporting image: %w", err)
60+ }
61+ defer reader.Close()
62+
63+ log.Info("Image exported successfully", "image", imageName, "size", actualSize)
64+
65+ reqPayload := common.RequestPayload{
66+ Type: "push",
67+ Payload: common.PushPayload{
68+ ImageName: imageName,
69+ Data: reader,
11570 },
11671 }
11772
118- return pushCmd
73+ resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
74+ if err != nil {
75+ return fmt.Errorf("error sending HTTP request: %w", err)
76+ }
77+
78+ var pushResponse common.PushResponse
79+ if err := json.Unmarshal(resp.Body, &pushResponse); err != nil {
80+ return fmt.Errorf("error parsing response: %w", err)
81+ }
82+
83+ if !pushResponse.Success {
84+ return fmt.Errorf("push failed: %s", pushResponse.Message)
85+ }
86+
87+ log.Info("Image pushed successfully", "image", imageName)
88+ return nil
11989 }
@@ -1,119 +1,89 @@
1 package cmd1 package cmd
2 2
3 import (3 import (
4- "bytes"4+ "encoding/json"
5- "crypto/tls"
6 "fmt"5 "fmt"
7- "io"
8- "net/http"
9 "os"6 "os"
10- "strings"
11- "sync"
12- "time"
13 7
14 "github.com/bnema/gordon/internal/cli"8 "github.com/bnema/gordon/internal/cli"
15 "github.com/bnema/gordon/internal/cli/handler"9 "github.com/bnema/gordon/internal/cli/handler"
16 "github.com/bnema/gordon/internal/common"10 "github.com/bnema/gordon/internal/common"
17- "github.com/fatih/color"11+ "github.com/charmbracelet/log"
18 "github.com/spf13/cobra"12 "github.com/spf13/cobra"
19 )13 )
20 14
21 func NewPushCommand(a *cli.App) *cobra.Command {15 func NewPushCommand(a *cli.App) *cobra.Command {
16+ var port string
17+
22 pushCmd := &cobra.Command{18 pushCmd := &cobra.Command{
23 Use: "push [image:tag]",19 Use: "push [image:tag]",
24 Short: "Push an image to your remote Gordon instance",20 Short: "Push an image to your remote Gordon instance",
25 Args: cobra.ExactArgs(1),21 Args: cobra.ExactArgs(1),
26 PreRun: func(cmd *cobra.Command, args []string) {22 PreRun: func(cmd *cobra.Command, args []string) {
27 if err := handler.FieldCheck(a); err != nil {23 if err := handler.FieldCheck(a); err != nil {
28- fmt.Println("Field check failed:", err)24+ log.Error("Field check failed", "error", err)
29 os.Exit(1)25 os.Exit(1)
30 }26 }
31 },27 },
32 Run: func(cmd *cobra.Command, args []string) {28 Run: func(cmd *cobra.Command, args []string) {
33 imageName := args[0]29 imageName := args[0]
34- color.White("Pushing image: %s", imageName)30+ log.Info("Pushing image", "image", imageName)
35 31
36- // Validate the image name32+ if err := pushImage(a, imageName, port); err != nil {
37- if err := handler.ValidateImageName(imageName); err != nil {33+ log.Error("Push failed", "error", err)
38- fmt.Println(err)34+ os.Exit(1)
39- return
40 }35 }
36+ },
37+ }
41 38
42- // Ensure the image has a tag39+ pushCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container (e.g., 8080:80/tcp)")
43- handler.EnsureImageTag(&imageName)
44 40
45- // Export the image to a reader and return its true size41+ return pushCmd
46- reader, _, successMsg, err := exportDockerImage(imageName)42+}
47- if err != nil {
48- fmt.Println("Error exporting image:", err)
49- return
50- }
51 43
52- fmt.Println(successMsg)44+func pushImage(a *cli.App, imageName, port string) error {
45+ if err := handler.ValidateImageName(imageName); err != nil {
46+ return fmt.Errorf("invalid image name: %w", err)
47+ }
53 48
54- // Create a progress bar49+ handler.EnsureImageTag(&imageName)
55- progressBar := cmd.OutOrStdout()
56- fmt.Fprintf(progressBar, "Uploading image... ")
57 50
58- // Create a RequestPayload and populate it51+ if port != "" {
59- reqPayload := common.RequestPayload{52+ if err := handler.ValidatePortMapping(port); err != nil {
60- Type: "push",53+ return fmt.Errorf("invalid port mapping: %w", err)
61- Payload: common.PushPayload{54+ }
62- ImageName: imageName,55+ }
63- Data: reader,
64- },
65- }
66 56
67- var wg sync.WaitGroup57+ reader, actualSize, err := exportDockerImage(imageName)
68- wg.Add(1)58+ if err != nil {
69-59+ return fmt.Errorf("error exporting image: %w", err)
70- go func() {60+ }
71- defer wg.Done()61+ defer reader.Close()
72- resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")62+
73- if err != nil {63+ log.Info("Image exported successfully", "image", imageName, "size", actualSize)
74- fmt.Println("Error sending request:", err)64+
75- return65+ reqPayload := common.RequestPayload{
76- }66+ Type: "push",
77-67+ Payload: common.PushPayload{
78- if resp.StatusCode != http.StatusOK {68+ ImageName: imageName,
79- bodyBytes, _ := io.ReadAll(bytes.NewReader(resp.Body))69+ Data: reader,
80- fmt.Fprintln(cmd.OutOrStderr(), "Server returned an error:", string(bodyBytes))
81- return
82- }
83-
84- // Check the response
85- targetDomain := string(resp.Body)
86- targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace
87- targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes
88- // Close the progress channel after the upload is complete
89- // Determine if the target is HTTPS or HTTP
90- isHTTPS := strings.HasPrefix(targetDomain, "https://")
91-
92- // Initialize HTTP client
93- client := &http.Client{
94- Timeout: 10 * time.Second,
95- }
96-
97- // Only set TLS config if target is HTTPS
98- if isHTTPS {
99- client.Transport = &http.Transport{
100- TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated
101- }
102- }
103-
104- }()
105-
106- // Wait for the HTTP request to finish
107- wg.Wait()
108-
109- // Close the reader
110- err = reader.Close()
111- if err != nil {
112- fmt.Println("Error closing reader:", err)
113- return
114- }
115 },70 },
116 }71 }
117 72
118- return pushCmd73+ resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push")
74+ if err != nil {
75+ return fmt.Errorf("error sending HTTP request: %w", err)
76+ }
77+
78+ var pushResponse common.PushResponse
79+ if err := json.Unmarshal(resp.Body, &pushResponse); err != nil {
80+ return fmt.Errorf("error parsing response: %w", err)
81+ }
82+
83+ if !pushResponse.Success {
84+ return fmt.Errorf("push failed: %s", pushResponse.Message)
85+ }
86+
87+ log.Info("Image pushed successfully", "image", imageName)
88+ return nil
119 }89 }
M internal/cli/handler/startserver.go +3 -3
@@ -2,7 +2,6 @@ package handler
22
33 import (
44 "fmt"
5- "log"
65 "os"
76 "os/signal"
87 "syscall"
@@ -10,6 +9,7 @@ import (
109
1110 "github.com/bnema/gordon/internal/httpserve"
1211 "github.com/bnema/gordon/internal/server"
12+ "github.com/charmbracelet/log"
1313 "github.com/labstack/echo/v4"
1414 )
1515
@@ -35,7 +35,7 @@ func StartServer(a *server.App, port string) error {
3535
3636 go func() {
3737 sig := <-sigs
38- log.Println("Received signal:", sig)
38+ log.Info(fmt.Println("Received signal:", sig))
3939 os.Exit(0)
4040 }()
4141
@@ -46,7 +46,7 @@ func StartServer(a *server.App, port string) error {
4646 e.HidePort = true
4747 e = httpserve.RegisterRoutes(e, a)
4848
49- log.Println("Starting server on port", a.Config.Http.Port)
49+ log.Info(fmt.Sprintf("Starting server on port %s", port))
5050 if err := e.Start(fmt.Sprintf(":%s", a.Config.Http.Port)); err != nil {
5151 log.Fatal(err)
5252 }
@@ -2,7 +2,6 @@ package handler
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5- "log"
6 "os"5 "os"
7 "os/signal"6 "os/signal"
8 "syscall"7 "syscall"
@@ -10,6 +9,7 @@ import (
10 9
11 "github.com/bnema/gordon/internal/httpserve"10 "github.com/bnema/gordon/internal/httpserve"
12 "github.com/bnema/gordon/internal/server"11 "github.com/bnema/gordon/internal/server"
12+ "github.com/charmbracelet/log"
13 "github.com/labstack/echo/v4"13 "github.com/labstack/echo/v4"
14 )14 )
15 15
@@ -35,7 +35,7 @@ func StartServer(a *server.App, port string) error {
35 35
36 go func() {36 go func() {
37 sig := <-sigs37 sig := <-sigs
38- log.Println("Received signal:", sig)38+ log.Info(fmt.Println("Received signal:", sig))
39 os.Exit(0)39 os.Exit(0)
40 }()40 }()
41 41
@@ -46,7 +46,7 @@ func StartServer(a *server.App, port string) error {
46 e.HidePort = true46 e.HidePort = true
47 e = httpserve.RegisterRoutes(e, a)47 e = httpserve.RegisterRoutes(e, a)
48 48
49- log.Println("Starting server on port", a.Config.Http.Port)49+ log.Info(fmt.Sprintf("Starting server on port %s", port))
50 if err := e.Start(fmt.Sprintf(":%s", a.Config.Http.Port)); err != nil {50 if err := e.Start(fmt.Sprintf(":%s", a.Config.Http.Port)); err != nil {
51 log.Fatal(err)51 log.Fatal(err)
52 }52 }
D internal/cli/mvu/progressbar.go +0 -107
@@ -1,107 +0,0 @@
1-package mvu
2-
3-import (
4- "io"
5- "strings"
6-
7- "github.com/charmbracelet/bubbles/progress"
8- tea "github.com/charmbracelet/bubbletea"
9- "github.com/charmbracelet/lipgloss"
10-)
11-
12-const (
13- padding = 2
14- maxWidth = 80
15-)
16-
17-var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
18-
19-type ProgressMsg float64
20-
21-type Model struct {
22- progress progress.Model
23- Done bool
24-}
25-
26-type ProgressReader struct {
27- Reader io.ReadCloser
28- Total int64
29- ReadBytes int64
30- ProgressCh chan<- ProgressMsg
31-}
32-
33-func NewPBModel() Model {
34- m := progress.New(progress.WithGradient("#007BC0", "#011E5C"))
35- m.Width = maxWidth
36-
37- return Model{
38- progress: m,
39- }
40-}
41-
42-func (pr *ProgressReader) Read(p []byte) (int, error) {
43- n, err := pr.Reader.Read(p)
44- if err != nil {
45- return n, err
46- }
47-
48- pr.ReadBytes += int64(n)
49- percentage := float64(pr.ReadBytes) / float64(pr.Total)
50- pr.ProgressCh <- ProgressMsg(percentage) // Send the percentage on the channel.
51- return n, err
52-}
53-
54-func (pr *ProgressReader) Close() error {
55- if closer, ok := pr.Reader.(io.Closer); ok {
56- return closer.Close()
57- }
58- return nil
59-}
60-
61-func (m Model) Init() tea.Cmd {
62- m.progress.Init()
63- m.progress.SetPercent(0)
64- return nil
65-}
66-
67-func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
68- switch msg := msg.(type) {
69- case tea.KeyMsg:
70- return m, tea.Quit
71-
72- case tea.WindowSizeMsg:
73- m.progress.Width = msg.Width - padding*2 - 4
74- if m.progress.Width > maxWidth {
75- m.progress.Width = maxWidth
76- }
77- return m, nil
78-
79- case ProgressMsg:
80- // Update the progress bar with the percentage received on the channel.
81- cmd := m.progress.SetPercent(float64(msg))
82-
83- // If the percentage is 100, set Done to true.
84- if msg == 1 {
85- m.Done = true
86- // quit
87- return m, tea.Quit
88- }
89-
90- return m, cmd
91-
92- case progress.FrameMsg:
93- progressModel, cmd := m.progress.Update(msg)
94- m.progress = progressModel.(progress.Model)
95- return m, cmd
96-
97- default:
98- return m, nil
99- }
100-}
101-
102-func (m Model) View() string {
103- pad := strings.Repeat(" ", padding)
104- return "\n" +
105- pad + m.progress.View() + "\n\n" +
106- pad + helpStyle("Press any key to quit")
107-}
@@ -1,107 +0,0 @@
1-package mvu
2-
3-import (
4- "io"
5- "strings"
6-
7- "github.com/charmbracelet/bubbles/progress"
8- tea "github.com/charmbracelet/bubbletea"
9- "github.com/charmbracelet/lipgloss"
10-)
11-
12-const (
13- padding = 2
14- maxWidth = 80
15-)
16-
17-var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
18-
19-type ProgressMsg float64
20-
21-type Model struct {
22- progress progress.Model
23- Done bool
24-}
25-
26-type ProgressReader struct {
27- Reader io.ReadCloser
28- Total int64
29- ReadBytes int64
30- ProgressCh chan<- ProgressMsg
31-}
32-
33-func NewPBModel() Model {
34- m := progress.New(progress.WithGradient("#007BC0", "#011E5C"))
35- m.Width = maxWidth
36-
37- return Model{
38- progress: m,
39- }
40-}
41-
42-func (pr *ProgressReader) Read(p []byte) (int, error) {
43- n, err := pr.Reader.Read(p)
44- if err != nil {
45- return n, err
46- }
47-
48- pr.ReadBytes += int64(n)
49- percentage := float64(pr.ReadBytes) / float64(pr.Total)
50- pr.ProgressCh <- ProgressMsg(percentage) // Send the percentage on the channel.
51- return n, err
52-}
53-
54-func (pr *ProgressReader) Close() error {
55- if closer, ok := pr.Reader.(io.Closer); ok {
56- return closer.Close()
57- }
58- return nil
59-}
60-
61-func (m Model) Init() tea.Cmd {
62- m.progress.Init()
63- m.progress.SetPercent(0)
64- return nil
65-}
66-
67-func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
68- switch msg := msg.(type) {
69- case tea.KeyMsg:
70- return m, tea.Quit
71-
72- case tea.WindowSizeMsg:
73- m.progress.Width = msg.Width - padding*2 - 4
74- if m.progress.Width > maxWidth {
75- m.progress.Width = maxWidth
76- }
77- return m, nil
78-
79- case ProgressMsg:
80- // Update the progress bar with the percentage received on the channel.
81- cmd := m.progress.SetPercent(float64(msg))
82-
83- // If the percentage is 100, set Done to true.
84- if msg == 1 {
85- m.Done = true
86- // quit
87- return m, tea.Quit
88- }
89-
90- return m, cmd
91-
92- case progress.FrameMsg:
93- progressModel, cmd := m.progress.Update(msg)
94- m.progress = progressModel.(progress.Model)
95- return m, cmd
96-
97- default:
98- return m, nil
99- }
100-}
101-
102-func (m Model) View() string {
103- pad := strings.Repeat(" ", padding)
104- return "\n" +
105- pad + m.progress.View() + "\n\n" +
106- pad + helpStyle("Press any key to quit")
107-}
D internal/cli/mvu/run.go +0 -50
@@ -1,50 +0,0 @@
1-package mvu
2-
3-import (
4- "fmt"
5- "net/http"
6- "os"
7-
8- tea "github.com/charmbracelet/bubbletea"
9-)
10-
11-// RunDeploymentTUI runs the complete deployment TUI and returns the final model
12-func RunDeploymentTUI(client *http.Client, imageName, targetDomain, port string) (SpinnerModel, error) {
13- m := NewSpinnerModel()
14- m.deployment.imageName = imageName
15- m.deployment.targetDomain = targetDomain
16- m.deployment.port = port
17- m.deployment.client = client
18-
19- p := tea.NewProgram(m)
20- modelInterface, err := p.Run()
21- if err != nil {
22- return SpinnerModel{}, fmt.Errorf("error running TUI program: %w", err)
23- }
24-
25- finalModel, ok := modelInterface.(SpinnerModel)
26- if !ok {
27- return SpinnerModel{}, fmt.Errorf("could not type assert tea model to concrete type")
28- }
29-
30- return finalModel, nil
31-}
32-
33-// RunProgressBarTUI runs the progress bar TUI and returns the final model
34-func RunProgressBarTUI(ProgressCh <-chan ProgressMsg) (*Model, error) {
35- m := NewPBModel()
36- p := tea.NewProgram(&m)
37- // Start a goroutine that updates the progress bar as percentages are received on the channel.
38- go func() {
39- for percent := range ProgressCh {
40- p.Send(ProgressMsg(percent))
41- }
42- }()
43-
44- if _, err := p.Run(); err != nil {
45- fmt.Println("Error running progress bar TUI:", err)
46- os.Exit(1)
47- }
48-
49- return &m, nil
50-}
@@ -1,50 +0,0 @@
1-package mvu
2-
3-import (
4- "fmt"
5- "net/http"
6- "os"
7-
8- tea "github.com/charmbracelet/bubbletea"
9-)
10-
11-// RunDeploymentTUI runs the complete deployment TUI and returns the final model
12-func RunDeploymentTUI(client *http.Client, imageName, targetDomain, port string) (SpinnerModel, error) {
13- m := NewSpinnerModel()
14- m.deployment.imageName = imageName
15- m.deployment.targetDomain = targetDomain
16- m.deployment.port = port
17- m.deployment.client = client
18-
19- p := tea.NewProgram(m)
20- modelInterface, err := p.Run()
21- if err != nil {
22- return SpinnerModel{}, fmt.Errorf("error running TUI program: %w", err)
23- }
24-
25- finalModel, ok := modelInterface.(SpinnerModel)
26- if !ok {
27- return SpinnerModel{}, fmt.Errorf("could not type assert tea model to concrete type")
28- }
29-
30- return finalModel, nil
31-}
32-
33-// RunProgressBarTUI runs the progress bar TUI and returns the final model
34-func RunProgressBarTUI(ProgressCh <-chan ProgressMsg) (*Model, error) {
35- m := NewPBModel()
36- p := tea.NewProgram(&m)
37- // Start a goroutine that updates the progress bar as percentages are received on the channel.
38- go func() {
39- for percent := range ProgressCh {
40- p.Send(ProgressMsg(percent))
41- }
42- }()
43-
44- if _, err := p.Run(); err != nil {
45- fmt.Println("Error running progress bar TUI:", err)
46- os.Exit(1)
47- }
48-
49- return &m, nil
50-}
D internal/cli/mvu/spinner.go +0 -137
@@ -1,137 +0,0 @@
1-package mvu
2-
3-import (
4- "crypto/tls"
5- "fmt"
6- "net/http"
7- "time"
8-
9- "github.com/charmbracelet/bubbles/spinner"
10- tea "github.com/charmbracelet/bubbletea"
11- "github.com/charmbracelet/lipgloss"
12-)
13-
14-const (
15- defaultMaxRetries = 20
16- httpTimeout = 10 * time.Second
17- retryInterval = 1 * time.Second
18-)
19-
20-type SpinnerModel struct {
21- spinner spinner.Model
22- quitting bool
23- err error
24- deployment DeploymentInfo
25- DeploymentDone bool
26-}
27-
28-type DeploymentInfo struct {
29- imageName string
30- targetDomain string
31- port string
32- client *http.Client
33- retryCount int
34- maxRetries int
35-}
36-
37-type urlCheckedMsg struct {
38- success bool
39- err error
40-}
41-
42-// NewSpinnerModel returns a new SpinnerModel
43-func NewSpinnerModel() SpinnerModel {
44- s := spinner.New()
45- s.Spinner = spinner.Dot
46- s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#54baff"))
47-
48- return SpinnerModel{
49- spinner: s,
50- deployment: DeploymentInfo{
51- maxRetries: defaultMaxRetries,
52- client: newHTTPClient(),
53- },
54- }
55-}
56-
57-func newHTTPClient() *http.Client {
58- return &http.Client{
59- Timeout: httpTimeout,
60- Transport: &http.Transport{
61- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
62- },
63- }
64-}
65-
66-func (m SpinnerModel) Init() tea.Cmd {
67- return tea.Batch(m.spinner.Tick, m.checkURL())
68-}
69-
70-// checkURL returns a command that checks the URL and returns a urlCheckedMsg
71-func (m *SpinnerModel) checkURL() tea.Cmd {
72- return func() tea.Msg {
73- success, err := attemptURLCheck(m.deployment)
74- if err != nil {
75- return urlCheckedMsg{success: false, err: err}
76- }
77- return urlCheckedMsg{success: success, err: nil}
78- }
79-}
80-
81-// attemptURLCheck attempts to check the URL and returns a bool and error
82-func attemptURLCheck(deployment DeploymentInfo) (bool, error) {
83- for deployment.retryCount = 0; deployment.retryCount < deployment.maxRetries; deployment.retryCount++ {
84- resp, err := deployment.client.Get(deployment.targetDomain)
85- if err != nil {
86- return false, fmt.Errorf("error checking URL: %w", err)
87- }
88- resp.Body.Close()
89- if resp.StatusCode == http.StatusOK {
90- return true, nil
91- }
92- time.Sleep(retryInterval)
93- }
94- return false, fmt.Errorf("the URL %s could not be reached after %d attempts", deployment.targetDomain, deployment.retryCount)
95-}
96-
97-// Update handles the messages sent to the model
98-func (m SpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
99- switch msg := msg.(type) {
100- case tea.KeyMsg:
101- switch msg.String() {
102- case "q", "esc", "ctrl+c":
103- m.quitting = true
104- return m, tea.Quit
105- default:
106- return m, nil
107- }
108- case urlCheckedMsg:
109- if msg.success {
110- m.DeploymentDone = true
111- return m, tea.Quit
112- } else {
113- m.err = msg.err
114- return m, nil
115- }
116- case spinner.TickMsg:
117- var cmd tea.Cmd
118- m.spinner, cmd = m.spinner.Update(msg)
119- return m, cmd
120-
121- default:
122- return m, nil
123- }
124-}
125-
126-// View returns the view for the model
127-func (m SpinnerModel) View() string {
128- if m.quitting {
129- if m.err != nil {
130- return fmt.Sprintf("Error: %v\n", m.err)
131- }
132- return "Deployment cancelled.\n"
133- }
134-
135- // Use the textStyle to format the entire string
136- return fmt.Sprintf("\n\n %s Wait while Traefik is setting up the domain and certificates... \n\n", m.spinner.View())
137-}
@@ -1,137 +0,0 @@
1-package mvu
2-
3-import (
4- "crypto/tls"
5- "fmt"
6- "net/http"
7- "time"
8-
9- "github.com/charmbracelet/bubbles/spinner"
10- tea "github.com/charmbracelet/bubbletea"
11- "github.com/charmbracelet/lipgloss"
12-)
13-
14-const (
15- defaultMaxRetries = 20
16- httpTimeout = 10 * time.Second
17- retryInterval = 1 * time.Second
18-)
19-
20-type SpinnerModel struct {
21- spinner spinner.Model
22- quitting bool
23- err error
24- deployment DeploymentInfo
25- DeploymentDone bool
26-}
27-
28-type DeploymentInfo struct {
29- imageName string
30- targetDomain string
31- port string
32- client *http.Client
33- retryCount int
34- maxRetries int
35-}
36-
37-type urlCheckedMsg struct {
38- success bool
39- err error
40-}
41-
42-// NewSpinnerModel returns a new SpinnerModel
43-func NewSpinnerModel() SpinnerModel {
44- s := spinner.New()
45- s.Spinner = spinner.Dot
46- s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#54baff"))
47-
48- return SpinnerModel{
49- spinner: s,
50- deployment: DeploymentInfo{
51- maxRetries: defaultMaxRetries,
52- client: newHTTPClient(),
53- },
54- }
55-}
56-
57-func newHTTPClient() *http.Client {
58- return &http.Client{
59- Timeout: httpTimeout,
60- Transport: &http.Transport{
61- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
62- },
63- }
64-}
65-
66-func (m SpinnerModel) Init() tea.Cmd {
67- return tea.Batch(m.spinner.Tick, m.checkURL())
68-}
69-
70-// checkURL returns a command that checks the URL and returns a urlCheckedMsg
71-func (m *SpinnerModel) checkURL() tea.Cmd {
72- return func() tea.Msg {
73- success, err := attemptURLCheck(m.deployment)
74- if err != nil {
75- return urlCheckedMsg{success: false, err: err}
76- }
77- return urlCheckedMsg{success: success, err: nil}
78- }
79-}
80-
81-// attemptURLCheck attempts to check the URL and returns a bool and error
82-func attemptURLCheck(deployment DeploymentInfo) (bool, error) {
83- for deployment.retryCount = 0; deployment.retryCount < deployment.maxRetries; deployment.retryCount++ {
84- resp, err := deployment.client.Get(deployment.targetDomain)
85- if err != nil {
86- return false, fmt.Errorf("error checking URL: %w", err)
87- }
88- resp.Body.Close()
89- if resp.StatusCode == http.StatusOK {
90- return true, nil
91- }
92- time.Sleep(retryInterval)
93- }
94- return false, fmt.Errorf("the URL %s could not be reached after %d attempts", deployment.targetDomain, deployment.retryCount)
95-}
96-
97-// Update handles the messages sent to the model
98-func (m SpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
99- switch msg := msg.(type) {
100- case tea.KeyMsg:
101- switch msg.String() {
102- case "q", "esc", "ctrl+c":
103- m.quitting = true
104- return m, tea.Quit
105- default:
106- return m, nil
107- }
108- case urlCheckedMsg:
109- if msg.success {
110- m.DeploymentDone = true
111- return m, tea.Quit
112- } else {
113- m.err = msg.err
114- return m, nil
115- }
116- case spinner.TickMsg:
117- var cmd tea.Cmd
118- m.spinner, cmd = m.spinner.Update(msg)
119- return m, cmd
120-
121- default:
122- return m, nil
123- }
124-}
125-
126-// View returns the view for the model
127-func (m SpinnerModel) View() string {
128- if m.quitting {
129- if m.err != nil {
130- return fmt.Sprintf("Error: %v\n", m.err)
131- }
132- return "Deployment cancelled.\n"
133- }
134-
135- // Use the textStyle to format the entire string
136- return fmt.Sprintf("\n\n %s Wait while Traefik is setting up the domain and certificates... \n\n", m.spinner.View())
137-}
M internal/common/payload.go +35 -13
@@ -16,6 +16,41 @@ type Payload interface {
1616 GetType() string
1717 }
1818
19+type DeployResponse struct {
20+ Success bool `json:"success"`
21+ Message string `json:"message"`
22+ Domain string `json:"domain,omitempty"`
23+}
24+
25+type PushResponse struct {
26+ Success bool `json:"success"`
27+ Message string `json:"message"`
28+}
29+
30+type DeploymentError struct {
31+ StatusCode int `json:"status_code"`
32+ Message string `json:"message"`
33+ RawResponse string `json:"raw_response"`
34+}
35+
36+func (e *DeploymentError) Error() string {
37+ return fmt.Sprintf("Deployment failed (status %d): %s", e.StatusCode, e.Message)
38+}
39+
40+type DeployPayload struct {
41+ Ports string `json:"ports"`
42+ TargetDomain string `json:"targetdomain"`
43+ ImageName string `json:"imagename"`
44+ ImageID string `json:"imageid"`
45+ Data io.ReadCloser
46+}
47+
48+type PushPayload struct {
49+ ImageName string `json:"imagename"`
50+ ImageID string `json:"imageid"`
51+ Data io.ReadCloser
52+}
53+
1954 func (p *RequestPayload) UnmarshalJSON(data []byte) error {
2055 var raw map[string]json.RawMessage
2156 err := json.Unmarshal(data, &raw)
@@ -76,16 +111,3 @@ func NewPingPayload(data map[string]interface{}) (PingPayload, error) {
76111 }
77112 return PingPayload{Message: message}, nil
78113 }
79-
80-type DeployPayload struct {
81- Ports string `json:"ports"`
82- TargetDomain string `json:"targetdomain"`
83- ImageName string `json:"imagename"`
84- ImageID string `json:"imageid"`
85- Data io.ReadCloser
86-}
87-
88-type PushPayload struct {
89- ImageName string `json:"imagename"`
90- Data io.ReadCloser
91-}
@@ -16,6 +16,41 @@ type Payload interface {
16 GetType() string16 GetType() string
17 }17 }
18 18
19+type DeployResponse struct {
20+ Success bool `json:"success"`
21+ Message string `json:"message"`
22+ Domain string `json:"domain,omitempty"`
23+}
24+
25+type PushResponse struct {
26+ Success bool `json:"success"`
27+ Message string `json:"message"`
28+}
29+
30+type DeploymentError struct {
31+ StatusCode int `json:"status_code"`
32+ Message string `json:"message"`
33+ RawResponse string `json:"raw_response"`
34+}
35+
36+func (e *DeploymentError) Error() string {
37+ return fmt.Sprintf("Deployment failed (status %d): %s", e.StatusCode, e.Message)
38+}
39+
40+type DeployPayload struct {
41+ Ports string `json:"ports"`
42+ TargetDomain string `json:"targetdomain"`
43+ ImageName string `json:"imagename"`
44+ ImageID string `json:"imageid"`
45+ Data io.ReadCloser
46+}
47+
48+type PushPayload struct {
49+ ImageName string `json:"imagename"`
50+ ImageID string `json:"imageid"`
51+ Data io.ReadCloser
52+}
53+
19 func (p *RequestPayload) UnmarshalJSON(data []byte) error {54 func (p *RequestPayload) UnmarshalJSON(data []byte) error {
20 var raw map[string]json.RawMessage55 var raw map[string]json.RawMessage
21 err := json.Unmarshal(data, &raw)56 err := json.Unmarshal(data, &raw)
@@ -76,16 +111,3 @@ func NewPingPayload(data map[string]interface{}) (PingPayload, error) {
76 }111 }
77 return PingPayload{Message: message}, nil112 return PingPayload{Message: message}, nil
78 }113 }
79-
80-type DeployPayload struct {
81- Ports string `json:"ports"`
82- TargetDomain string `json:"targetdomain"`
83- ImageName string `json:"imagename"`
84- ImageID string `json:"imageid"`
85- Data io.ReadCloser
86-}
87-
88-type PushPayload struct {
89- ImageName string `json:"imagename"`
90- Data io.ReadCloser
91-}
M internal/httpserve/handler/cli-endpoints.go +118 -13
@@ -1,6 +1,7 @@
11 package handler
22
33 import (
4+ "encoding/json"
45 "fmt"
56 "net/http"
67 "regexp"
@@ -18,6 +19,13 @@ type InfoResponse struct {
1819 Version string `json:"version"`
1920 }
2021
22+type DeployResponse = common.DeployResponse
23+type PushResponse = common.PushResponse
24+
25+func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error {
26+ return c.JSON(statusCode, response)
27+}
28+
2129 func (info *InfoResponse) Populate(a *server.App) {
2230 info.Uptime = a.GetUptime()
2331 info.Version = a.GetVersionstring()
@@ -50,6 +58,75 @@ func GetInfos(c echo.Context, a *server.App) error {
5058 return c.JSON(http.StatusOK, info)
5159 }
5260
61+func PostPush(c echo.Context, a *server.App) error {
62+ // Initialize pushPayload object
63+ payload := &common.PushPayload{
64+ ImageName: c.Request().Header.Get("X-Image-Name"),
65+ }
66+
67+ if payload.ImageName == "" {
68+ return c.JSON(http.StatusBadRequest, "Invalid image name")
69+ }
70+
71+ imageReader := c.Request().Body
72+ defer imageReader.Close()
73+
74+ // Rename the image to a valid name so that it can be saved (remove user/, :tag and add .tar)
75+ imageFileName := payload.ImageName
76+ imageFileName = regexp.MustCompile(`^([a-zA-Z0-9\-_.]+\/)?`).ReplaceAllString(imageFileName, "")
77+ imageFileName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageFileName, "")
78+ imageFileName = imageFileName + ".tar"
79+
80+ // Save the image tar in the storage
81+ imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
82+ if err != nil {
83+ return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
84+ Success: false,
85+ Message: fmt.Sprintf("Failed to save image: %v", err),
86+ })
87+ }
88+
89+ // Debug
90+ fmt.Printf("Image saved successfully to: %s\n", imagePath)
91+
92+ // Import the tar in docker
93+ imageID, err := docker.ImportImageToEngine(imagePath)
94+ if err != nil {
95+ store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails
96+ return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
97+ Success: false,
98+ Message: fmt.Sprintf("Failed to import image: %v", err),
99+ })
100+ }
101+
102+ if imageID == "" {
103+ return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
104+ Success: false,
105+ Message: "Imported image ID is empty",
106+ })
107+ }
108+
109+ // Remove the image from the storage
110+ err = store.RemoveFromStorage(imagePath)
111+ if err != nil {
112+ return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
113+ Success: false,
114+ Message: fmt.Sprintf("Failed to remove temporary image file: %v", err),
115+ })
116+ }
117+
118+ fmt.Printf("Image imported successfully: %s\n", payload.ImageName)
119+
120+ // Update the payload with the image ID
121+ payload.ImageID = imageID
122+
123+ // If we arrive here, send back a success response with the target domain
124+ return sendJSONResponse(c, http.StatusOK, common.PushResponse{
125+ Success: true,
126+ Message: "Deployment successful",
127+ })
128+}
129+
53130 func PostDeploy(c echo.Context, a *server.App) error {
54131 // Initialize pushPayload object
55132 payload := &common.DeployPayload{
@@ -84,29 +161,39 @@ func PostDeploy(c echo.Context, a *server.App) error {
84161 // Save the image tar in the storage
85162 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
86163 if err != nil {
87- return sendJsonError(c, fmt.Errorf("failed to save image: %v", err))
164+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
165+ Success: false,
166+ Message: fmt.Sprintf("Failed to save image: %v", err),
167+ })
88168 }
89169
170+ // Debug
90171 fmt.Printf("Image saved successfully to: %s\n", imagePath)
91172
92173 // Import the tar in docker
93174 imageID, err := docker.ImportImageToEngine(imagePath)
94175 if err != nil {
95176 store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails
96- return sendJsonError(c, fmt.Errorf("failed to import image: %v", err))
177+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
178+ Success: false,
179+ Message: fmt.Sprintf("Failed to import image: %v", err),
180+ })
97181 }
98182
99183 if imageID == "" {
100- return sendJsonError(c, fmt.Errorf("imported image ID is empty"))
184+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
185+ Success: false,
186+ Message: "Imported image ID is empty",
187+ })
101188 }
102189
103- // Debug
104- fmt.Printf("Image ID: %s\n", imageID)
105-
106190 // Remove the image from the storage
107191 err = store.RemoveFromStorage(imagePath)
108192 if err != nil {
109- return sendJsonError(c, fmt.Errorf("failed to remove temporary image file: %v", err))
193+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
194+ Success: false,
195+ Message: fmt.Sprintf("Failed to remove temporary image file: %v", err),
196+ })
110197 }
111198
112199 fmt.Printf("Image imported successfully: %s\n", payload.ImageName)
@@ -117,22 +204,40 @@ func PostDeploy(c echo.Context, a *server.App) error {
117204 // Create the container using cmdparams.FromPayloadStructToCmdParams
118205 params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, imageID)
119206 if err != nil {
120- return sendJsonError(c, fmt.Errorf("failed to create command parameters: %v", err))
207+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
208+ Success: false,
209+ Message: fmt.Sprintf("Failed to create command parameters: %v", err),
210+ })
121211 }
122-
123212 // Create the container
124213 containerID, err := docker.CreateContainer(params)
125214 if err != nil {
126- return sendJsonError(c, fmt.Errorf("failed to create container: %v", err))
215+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
216+ Success: false,
217+ Message: fmt.Sprintf("Failed to create container: %v", err),
218+ })
127219 }
128220
129221 // Start the container
130222 err = docker.StartContainer(containerID)
131223 if err != nil {
132224 docker.RemoveContainer(containerID) // Clean up if start fails
133- return sendJsonError(c, fmt.Errorf("failed to start container: %v", err))
225+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
226+ Success: false,
227+ Message: fmt.Sprintf("Failed to start container: %v", err),
228+ })
134229 }
135230
136- // If we arrive here, send back payload.TargetDomain so the client can test it
137- return c.JSON(http.StatusOK, payload.TargetDomain)
231+ // If we arrive here, send back a success response with the target domain
232+ response := DeployResponse{
233+ Success: true,
234+ Message: "Deployment successful",
235+ Domain: payload.TargetDomain,
236+ }
237+
238+ // Log the response before sending
239+ responseJSON, _ := json.Marshal(response)
240+ fmt.Printf("Server response: %s\n", string(responseJSON))
241+
242+ return c.JSON(http.StatusOK, response)
138243 }
@@ -1,6 +1,7 @@
1 package handler1 package handler
2 2
3 import (3 import (
4+ "encoding/json"
4 "fmt"5 "fmt"
5 "net/http"6 "net/http"
6 "regexp"7 "regexp"
@@ -18,6 +19,13 @@ type InfoResponse struct {
18 Version string `json:"version"`19 Version string `json:"version"`
19 }20 }
20 21
22+type DeployResponse = common.DeployResponse
23+type PushResponse = common.PushResponse
24+
25+func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error {
26+ return c.JSON(statusCode, response)
27+}
28+
21 func (info *InfoResponse) Populate(a *server.App) {29 func (info *InfoResponse) Populate(a *server.App) {
22 info.Uptime = a.GetUptime()30 info.Uptime = a.GetUptime()
23 info.Version = a.GetVersionstring()31 info.Version = a.GetVersionstring()
@@ -50,6 +58,75 @@ func GetInfos(c echo.Context, a *server.App) error {
50 return c.JSON(http.StatusOK, info)58 return c.JSON(http.StatusOK, info)
51 }59 }
52 60
61+func PostPush(c echo.Context, a *server.App) error {
62+ // Initialize pushPayload object
63+ payload := &common.PushPayload{
64+ ImageName: c.Request().Header.Get("X-Image-Name"),
65+ }
66+
67+ if payload.ImageName == "" {
68+ return c.JSON(http.StatusBadRequest, "Invalid image name")
69+ }
70+
71+ imageReader := c.Request().Body
72+ defer imageReader.Close()
73+
74+ // Rename the image to a valid name so that it can be saved (remove user/, :tag and add .tar)
75+ imageFileName := payload.ImageName
76+ imageFileName = regexp.MustCompile(`^([a-zA-Z0-9\-_.]+\/)?`).ReplaceAllString(imageFileName, "")
77+ imageFileName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageFileName, "")
78+ imageFileName = imageFileName + ".tar"
79+
80+ // Save the image tar in the storage
81+ imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
82+ if err != nil {
83+ return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
84+ Success: false,
85+ Message: fmt.Sprintf("Failed to save image: %v", err),
86+ })
87+ }
88+
89+ // Debug
90+ fmt.Printf("Image saved successfully to: %s\n", imagePath)
91+
92+ // Import the tar in docker
93+ imageID, err := docker.ImportImageToEngine(imagePath)
94+ if err != nil {
95+ store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails
96+ return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
97+ Success: false,
98+ Message: fmt.Sprintf("Failed to import image: %v", err),
99+ })
100+ }
101+
102+ if imageID == "" {
103+ return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
104+ Success: false,
105+ Message: "Imported image ID is empty",
106+ })
107+ }
108+
109+ // Remove the image from the storage
110+ err = store.RemoveFromStorage(imagePath)
111+ if err != nil {
112+ return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{
113+ Success: false,
114+ Message: fmt.Sprintf("Failed to remove temporary image file: %v", err),
115+ })
116+ }
117+
118+ fmt.Printf("Image imported successfully: %s\n", payload.ImageName)
119+
120+ // Update the payload with the image ID
121+ payload.ImageID = imageID
122+
123+ // If we arrive here, send back a success response with the target domain
124+ return sendJSONResponse(c, http.StatusOK, common.PushResponse{
125+ Success: true,
126+ Message: "Deployment successful",
127+ })
128+}
129+
53 func PostDeploy(c echo.Context, a *server.App) error {130 func PostDeploy(c echo.Context, a *server.App) error {
54 // Initialize pushPayload object131 // Initialize pushPayload object
55 payload := &common.DeployPayload{132 payload := &common.DeployPayload{
@@ -84,29 +161,39 @@ func PostDeploy(c echo.Context, a *server.App) error {
84 // Save the image tar in the storage161 // Save the image tar in the storage
85 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)162 imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader)
86 if err != nil {163 if err != nil {
87- return sendJsonError(c, fmt.Errorf("failed to save image: %v", err))164+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
165+ Success: false,
166+ Message: fmt.Sprintf("Failed to save image: %v", err),
167+ })
88 }168 }
89 169
170+ // Debug
90 fmt.Printf("Image saved successfully to: %s\n", imagePath)171 fmt.Printf("Image saved successfully to: %s\n", imagePath)
91 172
92 // Import the tar in docker173 // Import the tar in docker
93 imageID, err := docker.ImportImageToEngine(imagePath)174 imageID, err := docker.ImportImageToEngine(imagePath)
94 if err != nil {175 if err != nil {
95 store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails176 store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails
96- return sendJsonError(c, fmt.Errorf("failed to import image: %v", err))177+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
178+ Success: false,
179+ Message: fmt.Sprintf("Failed to import image: %v", err),
180+ })
97 }181 }
98 182
99 if imageID == "" {183 if imageID == "" {
100- return sendJsonError(c, fmt.Errorf("imported image ID is empty"))184+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
185+ Success: false,
186+ Message: "Imported image ID is empty",
187+ })
101 }188 }
102 189
103- // Debug
104- fmt.Printf("Image ID: %s\n", imageID)
105-
106 // Remove the image from the storage190 // Remove the image from the storage
107 err = store.RemoveFromStorage(imagePath)191 err = store.RemoveFromStorage(imagePath)
108 if err != nil {192 if err != nil {
109- return sendJsonError(c, fmt.Errorf("failed to remove temporary image file: %v", err))193+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
194+ Success: false,
195+ Message: fmt.Sprintf("Failed to remove temporary image file: %v", err),
196+ })
110 }197 }
111 198
112 fmt.Printf("Image imported successfully: %s\n", payload.ImageName)199 fmt.Printf("Image imported successfully: %s\n", payload.ImageName)
@@ -117,22 +204,40 @@ func PostDeploy(c echo.Context, a *server.App) error {
117 // Create the container using cmdparams.FromPayloadStructToCmdParams204 // Create the container using cmdparams.FromPayloadStructToCmdParams
118 params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, imageID)205 params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, imageID)
119 if err != nil {206 if err != nil {
120- return sendJsonError(c, fmt.Errorf("failed to create command parameters: %v", err))207+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
208+ Success: false,
209+ Message: fmt.Sprintf("Failed to create command parameters: %v", err),
210+ })
121 }211 }
122-
123 // Create the container212 // Create the container
124 containerID, err := docker.CreateContainer(params)213 containerID, err := docker.CreateContainer(params)
125 if err != nil {214 if err != nil {
126- return sendJsonError(c, fmt.Errorf("failed to create container: %v", err))215+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
216+ Success: false,
217+ Message: fmt.Sprintf("Failed to create container: %v", err),
218+ })
127 }219 }
128 220
129 // Start the container221 // Start the container
130 err = docker.StartContainer(containerID)222 err = docker.StartContainer(containerID)
131 if err != nil {223 if err != nil {
132 docker.RemoveContainer(containerID) // Clean up if start fails224 docker.RemoveContainer(containerID) // Clean up if start fails
133- return sendJsonError(c, fmt.Errorf("failed to start container: %v", err))225+ return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
226+ Success: false,
227+ Message: fmt.Sprintf("Failed to start container: %v", err),
228+ })
134 }229 }
135 230
136- // If we arrive here, send back payload.TargetDomain so the client can test it231+ // If we arrive here, send back a success response with the target domain
137- return c.JSON(http.StatusOK, payload.TargetDomain)232+ response := DeployResponse{
233+ Success: true,
234+ Message: "Deployment successful",
235+ Domain: payload.TargetDomain,
236+ }
237+
238+ // Log the response before sending
239+ responseJSON, _ := json.Marshal(response)
240+ fmt.Printf("Server response: %s\n", string(responseJSON))
241+
242+ return c.JSON(http.StatusOK, response)
138 }243 }
M internal/httpserve/handler/senderror.go +0 -4
@@ -15,7 +15,3 @@ func sendError(c echo.Context, err error) error {
1515 }
1616 return c.HTML(http.StatusInternalServerError, sanitizedHTML)
1717 }
18-
19-func sendJsonError(c echo.Context, err error) error {
20- return c.JSON(http.StatusInternalServerError, err.Error())
21-}
@@ -15,7 +15,3 @@ func sendError(c echo.Context, err error) error {
15 }15 }
16 return c.HTML(http.StatusInternalServerError, sanitizedHTML)16 return c.HTML(http.StatusInternalServerError, sanitizedHTML)
17 }17 }
18-
19-func sendJsonError(c echo.Context, err error) error {
20- return c.JSON(http.StatusInternalServerError, err.Error())
21-}
M internal/httpserve/middleware/requiretoken.go +3 -3
@@ -23,9 +23,9 @@ func RequireToken(a *server.App) echo.MiddlewareFunc {
2323 func validateToken(c echo.Context, a *server.App) error {
2424 token := c.Request().Header.Get("Authorization")
2525 if token == "" {
26- return c.JSON(401, "Unauthorized")
26+ return echo.NewHTTPError(401, "Token is missing")
2727 }
28- fmt.Println(token)
28+
2929 token = strings.Replace(token, "Bearer ", "", 1)
3030 configToken, err := a.Config.GetToken()
3131 if err != nil {
@@ -33,7 +33,7 @@ func validateToken(c echo.Context, a *server.App) error {
3333 }
3434
3535 if token != configToken {
36- return c.JSON(401, "Unauthorized")
36+ return echo.NewHTTPError(401, "Token does not match")
3737 }
3838
3939 return nil
@@ -23,9 +23,9 @@ func RequireToken(a *server.App) echo.MiddlewareFunc {
23 func validateToken(c echo.Context, a *server.App) error {23 func validateToken(c echo.Context, a *server.App) error {
24 token := c.Request().Header.Get("Authorization")24 token := c.Request().Header.Get("Authorization")
25 if token == "" {25 if token == "" {
26- return c.JSON(401, "Unauthorized")26+ return echo.NewHTTPError(401, "Token is missing")
27 }27 }
28- fmt.Println(token)28+
29 token = strings.Replace(token, "Bearer ", "", 1)29 token = strings.Replace(token, "Bearer ", "", 1)
30 configToken, err := a.Config.GetToken()30 configToken, err := a.Config.GetToken()
31 if err != nil {31 if err != nil {
@@ -33,7 +33,7 @@ func validateToken(c echo.Context, a *server.App) error {
33 }33 }
34 34
35 if token != configToken {35 if token != configToken {
36- return c.JSON(401, "Unauthorized")36+ return echo.NewHTTPError(401, "Token does not match")
37 }37 }
38 38
39 return nil39 return nil
M internal/httpserve/router.go +4 -1
@@ -54,9 +54,12 @@ func bindAPIEndpoints(e *echo.Echo, a *server.App) {
5454 apiGroup.GET("/ping", func(c echo.Context) error {
5555 return handler.GetInfos(c, a)
5656 })
57- apiGroup.POST("/push", func(c echo.Context) error {
57+ apiGroup.POST("/deploy", func(c echo.Context) error {
5858 return handler.PostDeploy(c, a)
5959 })
60+ apiGroup.POST("/push", func(c echo.Context) error {
61+ return handler.PostPush(c, a)
62+ })
6063 }
6164
6265 // bindStaticRoute bind static path
@@ -54,9 +54,12 @@ func bindAPIEndpoints(e *echo.Echo, a *server.App) {
54 apiGroup.GET("/ping", func(c echo.Context) error {54 apiGroup.GET("/ping", func(c echo.Context) error {
55 return handler.GetInfos(c, a)55 return handler.GetInfos(c, a)
56 })56 })
57- apiGroup.POST("/push", func(c echo.Context) error {57+ apiGroup.POST("/deploy", func(c echo.Context) error {
58 return handler.PostDeploy(c, a)58 return handler.PostDeploy(c, a)
59 })59 })
60+ apiGroup.POST("/push", func(c echo.Context) error {
61+ return handler.PostPush(c, a)
62+ })
60 }63 }
61 64
62 // bindStaticRoute bind static path65 // bindStaticRoute bind static path
M internal/templating/cmdparams/traefik.go +1 -1
@@ -24,6 +24,6 @@ func CreateTraefikLabels(params *docker.ContainerCommandParams) {
2424 }
2525
2626 if params.IsSSL {
27- params.Labels = append(params.Labels, fmt.Sprintf("%s.tls.certresolver=letsencrypt", baseRouter))
27+ params.Labels = append(params.Labels, fmt.Sprintf("%s.tls.certresolver=myresolver", baseRouter))
2828 }
2929 }
@@ -24,6 +24,6 @@ func CreateTraefikLabels(params *docker.ContainerCommandParams) {
24 }24 }
25 25
26 if params.IsSSL {26 if params.IsSSL {
27- params.Labels = append(params.Labels, fmt.Sprintf("%s.tls.certresolver=letsencrypt", baseRouter))27+ params.Labels = append(params.Labels, fmt.Sprintf("%s.tls.certresolver=myresolver", baseRouter))
28 }28 }
29 }29 }
M internal/webui/public/assets/js/custom.js +14 -10
@@ -1,15 +1,17 @@
1-import 'dotenv/config'; // Import dotenv to load environment variables
2-
3-
41 function initializeVersionCheck() {
52 // Dummy data for the remote version (as if fetched from a server)
6- const updateStrTemplate = "New version %VERSION% available, consider pulling the latest image";
3+ const updateStrTemplate =
4+ "New version %VERSION% available, consider pulling the latest image";
75
86 const checkVersion = (versionData, currentVersion) => {
9- const fetchedVersionNumber = versionData?.amd64?.name.match(/\d+\.\d+\.\d+/)?.[0];
7+ const fetchedVersionNumber =
8+ versionData?.amd64?.name.match(/\d+\.\d+\.\d+/)?.[0];
109
1110 if (currentVersion !== fetchedVersionNumber) {
12- const updateStr = updateStrTemplate.replace("%VERSION%", fetchedVersionNumber);
11+ const updateStr = updateStrTemplate.replace(
12+ "%VERSION%",
13+ fetchedVersionNumber,
14+ );
1315 console.log(updateStr);
1416 const updateElement = document.getElementById("update-available");
1517 updateElement.title = updateStr;
@@ -17,13 +19,15 @@ function initializeVersionCheck() {
1719 updateElement.classList.remove("hidden"); // Remove the Tailwind `hidden` class
1820 updateElement.classList.add("block"); // Add the Tailwind `block` class
1921 } else {
20- console.log("No new version. Current version is up-to-date:", currentVersion);
22+ console.log(
23+ "No new version. Current version is up-to-date:",
24+ currentVersion,
25+ );
2126 }
2227 };
2328
2429 const fetchVersionInfo = (currentVersion) => {
25- // os.Get env proxy url + /version
26- fetch(`${process.env.PROXY_URL}/version`)
30+ fetch(`https://gordon-proxy.bamen.dev/version`)
2731 .then((response) => response.json())
2832 .then((versionData) => checkVersion(versionData, currentVersion))
2933 .catch((error) => console.error("Error fetching version:", error));
@@ -46,4 +50,4 @@ function initializeVersionCheck() {
4650 init();
4751 }
4852
49-document.addEventListener('DOMContentLoaded', initializeVersionCheck);
53+document.addEventListener("DOMContentLoaded", initializeVersionCheck);
@@ -1,15 +1,17 @@
1-import 'dotenv/config'; // Import dotenv to load environment variables
2-
3-
4 function initializeVersionCheck() {1 function initializeVersionCheck() {
5 // Dummy data for the remote version (as if fetched from a server)2 // Dummy data for the remote version (as if fetched from a server)
6- const updateStrTemplate = "New version %VERSION% available, consider pulling the latest image";3+ const updateStrTemplate =
4+ "New version %VERSION% available, consider pulling the latest image";
7 5
8 const checkVersion = (versionData, currentVersion) => {6 const checkVersion = (versionData, currentVersion) => {
9- const fetchedVersionNumber = versionData?.amd64?.name.match(/\d+\.\d+\.\d+/)?.[0];7+ const fetchedVersionNumber =
8+ versionData?.amd64?.name.match(/\d+\.\d+\.\d+/)?.[0];
10 9
11 if (currentVersion !== fetchedVersionNumber) {10 if (currentVersion !== fetchedVersionNumber) {
12- const updateStr = updateStrTemplate.replace("%VERSION%", fetchedVersionNumber);11+ const updateStr = updateStrTemplate.replace(
12+ "%VERSION%",
13+ fetchedVersionNumber,
14+ );
13 console.log(updateStr);15 console.log(updateStr);
14 const updateElement = document.getElementById("update-available");16 const updateElement = document.getElementById("update-available");
15 updateElement.title = updateStr;17 updateElement.title = updateStr;
@@ -17,13 +19,15 @@ function initializeVersionCheck() {
17 updateElement.classList.remove("hidden"); // Remove the Tailwind `hidden` class19 updateElement.classList.remove("hidden"); // Remove the Tailwind `hidden` class
18 updateElement.classList.add("block"); // Add the Tailwind `block` class20 updateElement.classList.add("block"); // Add the Tailwind `block` class
19 } else {21 } else {
20- console.log("No new version. Current version is up-to-date:", currentVersion);22+ console.log(
23+ "No new version. Current version is up-to-date:",
24+ currentVersion,
25+ );
21 }26 }
22 };27 };
23 28
24 const fetchVersionInfo = (currentVersion) => {29 const fetchVersionInfo = (currentVersion) => {
25- // os.Get env proxy url + /version30+ fetch(`https://gordon-proxy.bamen.dev/version`)
26- fetch(`${process.env.PROXY_URL}/version`)
27 .then((response) => response.json())31 .then((response) => response.json())
28 .then((versionData) => checkVersion(versionData, currentVersion))32 .then((versionData) => checkVersion(versionData, currentVersion))
29 .catch((error) => console.error("Error fetching version:", error));33 .catch((error) => console.error("Error fetching version:", error));
@@ -46,4 +50,4 @@ function initializeVersionCheck() {
46 init();50 init();
47 }51 }
48 52
49-document.addEventListener('DOMContentLoaded', initializeVersionCheck);53+document.addEventListener("DOMContentLoaded", initializeVersionCheck);

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