新增代码扫描自定义步骤

Summary
  • 新增环境无关的自定义步骤,以新增sonarqube代码扫描为例子

新增代码扫描自定义步骤

  • 以新增sonarqube代码扫描为例子,让DORY支持在执行编译构建前先进行代码扫描
  • 所谓环境无关是指不依赖于应用的发布环境,例如代码扫描,不同发布环境代码扫描的参数没有区别
  • 所谓环境相关是指依赖于应用的发布环境,例如自动化接口测试,不同发布环境测试的参数会有所不同

任务演示视频

  • 演示如何让DORY支持在编译构建前执行sonarqube代码扫描

部署sonarqube服务

  • 把sonarqube部署到kubernetes中,sonarqube需要部署sonarqube-web和sonarqube-db两个服务
  • 样例部署文件如下:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    app: sonarqube-db
  name: sonarqube-db
  namespace: dory
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sonarqube-db
  serviceName: sonarqube-db
  template:
    metadata:
      labels:
        app: sonarqube-db
    spec:
      containers:
      - env:
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        - name: POSTGRES_USER
          value: sonar
        - name: POSTGRES_PASSWORD
          value: xxxx
        image: postgres:12.9-alpine
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          initialDelaySeconds: 90
          periodSeconds: 30
          successThreshold: 1
          tcpSocket:
            port: 5432
          timeoutSeconds: 1
        name: sonarqube-db
        ports:
        - containerPort: 5432
          protocol: TCP
        readinessProbe:
          failureThreshold: 3
          initialDelaySeconds: 15
          periodSeconds: 5
          successThreshold: 1
          tcpSocket:
            port: 5432
          timeoutSeconds: 1
        resources:
          limits:
            cpu: 500m
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 100Mi
        volumeMounts:
        - mountPath: /etc/timezone
          name: timezone
        - mountPath: /etc/localtime
          name: localtime
        - mountPath: /var/lib/postgresql/data/pgdata
          name: dory-pvc
          subPath: sonarqube-db
      imagePullSecrets:
      - name: ${HARBOR_DOMAIN_NAME}
      volumes:
      - hostPath:
          path: /etc/timezone
          type: File
        name: timezone
      - hostPath:
          path: /usr/share/zoneinfo/Asia/Shanghai
          type: File
        name: localtime
      - name: dory-pvc
        persistentVolumeClaim:
          claimName: dory-pvc
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    app: sonarqube-web
  name: sonarqube-web
  namespace: dory
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sonarqube-web
  serviceName: sonarqube-web-headless
  template:
    metadata:
      labels:
        app: sonarqube-web
    spec:
      containers:
      - env:
        - name: SONAR_JDBC_USERNAME
          value: sonar
        - name: SONAR_JDBC_PASSWORD
          value: xxxx
        - name: SONAR_JDBC_URL
          value: jdbc:postgresql://sonarqube-db:5432/sonar
        - name: SONAR_CE_JAVAOPTS
          value: -Xmx512m -Xms512m
        image: sonarqube:9.3.0-community
        imagePullPolicy: IfNotPresent
        livenessProbe:
          failureThreshold: 3
          initialDelaySeconds: 90
          periodSeconds: 30
          successThreshold: 1
          tcpSocket:
            port: 9000
          timeoutSeconds: 1
        name: sonarqube-web
        ports:
        - containerPort: 9000
          protocol: TCP
        readinessProbe:
          failureThreshold: 3
          initialDelaySeconds: 15
          periodSeconds: 5
          successThreshold: 1
          tcpSocket:
            port: 9000
          timeoutSeconds: 1
        resources:
          limits:
            cpu: "1"
            memory: 2Gi
          requests:
            cpu: 200m
            memory: 100Mi
        volumeMounts:
        - mountPath: /etc/timezone
          name: timezone
        - mountPath: /etc/localtime
          name: localtime
        - mountPath: /opt/sonarqube/extensions
          name: dory-pvc
          subPath: sonarqube-web/extensions
        - mountPath: /opt/sonarqube/data
          name: dory-pvc
          subPath: sonarqube-web/data
        - mountPath: /opt/sonarqube/logs
          name: dory-pvc
          subPath: sonarqube-web/logs
        - mountPath: /opt/sonarqube/temp
          name: dory-pvc
          subPath: sonarqube-web/temp
      imagePullSecrets:
      - name: ${HARBOR_DOMAIN_NAME}
      initContainers:
      - command:
        - sh
        - -c
        - until nc -z -w 5 -v sonarqube-db 5432; do echo waiting for sonarqube-db
          ready; done;
        image: busybox:1.32.0
        imagePullPolicy: IfNotPresent
        name: init-sonarqube-db
      volumes:
      - hostPath:
          path: /etc/timezone
          type: File
        name: timezone
      - hostPath:
          path: /usr/share/zoneinfo/Asia/Shanghai
          type: File
        name: localtime
      - name: dory-pvc
        persistentVolumeClaim:
          claimName: dory-pvc
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: sonarqube-db
  name: sonarqube-db
  namespace: dory
