'); /*! This file is auto-generated */ !function(e,a,t){var n,r,o,i=a.createElement("canvas"),p=i.getContext&&i.getContext("2d");function s(e,t){var a=String.fromCharCode,e=(p.clearRect(0,0,i.width,i.height),p.fillText(a.apply(this,e),0,0),i.toDataURL());return p.clearRect(0,0,i.width,i.height),p.fillText(a.apply(this,t),0,0),e===i.toDataURL()}function c(e){var t=a.createElement("script");t.src=e,t.defer=t.type="text/javascript",a.getElementsByTagName("head")[0].appendChild(t)}for(o=Array("flag","emoji"),t.supports={everything:!0,everythingExceptFlag:!0},r=0;r
您的位置 首页 教育

使用Jenkins的pipeline实现CI/CD

Pipeline Prerequisites 安装Pipeline插件 在你想创建一条流水线的时候,有可能发现…

Pipeline
Prerequisites
安装Pipeline插件

在你想创建一条流水线的时候,有可能发现并没有这个UI入口,原因是还没有下载相关插件,所以可以下载插件****,这是一整套和流水线相关的插件。(还有 BlueOcean等其他流水线相关的插件,这里先不使用,只使用classic的pipeline)。

下载完记得重启docker-compose restart。

创建流水线
Jenkinsfile模板

pipeline {
    agent any

    stages {
        stage('Stage 1: Fetch code from git') {
            steps {
                echo 'Stage 1: Fetch code from git -- SUCCESS'
            }
        }

        stage('Stage 2: Build the project using maven') {
            steps {
                echo 'Stage 2: Build the project using maven -- SUCCESS'
            }
        }

        stage('Stage 3: Make a custom image using docker') {
            steps {
                echo 'Stage 3: Make a custom image using docker -- SUCCESS'
            }
        }

        stage('Stage 4: Push image to Harbor') {
            steps {
                echo 'Stage 4: Push image to Harbor -- SUCCESS'
            }
        }

        stage('Stage 5: Publish over SSH') {
            steps {
                echo 'Stage 5: Publish over SSH -- SUCCESS'
            }
        }

    }
}

从GitLab拉取代码

流水线片段脚本:

                checkout([$class: 'GitSCM', branches: [[name: '${branch_tag}']], extensions: [], userRemoteConfigs: [[url: 'http://211.87.224.233:8929/sdu-weblab/springboot-helloworld.git']]])

其中branch_tag为git参数(此项功能需要 Git Parameter 插件)。

stage为:

        stage('Stage 1: Fetch code from git') {
            steps {
                checkout([$class: 'GitSCM', branches: [[name: '${branch_tag}']], extensions: [], userRemoteConfigs: [[url: 'http://211.87.224.233:8929/sdu-weblab/springboot-helloworld.git']]])
                echo 'Stage 1: Fetch code from git -- SUCCESS'
            }
        }

Git Parameter

该插件允许您在构建中分配branch, tag, pull request or revision number作为参数。

此插件将从你的项目中读取 GIT SCM 配置。
这个插件直接使用了Git Plugin和Git Client Plugin。

git 拉取分支:

checkout: Check out from version control

http://211.87.224.233:18080/

https://plugins.jenkins.io/git-parameter

使用WebHooks实现push分支自动build

http://211.87.224.233:8080/project/pipeline-springboot-helloworld

Hook executed successfully but returned HTTP 404 HTTP Status 404 – Not Foundbody {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}HTTP Status 404 – Not Found
  1. 下载插件Gitlab插件
  2. 在gitlab上设置Jenkins里插件生成的webhook,并添加secret token。此次webhook token为http://jenkins:8080/project/pipeline-springboot-helloworld

使用Jenkins容器内的Maven打包

打包命令

${MAVEN_HOME}/bin/mvn clean package -Dmaven.test.skip=true # 容器内的${MAVEN_HOME},不是宿主机的,在这里为/opt/maven

片段生成器生成的语句:

sh '/opt/maven/bin/mvn clean package -Dmaven.test.skip=true'

stage为:

        stage('Stage 2: Build the project using maven') {
            steps {
                // 下面两条shell作debug用,后面删去
                sh 'pwd'
                sh 'ls -al'
                sh '/opt/maven/bin/mvn clean package -Dmaven.test.skip=true'
                echo 'Stage 2: Build the project using maven -- SUCCESS'
            }
        }

日志:

