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 #
GitLab CI pipeline that builds, containerizes, and deploys a Go web application using the Gin framework. Added Lint test.