spec:
  clusterIP: None
  ports:
  - name: port-5432
    port: 5432
    protocol: TCP
    targetPort: 5432
  selector:
    app: sonarqube-db
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: sonarqube-web
  name: sonarqube-web
  namespace: dory
spec:
  ports:
  - name: port-9000
    nodePort: 30009
    port: 9000
    protocol: TCP
    targetPort: 9000
  selector:
    app: sonarqube-web
  type: NodePort
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: sonarqube-web
  name: sonarqube-web-headless
  namespace: dory
spec:
  clusterIP: None
  ports:
  - name: port-9000
    port: 9000
    protocol: TCP
    targetPort: 9000
  selector:
    app: sonarqube-web
  type: ClusterIP
1
2
# 等待sonarqube服务启动
kubectl -n dory get pods -w
  • sonarqube启动后,默认的管理员账号密码为admin/admin
  • 需要手工修改admin的密码。

/docs_old/tasks/add-customize-step-scan-code/images/sonarqube-update-password.png

制作sonar-scanner镜像

DORY自定义步骤的输入参数与输出参数:

  • 输入参数文件: 输入参数文件由DORY自动创建。DORY执行自定义步骤过程中会自动把运行以及步骤的执行参数(内置参数)以及用户输入的参数自动保存到步骤执行容器的输入参数文件中,输入参数文件默认保存在/tmp/dory-param-input.json或者/tmp/dory-param-input.yaml文件中(注意,输入参数文件路径可以配置,请根据实际配置进行相关设置)
    • 输入参数文件支持保存成json格式或者yaml格式,步骤执行容器中的代码可以读取输入参数文件作为输入参数,执行相关的步骤脚本
  • 输出参数文件: 输出参数文件由步骤执行容器在步骤执行过程中的步骤脚本创建。DORY执行自定义步骤结束的时候,会读取步骤执行容器中的输出参数文件输出参数文件默认保存在/tmp/dory-param-output.json或者/tmp/dory-param-output.yaml文件中(注意,输出参数文件路径可以配置,请根据实际配置进行相关设置)
    • 输出参数文件支持保存成json格式或者yaml格式,DORY在步骤执行结束后会读取输出参数文件,用于控制步骤执行结果、输出文件以及需要保存到步骤执行记录中的所有参数
    • 输出参数文件doryStepFail参数: 用于控制步骤执行结果是否失败,假如设置为true,即使步骤执行过程没有错误,步骤也会被标记为失败
    • 输出参数文件doryOutputFiles参数: 用于控制步骤执行结果是否有文件需要输出,例如自动化测试的测试报告,如果需要保存出来,那么需要把测试报告的文件路径设置到doryOutputFiles参数中
    • 输出参数文件的其他参数: 将会保存到步骤执行记录的输出参数中,用于后续执行结果检索用途
  • 制作sonar-scanner镜像,DORY使用sonar-scanner镜像把代码推送到sonarqube进行扫描