+ pwd
/var/jenkins_home/workspace/pipeline-springboot-helloworld
[Pipeline] sh
+ ls -al
total 8
drwxr-xr-x 5 jenkins jenkins  100 May  6 10:11 .
drwxr-xr-x 4 jenkins jenkins   98 May  6 10:09 ..
drwxr-xr-x 8 jenkins jenkins  210 May  6 10:13 .git
-rw-r--r-- 1 jenkins jenkins  387 May  5 15:35 .gitignore
-rw-r--r-- 1 jenkins jenkins 1437 May  5 15:35 pom.xml
drwxr-xr-x 4 jenkins jenkins   42 May  5 15:35 src
drwxr-xr-x 6 jenkins jenkins  199 May  6 10:12 target
[Pipeline] sh
+ /opt/maven/bin/mvn clean package -Dmaven.test.skip=true
[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------------------------------------
[INFO] Building helloworld 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ helloworld ---
[INFO] Deleting /var/jenkins_home/workspace/pipeline-springboot-helloworld/target
[INFO] 
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ helloworld ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ helloworld ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /var/jenkins_home/workspace/pipeline-springboot-helloworld/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ helloworld ---
[INFO] Not copying test resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ helloworld ---
[INFO] Not compiling test sources
[INFO] 
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ helloworld ---
[INFO] Tests are skipped.
[INFO] 
[INFO] --- maven-jar-plugin:3.2.2:jar (default-jar) @ helloworld ---
[INFO] Building jar: /var/jenkins_home/workspace/pipeline-springboot-helloworld/target/helloworld-0.0.1-SNAPSHOT.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:2.6.4:repackage (repackage) @ helloworld ---
[INFO] Replacing main artifact with repackaged archive
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.425 s
[INFO] Finished at: 2022-05-06T10:14:04Z
[INFO] ------------------------------------------------------------------------

从上面的日志输出可以得到如下结论:

  1. 对于Jenkins每一个流水线项目,Jenkins都会在存储在/var/jenkins_home/workspace/${projectName}
  2. 从gitlab拉取代码后存储在本流水线对应的目录下/var/jenkins_home/workspace/${projectName}
  3. 打完jar后存储在/var/jenkins_home/workspace/${projectName}/target/xxx.jar

跳过单元测试

  • -DskipTests,不执行测试用例,但编译测试用例类生成相应的class文件至target/test-classes下。

  • -Dmaven.test.skip=true,不执行测试用例,也不编译测试用例类。

参考
https://www.jianshu.com/p/6f91752f3962
https://www.jianshu.com/p/6f57c322e50e

使用Jenkins容器内的Docker制作自定义镜像

pwd: /var/jenkins_home/workspace/${projectName}
/target/xxx.jar

Dockerfile

FROM openjdk:8-jdk-alpine
ARG JarFileName
COPY ${JarFileName} /opt/weblab/
WORKDIR /opt/weblab/
CMD java -jar ${JarFileName}

执行shell

cp -r /opt/docker_workdir ./
cp ./target} ./docker_workdir --build-arg JarFileName=$(ls target -l | grep jar$ | awk '{print $9}')

流水线片段

sh '''cp -r /opt/docker_workdir ./
cp ./target} ./docker_workdir --build-arg JarFileName=$(ls target -l | grep jar$ | awk '{print $9}')'''

stage: 为

        stage('Stage 3: Make a custom image using docker') {
            steps {
                sh '''
                cp -r /opt/docker_workdir ./
                cp ./target} ./docker_workdir --build-arg JarFileName=$(ls target -l | grep jar$ | awk '{print $9}')
                '''
                echo 'Stage 3: Make a custom image using docker -- SUCCESS'
            }
        }

build_image.sh

cp -r /opt/docker_workdir ./
cp ./target} ./docker_workdir --build-arg  JarFileName="${jarFileName}"

将自定义镜像推送到Harbor

docker login -u admin -p admin $HARBOR_URL

Jenkinsfile:

增加以下环境变量

pipeline {
    agent any
    
		environment {
        harbor_user = 'admin'
        harbor_password = 'admin'
        harbor_url = '211.87.224.233:8930'
        harbor_repo = 'sdu-weblab'
    }
    // ......
}

Shell

