Skip to main content

GitLab CI Pipeline: Compile and Deploy a Gin-based Go Web-app via Multistage Dockerfile. Part 2: Adding Code Tests, Splitting the Pipeline and Enabling Manual Job Triggers

1376 words·
GitLab GitLab CI CI Pipeline Go Golang Gin Golang Linter Code Test Multistage Dockerfile Docker Webserver Unprivileged Container
Table of Contents
GitLab-CI-Go-Web-Application - This article is part of a series.
Part 2: This Article
GitHub Repository Available

Overview
#

In this tutorial I’m using the following setup based on Ubuntu 22.04 servers, GitLab is dockerized:

192.168.70.4 # GitLab
192.168.70.5 # GitLab Runner
192.168.70.6 # Deployment Server with Docker platform installed

I’m using the same setup for the deployment via SSH as described in my blog post: GitLab CI Pipeline: Compile and Deploy a Gin-based Go Web-app via Multistage Dockerfile

The main requirements for the deployment via SSH key are the following:

  • Create a SSH key pair on the server where GitLab is deployed

  • Copy the public SSH key to the authorized keys file of the Deployment server

  • SSH into the deployment servers to add the fingerprint

  • Add the private SSH key as variable to the GitLab Repository CI/CD settings

  • On the deployment server, a user named “gitlab-deployment”, that is member of the Docker group, is used for the authentication of for the GitLab CI pipeline



GitLab Repository
#

This tutorial adds code testing stages to the CI pipleine from my previous post: “GitLab CI Pipeline: Compile and Deploy a Go Application via Multistage Dockerfile”

Note: In this post I have only added the changed files!

This GitLab repository is available on GitHub:
https://github.com/jueklu/gitlab-ci-deploy-go-webserver-lint-test

Here is the GitHub repository from the previous blog post:
https://github.com/jueklu/gitlab-ci-deploy-go-webserver


File & Folder Structure
#

The file and folder structure of the GitLab repository looks like this:

GitLab-Repository
├── Dockerfile
├── .gitlab-ci.yml  # Adapted main CI pipeline
├── .gitlab-lint-test.yml  # Second part of the CI pipeline
├── golang-source-code
│   ├── go.mod
│   ├── go.sum
│   ├── main.go  # Adapted source code to pass Lint test
│   └── views
│       └── index.html
└── README.md

Adapted Main Pipeline Manifest
#

  • .gitlab-ci.yml
### Variables
variables:
  DEPLOY_IP: "192.168.70.6"  # Deployment server IP
  DEPLOY_USER: "gitlab-deployment"  # Deployment server SSH user
  DEPLOY_PORT_HOST: 8080  # Host port
  DEPLOY_PORT_CONT: 8080  # Container port
  CONTAINER_NAME: "go-webserver"  # Name of the deployed container

  # Define the image name, tagging it with the GitLab CI registry and the current commit SHA
  IMAGE_SHA: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA


### Stages
stages:
  - lint  # Added Lint stage
  - test  # Added test stage
  - build
  - deploy


### CI Pipeline Parts
include:
  - local: .gitlab-lint-test.yml


### Build Container Image
build_image:
  image: docker:stable
  stage: build
  when: manual  # Manually trigger stage via GitLab UI
  services:
    - docker:dind
  variables:
    DOCKER_TLS_CERTDIR: ""
  before_script:
    # Login to GitLab Container Registry using predefined CI/CD variables
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    # Build the Docker image from the specified Dockerfile in the Dockerfiles directory
    - docker build --pull -t $IMAGE_SHA -f Dockerfile .
    # Push the built Docker image to the GitLab Container Registry
    - docker push $IMAGE_SHA
  rules: 
    # Rule: Run this job only for the main branch and if the specified Dockerfile exists
    - if: $CI_COMMIT_BRANCH == "main"
      exists:
        - Dockerfile


### Deploy Container to Virtual Machine
deploy_container:
  stage: deploy
  when: manual  # Manually trigger stage via GitLab UI
  image: alpine:latest
  needs:
    - build_image  # Run this job only if the 'build_image' job succeeds
  before_script:
    # Update the package index, install the OpenSSH client for SSH connections
    - apk update && apk add openssh-client
    # If the private SSH key file ($ID_RSA) exists, set secure permissions (read/write for the owner only)
    - if [ -f "$ID_RSA" ]; then chmod og= $ID_RSA; fi
  script:
    #  SSH into the deployment server, log in to the GitLab Container registry
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
     # SSH into the deployment server, pull the image from the registry
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker pull $IMAGE_SHA"
    # SSH into the deployment server, remove the existing container (if it exists)
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker container rm -f $CONTAINER_NAME || true"
    # SSH into the deployment server, run the new container
    - ssh -i $ID_RSA -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_IP "docker run -d -p $DEPLOY_PORT_HOST:$DEPLOY_PORT_CONT --restart=unless-stopped --name $CONTAINER_NAME $IMAGE_SHA"
  rules:
    # Rule: Run this job only for main branch
    - if: $CI_COMMIT_BRANCH == "main"
  • when: manual Stages with this option must be manually triggered.

  • include: Reference for other CI pipeline files.


Lint & Test Pipeline Manifest
#

Overview
#

The Lint stage is used to check the code for common mistakes, bad practices, or coding style issues, for example:

  • Unused variables.

  • Functions that don’t handle errors properly.

  • Code that’s unnecessarily complicated.