1
2
# 拉取sonar-scanner镜像
docker pull sonarsource/sonar-scanner-cli:4.6
  • sonar-scanner镜像包含python3的运行环境,需要编写两个python3步骤执行脚本:

    • create_sonar_project_properties.py脚本,读取DORY的输入参数文件,生并自动创建sonar-scanner运行配置文件sonar-project.properties

      1. 读取DORY的输入参数文件/tmp/dory-param-input.json,格式例如:
      1
      2
      3
      4
      5
      6
      7
      8
      
      {
        "projectName": "test-project1",
        "moduleName": "tp1-gin-demo",
        "path": "Codes/Backend/tp1-gin-demo",
        "sonarExtraProperties": [
          "sonar.sourceEncoding=UTF-8"
        ]
      }
      
      1. 并自动创建sonar-scanner运行配置文件sonar-project.properties,格式例如:
      1
      2
      3
      4
      5
      
      sonar.host.url=http://example.com:8080
      sonar.login=xxxx
      sonar.projectKey=test-project1_tp1-gin-demo
      sonar.projectName=test-project1_tp1-gin-demo
      sonar.sources=.
      
    • get_scan_result.py脚本,从sonarqube接口获取扫描结果,并根据扫描结果设置步骤执行成功还是失败,并生成DORY的输出参数文件

      1. 从sonarqube的api/qualitygates/project_status接口获取扫描结果
      2. 判断projectStatus.status状态是否为OK,如果不为OK,设置输出参数文件的doryStepFail参数为true,把本步骤状态设置为FAIL
  • create_sonar_project_properties.py脚本文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# create_sonar_project_properties.py
import json
import os
print("# create sonar-project.properties")
f = open("/tmp/dory-param-input.json", "r")
json_str = f.read()
data = json.loads(json_str)
sonar_url = os.getenv("SONAR_URL")
sonar_token = os.getenv("SONAR_TOKEN")
sonar_extra_properties = "\n".join(data["sonarExtraProperties"])
project_name = "{0}_{1}".format(data["projectName"], data["moduleName"])
sonar_properties = """
sonar.host.url={0}
sonar.login={1}
sonar.projectKey={2}
sonar.projectName={3}
sonar.sources=.
{4}
""".format(sonar_url, sonar_token, project_name, project_name, sonar_extra_properties)
f = open("sonar-project.properties", "w")
f.write(sonar_properties)
f.close()
  • get_scan_result.py脚本文件:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# get_scan_result.py
import requests
import json
import os
print("# get sonar scan result")
f = open("/tmp/dory-param-input.json", "r")
json_str = f.read()
data = json.loads(json_str)
sonar_url = os.getenv("SONAR_URL")
sonar_token = os.getenv("SONAR_TOKEN")
project_name = "{0}_{1}".format(data["projectName"], data["moduleName"])
url = "{0}api/qualitygates/project_status?projectKey={1}".format(sonar_url, project_name)
r = requests.get(url, auth=(sonar_token, ''))
sonar_output = r.text
sonar_data = json.loads(sonar_output)
status = sonar_data["projectStatus"]["status"]
print("# sonarqube scan status:", status)
if status != "OK":
  sonar_data["doryStepFail"] = True
else:
  sonar_data["doryStepFail"] = False
sonar_pretty = json.dumps(sonar_data, indent=4, sort_keys=True)
print("# sonarqube scan result")
print(sonar_pretty)
f = open("/tmp/dory-param-output.json", "w")
f.write(sonar_pretty)
f.close()
  • sonar-scanner镜像的Dockerfile
1
2
3
4
5
6
7
8
9
FROM sonarsource/sonar-scanner-cli:4.6
LABEL maintainer="cookeem"
LABEL email="cookeem@qq.com"
LABEL version="4.6"
COPY create_sonar_project_properties.py /tmp/
COPY get_scan_result.py /tmp/
RUN chown -R 1000:1000 /tmp/
WORKDIR /tmp/
USER 1000:1000
  • 制作sonar-scanner镜像
1
2
docker build -t ${HARBOR_DOMAIN_NAME}/public/sonar-scanner-cli:4.6-dory .
docker push ${HARBOR_DOMAIN_NAME}/public/sonar-scanner-cli:4.6-dory

设置自定义步骤配置

  • 为安全考虑设置sonarqube项目默认可见性为私有
  • 设置地址: ${SONARQUBE_URL}/admin/projects_management

/docs_old/tasks/add-customize-step-scan-code/images/sonarqube-default-private.png

  • 创建sonarqube管理员token,用于sonar-scanner访问sonarqube
  • 设置地址: ${SONARQUBE_URL}/account/security/