docker login -u ${harborUser} -p ${harborPassword} ${harbor_url}
docker tag ${JOB_NAME}:${branch_tag##*/} ${harbor_url}/${harbor_repo}/${JOB_NAME}:${branch_tag##*/}
docker rmi $(docker images -f "dangling=true" -q) -f
docker push ${harbor_url}/${harbor_repo}/${JOB_NAME}:${branch_tag##*/}

pipeline script:

                sh '''
                    docker login -u ${harborUser} -p ${harborPassword} ${harbor_url}
                    docker tag ${JOB_NAME}:${branch_tag##*/} ${harbor_url}/${harbor_repo}/${JOB_NAME}:${branch_tag##*/}
                    docker rmi $(docker images -f "dangling=true" -q) -f
                    docker push ${harbor_url}/${harbor_repo}/${JOB_NAME}:${branch_tag##*/}
                 '''

stage为:

        stage('Stage 4: Push image to Harbor') {
            steps {
                sh '''
                    docker login -u ${harborUser} -p ${harborPassword} ${harbor_url}
                    docker tag ${JOB_NAME}:${branch_tag##*/} ${harbor_url}/${harbor_repo}/${JOB_NAME}:${branch_tag##*/}
                    docker rmi $(docker images -f "dangling=true" -q) -f
                    docker push ${harbor_url}/${harbor_repo}/${JOB_NAME}:${branch_tag##*/}
                 '''
                echo 'Stage 4: Push image to Harbor -- SUCCESS'
            }
        }

改为执行本地的脚本。

harbor_user=$1
harbor_password=$2
harbor_url=$3
harbor_repo=$4
project=$5
branch_tag=$6

image_name=${harbor_url}/${harbor_repo}/${project}:${branch_tag##*/}

# 删除和镜像关联的容器(正常情况不会有,但这里是单机部署)
container_id=$(docker ps -a | grep "${image_name}" | awk '{print $1}')
if [ "${container_id}" != "" ] ; then
  echo "Container ${container_id} of ${image_name} is running, now try to kill it..."
  docker stop "$container_id"
  docker rm "$container_id"
fi
# todo 这样做其实还有是问题的,就是上面的 container_id 可能会有多个

# shellcheck disable=SC2086
docker login -u "${harbor_user}" -p "${harbor_password}" ${harbor_url}

docker tag "${project}":"${branch_tag##*/}" "${image_name}"
docker rmi $(docker images -f "dangling=true" -q) -f
docker push image_name

Jenkins端shell:

/opt/docker_workdir/push_to_harbor.sh $harbor_user $harbor_password $harbor_url $harbor_repo $JOB_NAME $branch_tag

流水线片段

sh '/opt/docker_workdir/push_to_harbor.sh $harbor_user $harbor_password $harbor_url $harbor_repo $JOB_NAME $branch_tag'

其中:

# 删除悬挂镜像
docker rmi $(docker images -f "dangling=true" -q) 

docker中的none:none镜像是怎么回事?
https://projectatomic.io/blog/2015/07/what-are-docker-none-none-images/

用 go 改写:

package main

import (
	"context"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"strings"
	"time"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/client"
)

var (
	harborUser     string
	harborPassword string
	harborUrl      string
	harborRepo     string
	project        string
	version        string
)

var (
	logFileName string
	logFile     *os.File
)

func init() {
	var err error
	logFileName = time.Now().Format("2006-01-02 15:04:05") + "[push_to_harbor].log"
	logFile, err = os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		panic(err)
	}
	log.SetOutput(logFile)
	log.SetPrefix("[sdu-weblab-deploy]")
	log.SetFlags(log.LstdFlags | log.Lshortfile | log.LUTC)
}

func main() {
	flag.StringVar(&harborUser, "harbor_User", "", "")
	flag.StringVar(&harborPassword, "harbor_password", "", "")
	flag.StringVar(&harborUrl, "harbor_url", "", "")
	flag.StringVar(&harborRepo, "harbor_repo", "", "")
	flag.StringVar(&project, "project", "", "")
	flag.StringVar(&version, "version", "", "")
	if len(harborUser) == 0 || len(harborPassword) == 0 || len(harborUrl) == 0 ||
		len(harborRepo) == 0 || len(project) == 0 || len(version) == 0 {
		log.Fatal("key args missing")
	}
	log.Printf("Notice: harbor_url=%s, harbor_repo=%s, project=%s, version=%s",
		harborUrl, harborRepo, project, version)

	ctx := context.Background()
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		log.Fatalf("New docker client fail, err=%v", err)
	}

	version = getRealVersion(version)
	imageName := getHarborImageName(harborUrl, harborRepo, project, version)

	// 删除和镜像关联的容器(正常情况不会有,但这里是单机部署)
	// 1. check existing containers and remove it if so
	log.Printf("Check if container of %s exists...n", imageName)
	containers, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true})
	if err != nil {
		log.Fatalf("List all containers fail, err=%v", err)
	}

	var timeOut = time.Second
	for _, c := range containers {
		if c.Image == imageName {
			cid := c.ID
			err = cli.ContainerStop(ctx, cid, &timeOut)
			if err != nil {
				log.Printf("Warning: container.ID=%s stop failed", cid)
			}
			err = cli.ContainerRemove(ctx, cid, types.ContainerRemoveOptions{Force: true})
			if err != nil {
				log.Fatalf("Container.ID=%s remove failed", cid)
			}
		}
	}

	// 2. tag by the rule of harbor
	// 2.1 check and remove outdated version images
	log.Printf("Check if image %s exists...n", imageName)
	imageList, err := cli.ImageList(ctx, types.ImageListOptions{})
	if err != nil {
		log.Fatalf("List all images fail, err=%v", err)
	}
	for _, image := range imageList {
		for _, repoTag := range image.RepoTags {
			if repoTag == imageName {
				log.Printf("Image=%s exists, next to remove it", image.ID)
				cli.ImageRemove(ctx, image.ID, types.ImageRemoveOptions{
					Force: true,
				})
				break
			}
		}
	}
	// 2.2 tag
	sourceImageName := getSourceImageName(harborRepo, project, version)
	err = cli.ImageTag(ctx, sourceImageName, imageName)
	if err != nil {
		log.Fatalf("image tag fail, err=%v", err.Error())
	}
	// todo docker rmi $(docker images -f "dangling=true" -q) -f

	// 3. push image to harbor
	log.Printf("Push image %s to Harbor...n", imageName)
	_, err = cli.RegistryLogin(ctx, types.AuthConfig{
		// todo security
		Username:      "admin",
		Password:      "admin",
		ServerAddress: harborUrl,
	})
	if err != nil {
		log.Fatalf("push to harbor fail, err=%v", err.Error())
	}

	out, err := cli.ImagePush(ctx, imageName, types.ImagePushOptions{})
	if err != nil {
		log.Fatalf("Pulling image failed, err=%v", err)
	}
	defer out.Close()
	io.Copy(logFile, out)

}

