Статьи

Конвейер как код с приложением Spring Boot

Это последний из серии сообщений о непрерывной доставке, основанных на моем локальном стеке Docker (см. Первый и второй посты здесь). В этом посте я использую простой проект Spring Boot, чтобы показать, как использовать концепцию « конвейер как код ». Пожалуйста, обратите внимание, что это только пример, и гораздо больше возможно. Приложение, которое я использую, взято с сайта Spring Boot . Jenkinsfile вдохновлен тем, что в этом посте, но мне пришлось изменить некоторые вещи, чтобы он работал с моим стеком. Источники моего проекта можно найти здесь . Я объясню наиболее важные фрагменты в этом посте.
Конвейер, который я использую, содержит следующие этапы:

этап строительства
На этапе сборки я использую плагин GitLab для проверки источников моего проекта. Я также поместил текущий commitId в textFile в рабочем каталоге. Затем я использую Maven (тот, который мы назвали «M3» в конфигурации Jenkins, как я описал здесь ) для упаковки кода. Я также проверяю, что commitId передается в качестве параметра в Maven.

этап развертывания
на этапе развертывания я закрываю работающий экземпляр приложения, отправляя «true» в путь / shutdown. Затем я просто запускаю банку, которую я построил на предыдущем шаге. После этого задание ожидает, пока приложение ответит на простой запрос.

тест на дым
На этом простом шаге тестирования я сравниваю возвращенный commitId моего развернутого сервиса с commitId, который мы получили, когда я извлек последний код фиксации. Если все прошло хорошо, эти два идентификатора должны совпадать, если не что-то в цепочке пошло не так.

Вот и все для этого примера. Давайте посмотрим, что это значит для исходного кода. Поскольку это проект Maven, я начинаю с pom.xml:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <!-- used for metrics like status, health etc -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
  <dependency>
    <!-- used for unit tests -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

Никаких особых зависимостей для этого проекта не требуется. Spring-boot-starter-web используется для нашего REST-контроллера. « Sprint-boot-starter-привод » может использоваться для проверки работоспособности и многого другого .
Наконец, ‘spring-boot-starter-test’ используется для того, чтобы иметь возможность (модульно) протестировать контроллер.
Давайте посмотрим на источники Java. Приложение просто запускает приложение Spring Boot. Класс Controller также очень прост:

01
02
03
04
05
06
07
08
09
10
11
12
13
package hello;
 
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
 
@RestController
public class HelloController {
    
  @RequestMapping("/")
  public String index() {
    return "Greetings from Spring Boot!";
  }   
}

