Использование Ansible для изменения pom.xml

Я хочу изменить файлы pom.xml на лету, чтобы Maven использовал локально установленные JAR-файлы для определенного groupId. Итак, мне нужно:

  1. Измените/установите область действия каждой соответствующей зависимости на system.
  2. Добавьте systemPath к каждой соответствующей зависимости, указав на файл, имя которого будет функцией artifactId.

Например,

                <dependency>
                       <groupId>myGroup</groupId>
                       <artifactId>myGroup-agent-api</artifactId>
                       <version>3.1.38.13</version>
                       <scope>provided</scope>
                </dependency>

необходимо стать:

                <dependency>
                       <groupId>myGroup</groupId>
                       <artifactId>myGroup-agent-api</artifactId>
                       <version>3.1.38.13</version>
                       <scope>system</scope>
                       <systemPath>${application_path}/jar/agent-api.jar</systemPath>
                </dependency>

С первой частью я разобрался. Эта задача Ansible с использованием модуля xml должна изменять/устанавливать область действия:

- name: Set scope to system for myGroup if {{ module.name }} uses any
  xml:
    path: '{{ app_path }}/{{ module.name }}/pom.xml'
    namespaces:
      x: http://maven.apache.org/POM/4.0.0
    xpath: '//x:dependency/x:groupId[text() = "myGroup"]/../x:scope'
    value: system
  register: xmlfound
  when: java_files.matched
  failed_when:
    - xmlfound is failed
    - >-
      'in order to spawn nodes' not in xmlfound.msg

(Возиться с failed_when необходимо, потому что lxml бросает вызов xpath, если не может найти соответствующие зависимости.)

Но как мне достичь второй части — создания systemPath на основе artifactId каждой соответствующей зависимости?

Обновление: отвечая на вопросы в комментариях:

  1. Да, я совершенно уверен, что хочу использовать Ansible — изменение «механическое», добавление нового профиля не имеет особого смысла, так как это просто увеличит нагрузку на обслуживание разработчиков, которые создают свои собственные рабочие столы — а не сервер - и вам нужны файлы JAR, загруженные из репозитория. Под «на лету» я подразумеваю, что Ansible проверяет исходные коды из git, массирует pom.xml и вызывает Maven — локально, на каждом сервере, предназначенном для запуска этих программ.
  2. Да, я бы поставил add_children, но какой будет аргумент? systemPath каждой зависимости различен и является производным от artifactId.
  3. Спасибо за xpath. К сожалению, он по-прежнему вызывает ошибки для POM без единой соответствующей зависимости, поэтому мне все еще нужен хакерский подход failed_when...

🤔 А знаете ли вы, что...
XML может быть преобразован в другие форматы данных с использованием технологии XSLT (Extensible Stylesheet Language Transformations).


1
163
2

Ответы:

Я бы предпочел решение Maven Profiles, упомянутое Александр Плетнев в комментарии к вопросу , поскольку это делает ваши сборки более независимыми от внешних инструментов/конфигураций:

Прежде всего, я бы сохранил вашу зависимость myGroup-agent-api в вашем:

  • внутренний удаленный репозиторий Maven с развертыванием:deploy-file
  • или в локальном репозитории Maven (если у вас нет внутреннего удаленного репозитория) с помощью install:install-file

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

Во-вторых, в вашем POM:


  <properties>
    <myGroup-agent-api.version>3.1.38.13</myGroup-agent-api.version>
  <properties>
  ...
  <profiles>
    <profile>
      <id>agent-provided</id>
      <!-- See https://maven.apache.org/guides/introduction/introduction-to-profiles.html#details-on-profile-activation
          "All profiles that are active by default are automatically
          deactivated when a profile in the POM is activated on the
          command line or through its activation config."
      --> 
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <dependencies>
        <dependency>
          <groupId>myGroup</groupId>
          <artifactId>myGroup-agent-api</artifactId>
          <version>${myGroup-agent-api.version}</version>
          <scope>provided</scope>
        </dependency>
      <dependencies>
    </profile>

    <profile>
      <id>agent-repo</id>
      <dependencies>
        <dependency>
          <groupId>myGroup</groupId>
          <artifactId>myGroup-agent-api</artifactId>
          <version>${myGroup-agent-api.version}</version>   
        </dependency>
      <dependencies>
    </profile>
  </profiles>
  ...

Запустите mvn <phase> для сборки с зависимостями <scope>provided.

Запустите mvn <phase> -P agent-repo для сборки с разрешенной зависимостью из вашего репозитория. При этом профиль agent-provided будет деактивирован; см. комментарий в декларации POM выше.


Решено

Отказ от ответственности: это только ответ на исходный вопрос. Чтобы решить эту проблему, используйте вместо этого профили Maven.

Рассмотрим следующий pom.xml пример:

<project xmlns = "http://maven.apache.org/POM/4.0.0"
         xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>your-group</groupId>
    <artifactId>your-artifact</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>myGroup</groupId>
            <artifactId>myGroup-agent-api</artifactId>
            <version>3.1.38.13</version>
            <classifier>yaml</classifier>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>my-artifact</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>myGroup</groupId>
            <artifactId>myGroup-another-api</artifactId>
            <version>3.5.79.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

Но как мне достичь второй части — создания systemPath на основе artifactId каждой соответствующей зависимости?

К сожалению, этот вариант использования кажется слишком сложным для модуля xml. Сначала вам нужно получить список этих зависимостей, и вот в чем проблема: список совпадений плоский. Таким образом, вам придется правильно воссоздать структуру, что является непростой задачей, особенно если ваши узлы dependency имеют нечетное количество вложенных узлов. Поскольку я не использовал XPath в течение многих лет, возможно, я использую неправильный, но вот о чем я говорю (учитывая простейший pom.xml, где 2 из 3 зависимостей находятся на артефактах с «myGroup» groupId):