// remove possible "/"
func getRealVersion(rawVersion string) string {
	parts := strings.Split(rawVersion, "/")
	if len(parts) > 0 {
		return parts[len(parts)-1]
	}
	return version
}

func getSourceImageName(team, project, version string) string {
	return fmt.Sprintf("%s/%s/%s:%s", team, harborRepo, project, version)
}

func getHarborImageName(harborUrl, harborRepo, project, version string) string {
	return fmt.Sprintf("%s/%s/%s:%s", harborUrl, harborRepo, project, version)
}

通过SSH通知目标服务器部署

这里使用Publish Over SSH。

部署端的执行脚本:

harbor_url=$1
harbor_repo=$2
project=$3
version=$4
container_port=$5

image_name=$harbor_url/$harbor_repo/$project:$version

container_id=$(docker ps -a | grep "${image_name}" | awk '{print $1}')

echo "Check if container of ${image_name} exists..."
if [ "${container_id}" != "" ] ; then
  echo "Container ${container_id} of ${image_name} is running, now try to kill it..."
  docker stop "$container_id"
  docker rm "$container_id"
fi

old_image=$(docker images | grep "${image_name}")

if [ "${old_image}" != "" ] ; then
  echo "Image $image_name exists, now try to remove it"
  docker rmi "$image_name" -f
fi

echo "Try to login Harbor..."
docker login -u amdin -p admin "$harbor_url"
echo "Login Harbor successfully."

echo "Try to pull $image_name from Harbor..."
docker pull "${image_name}"
echo "Pull image ${image_name} from Harbor successfully."

docker run -d -p "$container_port" --name "$harbor_repo.$project.$version" "$image_name"

echo "Deploy service successfully."

以上脚本执行了如下事情:

  1. 检查旧版本的容器是否在运行,如果有停止并删除;
  2. 检查旧镜像是否存在,如果有删除;
  3. 登录私服Harbor
  4. 拉取最新的镜像
  5. 运行容器

**发布端(Jenkins)**的执行脚本

