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
- 3b4e5c53980918cc6e0cfa62f652d3484359b5bc
- author
- bamen · · 2024-08-28 19:17 UTC
- committer
- bamen · · 2024-08-28 16:37 UTC
- parent
- 6468aee
- signature
- unsigned
20 files · +403 -642
M .goreleaser.yaml +5 -5
| @@ -1,3 +1,5 @@ | |||
| 1 | + | version: 2 | |
| 2 | + | ||
| 1 | 3 | before: | |
| 2 | 4 | hooks: | |
| 3 | 5 | - go mod tidy | |
| @@ -27,13 +29,12 @@ dockers: | |||
| 27 | 29 | build_flag_templates: | |
| 28 | 30 | - "--pull" | |
| 29 | 31 | - "--platform=linux/amd64" | |
| 32 | + | - "--build-arg=ARCH=amd64" | |
| 30 | 33 | - "--label=org.opencontainers.image.created={{.Date}}" | |
| 31 | 34 | - "--label=org.opencontainers.image.title={{.ProjectName}}" | |
| 32 | 35 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" | |
| 33 | 36 | - "--label=org.opencontainers.image.version={{.Version}}" | |
| 34 | 37 | - "--label=org.opencontainers.image.source=https://github.com/bnema/gordon" | |
| 35 | - | extra_files: | |
| 36 | - | - .iscontainer | |
| 37 | 38 | - image_templates: | |
| 38 | 39 | - "ghcr.io/bnema/gordon:{{ .Tag }}-arm64" | |
| 39 | 40 | use: buildx | |
| @@ -42,13 +43,12 @@ dockers: | |||
| 42 | 43 | build_flag_templates: | |
| 43 | 44 | - "--pull" | |
| 44 | 45 | - "--platform=linux/arm64" | |
| 46 | + | - "--build-arg=ARCH=arm64" | |
| 45 | 47 | - "--label=org.opencontainers.image.created={{.Date}}" | |
| 46 | 48 | - "--label=org.opencontainers.image.title={{.ProjectName}}" | |
| 47 | 49 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" | |
| 48 | 50 | - "--label=org.opencontainers.image.version={{.Version}}" | |
| 49 | 51 | - "--label=org.opencontainers.image.source=https://github.com/bnema/gordon" | |
| 50 | - | extra_files: | |
| 51 | - | - .iscontainer | |
| 52 | 52 | ||
| 53 | 53 | archives: | |
| 54 | 54 | - format: zip | |
| @@ -59,4 +59,4 @@ changelog: | |||
| 59 | 59 | filters: | |
| 60 | 60 | exclude: | |
| 61 | 61 | - "^docs:" | |
| 62 | - | - "^test:"⏎̸ | |
| 62 | + | - "^test:" | |
| @@ -1,3 +1,5 @@ | |||||
| 1 | + | version: 2 | |||
| 2 | + | ||||
| 1 | before: | 3 | before: | ||
| 2 | hooks: | 4 | hooks: | ||
| 3 | - go mod tidy | 5 | - 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: buildx | 40 | 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: zip | 54 | - 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 | |
| 2 | 1 | FROM alpine:latest | |
| 3 | 2 | ||
| 4 | - | ARG ARCH | |
| 3 | + | 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 | 5 | RUN touch /.iscontainer | |
| 10 | 6 | ||
| 11 | - | # Set the entrypoint | |
| 12 | 7 | ENTRYPOINT ["/gordon"] | |
| 13 | - | ||
| 14 | - | # Default command | |
| 15 | 8 | CMD ["serve"] | |
| @@ -1,15 +1,8 @@ | |||||
| 1 | - | # Start from scratch | |||
| 2 | FROM alpine:latest | 1 | FROM alpine:latest | ||
| 3 | 2 | ||||
| 4 | - | ARG ARCH | 3 | + | 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 /.iscontainer | 5 | 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 | |||
| 71 | 71 | Type "deploy" | |
| 72 | 72 | Sleep 1 | |
| 73 | 73 | Space | |
| 74 | - | Type "bnema/go-helloworld-http:latest" | |
| 74 | + | Type "bnema/go-helloworld:latest" | |
| 75 | 75 | Sleep 1 | |
| 76 | 76 | Space | |
| 77 | 77 | Type "-p 8888:8080" | |
| 78 | 78 | Sleep 1 | |
| 79 | 79 | Space | |
| 80 | - | Type "-t hello.bamen.dev" | |
| 80 | + | Type "-t hello.bamen.dev" | |
| 81 | 81 | Sleep 1 | |
| 82 | 82 | Enter | |
| 83 | 83 | ||
| 84 | - | Sleep 15 | |
| 84 | + | Sleep 14 | |
| 85 | + | ||
| 86 | + | Hide | |
| 87 | + | Type "clear" | |
| 88 | + | sleep 1 | |
| 89 | + | Enter | |
| 90 | + | Show | |
| 91 | + | ||
| 92 | + | ||
| 85 | 93 | Enter | |
| 86 | 94 | Type "curl https://hello.bamen.dev" | |
| 87 | 95 | Sleep 1 | |
| @@ -71,17 +71,25 @@ Space | |||||
| 71 | Type "deploy" | 71 | Type "deploy" | ||
| 72 | Sleep 1 | 72 | Sleep 1 | ||
| 73 | Space | 73 | Space | ||
| 74 | - | Type "bnema/go-helloworld-http:latest" | 74 | + | Type "bnema/go-helloworld:latest" |
| 75 | Sleep 1 | 75 | Sleep 1 | ||
| 76 | Space | 76 | Space | ||
| 77 | Type "-p 8888:8080" | 77 | Type "-p 8888:8080" | ||
| 78 | Sleep 1 | 78 | Sleep 1 | ||
| 79 | Space | 79 | Space | ||
| 80 | - | Type "-t hello.bamen.dev" | 80 | + | Type "-t hello.bamen.dev" |
| 81 | Sleep 1 | 81 | Sleep 1 | ||
| 82 | Enter | 82 | Enter | ||
| 83 | 83 | ||||
| 84 | - | Sleep 15 | 84 | + | Sleep 14 |
| 85 | + | ||||
| 86 | + | Hide | |||
| 87 | + | Type "clear" | |||
| 88 | + | sleep 1 | |||
| 89 | + | Enter | |||
| 90 | + | Show | |||
| 91 | + | ||||
| 92 | + | ||||
| 85 | Enter | 93 | Enter | ||
| 86 | Type "curl https://hello.bamen.dev" | 94 | Type "curl https://hello.bamen.dev" | ||
| 87 | Sleep 1 | 95 | Sleep 1 | ||
M buildpush-dev.sh +39 -14
| @@ -1,11 +1,20 @@ | |||
| 1 | 1 | #!/bin/bash | |
| 2 | - | ||
| 3 | 2 | set -e | |
| 4 | - | ||
| 3 | + | ENGINE="podman" | |
| 5 | 4 | REPO="ghcr.io/bnema/gordon" | |
| 6 | 5 | TAG="dev" | |
| 7 | 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 | 18 | # Ensure dist directory exists | |
| 10 | 19 | mkdir -p $DIST_DIR | |
| 11 | 20 | ||
| @@ -14,25 +23,41 @@ echo "Building Go binaries..." | |||
| 14 | 23 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $DIST_DIR/gordon-linux-amd64 ./cmd/cli | |
| 15 | 24 | 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 | 30 | # Build Docker images for each architecture | |
| 18 | 31 | 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 . | |
| 21 | 34 | ||
| 22 | 35 | # Push images | |
| 23 | 36 | 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 | |
| 26 | 43 | ||
| 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 \ | |
| 30 | 47 | $REPO:${TAG}-amd64 \ | |
| 31 | - | $REPO:${TAG}-arm64v8 \ | |
| 32 | - | --amend | |
| 48 | + | $REPO:${TAG}-arm64 | |
| 33 | 49 | ||
| 34 | 50 | # 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 | |
| 37 | 62 | ||
| 38 | - | docker manifest push $REPO:$TAG | |
| 63 | + | echo "Script completed successfully." | |
| @@ -1,11 +1,20 @@ | |||||
| 1 | #!/bin/bash | 1 | #!/bin/bash | ||
| 2 | - | ||||
| 3 | set -e | 2 | 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 exists | 18 | # Ensure dist directory exists | ||
| 10 | mkdir -p $DIST_DIR | 19 | 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/cli | 23 | 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/cli | 24 | 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 architecture | 30 | # 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 images | 35 | # Push images | ||
| 23 | echo "Pushing Docker images..." | 36 | echo "Pushing Docker images..." | ||
| 24 | - | docker push $REPO:${TAG}-amd64 | 37 | + | $ENGINE push $REPO:${TAG}-amd64 |
| 25 | - | docker push $REPO:${TAG}-arm64v8 | 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 | |||
| 26 | 43 | ||||
| 27 | - | # Create and push multi-arch manifest | 44 | + | # 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 information | 50 | # 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 v8 | 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 | |||
| 37 | 62 | ||||
| 38 | - | docker manifest push $REPO:$TAG | 63 | + | echo "Script completed successfully." |
M cmd/cli/main.go +1 -2
| @@ -2,7 +2,6 @@ package main | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | - | "log" | |
| 6 | 5 | "regexp" | |
| 7 | 6 | ||
| 8 | 7 | "github.com/bnema/gordon/internal/cli" | |
| @@ -61,7 +60,7 @@ func main() { | |||
| 61 | 60 | go func() { | |
| 62 | 61 | msg, err := common.CheckVersionPeriodically(&s.Config) | |
| 63 | 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 | ||
| @@ -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 | |||
| 5 | 5 | require ( | |
| 6 | 6 | github.com/AlecAivazis/survey/v2 v2.3.7 | |
| 7 | 7 | 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 | |
| 11 | 9 | github.com/docker/docker v27.1.2+incompatible | |
| 12 | 10 | github.com/docker/go-connections v0.5.0 | |
| 13 | 11 | github.com/fatih/color v1.17.0 | |
| @@ -24,15 +22,14 @@ require ( | |||
| 24 | 22 | ||
| 25 | 23 | require ( | |
| 26 | 24 | 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 | |
| 28 | 26 | github.com/charmbracelet/x/ansi v0.2.3 // indirect | |
| 29 | - | github.com/charmbracelet/x/term v0.2.0 // indirect | |
| 30 | 27 | github.com/containerd/log v0.1.0 // indirect | |
| 31 | 28 | github.com/creack/pty v1.1.18 // indirect | |
| 32 | 29 | github.com/distribution/reference v0.6.0 // indirect | |
| 33 | 30 | github.com/dustin/go-humanize v1.0.1 // indirect | |
| 34 | - | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | |
| 35 | 31 | github.com/felixge/httpsnoop v1.0.4 // indirect | |
| 32 | + | github.com/go-logfmt/logfmt v0.6.0 // indirect | |
| 36 | 33 | github.com/go-logr/logr v1.4.2 // indirect | |
| 37 | 34 | github.com/go-logr/stdr v1.2.2 // indirect | |
| 38 | 35 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect | |
| @@ -40,12 +37,9 @@ require ( | |||
| 40 | 37 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect | |
| 41 | 38 | github.com/kr/pretty v0.3.1 // indirect | |
| 42 | 39 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | |
| 43 | - | github.com/mattn/go-localereader v0.0.1 // indirect | |
| 44 | 40 | github.com/mattn/go-runewidth v0.0.16 // indirect | |
| 45 | 41 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect | |
| 46 | 42 | 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 | 43 | github.com/muesli/termenv v0.15.2 // indirect | |
| 50 | 44 | github.com/ncruces/go-strftime v0.1.9 // indirect | |
| 51 | 45 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect | |
| @@ -57,7 +51,7 @@ require ( | |||
| 57 | 51 | go.opentelemetry.io/otel/metric v1.29.0 // indirect | |
| 58 | 52 | go.opentelemetry.io/otel/sdk v1.21.0 // indirect | |
| 59 | 53 | 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 | |
| 61 | 55 | golang.org/x/term v0.23.0 // indirect | |
| 62 | 56 | google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect | |
| 63 | 57 | 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.7 | 6 | github.com/AlecAivazis/survey/v2 v2.3.7 | ||
| 7 | github.com/PuerkitoBio/goquery v1.9.2 | 7 | github.com/PuerkitoBio/goquery v1.9.2 | ||
| 8 | - | github.com/charmbracelet/bubbles v0.19.0 | 8 | + | 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+incompatible | 9 | github.com/docker/docker v27.1.2+incompatible | ||
| 12 | github.com/docker/go-connections v0.5.0 | 10 | github.com/docker/go-connections v0.5.0 | ||
| 13 | github.com/fatih/color v1.17.0 | 11 | 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 // indirect | 24 | 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 |
| 28 | github.com/charmbracelet/x/ansi v0.2.3 // indirect | 26 | 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 // indirect | 27 | github.com/containerd/log v0.1.0 // indirect | ||
| 31 | github.com/creack/pty v1.1.18 // indirect | 28 | github.com/creack/pty v1.1.18 // indirect | ||
| 32 | github.com/distribution/reference v0.6.0 // indirect | 29 | github.com/distribution/reference v0.6.0 // indirect | ||
| 33 | github.com/dustin/go-humanize v1.0.1 // indirect | 30 | 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 // indirect | 31 | 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 // indirect | 33 | github.com/go-logr/logr v1.4.2 // indirect | ||
| 37 | github.com/go-logr/stdr v1.2.2 // indirect | 34 | github.com/go-logr/stdr v1.2.2 // indirect | ||
| 38 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect | 35 | 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 // indirect | 37 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect | ||
| 41 | github.com/kr/pretty v0.3.1 // indirect | 38 | github.com/kr/pretty v0.3.1 // indirect | ||
| 42 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | 39 | 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 // indirect | 40 | github.com/mattn/go-runewidth v0.0.16 // indirect | ||
| 45 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect | 41 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect | ||
| 46 | github.com/moby/docker-image-spec v1.3.1 // indirect | 42 | 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 // indirect | 43 | github.com/muesli/termenv v0.15.2 // indirect | ||
| 50 | github.com/ncruces/go-strftime v0.1.9 // indirect | 44 | github.com/ncruces/go-strftime v0.1.9 // indirect | ||
| 51 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect | 45 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect | ||
| @@ -57,7 +51,7 @@ require ( | |||||
| 57 | go.opentelemetry.io/otel/metric v1.29.0 // indirect | 51 | go.opentelemetry.io/otel/metric v1.29.0 // indirect | ||
| 58 | go.opentelemetry.io/otel/sdk v1.21.0 // indirect | 52 | go.opentelemetry.io/otel/sdk v1.21.0 // indirect | ||
| 59 | go.opentelemetry.io/otel/trace v1.29.0 // indirect | 53 | 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 |
| 61 | golang.org/x/term v0.23.0 // indirect | 55 | golang.org/x/term v0.23.0 // indirect | ||
| 62 | google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect | 56 | google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect | ||
| 63 | modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect | 57 | 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 | + | ||
| 1 | 3 | package cmd | |
| 2 | 4 | ||
| 3 | 5 | import ( | |
| 4 | - | "crypto/tls" | |
| 6 | + | "encoding/json" | |
| 5 | 7 | "fmt" | |
| 6 | 8 | "io" | |
| 7 | 9 | "net/http" | |
| 8 | 10 | "os" | |
| 9 | 11 | "strings" | |
| 10 | - | "sync" | |
| 11 | 12 | "time" | |
| 12 | 13 | ||
| 13 | 14 | "github.com/bnema/gordon/internal/cli" | |
| 14 | 15 | "github.com/bnema/gordon/internal/cli/handler" | |
| 15 | - | "github.com/bnema/gordon/internal/cli/mvu" | |
| 16 | 16 | "github.com/bnema/gordon/internal/common" | |
| 17 | 17 | "github.com/bnema/gordon/pkg/docker" | |
| 18 | - | "github.com/fatih/color" | |
| 18 | + | "github.com/charmbracelet/log" | |
| 19 | 19 | "github.com/spf13/cobra" | |
| 20 | 20 | ) | |
| 21 | 21 | ||
| @@ -29,205 +29,152 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||
| 29 | 29 | Args: cobra.ExactArgs(1), | |
| 30 | 30 | PreRun: func(cmd *cobra.Command, args []string) { | |
| 31 | 31 | if err := handler.FieldCheck(a); err != nil { | |
| 32 | - | fmt.Println("Field check failed:", err) | |
| 32 | + | log.Error("Field check failed", "error", err) | |
| 33 | 33 | os.Exit(1) | |
| 34 | 34 | } | |
| 35 | 35 | }, | |
| 36 | 36 | Run: func(cmd *cobra.Command, args []string) { | |
| 37 | 37 | 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) | |
| 54 | 39 | ||
| 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) | |
| 58 | 42 | return | |
| 59 | 43 | } | |
| 60 | 44 | ||
| 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) | |
| 63 | 46 | if err != nil { | |
| 64 | - | fmt.Println("Error exporting image:", err) | |
| 47 | + | log.Error("Error exporting image", "error", err) | |
| 65 | 48 | return | |
| 66 | 49 | } | |
| 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) | |
| 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 | 63 | return | |
| 145 | 64 | } | |
| 146 | 65 | ||
| 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!") | |
| 188 | 67 | }, | |
| 189 | 68 | } | |
| 190 | 69 | ||
| 191 | - | // Add flags | |
| 192 | 70 | deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container") | |
| 193 | 71 | deployCmd.Flags().StringVarP(&targetDomain, "target", "t", "", "Target domain for Traefik") | |
| 194 | 72 | ||
| 195 | 73 | return deployCmd | |
| 196 | 74 | } | |
| 197 | 75 | ||
| 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) { | |
| 201 | 88 | exists, err := docker.CheckIfImageExists(imageName) | |
| 202 | 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 | 93 | var imageID string | |
| 207 | 94 | if exists { | |
| 208 | - | // What the user submitted is a valid image ID | |
| 209 | 95 | imageID = imageName | |
| 210 | 96 | } else { | |
| 211 | - | // What the user submitted is not a valid image ID, search by name | |
| 212 | 97 | imageID, err = docker.GetImageIDByName(imageName) | |
| 213 | 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 | 103 | actualSize, err := docker.GetImageSizeFromReader(imageID) | |
| 219 | 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 | 108 | reader, err := docker.ExportImageFromEngine(imageID) | |
| 224 | 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 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 | + | } | |
| 230 | 178 | ||
| 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) | |
| 233 | 180 | } | |
| @@ -1,21 +1,21 @@ | |||||
| 1 | + | // gordon/internal/cli/cmd/deploy.go | |||
| 2 | + | ||||
| 1 | package cmd | 3 | 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 domain | 40 | + | 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 | return | 42 | return | ||
| 59 | } | 43 | } | ||
| 60 | 44 | ||||
| 61 | - | // Export the image to a reader and return its true size | 45 | + | 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 | return | 48 | 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 send | 54 | + | 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 bar | 56 | + | log.Error("Deployment failed", |
| 74 | - | progressReader := &mvu.ProgressReader{ | 57 | + | "status_code", deployErr.StatusCode, |
| 75 | - | Reader: reader, // This is the actual reader from exportDockerImage | 58 | + | "message", deployErr.Message, |
| 76 | - | Total: actualSize, // This is the total size of the data to be read | 59 | + | "raw_response", deployErr.RawResponse) |
| 77 | - | ProgressCh: progressCh, // This is the channel used to send progress updates | 60 | + | } 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 | return | 63 | return | ||
| 145 | } | 64 | } | ||
| 146 | 65 | ||||
| 147 | - | // Wait for the deployment goroutine to complete | 66 | + | 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 deployCmd | 73 | return deployCmd | ||
| 196 | } | 74 | } | ||
| 197 | 75 | ||||
| 198 | - | // export the docker image to a reader and return its true size | 76 | + | 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 ID | 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) { | |||
| 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 string | 93 | var imageID string | ||
| 207 | if exists { | 94 | if exists { | ||
| 208 | - | // What the user submitted is a valid image ID | |||
| 209 | imageID = imageName | 95 | 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 message | 113 | + | 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 message | 179 | + | 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 @@ | |||
| 1 | 1 | package cmd | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "bytes" | |
| 5 | - | "crypto/tls" | |
| 4 | + | "encoding/json" | |
| 6 | 5 | "fmt" | |
| 7 | - | "io" | |
| 8 | - | "net/http" | |
| 9 | 6 | "os" | |
| 10 | - | "strings" | |
| 11 | - | "sync" | |
| 12 | - | "time" | |
| 13 | 7 | ||
| 14 | 8 | "github.com/bnema/gordon/internal/cli" | |
| 15 | 9 | "github.com/bnema/gordon/internal/cli/handler" | |
| 16 | 10 | "github.com/bnema/gordon/internal/common" | |
| 17 | - | "github.com/fatih/color" | |
| 11 | + | "github.com/charmbracelet/log" | |
| 18 | 12 | "github.com/spf13/cobra" | |
| 19 | 13 | ) | |
| 20 | 14 | ||
| 21 | 15 | func NewPushCommand(a *cli.App) *cobra.Command { | |
| 16 | + | var port string | |
| 17 | + | ||
| 22 | 18 | pushCmd := &cobra.Command{ | |
| 23 | 19 | Use: "push [image:tag]", | |
| 24 | 20 | Short: "Push an image to your remote Gordon instance", | |
| 25 | 21 | Args: cobra.ExactArgs(1), | |
| 26 | 22 | PreRun: func(cmd *cobra.Command, args []string) { | |
| 27 | 23 | if err := handler.FieldCheck(a); err != nil { | |
| 28 | - | fmt.Println("Field check failed:", err) | |
| 24 | + | log.Error("Field check failed", "error", err) | |
| 29 | 25 | os.Exit(1) | |
| 30 | 26 | } | |
| 31 | 27 | }, | |
| 32 | 28 | Run: func(cmd *cobra.Command, args []string) { | |
| 33 | 29 | imageName := args[0] | |
| 34 | - | color.White("Pushing image: %s", imageName) | |
| 30 | + | log.Info("Pushing image", "image", imageName) | |
| 35 | 31 | ||
| 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) | |
| 40 | 35 | } | |
| 36 | + | }, | |
| 37 | + | } | |
| 41 | 38 | ||
| 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)") | |
| 44 | 40 | ||
| 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 | + | } | |
| 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 bar | |
| 55 | - | progressBar := cmd.OutOrStdout() | |
| 56 | - | fmt.Fprintf(progressBar, "Uploading image... ") | |
| 49 | + | handler.EnsureImageTag(&imageName) | |
| 57 | 50 | ||
| 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 | + | } | |
| 66 | 56 | ||
| 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, | |
| 115 | 70 | }, | |
| 116 | 71 | } | |
| 117 | 72 | ||
| 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 | |
| 119 | 89 | } | |
| @@ -1,119 +1,89 @@ | |||||
| 1 | package cmd | 1 | 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 name | 32 | + | 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 tag | 39 | + | 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 size | 41 | + | 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 bar | 49 | + | handler.EnsureImageTag(&imageName) |
| 55 | - | progressBar := cmd.OutOrStdout() | |||
| 56 | - | fmt.Fprintf(progressBar, "Uploading image... ") | |||
| 57 | 50 | ||||
| 58 | - | // Create a RequestPayload and populate it | 51 | + | 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.WaitGroup | 57 | + | 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 | - | return | 65 | + | 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 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 | |||
| 119 | } | 89 | } | ||
M internal/cli/handler/startserver.go +3 -3
| @@ -2,7 +2,6 @@ package handler | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | - | "log" | |
| 6 | 5 | "os" | |
| 7 | 6 | "os/signal" | |
| 8 | 7 | "syscall" | |
| @@ -10,6 +9,7 @@ import ( | |||
| 10 | 9 | ||
| 11 | 10 | "github.com/bnema/gordon/internal/httpserve" | |
| 12 | 11 | "github.com/bnema/gordon/internal/server" | |
| 12 | + | "github.com/charmbracelet/log" | |
| 13 | 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 | 36 | go func() { | |
| 37 | 37 | sig := <-sigs | |
| 38 | - | log.Println("Received signal:", sig) | |
| 38 | + | log.Info(fmt.Println("Received signal:", sig)) | |
| 39 | 39 | os.Exit(0) | |
| 40 | 40 | }() | |
| 41 | 41 | ||
| @@ -46,7 +46,7 @@ func StartServer(a *server.App, port string) error { | |||
| 46 | 46 | e.HidePort = true | |
| 47 | 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 | 50 | if err := e.Start(fmt.Sprintf(":%s", a.Config.Http.Port)); err != nil { | |
| 51 | 51 | log.Fatal(err) | |
| 52 | 52 | } | |
| @@ -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 := <-sigs | 37 | 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 = true | 46 | 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 { | |||
| 16 | 16 | 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 | 54 | func (p *RequestPayload) UnmarshalJSON(data []byte) error { | |
| 20 | 55 | var raw map[string]json.RawMessage | |
| 21 | 56 | err := json.Unmarshal(data, &raw) | |
| @@ -76,16 +111,3 @@ func NewPingPayload(data map[string]interface{}) (PingPayload, error) { | |||
| 76 | 111 | } | |
| 77 | 112 | 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 | - | } | |
| @@ -16,6 +16,41 @@ type Payload interface { | |||||
| 16 | GetType() string | 16 | 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.RawMessage | 55 | 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}, nil | 112 | 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 @@ | |||
| 1 | 1 | package handler | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "encoding/json" | |
| 4 | 5 | "fmt" | |
| 5 | 6 | "net/http" | |
| 6 | 7 | "regexp" | |
| @@ -18,6 +19,13 @@ type InfoResponse struct { | |||
| 18 | 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 | 29 | func (info *InfoResponse) Populate(a *server.App) { | |
| 22 | 30 | info.Uptime = a.GetUptime() | |
| 23 | 31 | info.Version = a.GetVersionstring() | |
| @@ -50,6 +58,75 @@ func GetInfos(c echo.Context, a *server.App) error { | |||
| 50 | 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 | 130 | func PostDeploy(c echo.Context, a *server.App) error { | |
| 54 | 131 | // Initialize pushPayload object | |
| 55 | 132 | payload := &common.DeployPayload{ | |
| @@ -84,29 +161,39 @@ func PostDeploy(c echo.Context, a *server.App) error { | |||
| 84 | 161 | // Save the image tar in the storage | |
| 85 | 162 | imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) | |
| 86 | 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 | 171 | fmt.Printf("Image saved successfully to: %s\n", imagePath) | |
| 91 | 172 | ||
| 92 | 173 | // Import the tar in docker | |
| 93 | 174 | imageID, err := docker.ImportImageToEngine(imagePath) | |
| 94 | 175 | if err != nil { | |
| 95 | 176 | 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 | 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 | 190 | // Remove the image from the storage | |
| 107 | 191 | err = store.RemoveFromStorage(imagePath) | |
| 108 | 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 | 199 | fmt.Printf("Image imported successfully: %s\n", payload.ImageName) | |
| @@ -117,22 +204,40 @@ func PostDeploy(c echo.Context, a *server.App) error { | |||
| 117 | 204 | // Create the container using cmdparams.FromPayloadStructToCmdParams | |
| 118 | 205 | params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, imageID) | |
| 119 | 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 | 212 | // Create the container | |
| 124 | 213 | containerID, err := docker.CreateContainer(params) | |
| 125 | 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 | 221 | // Start the container | |
| 130 | 222 | err = docker.StartContainer(containerID) | |
| 131 | 223 | if err != nil { | |
| 132 | 224 | 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 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) | |
| 138 | 243 | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package handler | 1 | 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 object | 131 | // 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 storage | 161 | // 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 docker | 173 | // 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 fails | 176 | 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 storage | 190 | // 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.FromPayloadStructToCmdParams | 204 | // 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 container | 212 | // 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 container | 221 | // 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 fails | 224 | 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 it | 231 | + | // 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 { | |||
| 15 | 15 | } | |
| 16 | 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 | - | } | |
| @@ -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 { | |||
| 23 | 23 | func validateToken(c echo.Context, a *server.App) error { | |
| 24 | 24 | token := c.Request().Header.Get("Authorization") | |
| 25 | 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 | 29 | token = strings.Replace(token, "Bearer ", "", 1) | |
| 30 | 30 | configToken, err := a.Config.GetToken() | |
| 31 | 31 | if err != nil { | |
| @@ -33,7 +33,7 @@ func validateToken(c echo.Context, a *server.App) error { | |||
| 33 | 33 | } | |
| 34 | 34 | ||
| 35 | 35 | if token != configToken { | |
| 36 | - | return c.JSON(401, "Unauthorized") | |
| 36 | + | return echo.NewHTTPError(401, "Token does not match") | |
| 37 | 37 | } | |
| 38 | 38 | ||
| 39 | 39 | 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 nil | 39 | return nil | ||
M internal/httpserve/router.go +4 -1
| @@ -54,9 +54,12 @@ func bindAPIEndpoints(e *echo.Echo, a *server.App) { | |||
| 54 | 54 | apiGroup.GET("/ping", func(c echo.Context) error { | |
| 55 | 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 | 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 | 65 | // 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 path | 65 | // bindStaticRoute bind static path | ||
M internal/templating/cmdparams/traefik.go +1 -1
| @@ -24,6 +24,6 @@ func CreateTraefikLabels(params *docker.ContainerCommandParams) { | |||
| 24 | 24 | } | |
| 25 | 25 | ||
| 26 | 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 | } | |
| @@ -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 | - | ||
| 4 | 1 | function initializeVersionCheck() { | |
| 5 | 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 | 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 | 10 | if (currentVersion !== fetchedVersionNumber) { | |
| 12 | - | const updateStr = updateStrTemplate.replace("%VERSION%", fetchedVersionNumber); | |
| 11 | + | const updateStr = updateStrTemplate.replace( | |
| 12 | + | "%VERSION%", | |
| 13 | + | fetchedVersionNumber, | |
| 14 | + | ); | |
| 13 | 15 | console.log(updateStr); | |
| 14 | 16 | const updateElement = document.getElementById("update-available"); | |
| 15 | 17 | updateElement.title = updateStr; | |
| @@ -17,13 +19,15 @@ function initializeVersionCheck() { | |||
| 17 | 19 | updateElement.classList.remove("hidden"); // Remove the Tailwind `hidden` class | |
| 18 | 20 | updateElement.classList.add("block"); // Add the Tailwind `block` class | |
| 19 | 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 | 29 | 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`) | |
| 27 | 31 | .then((response) => response.json()) | |
| 28 | 32 | .then((versionData) => checkVersion(versionData, currentVersion)) | |
| 29 | 33 | .catch((error) => console.error("Error fetching version:", error)); | |
| @@ -46,4 +50,4 @@ function initializeVersionCheck() { | |||
| 46 | 50 | init(); | |
| 47 | 51 | } | |
| 48 | 52 | ||
| 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` class | 19 | updateElement.classList.remove("hidden"); // Remove the Tailwind `hidden` class | ||
| 18 | updateElement.classList.add("block"); // Add the Tailwind `block` class | 20 | 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 + /version | 30 | + | 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); |