# playbook.yaml
---
- name: Modify the pom.xml
  hosts: localhost
  connection: local
  gather_facts: false
  tasks:
    - name: Find the dependencies on myGroup modules
      xml:
        path: 'pom.xml'
        namespaces:
          x: http://maven.apache.org/POM/4.0.0
        xpath: '//x:dependency/x:groupId[text() = "myGroup"]/..//*'
        content: text
      register: xmlfound

    - name: Show the results
      debug:
        var: xmlfound
TASK [debug] **************************************************************************
ok: [localhost] => 
  xmlfound:
    actions:
      namespaces:
        x: http://maven.apache.org/POM/4.0.0
      state: present
      xpath: //x:dependency/x:groupId[text() = "myGroup"]/..//*
    changed: false
    count: 8
    failed: false
    matches:
    - '{http://maven.apache.org/POM/4.0.0}groupId': myGroup
    - '{http://maven.apache.org/POM/4.0.0}artifactId': myGroup-agent-api
    - '{http://maven.apache.org/POM/4.0.0}version': 3.1.38.13
    - '{http://maven.apache.org/POM/4.0.0}scope': provided
    - '{http://maven.apache.org/POM/4.0.0}groupId': myGroup
    - '{http://maven.apache.org/POM/4.0.0}artifactId': myGroup-another-api
    - '{http://maven.apache.org/POM/4.0.0}version': 3.5.79.0
    - '{http://maven.apache.org/POM/4.0.0}scope': provided
    msg: 8

И я даже не говорю об переборе всех POM в проектах, вложенных циклах и о том, что вам нужно использовать модуль win_xml, если у вас Windows-машины.

Вместо этого вы можете использовать фильтр ansible.utils.from_xml для загрузки всего файла, а затем найти зависимости для обработки с помощью фильтра selectattr Jinja:

# playbook.yaml
- name: Modify the pom.xml
  hosts: localhost
  connection: local
  gather_facts: false
  vars:
    vendor_group_id: myGroup
  tasks:
    - name: Read the pom.xml
      set_fact:
        current_pom: "{{ lookup('file', 'pom.xml') | ansible.utils.from_xml }}"

    - name: Detect the dependencies to process
      set_fact:
        dependencies_to_process: >-
          {{
            current_pom.project.dependencies.dependency 
            | selectattr('groupId', 'equalto', vendor_group_id)
          }}

Следующий шаг — использовать add_elements. Сложные моменты здесь заключаются в следующем:

  • он добавляет элементы только к последнему совпадению;
  • добавленные элементы нарушают форматирование. pretty_print: true помогает, но меняет отступ с 4 пробелов (типично для POM) на 2 пробела;
  • он не идемпотентен, поэтому в следующий раз, когда вы запустите плейбук, он снова добавит вложенный узел systemPath.

Чтобы преодолеть это, вам необходимо:

  • искать узлы artifactId вместо узлов groupId, перебирая список, построенный в предыдущей задаче;
  • добавлять вложенный узел systemPath только в том случае, если элемент списка его еще не содержит;
  • в конце восстановите форматирование, используя xmllint. Конечно, это можно (и вообще нужно) сделать с помощью встроенных модулей Ansible, таких как replace, но в данном случае это была бы слишком сложная задача. Самым простым решением было бы использовать фильтр ansible.utils.to_xml, но при этом комментарии, если они были, будут потеряны.

Вот почти (см. примечания ниже) полный идемпотентный пример (обратите внимание, что я использовал lstrip(vendor_group_id + "-") для определения пути — в вашем случае он может отличаться):

- name: Modify the pom.xml
  hosts: localhost
  connection: local
  gather_facts: false
  vars:
    vendor_group_id: myGroup
  tasks:
    - name: Read the pom.xml
      set_fact:
        current_pom: "{{ lookup('file', 'pom.xml') | ansible.utils.from_xml }}"

    - name: Replace the scope
      set_fact:
        dependencies_to_process: >-
          {{
            current_pom.project.dependencies.dependency
            | selectattr('groupId', 'equalto', vendor_group_id)
          }}

    - name: Set scope to system for myGroup if uses any
      xml:
        path: 'pom.xml'
        namespaces:
          x: http://maven.apache.org/POM/4.0.0
        xpath: '//x:dependency[x:groupId[text() = "{{ vendor_group_id }}"]]/x:scope'
        value: system

    - name: Set systemPath for myGroup
      xml:
        path: 'pom.xml'
        namespaces:
          x: http://maven.apache.org/POM/4.0.0
        xpath: '//x:dependency[x:artifactId[text() = "{{ item.artifactId }}"]]'
        pretty_print: true
        add_children:
          - systemPath: >-
              ${application_path}/jar/{{ item.artifactId.lstrip(vendor_group_id + "-") }}.jar
      loop: '{{ dependencies_to_process }}'
      register: add_children_result
      when: item.systemPath is not defined

    - name: Restore the indentation
      environment:
        XMLLINT_INDENT: '    '
      command: 'xmllint pom.xml --output pom.xml --format'
      when: add_children_result is defined and add_children_result.changed

ПРИМЕЧАНИЯ:

Возиться с failed_when необходимо, потому что lxml вызывает раздражение у xpath, если он не может найти соответствующие зависимости.

Я бы не сказал «необходимо», поскольку вы можете обработать список соответствующих зависимостей, как я предложил. Более того, вы наверняка захотите добавить scope, если ни одного набора не было.

Важно: ради простоты я даже не учел dependencyManagement. Как вы можете видеть, это решение уже слишком сложно даже для простого случая.