From fb536a296499e05f0db257fc2cae63302f05285d Mon Sep 17 00:00:00 2001 From: bamen Date: Thu, 29 Aug 2024 10:04:37 +0200 Subject: [PATCH] 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 --- .goreleaser.yaml | 10 +- Dockerfile | 9 +- assets/vhs/demo.tape | 14 +- buildpush-dev.sh | 53 +++- cmd/cli/main.go | 3 +- go.mod | 14 +- go.sum | 23 +- internal/cli/cmd/deploy.go | 263 +++++++----------- internal/cli/cmd/push.go | 136 ++++----- internal/cli/handler/startserver.go | 6 +- internal/cli/mvu/progressbar.go | 107 ------- internal/cli/mvu/run.go | 50 ---- internal/cli/mvu/spinner.go | 137 --------- internal/common/payload.go | 48 +++- internal/httpserve/handler/cli-endpoints.go | 131 ++++++++- internal/httpserve/handler/senderror.go | 4 - internal/httpserve/middleware/requiretoken.go | 6 +- internal/httpserve/router.go | 5 +- internal/templating/cmdparams/traefik.go | 2 +- internal/webui/public/assets/js/custom.js | 24 +- 20 files changed, 403 insertions(+), 642 deletions(-) delete mode 100644 internal/cli/mvu/progressbar.go delete mode 100644 internal/cli/mvu/run.go delete mode 100644 internal/cli/mvu/spinner.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7ec22d7..6c23385 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,3 +1,5 @@ +version: 2 + before: hooks: - go mod tidy @@ -27,13 +29,12 @@ dockers: build_flag_templates: - "--pull" - "--platform=linux/amd64" + - "--build-arg=ARCH=amd64" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.source=https://github.com/bnema/gordon" - extra_files: - - .iscontainer - image_templates: - "ghcr.io/bnema/gordon:{{ .Tag }}-arm64" use: buildx @@ -42,13 +43,12 @@ dockers: build_flag_templates: - "--pull" - "--platform=linux/arm64" + - "--build-arg=ARCH=arm64" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.source=https://github.com/bnema/gordon" - extra_files: - - .iscontainer archives: - format: zip @@ -59,4 +59,4 @@ changelog: filters: exclude: - "^docs:" - - "^test:" \ No newline at end of file + - "^test:" diff --git a/Dockerfile b/Dockerfile index 6a0e168..5126816 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,8 @@ -# Start from scratch FROM alpine:latest -ARG ARCH +COPY gordon /gordon -# Copy the pre-built binary for the specific architecture -COPY dist/gordon-linux-${ARCH} /gordon -# Create the .iscontainer file RUN touch /.iscontainer -# Set the entrypoint ENTRYPOINT ["/gordon"] - -# Default command CMD ["serve"] diff --git a/assets/vhs/demo.tape b/assets/vhs/demo.tape index 77835ae..e94222f 100644 --- a/assets/vhs/demo.tape +++ b/assets/vhs/demo.tape @@ -71,17 +71,25 @@ Space Type "deploy" Sleep 1 Space -Type "bnema/go-helloworld-http:latest" +Type "bnema/go-helloworld:latest" Sleep 1 Space Type "-p 8888:8080" Sleep 1 Space -Type "-t hello.bamen.dev" +Type "-t hello.bamen.dev" Sleep 1 Enter -Sleep 15 +Sleep 14 + +Hide +Type "clear" +sleep 1 +Enter +Show + + Enter Type "curl https://hello.bamen.dev" Sleep 1 diff --git a/buildpush-dev.sh b/buildpush-dev.sh index 77b3d9d..0ae493c 100755 --- a/buildpush-dev.sh +++ b/buildpush-dev.sh @@ -1,11 +1,20 @@ #!/bin/bash - set -e - +ENGINE="podman" REPO="ghcr.io/bnema/gordon" TAG="dev" DIST_DIR="./dist" +# Function to handle errors +handle_error() { + echo "Error occurred. Cleaning up..." + $ENGINE system prune -f + exit 1 +} + +# Set up error handling +trap 'handle_error' ERR + # Ensure dist directory exists mkdir -p $DIST_DIR @@ -14,25 +23,41 @@ echo "Building Go binaries..." CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $DIST_DIR/gordon-linux-amd64 ./cmd/cli CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o $DIST_DIR/gordon-linux-arm64 ./cmd/cli +# Clean up any dangling images before building +echo "Cleaning up dangling images..." +$ENGINE image prune -f + # Build Docker images for each architecture echo "Building Docker images..." -docker build -t $REPO:${TAG}-amd64 --build-arg ARCH=amd64 -f Dockerfile . -docker build -t $REPO:${TAG}-arm64v8 --build-arg ARCH=arm64 -f Dockerfile . +$ENGINE build -t $REPO:${TAG}-amd64 --build-arg ARCH=amd64 -f Dockerfile . +$ENGINE build -t $REPO:${TAG}-arm64v8 --build-arg ARCH=arm64 -f Dockerfile . # Push images echo "Pushing Docker images..." -docker push $REPO:${TAG}-amd64 -docker push $REPO:${TAG}-arm64v8 +$ENGINE push $REPO:${TAG}-amd64 +$ENGINE push $REPO:${TAG}-arm64v8 + +# Remove existing manifest if it exists +echo "Removing existing manifest..." +$ENGINE manifest rm $REPO:$TAG || true -# Create and push multi-arch manifest -echo "Creating and pushing multi-arch manifest..." -docker manifest create $REPO:$TAG \ +# Create multi-arch manifest +echo "Creating multi-arch manifest..." +$ENGINE manifest create $REPO:$TAG \ $REPO:${TAG}-amd64 \ - $REPO:${TAG}-arm64v8 \ - --amend + $REPO:${TAG}-arm64 # Annotate the arm64 image with variant information -docker manifest annotate $REPO:$TAG \ - $REPO:${TAG}-arm64v8 --arch arm64 --variant v8 +echo "Annotating arm64 image..." +$ENGINE manifest annotate $REPO:$TAG \ + $REPO:${TAG}-arm64 --arch arm64 --variant v8 + +# Debug: List manifests +echo "Listing manifests..." +$ENGINE manifest inspect $REPO:$TAG + +# Push multi-arch manifest +echo "Pushing multi-arch manifest..." +$ENGINE manifest push --all $REPO:$TAG -docker manifest push $REPO:$TAG +echo "Script completed successfully." diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 1dfc679..dfc076a 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "log" "regexp" "github.com/bnema/gordon/internal/cli" @@ -61,7 +60,7 @@ func main() { go func() { msg, err := common.CheckVersionPeriodically(&s.Config) if err != nil || msg != "" { - log.Println(msg) + // log.Warnf("Error checking for new version: %s", err) } }() diff --git a/go.mod b/go.mod index f3d7566..6704049 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,7 @@ go 1.23 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/PuerkitoBio/goquery v1.9.2 - github.com/charmbracelet/bubbles v0.19.0 - github.com/charmbracelet/bubbletea v0.27.1 - github.com/charmbracelet/lipgloss v0.13.0 + github.com/charmbracelet/log v0.4.0 github.com/docker/docker v27.1.2+incompatible github.com/docker/go-connections v0.5.0 github.com/fatih/color v1.17.0 @@ -24,15 +22,14 @@ require ( require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect github.com/charmbracelet/x/ansi v0.2.3 // indirect - github.com/charmbracelet/x/term v0.2.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/creack/pty v1.1.18 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -40,12 +37,9 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -57,7 +51,7 @@ require ( go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/sdk v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect - golang.org/x/sync v0.8.0 // indirect + golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect golang.org/x/term v0.23.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect diff --git a/go.sum b/go.sum index a3c7118..40c51de 100644 --- a/go.sum +++ b/go.sum @@ -16,18 +16,12 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0= -github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA= -github.com/charmbracelet/bubbletea v0.27.1 h1:/yhaJKX52pxG4jZVKCNWj/oq0QouPdXycriDRA6m6r8= -github.com/charmbracelet/bubbletea v0.27.1/go.mod h1:xc4gm5yv+7tbniEvQ0naiG9P3fzYhk16cTgDZQQW6YE= -github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= -github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= -github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -48,12 +42,12 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -112,8 +106,6 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -127,10 +119,6 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -194,6 +182,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -223,7 +213,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/cli/cmd/deploy.go b/internal/cli/cmd/deploy.go index 2859254..21537cd 100644 --- a/internal/cli/cmd/deploy.go +++ b/internal/cli/cmd/deploy.go @@ -1,21 +1,21 @@ +// gordon/internal/cli/cmd/deploy.go + package cmd import ( - "crypto/tls" + "encoding/json" "fmt" "io" "net/http" "os" "strings" - "sync" "time" "github.com/bnema/gordon/internal/cli" "github.com/bnema/gordon/internal/cli/handler" - "github.com/bnema/gordon/internal/cli/mvu" "github.com/bnema/gordon/internal/common" "github.com/bnema/gordon/pkg/docker" - "github.com/fatih/color" + "github.com/charmbracelet/log" "github.com/spf13/cobra" ) @@ -29,205 +29,152 @@ func NewDeployCommand(a *cli.App) *cobra.Command { Args: cobra.ExactArgs(1), PreRun: func(cmd *cobra.Command, args []string) { if err := handler.FieldCheck(a); err != nil { - fmt.Println("Field check failed:", err) + log.Error("Field check failed", "error", err) os.Exit(1) } }, Run: func(cmd *cobra.Command, args []string) { imageName := args[0] - color.White("Pushing image: %s", imageName) - - // Validate the image name - if err := handler.ValidateImageName(imageName); err != nil { - fmt.Println(err) - return - } - - // Ensure the image has a tag - handler.EnsureImageTag(&imageName) - - // Validate the port mapping - if err := handler.ValidatePortMapping(port); err != nil { - fmt.Println(err) - return - } + log.Info("Pushing image", "image", imageName) - // Validate the target domain - if err := handler.ValidateTargetDomain(targetDomain); err != nil { - fmt.Println(err) + if err := validateInputs(imageName, port, targetDomain); err != nil { + log.Error("Validation failed", "error", err) return } - // Export the image to a reader and return its true size - reader, actualSize, successMsg, err := exportDockerImage(imageName) + reader, actualSize, err := exportDockerImage(imageName) if err != nil { - fmt.Println("Error exporting image:", err) + log.Error("Error exporting image", "error", err) return } - - fmt.Println(successMsg) - - progressCh := make(chan mvu.ProgressMsg) - errCh := make(chan error, 1) // Buffer of 1 to prevent goroutine leak in case of non-blocking send - - // Create a progress function to update the progress bar - progressReader := &mvu.ProgressReader{ - Reader: reader, // This is the actual reader from exportDockerImage - Total: actualSize, // This is the total size of the data to be read - ProgressCh: progressCh, // This is the channel used to send progress updates - } - - // Create a RequestPayload and populate it - reqPayload := common.RequestPayload{ - Type: "deploy", - Payload: common.DeployPayload{ - Ports: port, - TargetDomain: targetDomain, - ImageName: imageName, - Data: progressReader, - }, - } - - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push") - if err != nil { - errCh <- fmt.Errorf("error sending HTTP request: %w", err) - return - } - - // Check the response - targetDomain := string(resp.Body) - targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace - targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes - // Close the progress channel after the upload is complete - // Determine if the target is HTTPS or HTTP - isHTTPS := strings.HasPrefix(targetDomain, "https://") - - // Initialize HTTP client - client := &http.Client{ - Timeout: 60 * time.Second, - } - - // Only set TLS config if target is HTTPS - if isHTTPS { - client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated - } - } - - // Run the TUI program - finalModel, err := mvu.RunDeploymentTUI(client, imageName, targetDomain, port) - if err != nil { - errCh <- fmt.Errorf("error running deployment TUI: %w", err) - return + defer reader.Close() + + log.Info("Image exported successfully", "image", imageName, "size", actualSize) + + if err := deployImage(a, reader, imageName, port, targetDomain); err != nil { + if deployErr, ok := err.(*common.DeploymentError); ok { + log.Error("Deployment failed", + "status_code", deployErr.StatusCode, + "message", deployErr.Message, + "raw_response", deployErr.RawResponse) + } else { + log.Error("Deployment failed", "error", err) } - - // Check if the deployment was successful - if !finalModel.DeploymentDone { - errCh <- fmt.Errorf("deployment failed") - return - } - - // Print the final message - color.Blue("Deployment successful!") - fmt.Println("Your application is now available at:", targetDomain) - close(progressCh) - }() - // Run the progress bar TUI - m, err := mvu.RunProgressBarTUI(progressCh) - if err != nil { - errCh <- fmt.Errorf("error running progress bar TUI: %w", err) return } - // Wait for the deployment goroutine to complete - wg.Wait() - - done := false - - for !done { - select { - case err := <-errCh: - if err != nil { - fmt.Println(err) - return - } - case <-time.After(1 * time.Second): - // Check if the progress bar is done - if m.Done { - done = true - } - } - } - - // Check for errors from the deployment goroutine - select { - case err := <-errCh: - if err != nil { - fmt.Println(err) - return - } - default: - } - - // Check if the progress bar is done - if m.Done { - // Close the reader - err = progressReader.Close() - if err != nil { - fmt.Println("Error closing reader:", err) - return - } - } else { - fmt.Println("Deployment completed, but progress bar is not done.") - } + log.Info("Deployment successful!") }, } - // Add flags deployCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container") deployCmd.Flags().StringVarP(&targetDomain, "target", "t", "", "Target domain for Traefik") return deployCmd } -// export the docker image to a reader and return its true size -func exportDockerImage(imageName string) (io.ReadCloser, int64, string, error) { - // Check if what the user submitted is a valid image ID +func validateInputs(imageName, port, targetDomain string) error { + if err := handler.ValidateImageName(imageName); err != nil { + return err + } + handler.EnsureImageTag(&imageName) + if err := handler.ValidatePortMapping(port); err != nil { + return err + } + return handler.ValidateTargetDomain(targetDomain) +} + +func exportDockerImage(imageName string) (io.ReadCloser, int64, error) { exists, err := docker.CheckIfImageExists(imageName) if err != nil { - return nil, 0, "", fmt.Errorf("error while checking image existence: %w", err) + return nil, 0, fmt.Errorf("error checking image existence: %w", err) } var imageID string if exists { - // What the user submitted is a valid image ID imageID = imageName } else { - // What the user submitted is not a valid image ID, search by name imageID, err = docker.GetImageIDByName(imageName) if err != nil { - return nil, 0, "", fmt.Errorf("error while searching for image by name: %w", err) + return nil, 0, fmt.Errorf("error searching for image by name: %w", err) } } actualSize, err := docker.GetImageSizeFromReader(imageID) if err != nil { - return nil, 0, "", fmt.Errorf("error while retrieving image size: %w", err) + return nil, 0, fmt.Errorf("error retrieving image size: %w", err) } reader, err := docker.ExportImageFromEngine(imageID) if err != nil { - return nil, 0, "", fmt.Errorf("error while exporting image: %w", err) + return nil, 0, fmt.Errorf("error exporting image: %w", err) } - // Create a success message - successMsg := fmt.Sprintf("Image %s exported successfully", imageName) + return reader, actualSize, nil +} + +func deployImage(a *cli.App, reader io.Reader, imageName, port, targetDomain string) error { + reqPayload := common.RequestPayload{ + Type: "deploy", + Payload: common.DeployPayload{ + Ports: port, + TargetDomain: targetDomain, + ImageName: imageName, + Data: io.NopCloser(reader), + }, + } + + resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/deploy") + if err != nil { + return &common.DeploymentError{ + StatusCode: 0, + Message: fmt.Sprintf("error sending HTTP request: %v", err), + } + } + + var deployResponse common.DeployResponse + if err := json.Unmarshal(resp.Body, &deployResponse); err != nil { + return &common.DeploymentError{ + StatusCode: resp.StatusCode, + Message: fmt.Sprintf("error parsing response: %v", err), + RawResponse: string(resp.Body), + } + } + + if !deployResponse.Success { + return &common.DeploymentError{ + StatusCode: resp.StatusCode, + Message: deployResponse.Message, + RawResponse: string(resp.Body), + } + } + + log.Info("Application deployed", "domain", deployResponse.Domain) + return waitForDeployment(deployResponse.Domain) +} + +func waitForDeployment(domain string) error { + client := &http.Client{Timeout: 10 * time.Second} + maxRetries := 20 + retryInterval := time.Second + + log.Info("Waiting for deployment to be reachable") + for i := 0; i < maxRetries; i++ { + resp, err := client.Get(domain) + if err == nil { + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return nil + } + // Check for error messages in the response body + body, _ := io.ReadAll(resp.Body) + if strings.Contains(string(body), "failed to create container:") { + return fmt.Errorf("deployment failed: %s", string(body)) + } + } + log.Warn("Deployment not ready yet, retrying", "attempt", fmt.Sprintf("%d/20", i+1)) + time.Sleep(retryInterval) + } - // Return the reader, actual size, and success message - return reader, actualSize, successMsg, nil + return fmt.Errorf("deployment not ready after %d attempts, giving up", maxRetries) } diff --git a/internal/cli/cmd/push.go b/internal/cli/cmd/push.go index 3c4aa30..96a0483 100644 --- a/internal/cli/cmd/push.go +++ b/internal/cli/cmd/push.go @@ -1,119 +1,89 @@ package cmd import ( - "bytes" - "crypto/tls" + "encoding/json" "fmt" - "io" - "net/http" "os" - "strings" - "sync" - "time" "github.com/bnema/gordon/internal/cli" "github.com/bnema/gordon/internal/cli/handler" "github.com/bnema/gordon/internal/common" - "github.com/fatih/color" + "github.com/charmbracelet/log" "github.com/spf13/cobra" ) func NewPushCommand(a *cli.App) *cobra.Command { + var port string + pushCmd := &cobra.Command{ Use: "push [image:tag]", Short: "Push an image to your remote Gordon instance", Args: cobra.ExactArgs(1), PreRun: func(cmd *cobra.Command, args []string) { if err := handler.FieldCheck(a); err != nil { - fmt.Println("Field check failed:", err) + log.Error("Field check failed", "error", err) os.Exit(1) } }, Run: func(cmd *cobra.Command, args []string) { imageName := args[0] - color.White("Pushing image: %s", imageName) + log.Info("Pushing image", "image", imageName) - // Validate the image name - if err := handler.ValidateImageName(imageName); err != nil { - fmt.Println(err) - return + if err := pushImage(a, imageName, port); err != nil { + log.Error("Push failed", "error", err) + os.Exit(1) } + }, + } - // Ensure the image has a tag - handler.EnsureImageTag(&imageName) + pushCmd.Flags().StringVarP(&port, "port", "p", "", "Port mapping for the container (e.g., 8080:80/tcp)") - // Export the image to a reader and return its true size - reader, _, successMsg, err := exportDockerImage(imageName) - if err != nil { - fmt.Println("Error exporting image:", err) - return - } + return pushCmd +} - fmt.Println(successMsg) +func pushImage(a *cli.App, imageName, port string) error { + if err := handler.ValidateImageName(imageName); err != nil { + return fmt.Errorf("invalid image name: %w", err) + } - // Create a progress bar - progressBar := cmd.OutOrStdout() - fmt.Fprintf(progressBar, "Uploading image... ") + handler.EnsureImageTag(&imageName) - // Create a RequestPayload and populate it - reqPayload := common.RequestPayload{ - Type: "push", - Payload: common.PushPayload{ - ImageName: imageName, - Data: reader, - }, - } + if port != "" { + if err := handler.ValidatePortMapping(port); err != nil { + return fmt.Errorf("invalid port mapping: %w", err) + } + } - var wg sync.WaitGroup - wg.Add(1) - - go func() { - defer wg.Done() - resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push") - if err != nil { - fmt.Println("Error sending request:", err) - return - } - - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(bytes.NewReader(resp.Body)) - fmt.Fprintln(cmd.OutOrStderr(), "Server returned an error:", string(bodyBytes)) - return - } - - // Check the response - targetDomain := string(resp.Body) - targetDomain = strings.TrimSpace(targetDomain) // Remove leading and trailing whitespace - targetDomain = strings.Trim(targetDomain, "\"") // Remove leading and trailing quotes - // Close the progress channel after the upload is complete - // Determine if the target is HTTPS or HTTP - isHTTPS := strings.HasPrefix(targetDomain, "https://") - - // Initialize HTTP client - client := &http.Client{ - Timeout: 10 * time.Second, - } - - // Only set TLS config if target is HTTPS - if isHTTPS { - client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, // set false to ensure certificate is validated - } - } - - }() - - // Wait for the HTTP request to finish - wg.Wait() - - // Close the reader - err = reader.Close() - if err != nil { - fmt.Println("Error closing reader:", err) - return - } + reader, actualSize, err := exportDockerImage(imageName) + if err != nil { + return fmt.Errorf("error exporting image: %w", err) + } + defer reader.Close() + + log.Info("Image exported successfully", "image", imageName, "size", actualSize) + + reqPayload := common.RequestPayload{ + Type: "push", + Payload: common.PushPayload{ + ImageName: imageName, + Data: reader, }, } - return pushCmd + resp, err := handler.SendHTTPRequest(a, &reqPayload, "POST", "/push") + if err != nil { + return fmt.Errorf("error sending HTTP request: %w", err) + } + + var pushResponse common.PushResponse + if err := json.Unmarshal(resp.Body, &pushResponse); err != nil { + return fmt.Errorf("error parsing response: %w", err) + } + + if !pushResponse.Success { + return fmt.Errorf("push failed: %s", pushResponse.Message) + } + + log.Info("Image pushed successfully", "image", imageName) + return nil } diff --git a/internal/cli/handler/startserver.go b/internal/cli/handler/startserver.go index 52052a5..dc53db1 100644 --- a/internal/cli/handler/startserver.go +++ b/internal/cli/handler/startserver.go @@ -2,7 +2,6 @@ package handler import ( "fmt" - "log" "os" "os/signal" "syscall" @@ -10,6 +9,7 @@ import ( "github.com/bnema/gordon/internal/httpserve" "github.com/bnema/gordon/internal/server" + "github.com/charmbracelet/log" "github.com/labstack/echo/v4" ) @@ -35,7 +35,7 @@ func StartServer(a *server.App, port string) error { go func() { sig := <-sigs - log.Println("Received signal:", sig) + log.Info(fmt.Println("Received signal:", sig)) os.Exit(0) }() @@ -46,7 +46,7 @@ func StartServer(a *server.App, port string) error { e.HidePort = true e = httpserve.RegisterRoutes(e, a) - log.Println("Starting server on port", a.Config.Http.Port) + log.Info(fmt.Sprintf("Starting server on port %s", port)) if err := e.Start(fmt.Sprintf(":%s", a.Config.Http.Port)); err != nil { log.Fatal(err) } diff --git a/internal/cli/mvu/progressbar.go b/internal/cli/mvu/progressbar.go deleted file mode 100644 index 9b26032..0000000 --- a/internal/cli/mvu/progressbar.go +++ /dev/null @@ -1,107 +0,0 @@ -package mvu - -import ( - "io" - "strings" - - "github.com/charmbracelet/bubbles/progress" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -const ( - padding = 2 - maxWidth = 80 -) - -var helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render - -type ProgressMsg float64 - -type Model struct { - progress progress.Model - Done bool -} - -type ProgressReader struct { - Reader io.ReadCloser - Total int64 - ReadBytes int64 - ProgressCh chan<- ProgressMsg -} - -func NewPBModel() Model { - m := progress.New(progress.WithGradient("#007BC0", "#011E5C")) - m.Width = maxWidth - - return Model{ - progress: m, - } -} - -func (pr *ProgressReader) Read(p []byte) (int, error) { - n, err := pr.Reader.Read(p) - if err != nil { - return n, err - } - - pr.ReadBytes += int64(n) - percentage := float64(pr.ReadBytes) / float64(pr.Total) - pr.ProgressCh <- ProgressMsg(percentage) // Send the percentage on the channel. - return n, err -} - -func (pr *ProgressReader) Close() error { - if closer, ok := pr.Reader.(io.Closer); ok { - return closer.Close() - } - return nil -} - -func (m Model) Init() tea.Cmd { - m.progress.Init() - m.progress.SetPercent(0) - return nil -} - -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - return m, tea.Quit - - case tea.WindowSizeMsg: - m.progress.Width = msg.Width - padding*2 - 4 - if m.progress.Width > maxWidth { - m.progress.Width = maxWidth - } - return m, nil - - case ProgressMsg: - // Update the progress bar with the percentage received on the channel. - cmd := m.progress.SetPercent(float64(msg)) - - // If the percentage is 100, set Done to true. - if msg == 1 { - m.Done = true - // quit - return m, tea.Quit - } - - return m, cmd - - case progress.FrameMsg: - progressModel, cmd := m.progress.Update(msg) - m.progress = progressModel.(progress.Model) - return m, cmd - - default: - return m, nil - } -} - -func (m Model) View() string { - pad := strings.Repeat(" ", padding) - return "\n" + - pad + m.progress.View() + "\n\n" + - pad + helpStyle("Press any key to quit") -} diff --git a/internal/cli/mvu/run.go b/internal/cli/mvu/run.go deleted file mode 100644 index d12a033..0000000 --- a/internal/cli/mvu/run.go +++ /dev/null @@ -1,50 +0,0 @@ -package mvu - -import ( - "fmt" - "net/http" - "os" - - tea "github.com/charmbracelet/bubbletea" -) - -// RunDeploymentTUI runs the complete deployment TUI and returns the final model -func RunDeploymentTUI(client *http.Client, imageName, targetDomain, port string) (SpinnerModel, error) { - m := NewSpinnerModel() - m.deployment.imageName = imageName - m.deployment.targetDomain = targetDomain - m.deployment.port = port - m.deployment.client = client - - p := tea.NewProgram(m) - modelInterface, err := p.Run() - if err != nil { - return SpinnerModel{}, fmt.Errorf("error running TUI program: %w", err) - } - - finalModel, ok := modelInterface.(SpinnerModel) - if !ok { - return SpinnerModel{}, fmt.Errorf("could not type assert tea model to concrete type") - } - - return finalModel, nil -} - -// RunProgressBarTUI runs the progress bar TUI and returns the final model -func RunProgressBarTUI(ProgressCh <-chan ProgressMsg) (*Model, error) { - m := NewPBModel() - p := tea.NewProgram(&m) - // Start a goroutine that updates the progress bar as percentages are received on the channel. - go func() { - for percent := range ProgressCh { - p.Send(ProgressMsg(percent)) - } - }() - - if _, err := p.Run(); err != nil { - fmt.Println("Error running progress bar TUI:", err) - os.Exit(1) - } - - return &m, nil -} diff --git a/internal/cli/mvu/spinner.go b/internal/cli/mvu/spinner.go deleted file mode 100644 index 9f14cf3..0000000 --- a/internal/cli/mvu/spinner.go +++ /dev/null @@ -1,137 +0,0 @@ -package mvu - -import ( - "crypto/tls" - "fmt" - "net/http" - "time" - - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -const ( - defaultMaxRetries = 20 - httpTimeout = 10 * time.Second - retryInterval = 1 * time.Second -) - -type SpinnerModel struct { - spinner spinner.Model - quitting bool - err error - deployment DeploymentInfo - DeploymentDone bool -} - -type DeploymentInfo struct { - imageName string - targetDomain string - port string - client *http.Client - retryCount int - maxRetries int -} - -type urlCheckedMsg struct { - success bool - err error -} - -// NewSpinnerModel returns a new SpinnerModel -func NewSpinnerModel() SpinnerModel { - s := spinner.New() - s.Spinner = spinner.Dot - s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#54baff")) - - return SpinnerModel{ - spinner: s, - deployment: DeploymentInfo{ - maxRetries: defaultMaxRetries, - client: newHTTPClient(), - }, - } -} - -func newHTTPClient() *http.Client { - return &http.Client{ - Timeout: httpTimeout, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } -} - -func (m SpinnerModel) Init() tea.Cmd { - return tea.Batch(m.spinner.Tick, m.checkURL()) -} - -// checkURL returns a command that checks the URL and returns a urlCheckedMsg -func (m *SpinnerModel) checkURL() tea.Cmd { - return func() tea.Msg { - success, err := attemptURLCheck(m.deployment) - if err != nil { - return urlCheckedMsg{success: false, err: err} - } - return urlCheckedMsg{success: success, err: nil} - } -} - -// attemptURLCheck attempts to check the URL and returns a bool and error -func attemptURLCheck(deployment DeploymentInfo) (bool, error) { - for deployment.retryCount = 0; deployment.retryCount < deployment.maxRetries; deployment.retryCount++ { - resp, err := deployment.client.Get(deployment.targetDomain) - if err != nil { - return false, fmt.Errorf("error checking URL: %w", err) - } - resp.Body.Close() - if resp.StatusCode == http.StatusOK { - return true, nil - } - time.Sleep(retryInterval) - } - return false, fmt.Errorf("the URL %s could not be reached after %d attempts", deployment.targetDomain, deployment.retryCount) -} - -// Update handles the messages sent to the model -func (m SpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "q", "esc", "ctrl+c": - m.quitting = true - return m, tea.Quit - default: - return m, nil - } - case urlCheckedMsg: - if msg.success { - m.DeploymentDone = true - return m, tea.Quit - } else { - m.err = msg.err - return m, nil - } - case spinner.TickMsg: - var cmd tea.Cmd - m.spinner, cmd = m.spinner.Update(msg) - return m, cmd - - default: - return m, nil - } -} - -// View returns the view for the model -func (m SpinnerModel) View() string { - if m.quitting { - if m.err != nil { - return fmt.Sprintf("Error: %v\n", m.err) - } - return "Deployment cancelled.\n" - } - - // Use the textStyle to format the entire string - return fmt.Sprintf("\n\n %s Wait while Traefik is setting up the domain and certificates... \n\n", m.spinner.View()) -} diff --git a/internal/common/payload.go b/internal/common/payload.go index 13efe39..ab24eed 100644 --- a/internal/common/payload.go +++ b/internal/common/payload.go @@ -16,6 +16,41 @@ type Payload interface { GetType() string } +type DeployResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Domain string `json:"domain,omitempty"` +} + +type PushResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +type DeploymentError struct { + StatusCode int `json:"status_code"` + Message string `json:"message"` + RawResponse string `json:"raw_response"` +} + +func (e *DeploymentError) Error() string { + return fmt.Sprintf("Deployment failed (status %d): %s", e.StatusCode, e.Message) +} + +type DeployPayload struct { + Ports string `json:"ports"` + TargetDomain string `json:"targetdomain"` + ImageName string `json:"imagename"` + ImageID string `json:"imageid"` + Data io.ReadCloser +} + +type PushPayload struct { + ImageName string `json:"imagename"` + ImageID string `json:"imageid"` + Data io.ReadCloser +} + func (p *RequestPayload) UnmarshalJSON(data []byte) error { var raw map[string]json.RawMessage err := json.Unmarshal(data, &raw) @@ -76,16 +111,3 @@ func NewPingPayload(data map[string]interface{}) (PingPayload, error) { } return PingPayload{Message: message}, nil } - -type DeployPayload struct { - Ports string `json:"ports"` - TargetDomain string `json:"targetdomain"` - ImageName string `json:"imagename"` - ImageID string `json:"imageid"` - Data io.ReadCloser -} - -type PushPayload struct { - ImageName string `json:"imagename"` - Data io.ReadCloser -} diff --git a/internal/httpserve/handler/cli-endpoints.go b/internal/httpserve/handler/cli-endpoints.go index 810da0d..4234a29 100644 --- a/internal/httpserve/handler/cli-endpoints.go +++ b/internal/httpserve/handler/cli-endpoints.go @@ -1,6 +1,7 @@ package handler import ( + "encoding/json" "fmt" "net/http" "regexp" @@ -18,6 +19,13 @@ type InfoResponse struct { Version string `json:"version"` } +type DeployResponse = common.DeployResponse +type PushResponse = common.PushResponse + +func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error { + return c.JSON(statusCode, response) +} + func (info *InfoResponse) Populate(a *server.App) { info.Uptime = a.GetUptime() info.Version = a.GetVersionstring() @@ -50,6 +58,75 @@ func GetInfos(c echo.Context, a *server.App) error { return c.JSON(http.StatusOK, info) } +func PostPush(c echo.Context, a *server.App) error { + // Initialize pushPayload object + payload := &common.PushPayload{ + ImageName: c.Request().Header.Get("X-Image-Name"), + } + + if payload.ImageName == "" { + return c.JSON(http.StatusBadRequest, "Invalid image name") + } + + imageReader := c.Request().Body + defer imageReader.Close() + + // Rename the image to a valid name so that it can be saved (remove user/, :tag and add .tar) + imageFileName := payload.ImageName + imageFileName = regexp.MustCompile(`^([a-zA-Z0-9\-_.]+\/)?`).ReplaceAllString(imageFileName, "") + imageFileName = regexp.MustCompile(`(:[a-zA-Z0-9\-_.]+)?$`).ReplaceAllString(imageFileName, "") + imageFileName = imageFileName + ".tar" + + // Save the image tar in the storage + imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) + if err != nil { + return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ + Success: false, + Message: fmt.Sprintf("Failed to save image: %v", err), + }) + } + + // Debug + fmt.Printf("Image saved successfully to: %s\n", imagePath) + + // Import the tar in docker + imageID, err := docker.ImportImageToEngine(imagePath) + if err != nil { + store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails + return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ + Success: false, + Message: fmt.Sprintf("Failed to import image: %v", err), + }) + } + + if imageID == "" { + return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ + Success: false, + Message: "Imported image ID is empty", + }) + } + + // Remove the image from the storage + err = store.RemoveFromStorage(imagePath) + if err != nil { + return sendJSONResponse(c, http.StatusInternalServerError, PushResponse{ + Success: false, + Message: fmt.Sprintf("Failed to remove temporary image file: %v", err), + }) + } + + fmt.Printf("Image imported successfully: %s\n", payload.ImageName) + + // Update the payload with the image ID + payload.ImageID = imageID + + // If we arrive here, send back a success response with the target domain + return sendJSONResponse(c, http.StatusOK, common.PushResponse{ + Success: true, + Message: "Deployment successful", + }) +} + func PostDeploy(c echo.Context, a *server.App) error { // Initialize pushPayload object payload := &common.DeployPayload{ @@ -84,29 +161,39 @@ func PostDeploy(c echo.Context, a *server.App) error { // Save the image tar in the storage imagePath, err := store.SaveImageToStorage(&a.Config, imageFileName, imageReader) if err != nil { - return sendJsonError(c, fmt.Errorf("failed to save image: %v", err)) + return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{ + Success: false, + Message: fmt.Sprintf("Failed to save image: %v", err), + }) } + // Debug fmt.Printf("Image saved successfully to: %s\n", imagePath) // Import the tar in docker imageID, err := docker.ImportImageToEngine(imagePath) if err != nil { store.RemoveFromStorage(imagePath) // Clean up the saved image if import fails - return sendJsonError(c, fmt.Errorf("failed to import image: %v", err)) + return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{ + Success: false, + Message: fmt.Sprintf("Failed to import image: %v", err), + }) } if imageID == "" { - return sendJsonError(c, fmt.Errorf("imported image ID is empty")) + return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{ + Success: false, + Message: "Imported image ID is empty", + }) } - // Debug - fmt.Printf("Image ID: %s\n", imageID) - // Remove the image from the storage err = store.RemoveFromStorage(imagePath) if err != nil { - return sendJsonError(c, fmt.Errorf("failed to remove temporary image file: %v", err)) + return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{ + Success: false, + Message: fmt.Sprintf("Failed to remove temporary image file: %v", err), + }) } fmt.Printf("Image imported successfully: %s\n", payload.ImageName) @@ -117,22 +204,40 @@ func PostDeploy(c echo.Context, a *server.App) error { // Create the container using cmdparams.FromPayloadStructToCmdParams params, err := cmdparams.FromPayloadStructToCmdParams(payload, a, imageID) if err != nil { - return sendJsonError(c, fmt.Errorf("failed to create command parameters: %v", err)) + return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{ + Success: false, + Message: fmt.Sprintf("Failed to create command parameters: %v", err), + }) } - // Create the container containerID, err := docker.CreateContainer(params) if err != nil { - return sendJsonError(c, fmt.Errorf("failed to create container: %v", err)) + return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{ + Success: false, + Message: fmt.Sprintf("Failed to create container: %v", err), + }) } // Start the container err = docker.StartContainer(containerID) if err != nil { docker.RemoveContainer(containerID) // Clean up if start fails - return sendJsonError(c, fmt.Errorf("failed to start container: %v", err)) + return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{ + Success: false, + Message: fmt.Sprintf("Failed to start container: %v", err), + }) } - // If we arrive here, send back payload.TargetDomain so the client can test it - return c.JSON(http.StatusOK, payload.TargetDomain) + // If we arrive here, send back a success response with the target domain + response := DeployResponse{ + Success: true, + Message: "Deployment successful", + Domain: payload.TargetDomain, + } + + // Log the response before sending + responseJSON, _ := json.Marshal(response) + fmt.Printf("Server response: %s\n", string(responseJSON)) + + return c.JSON(http.StatusOK, response) } diff --git a/internal/httpserve/handler/senderror.go b/internal/httpserve/handler/senderror.go index 2551ca7..970133e 100644 --- a/internal/httpserve/handler/senderror.go +++ b/internal/httpserve/handler/senderror.go @@ -15,7 +15,3 @@ func sendError(c echo.Context, err error) error { } return c.HTML(http.StatusInternalServerError, sanitizedHTML) } - -func sendJsonError(c echo.Context, err error) error { - return c.JSON(http.StatusInternalServerError, err.Error()) -} diff --git a/internal/httpserve/middleware/requiretoken.go b/internal/httpserve/middleware/requiretoken.go index 6cf3589..a92ab6a 100644 --- a/internal/httpserve/middleware/requiretoken.go +++ b/internal/httpserve/middleware/requiretoken.go @@ -23,9 +23,9 @@ func RequireToken(a *server.App) echo.MiddlewareFunc { func validateToken(c echo.Context, a *server.App) error { token := c.Request().Header.Get("Authorization") if token == "" { - return c.JSON(401, "Unauthorized") + return echo.NewHTTPError(401, "Token is missing") } - fmt.Println(token) + token = strings.Replace(token, "Bearer ", "", 1) configToken, err := a.Config.GetToken() if err != nil { @@ -33,7 +33,7 @@ func validateToken(c echo.Context, a *server.App) error { } if token != configToken { - return c.JSON(401, "Unauthorized") + return echo.NewHTTPError(401, "Token does not match") } return nil diff --git a/internal/httpserve/router.go b/internal/httpserve/router.go index 42e2bdb..6fc28ce 100644 --- a/internal/httpserve/router.go +++ b/internal/httpserve/router.go @@ -54,9 +54,12 @@ func bindAPIEndpoints(e *echo.Echo, a *server.App) { apiGroup.GET("/ping", func(c echo.Context) error { return handler.GetInfos(c, a) }) - apiGroup.POST("/push", func(c echo.Context) error { + apiGroup.POST("/deploy", func(c echo.Context) error { return handler.PostDeploy(c, a) }) + apiGroup.POST("/push", func(c echo.Context) error { + return handler.PostPush(c, a) + }) } // bindStaticRoute bind static path diff --git a/internal/templating/cmdparams/traefik.go b/internal/templating/cmdparams/traefik.go index 2770afd..cf47b47 100644 --- a/internal/templating/cmdparams/traefik.go +++ b/internal/templating/cmdparams/traefik.go @@ -24,6 +24,6 @@ func CreateTraefikLabels(params *docker.ContainerCommandParams) { } if params.IsSSL { - params.Labels = append(params.Labels, fmt.Sprintf("%s.tls.certresolver=letsencrypt", baseRouter)) + params.Labels = append(params.Labels, fmt.Sprintf("%s.tls.certresolver=myresolver", baseRouter)) } } diff --git a/internal/webui/public/assets/js/custom.js b/internal/webui/public/assets/js/custom.js index 1a42f17..c2bf8b0 100644 --- a/internal/webui/public/assets/js/custom.js +++ b/internal/webui/public/assets/js/custom.js @@ -1,15 +1,17 @@ -import 'dotenv/config'; // Import dotenv to load environment variables - - function initializeVersionCheck() { // Dummy data for the remote version (as if fetched from a server) - const updateStrTemplate = "New version %VERSION% available, consider pulling the latest image"; + const updateStrTemplate = + "New version %VERSION% available, consider pulling the latest image"; const checkVersion = (versionData, currentVersion) => { - const fetchedVersionNumber = versionData?.amd64?.name.match(/\d+\.\d+\.\d+/)?.[0]; + const fetchedVersionNumber = + versionData?.amd64?.name.match(/\d+\.\d+\.\d+/)?.[0]; if (currentVersion !== fetchedVersionNumber) { - const updateStr = updateStrTemplate.replace("%VERSION%", fetchedVersionNumber); + const updateStr = updateStrTemplate.replace( + "%VERSION%", + fetchedVersionNumber, + ); console.log(updateStr); const updateElement = document.getElementById("update-available"); updateElement.title = updateStr; @@ -17,13 +19,15 @@ function initializeVersionCheck() { updateElement.classList.remove("hidden"); // Remove the Tailwind `hidden` class updateElement.classList.add("block"); // Add the Tailwind `block` class } else { - console.log("No new version. Current version is up-to-date:", currentVersion); + console.log( + "No new version. Current version is up-to-date:", + currentVersion, + ); } }; const fetchVersionInfo = (currentVersion) => { - // os.Get env proxy url + /version - fetch(`${process.env.PROXY_URL}/version`) + fetch(`https://gordon-proxy.bamen.dev/version`) .then((response) => response.json()) .then((versionData) => checkVersion(versionData, currentVersion)) .catch((error) => console.error("Error fetching version:", error)); @@ -46,4 +50,4 @@ function initializeVersionCheck() { init(); } -document.addEventListener('DOMContentLoaded', initializeVersionCheck); +document.addEventListener("DOMContentLoaded", initializeVersionCheck);