cd deploy && touch deploy.log && /home/jsy/devops/docker_workdir/deploy.sh $harbor_url $harbor_repo $JOB_NAME $branch_tag $container_port > deploy.log

测试一下:

cd deploy && touch deploy.log && /home/jsy/devops/docker_workdir/deploy.sh 211.87.224.233:8930 sdu-weblab springboot-helloworld origin/master 8080 > deploy.log

使用sshPublisher生成流水线脚本:

sshPublisher(publishers: [sshPublisherDesc(configName: 'sdu-weblab-local-machine', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "cd deploy && touch deploy.log && /home/jsy/devops/docker_workdir/deploy.sh $harbor_url $harbor_repo $JOB_NAME $branch_tag $container_port > deploy.log", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])

stage为

        stage('Stage 5: Publish over SSH') {
            steps {
                sshPublisher(publishers: [sshPublisherDesc(configName: 'sdu-weblab-local-machine', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "cd deploy && touch deploy.log && /home/jsy/devops/docker_workdir/deploy.sh $harbor_url $harbor_repo $JOB_NAME $branch_tag $container_port > deploy.log", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
                echo 'Stage 5: Publish over SSH -- SUCCESS'
            }
        }

通过SSH通知目标服务器部署(部署端使用go部署)
部署程序

package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
)

const (
	DefaultVersion       = "master"
	MonitorProgrammeName = "monitor"
)

var (
	harborUrl     string
	harborRepo    string
	project       string
	version       string
	containerPort int
)

var (
	logFileName string
	containerID string
)

func init() {
	logFileName = time.Now().Format("2006-01-02 15:04:05") + ".log"
	logFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		panic(err)
	}
	log.SetOutput(logFile)
	log.SetPrefix("[sdu-weblab-deploy]")
	log.SetFlags(log.LstdFlags | log.Lshortfile | log.LUTC)
	return
}

func main() {
	flag.StringVar(&harborUrl, "harbor_url", "", "")
	flag.StringVar(&harborRepo, "harbor_repo", "", "")
	flag.StringVar(&project, "project", "", "")
	flag.StringVar(&version, "version", DefaultVersion, "")
	flag.IntVar(&containerPort, "container_port", 8080, "")
	flag.Parse()
	if len(harborUrl) == 0 || len(harborRepo) == 0 || len(project) == 0 {
		log.Fatal("key args missing")
	}
	log.Printf("Notice: harbor_url=%s, harbor_repo=%s, project=%s, version=%s, container_port=%d",
		harborUrl, harborRepo, project, version, containerPort)

	version = getBranch(version)
	imageName := harborUrl + "/" + harborRepo + "/" + project + ":" + version
	log.Printf("Docker image name is %s", imageName)

	ctx := context.Background()
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		log.Fatalf("New docker client fail, err=%v", err)
	}

	// 1. check existing containers
	log.Printf("Check if container of %s exists...n", imageName)
	containers, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true})
	if err != nil {
		log.Fatalf("List all containers fail, err=%v", err)
	}

	var timeOut = time.Second
	for _, c := range containers {
		if c.Image == imageName {
			cid := c.ID
			err = cli.ContainerStop(ctx, cid, &timeOut)
			if err != nil {
				log.Printf("Warning: container.ID=%s stop failed", cid)
			}
			err = cli.ContainerRemove(ctx, cid, types.ContainerRemoveOptions{Force: true})
			if err != nil {
				log.Fatalf("Container.ID=%s remove failed", cid)
			}
		}
	}

	// 2. check outdated version images
	log.Printf("Check if image %s exists...n", imageName)
	imageList, err := cli.ImageList(ctx, types.ImageListOptions{
		All: true,
	})
	if err != nil {
		log.Fatalf("List all images fail, err=%v", err)
	}
	for _, image := range imageList {
		for _, repoTag := range image.RepoTags {
			if repoTag == imageName {
				cli.ImageRemove(ctx, image.ID, types.ImageRemoveOptions{
					Force: true,
				})
				break
			}
		}
	}

	// 3. pull image from harbor
	log.Printf("Pull image %s from Harbor...n", imageName)
	_, err = cli.RegistryLogin(ctx, types.AuthConfig{
		// todo security
		Username:      "admin",
		Password:      "admin",
		ServerAddress: harborUrl,
	})

	if err != nil {
		panic(nil)
	}

	// out 是响应输出流,可以不输出
	out, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
	if err != nil {
		panic(err)
	}
	defer out.Close()
	io.Copy(os.Stdout, out)

	// 4. docker run -d -p "$container_port" --name "$harbor_repo.$project.$version" "$image_name"
	log.Printf("Run image %s to be contaniner...n", imageName)
	containerName := harborRepo + "." + project + "." + version
	containerNatPort := nat.Port(strconv.Itoa(containerPort) + "/tcp")

	resp, err := cli.ContainerCreate(ctx,
		&container.Config{
			Image: imageName,
			ExposedPorts: nat.PortSet{
				containerNatPort: struct{}{},
			},
		}, &container.HostConfig{
			PublishAllPorts: true,
		}, nil, nil, containerName)

	if err != nil {
		log.Fatalf("Creating container failed, err=%v", err)
	}
	if err = cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
		log.Fatalf("Starting container failed, err=%v", err)
	}

	containerID = resp.ID
	log.Printf("Container ID = %s", containerID)

	// todo register to center
	hostPortOfContainerPort, err := getHostPortOfContainerPort(containerID, strconv.Itoa(containerPort))
	if err != nil {
		log.Printf("Error: get port binding failed, err=%v", err)
	} else {
		log.Printf("Container hostPort of container port is %s", hostPortOfContainerPort)
	}
	log.Println("Deploy successfully.")

	// 5. run a new programme to fetch log stream and transfer it to websocket
	runGoProgramme(MonitorProgrammeName)
}