The Test stage runs a series of commands to format, analyze, and test the Go code:

  • go fmt Automatically formats the Go code to follow Go’s standard style.

  • go vet Examines the code for potential bugs, suspicious constructs, or performance issues.

  • go test Runs all unit tests in your project.


Manifest
#

  • .gitlab-lint-test.yml
### Caching of Go modules and dependencies
.go-cache:
  variables:
    GOPATH: $CI_PROJECT_DIR/.go
  cache:
    paths:
      # Caching directory
      - .go/pkg/mod/


### Lint
lint:
  image: golangci/golangci-lint:latest
  stage: lint
  extends: .go-cache
  script:
    # Navigate to the Go source directory
    - cd golang-source-code
    # Initialize the Go module environment
    - go mod tidy
    # Run golangci-lint in verbose mode
    - golangci-lint run -v .


### Test
test:
  image: golang:latest  # Official Go image
  stage: test
  script:
    # Navigate to the Go source directory
    - cd golang-source-code
    # Format Go code
    - go fmt $(go list ./...)
    # Analyze Go code for potential issues
    - go vet $(go list ./...)
    # Run tests and detect race conditions
    - go test -race $(go list ./...)
  • .go-cache: Pipeline performance improvement by reusing previously downloaded Go dependencies rather than fetching them from the internet each time the pipeline runs.



Adapted Go Source Code
#

Overview
#

The main.go code needs to be adopted to handle the return value of r.Run().

This satisfies the Lint errcheck because the return value is no longer ignored.

This is a great example of the Lint test.


Main.go
#

  • golang-source-code/main.go
// Declare the main package / required for any standalone executable Go program
package main

import (
	"fmt"
	"os"
	"github.com/gin-gonic/contrib/static"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	// Serve static files from the "./views" directory
	r.Use(static.Serve("/", static.LocalFile("./views", true)))

	// Start the server on the default port (8080)
	// Handle the error return value of r.Run()
	if err := r.Run(); err != nil {
		fmt.Printf("Server failed to start: %v\n", err)
		os.Exit(1) // Exit with an error code if the server fails
	}
}



Lint Logs
#

Access Lint Logs
#

  • Go to: (Project) > Artifacts

  • Expand “1 file” from the “lint” Job

  • Select “job.log”

  • Click download


Logs Example
#

$ golangci-lint run -v .
level=info msg="golangci-lint has version 1.62.0 built with go1.23.2 from 22b58c9b on 2024-11-10T19:09:02Z"
level=info msg="[config_reader] Config search paths: [./ /builds/root/binary-example01/golang-source-code /builds/root/binary-example01 /builds/root /builds / /root]"
level=info msg="[lintersdb] Active 6 linters: [errcheck gosimple govet ineffassign staticcheck unused]"
level=info msg="[loader] Go packages loading at mode 8767 (compiled_files|exports_file|name|deps|files|imports|types_sizes) took 14.767631834s"
level=info msg="[runner/filename_unadjuster] Pre-built 0 adjustments in 132.841µs"
level=info msg="[linters_context/goanalysis] analyzers took 5.718762879s with top 10 stages: buildir: 4.799582344s, inspect: 234.907726ms, fact_deprecated: 165.632957ms, printf: 136.562681ms, ctrlflow: 104.002502ms, fact_purity: 86.103243ms, nilness: 67.277764ms, SA5012: 66.990356ms, typedness: 44.185513ms, tokenfileanalyzer: 11.939694ms"
level=info msg="[runner] processing took 2.01µs with stages: max_same_issues: 330ns, skip_dirs: 300ns, nolint: 230ns, max_from_linter: 190ns, cgo: 130ns, filename_unadjuster: 120ns, invalid_issue: 110ns, identifier_marker: 110ns, skip_files: 110ns, fixer: 30ns, source_code: 30ns, severity-rules: 30ns, autogenerated_exclude: 30ns, max_per_file_from_linter: 30ns, diff: 30ns, exclude: 30ns, exclude-rules: 30ns, path_prettifier: 30ns, path_shortener: 30ns, path_prefixer: 30ns, sort_results: 30ns, uniq_by_line: 20ns"
level=info msg="[runner] linters took 4.482710866s with stages: goanalysis_metalinter: 4.482681996s"
level=info msg="File cache stats: 0 entries of total size 0B"
level=info msg="Memory: 194 samples, avg is 98.3MB, max is 398.4MB"
level=info msg="Execution took 19.253954376s"
Saving cache for successful job
00:04
Creating cache default-protected...
.go/pkg/mod/: found 8471 matching artifact files and directories 
No URL provided, cache will not be uploaded to shared cache server. Cache will be stored only locally. 
Created cache
Cleaning up project directory and file based variables
00:00
Job succeeded

No URL provided, cache will not be uploaded to shared cache server. Cache will be stored only locally.:

  • The cache is stored locally on the GitLab Runner and not uploaded to a shared cache server.

  • The pipeline configuration does not specify a cache server URL.

Created cache:

  • The cache was successfully saved for future jobs.

Job succeeded:

  • The lint job completed without errors.



Links #

jueklu/gitlab-ci-deploy-go-webserver-lint-test

GitLab CI pipeline that builds, containerizes, and deploys a Go web application using the Gin framework. Added Lint test.

Dockerfile
0
0
GitLab-CI-Go-Web-Application - This article is part of a series.
Part 2: This Article