Как видите, я просто возвращаю фиксированную строку, когда GET-запрос поступает в ‘/’. Тестовый класс имеет следующий тестовый код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
/**
 * Created by pascal on 19/01/2017.
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTest {
 
    @Autowired
    private MockMvc mvc;
 
    @Test
    public void getHello() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("Greetings from Spring Boot!")));
    }
}

Это также просто, я думаю, я ожидаю, что фиксированная строка будет ответом на запрос GET. Рядом с кодом Java находится файл application.properties:

1
2
3
4
5
6
server.port=8888
info.app.commitid=@commitid@
endpoints.shutdown.enabled=true

Помимо двух функциональных свойств: порта, на котором мы запускаем приложение (8888), и возможности завершать работу приложения, вызывая конечную точку (endpoints.shutdown.enabled = true), остальные должны отображаться при вызове конечной точки ‘/ Информация’. Параметры @… @ будут заменены реальными значениями на Maven, поскольку мы фильтруем ресурсы:

01
02
03
04
05
06
07
08
09
10
...
<resources>
  <!-- used for variable substitution in application.properties -->
  <resource>
    <directory>src/main/resources</directory>
    <filtering>true</filtering>
  </resource>
</resources>
...

Наконец у нас есть Jenkinsfile в проекте:

01
02
03
04
05
06
07
08
09
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
import groovy.json.JsonSlurper;
  
properties([[$class: 'GitLabConnectionProperty', gitLabConnection: 'my-gitlab-connection']])
 
node{
    stage 'Build, Test and Package'
    env.PATH = "${tool 'M3'}/bin:${env.PATH}"
    checkout scm
    def commitid = sh(returnStdout: true, script: 'git rev-parse HEAD').trim()
    def workspacePath = pwd()
    sh "echo ${commitid} > ${workspacePath}/expectedCommitid.txt"
     
    withMaven(
                maven: 'M3',
                mavenSettingsConfig: 'a1adf035-653b-410d-b5a6-16b6da77b322',
                mavenLocalRepo: '.repository') {
     
            // Run the maven build
            sh "mvn clean package -Dcommitid=${commitid}"
        }
}
  
node{
    stage 'Stop, Deploy and Start'
    // shutdown
    sh 'curl -X POST http://localhost:8888/shutdown || true'
    // copy file to target location
    sh 'cp target/*.jar /tmp/'
    // start the application
    sh 'nohup java -jar /tmp/*.jar &'
    // wait for application to respond
    sh 'while ! httping -qc1 http://localhost:8888 ; do sleep 1 ; done'
}
  
node{
    stage 'Smoketest'
    def workspacePath = pwd()
    sh "curl --retry-delay 10 --retry 5 http://localhost:8888/info -o ${workspacePath}/info.json"
    if (deploymentOk()){
        return 0
    } else {
        return 1
    }
}
  
def deploymentOk(){
    def workspacePath = pwd()
    expectedCommitid = new File("${workspacePath}/expectedCommitid.txt").text.trim()
    actualCommitid = readCommitidFromJson()
    println "expected commitid from txt: ${expectedCommitid}"
    println "actual commitid from json: ${actualCommitid}"
    return expectedCommitid == actualCommitid
}
  
def readCommitidFromJson() {
    def workspacePath = pwd()
    def slurper = new JsonSlurper()
    def json = slurper.parseText(new File("${workspacePath}/info.json").text)
    def commitid = json.app.commitid
    return commitid
}

Я описал работу сценария ранее. Есть три важных константы, которые должны соответствовать нашей установке Jenkins:

  • В операторе: properties([[$class: 'GitLabConnectionProperty', gitLabConnection: 'my-gitlab-connection']])my-gitlab-connection ‘ соответствует имени, которое я дал своему gitlabConnection в плагине Jenkins, как я описал здесь .
  • Как я описал перед «M3» в заявлении:
    env.PATH = "${tool 'M3'}/bin:${env.PATH}" должен соответствовать установке Maven в Jenkins, как я описал здесь .
  • Наконец, есть строка mavenSettingsConfig: 'a1adf035-653b-410d-b5a6-16b6da77b322' . Упомянутый здесь идентификатор скопирован из файла настроек, который я настроил с помощью плагина Config File Provider, как описано здесь .

Это все об источниках проекта. Позвольте мне показать вам, как создать конвейерную работу в Jenkins. В панели управления выберите создание нового задания типа «конвейер»:

Далее настройте эту работу, где самое важное — использовать Jenkinsfile, полученный из git. Чтобы настроить это, мы должны использовать имя пользователя / пароль для входа в Gitlab (я пока не нашел способа использовать плагин Gitlab. Вы также можете использовать другое хранилище здесь, если вы хотите, чтобы ваши Jenkinsfiles были отделены от вашего проекта. источники):

Теперь, когда я запускаю задание, на последнем шаге произойдет сбой со следующей ошибкой:

org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: сценариям не разрешено использовать новый java.io.File java.lang.String
в org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.StaticWhitelist.rejectNew (StaticWhitelist.java:187)
….

Существует одна последняя настройка, чтобы сделать эту работу успешной. По умолчанию определенные действия не разрешены конвейерным заданием, поэтому я должен сказать Дженкинсу, что в этом случае они разрешены.
Для этого перейдите к «Управление Jenkins» и перейдите к «Утверждение сценариев в процессе»:

Существует упоминание о возможной уязвимости безопасности, которую вы должны утвердить, прежде чем задание позволит выполнить действие:

После нажатия кнопки «Подтвердить» и повторного запуска задания будет еще одна уязвимость, которая должна быть утверждена для успешного завершения задания.
Теперь сборка будет успешной для всех трех этапов, как показано на панели инструментов:

На этом завершается пример непрерывной доставки и конвейерной обработки в виде кода. Как упоминалось ранее, это просто очень простой пример конвейера, но вы можете использовать его, чтобы начать работу с концепцией и извлечь из нее гораздо больше.

Ссылка: Передайте в виде кода приложение Spring Boot от нашего партнера по JCG Паскаля Альмы в блоге Pragmatic Integrator .