func runGoProgramme(programme string) {
	log.Printf("Next to run programme %s ", programme)
	sduWeblabBinHome := os.Getenv("SDU_WEBLAB_BIN_HOME")
	s := fmt.Sprintf("%s/%s --logFileName=%s --containerID=%s", sduWeblabBinHome, programme, logFileName, containerID)
	log.Printf("Shell script is %s", s)
	err := exec.Command("/bin/bash", "-c", s).Start()
	if err != nil {
		log.Fatalf("Fail to run programe %s, err=%v", programme, err)
	}
}

func getBranch(repoBranch string) string {
	parts := strings.Split(repoBranch, "/")
	if len(parts) > 0 {
		return parts[len(parts)-1]
	}
	return DefaultVersion
}

func getHostPortOfContainerPort(containerId string, containerPort string) (port string, err error) {
	shell := "docker port " + containerId + " | grep " + containerPort + " | awk '{print $3}'"
	output, err := exec.Command("bash", "-c", shell).Output()
	if err != nil {
		return
	}
	shellOut := string(output)
	parts := strings.Split(shellOut, "n")
	if len(parts) == 0 {
		err = fmt.Errorf("containerPort=%s has no bindings", containerPort)
		return
	}
	ipPort := parts[0]
	splits := strings.Split(ipPort, ":")
	if len(splits) != 2 {
		err = errors.New("invalid format")
		return
	}
	port = splits[1]
	return
}

监控程序

package main

import (
	"bufio"
	"context"
	"flag"
	"io"
	"log"
	"net"
	"net/http"
	"os"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/client"
	"github.com/gorilla/websocket"
)

var (
	logFileName string
	containerID string
)

var (
	logOut   io.ReadCloser
	upgrader = websocket.Upgrader{
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
	} // use default options
	conn       *websocket.Conn
	listenPort int
)

func InitLog() {
	logFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		panic(err)
	}
	log.SetOutput(logFile)
	log.SetPrefix("[sdu-weblab-deploy]")
	log.SetFlags(log.LstdFlags | log.Lshortfile | log.LUTC)
	return
}

func main() {
	// fetch log stream and transfer it to websocket
	flag.StringVar(&logFileName, "logFileName", "", "")
	flag.StringVar(&containerID, "containerID", "", "")
	flag.Parse()

	if len(logFileName) == 0 || len(containerID) == 0 {
		log.Fatal("key args missing")
	}

	// init log settings
	InitLog()

	log.Printf("Fetch log stream and transfer it to websocket...n")
	ctx := context.Background()
	var err error
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		log.Fatalf("New docker client fail, err=%v", err)
	}

	options := types.ContainerLogsOptions{
		ShowStdout: true,
		ShowStderr: true,
		Follow:     true,
	}
	logOut, err = cli.ContainerLogs(ctx, containerID, options) // remote

	if err != nil {
		log.Fatalf("Get log of container failed, err=%v", err)
	}

	listener, err := net.Listen("tcp", ":0")
	if err != nil {
		log.Fatalf("Net listen failed, err=%v", err)
	}
	listenPort = listener.Addr().(*net.TCPAddr).Port
	// todo register to center
	log.Printf("Websocket port is %d", listenPort)

	http.HandleFunc("/logs", socketHandler)
	log.Fatal(http.Serve(listener, nil))
}