/docs_old/tasks/add-customize-step-scan-code/images/sonarqube-generate-token.png

  • 在dory-dashboard的管理控制台 - 自定义步骤 中新增代码扫描的自定义步骤配置,样例如下图:

/docs_old/tasks/add-customize-step-scan-code/images/scan-code-custom-step-config-detail.png

  • 详细配置参见下边定义:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
customStepName: scanCode
customStepActionDesc: sonarqube scan code
customStepDesc: sonarqube代码扫描
customStepUsage: 扫描代码仓库特定目录
customStepDockerConf:
  dockerImage: public/sonar-scanner-cli:4.6-dory
  dockerCommands:
    - python3 /tmp/create_sonar_project_properties.py
    - sonar-scanner
    - python3 /tmp/get_scan_result.py
  dockerRunAsRoot: false
  dockerVolumes:
    - '{{ $.path }}:/src/{{ $.moduleName }}'
  dockerEnvs:
    - SONAR_URL=http://sonarqube-web.dory:9000/
    - SONAR_TOKEN=xxxx
  dockerWorkDir: '/src/{{ $.moduleName }}'
  paramInputFormat: json
  paramOutputFormat: json
paramInputYamlDef: |
  # 要扫描的代码目录
  path: Codes/Backend/tp1-gin-demo
  # sonarqube额外扫描参数
  sonarExtraProperties:
  - sonar.sourceEncoding=UTF-8  
paramOutputYamlDef: |
  # 以下为sonarqube接口返回的扫描结果格式
  projectStatus:
  status: "OK"
  conditions: []
  periods: []
  ignoredConditions: false
  period:
    mode: PREVIOUS_VERSION
    date: "2022-02-04T15:26:51+0800"  

在项目定义中设置代码扫描的模块定义

  • 在dory-dashboard的项目定义中新增scanCode自定义步骤的自定义步骤模块定义,样例如下图:

/docs_old/tasks/add-customize-step-scan-code/images/scan-code-custom-step-def.png

  • 详细配置参见下边定义:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enableMode: ""
customStepModuleDefs:
  - moduleName: tp1-gin-demo
    manualEnable: true
    paramInputYaml: |
      # 要扫描的代码目录
      path: Codes/Backend/tp1-gin-demo
      # sonarqube额外扫描参数
      sonarExtraProperties:
        - sonar.sourceEncoding=UTF-8      

在流水线定义中插入代码扫描步骤

  • 在dory-dashboard的项目定义流水线定义createRunFiles步骤后边插入自定义步骤scanCode,样例如下图:

/docs_old/tasks/add-customize-step-scan-code/images/scan-code-pipeline-def.png

  • 详细配置参见下边定义:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
isAutoDetectBuild: false
isQueue: false
builds:
  - name: tp1-gin-demo
    run: true
  - name: tp1-go-demo
    run: false
  - name: tp1-gradle-demo
    run: false
  - name: tp1-new-demo
    run: false
  - name: tp1-node-demo
    run: false
  - name: tp1-python-demo
    run: false
  - name: tp1-rust-demo
    run: false
  - name: tp1-spring-demo
    run: false
pipelineStep:
  gitPull:
    timeout: 60
  build:
    enable: true
    timeout: 0
    retry: 0
  packageImage:
    enable: false
    timeout: 0
    retry: 0
  syncImage:
    enable: true
    retry: 0
  deploy:
    enable: true
    retry: 0
  applyIngress:
    enable: true
    retry: 0
  checkDeploy:
    enable: true
    ignoreError: false
    retry: 0
  checkQuota:
    enable: true
    retry: 0
customStepPhaseDefs:
  scanCode:
    enable: true
    ignoreError: true
    timeout: 0
    retry: 0
customStepInsertDefs:
  createRunFiles:
    - scanCode

执行流水线,在编译构建前进行代码扫描

  • 在编译构建前插入了代码扫描步骤

/docs_old/tasks/add-customize-step-scan-code/images/scan-code-pipeline-run-result.png

检查sonarqube代码扫描结果

  • 打开sonarqube新增了test-project1_tp1-gin-demo项目,并可以查看详细扫描结果

/docs_old/tasks/add-customize-step-scan-code/images/sonarqube-scan-code-result.png