func socketHandler(w http.ResponseWriter, r *http.Request) {
	// Upgrade our raw HTTP connection to a websocket based one
	_conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Print("Error during connection upgradation:", err)
		os.Exit(3)
	}
	conn = _conn
	defer conn.Close()

	reader := bufio.NewReader(logOut)
	// The event loop
	for {
		message, err := reader.ReadString('n')

		if err != nil {
			if err == io.EOF {
				exitAndNotifyPeer(message, 0)
			}
			exitAndNotifyPeer("n", 1)
		}
		//log.Printf("Log read is: %s", message)
		err = conn.WriteMessage(websocket.TextMessage, []byte(message))
		if err != nil {
			log.Println("Error during message writing: ", err)
			os.Exit(2)
		}
	}
}

func exitAndNotifyPeer(msg string, exitCode int) {
	conn.WriteMessage(websocket.CloseMessage, []byte(msg))
	os.Exit(exitCode)
}

构建脚本

jsy@wzai:~/devops/docker_workdir$ tree
.
├── deploy
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   └── output
│       └── bin
│           └── deploy
├── monitor
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   └── output
│       └── bin
│           └── monitor

go build

首次构建

cd ~/devops/docker_workdir/
cd deploy && go mod tidy && mkdir -p output/bin && go build -o output/bin/deploy
cd monitor && go mod tidy && mkdir -p output/bin && go build -o output/bin/monitor

后面更新

cd ~/devops/docker_workdir/
cd deploy && go mod tidy && go build -o output/bin/deploy
cd ~/devops/docker_workdir/
cd monitor && go mod tidy && go build -o output/bin/monitor

构建或更新软链接

sudo ln -f -s /home/jsy/devops/docker_workdir/deploy/output/bin/deploy /opt/sduweblab/bin/deploy
sudo ln -f -s /home/jsy/devops/docker_workdir/monitor/output/bin/monitor /opt/sduweblab/bin/monitor

更新产物

cd ~/devops/docker_workdir/
cd deploy && go mod tidy && go build -o output/bin/deploy
cd ~/devops/docker_workdir/
cd monitor && go mod tidy && go build -o output/bin/monitor
sudo ln -f -s /home/jsy/devops/docker_workdir/deploy/output/bin/deploy /opt/sduweblab/bin/deploy
sudo ln -f -s /home/jsy/devops/docker_workdir/monitor/output/bin/monitor /opt/sduweblab/bin/monitor

更新产物(推送镜像到harbor也使用go改写)

sudo rm /opt/sduweblab/bin/deploy
sudo rm /opt/sduweblab/bin/monitor
sudo rm /opt/sduweblab/bin/push_to_harbor
cd ~/devops/docker_workdir/
cd deploy && go mod tidy && go build -o output/bin/deploy
cd ~/devops/docker_workdir/
cd monitor && go mod tidy && go build -o output/bin/monitor
cd ~/devops/docker_workdir/
cd push_to_harbor && go mod tidy && go build -o output/bin/push_to_harbor
sudo cp /home/jsy/devops/docker_workdir/deploy/output/bin/deploy /opt/sduweblab/bin/deploy
sudo cp /home/jsy/devops/docker_workdir/monitor/output/bin/monitor /opt/sduweblab/bin/monitor
sudo cp /home/jsy/devops/docker_workdir/push_to_harbor/output/bin/push_to_harbor /opt/sduweblab/bin/push_to_harbor
cd

原先的shell换成go程序即可。

测试脚本:

cd deploy && /opt/sduweblab/bin/deploy --harbor_url=211.87.224.233:8930 --harbor_repo=sdu-weblab --project=springboot-helloworld --version=origin/master --container_port=8080

本文来自网络,不代表0514资讯网立场,转载请注明出处:https://www.0514zx.com/info/6412.html
0514zx.com

作者: 0514zx.com

优质职场领域创作者
联系我们

联系我们

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

CMS教程网 趣丁号猫咪日记趣文库每日星座运势电脑技术初夏网站长分类目录丝路百科趣学百科趣科技号热门榜论文之家问百科网