[GATING] Add configuration for Azure3 gating in the fork of chained-ci 18/133518/3
authorMarek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl>
Wed, 1 Mar 2023 11:27:28 +0000 (12:27 +0100)
committerMarek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl>
Fri, 3 Mar 2023 12:46:02 +0000 (13:46 +0100)
This change includes:
* moving submodules of chained-ci-roles and chained-ci-vue as static folders
  to the repo (they were quite old and not updated for some time)
* create azure access artifacts
* add config for azure3 gating pipeline

Issue-ID: INT-2207
Signed-off-by: Marek Szwałkiewicz <marek.szwalkiewicz@external.t-mobile.pl>
Change-Id: Idb475c166d78f10ed4204153ab634110aa9093f6

50 files changed:
.gitlab-ci.yml
.gitmodules [deleted file]
chained-ci-vue/LICENSE [new file with mode: 0644]
chained-ci-vue/README.md [new file with mode: 0644]
chained-ci-vue/favicon.png [new file with mode: 0644]
chained-ci-vue/index.html [new file with mode: 0644]
chained-ci-vue/init.sh [new file with mode: 0755]
chained-ci-vue/js/config.js [new file with mode: 0644]
chained-ci-vue/js/index.js [new file with mode: 0644]
chained-ci-vue/js/lib.js [new file with mode: 0644]
chained-ci-vue/js/visibility.LICENSE [new file with mode: 0644]
chained-ci-vue/js/visibility.core.js [new file with mode: 0644]
chained-ci-vue/js/visibility.timers.js [new file with mode: 0644]
chained-ci-vue/logo.svg [new file with mode: 0644]
chained-ci-vue/style.css [new file with mode: 0644]
pod_config/config/artifacts/azure.zip [new file with mode: 0644]
pod_config/config/az14-gating3.yaml [new file with mode: 0644]
pod_config/config/idf-az14-gating3.yaml [new file with mode: 0644]
pod_inventory/group_vars/all.yml
pod_inventory/host_vars/onap_oom_gating_azure_3.yml [new file with mode: 0644]
pod_inventory/inventory
roles/LICENSE [new file with mode: 0644]
roles/README.md [new file with mode: 0644]
roles/artifact_init/defaults/main.yaml [new file with mode: 0644]
roles/artifact_init/filter_plugins/filters.py [new file with mode: 0644]
roles/artifact_init/tasks/main.yml [new file with mode: 0644]
roles/get_artifacts/defaults/main.yml [new file with mode: 0644]
roles/get_artifacts/filter_plugins/filters.py [new file with mode: 0644]
roles/get_artifacts/tasks/binary.yml [new file with mode: 0644]
roles/get_artifacts/tasks/get_one_artifact.yml [new file with mode: 0644]
roles/get_artifacts/tasks/job_id_fetch.yml [new file with mode: 0644]
roles/get_artifacts/tasks/limit_to.yml [new file with mode: 0644]
roles/get_artifacts/tasks/main.yml [new file with mode: 0644]
roles/get_artifacts/tasks/url.yml [new file with mode: 0644]
roles/gitlab-ci-generator/defaults/main.yml [new file with mode: 0644]
roles/gitlab-ci-generator/tasks/main.yml [new file with mode: 0644]
roles/gitlab-ci-generator/templates/gitlab-ci.yml [new file with mode: 0644]
roles/library/filepath.py [new file with mode: 0644]
roles/logo.png [new file with mode: 0644]
roles/logo.svg [new file with mode: 0644]
roles/prepare/README.md [new file with mode: 0644]
roles/prepare/tasks/continue.yml [new file with mode: 0644]
roles/prepare/tasks/except.yml [new file with mode: 0644]
roles/prepare/tasks/exit.yml [new file with mode: 0644]
roles/prepare/tasks/main.yml [new file with mode: 0644]
roles/prepare/tasks/only.yml [new file with mode: 0644]
roles/run-ci/tasks/grafana_start.yml [new file with mode: 0644]
roles/run-ci/tasks/grafana_stop.yml [new file with mode: 0644]
roles/run-ci/tasks/main.yml [new file with mode: 0644]
roles/trigger_myself/tasks/main.yml [new file with mode: 0644]

index 6c4b312..c953d1f 100644 (file)
 stages:
   - lint
   - config
-  - infra_install
-  - virt_install
-  - apps
-  - check
+  - infra_install
+  - virt_install
+  - apps
+  - check
 
 variables:
   GIT_SUBMODULE_STRATEGY: recursive
   VAULT_FILE: .vault
-  RUNNER_TAG: <SET ME>
 
 ################################################################################
 # Shared parameters
@@ -49,8 +48,7 @@ variables:
     expire_in: 1 yrs
 
 .runner_env: &runner_env
-  CHAINED_CI_SRC: "<SET ME>" # Url to the gitlab chained ci project
-  # CHAINED_CI_SRC: "https://gitlab.devops.telekom.de/tnap/onapcommunity/integrationproject/onapdeployment/chained-ci.git"
+  CHAINED_CI_SRC: "https://gitlab.com/onap/integration/pipelines/chained-ci.git"
 
 ################################################################################
 # Linting
@@ -170,25 +168,62 @@ config:onap-daily-unh-oom-master:
   <<: *onap-daily-unh-oom-master_global
   <<: *set_config
   <<: *artifacts_longexpire
-# infra_deploy:onap-daily-unh-oom-master:
-#   stage: infra_install
-#   <<: *onap-daily-unh-oom-master_global
-#   <<: *run_ci
-#   <<: *artifacts_longexpire
-# virt_install:onap-daily-unh-oom-master:
-#   stage: virt_install
-#   <<: *onap-daily-unh-oom-master_global
-#   <<: *run_ci
-#   <<: *artifacts_longexpire
-# apps_deploy:onap-daily-unh-oom-master:
-#   stage: apps
-#   <<: *onap-daily-unh-oom-master_global
-#   <<: *run_ci
-#   <<: *artifacts_longexpire
-# apps_test:onap-daily-unh-oom-master:
-#   stage: check
-#   <<: *onap-daily-unh-oom-master_global
-#   <<: *run_ci
+infra_deploy:onap-daily-unh-oom-master:
+  stage: infra_install
+  <<: *onap-daily-unh-oom-master_global
+  <<: *run_ci
+  <<: *artifacts_longexpire
+virt_install:onap-daily-unh-oom-master:
+  stage: virt_install
+  <<: *onap-daily-unh-oom-master_global
+  <<: *run_ci
+  <<: *artifacts_longexpire
+apps_deploy:onap-daily-unh-oom-master:
+  stage: apps
+  <<: *onap-daily-unh-oom-master_global
+  <<: *run_ci
+  <<: *artifacts_longexpire
+apps_test:onap-daily-unh-oom-master:
+  stage: check
+  <<: *onap-daily-unh-oom-master_global
+  <<: *run_ci
+
+################################################################################
+# onap_oom_gating_azure_3
+################################################################################
+
+.onap_oom_gating_azure_3_global: &onap_oom_gating_azure_3_global
+  variables:
+    pod: onap_oom_gating_azure_3
+    <<: *runner_env
+  environment:
+    name: azure/onap_gating_3/oom_gating
+  only:
+    variables:
+      - $POD == "onap_oom_gating_azure_3"
+    refs:
+      - web
+      - schedules
+      - triggers
+
+config:onap_oom_gating_azure_3:
+  stage: config
+  <<: *onap_oom_gating_azure_3_global
+  <<: *set_config
+  <<: *artifacts
+build_integration:onap_oom_gating_azure_3:
+  stage: infra_install
+  <<: *onap_oom_gating_azure_3_global
+  <<: *run_ci
+onap_deploy:onap_oom_gating_azure_3:
+  stage: apps
+  <<: *onap_oom_gating_azure_3_global
+  <<: *run_ci
+  <<: *artifacts
+onap_test:onap_oom_gating_azure_3:
+  stage: check
+  <<: *onap_oom_gating_azure_3_global
+  <<: *run_ci
 
 ##
 # End of generated file
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644 (file)
index b0a82e6..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-[submodule "chained-ci-vue"]
-       path = chained-ci-vue
-       url = https://gitlab.com/Orange-OpenSource/lfn/ci_cd/chained-ci-vue.git
-[submodule "roles"]
-       path = roles
-       url = https://gitlab.com/Orange-OpenSource/lfn/ci_cd/chained-ci-roles.git
diff --git a/chained-ci-vue/LICENSE b/chained-ci-vue/LICENSE
new file mode 100644 (file)
index 0000000..3be62f8
--- /dev/null
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2018 Orange-OpenSource / lfn / ci_cd
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/chained-ci-vue/README.md b/chained-ci-vue/README.md
new file mode 100644 (file)
index 0000000..e4baa04
--- /dev/null
@@ -0,0 +1,4 @@
+Chained-ci-vue
+================
+
+Submodule for a better visualization of the chained-ci
diff --git a/chained-ci-vue/favicon.png b/chained-ci-vue/favicon.png
new file mode 100644 (file)
index 0000000..6e07930
Binary files /dev/null and b/chained-ci-vue/favicon.png differ
diff --git a/chained-ci-vue/index.html b/chained-ci-vue/index.html
new file mode 100644 (file)
index 0000000..abc92bc
--- /dev/null
@@ -0,0 +1,238 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+
+    <title>Pipelines</title>
+
+    <!-- VUE JS development version, includes helpful console warnings -->
+    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
+
+    <!-- My scripts -->
+    <script src="js/config.js"></script>
+    <script src="js/lib.js"></script>
+
+    <!-- Visisbilityjs -->
+    <script src="js/visibility.core.js"></script>
+    <script src="js/visibility.timers.js"></script>
+
+    <!-- CSS -->
+    <link rel="icon" href="favicon.png">
+    <link rel="stylesheet" href="style.css">
+    <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
+    <link rel="stylesheet" href="https://www.w3schools.com/lib/w3-theme-blue-grey.css">
+    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU" crossorigin="anonymous">
+  </head>
+
+  <body>
+    <div class="main">
+      <header class="w3-container w3-theme w3-card" id="header">
+        <h1   v-on:click="update($event)"
+              class='header w3-center'>{{ project.name }} UI</h1>
+      </header>
+
+      <script type="text/x-template" id="modal-template">
+        <transition name="modal">
+          <div class="modal-mask" v-on:click="closeModal($event, $emit)">
+            <div class="modal-wrapper">
+              <div class="modal-container">
+                <div class="modal-header"><slot name="header"></slot></div>
+                <div class="modal-body"><slot name="body"></slot></div>
+                <div class="modal-footer">
+                  <slot name="footer">
+                  </slot>
+                </div>
+              </div>
+            </div>
+          </div>
+        </transition>
+      </script>
+
+      <section id="auth">
+        <div class="w3-card-4" v-if="!globalAccessGranted">
+          <div class="w3-container">
+            <h2>Please set your gitlab<span v-if="privateTokens.length > 1">s</span>
+              private token<span v-if="privateTokens.length > 1">s</span>
+              </h2>
+          </div>
+          <form @submit="checkForm" class="w3-container">
+            <div v-for="token in privateTokens">
+              <label>
+                <a v-bind:href="'https://'+token.target+gitlabProfileToken">{{ token.target }}</a></label>
+              <label v-if="token.msg">[ {{ token.msg }} ]</label>
+              <div class="w3-xlarge statusIcon"
+                    v-bind:class="[ token.icon ]"></div>
+              <input
+                v-model="token.value"
+                class="w3-input"
+                v-bind:id="token.target"
+                type="password">
+              </input>
+            </div>
+            <button type="submit" class="w3-btn">Validate</button>
+          </form>
+          <div>
+            <div>
+              <div>this is required and can be generated on your user profile like:
+              <a v-bind="{ href: gitlabProfileToken }">{{gitlabProfileToken}}</a>
+              (Only API option is needed)</div>
+            </div>
+          </div>
+          </div>
+      </section>
+
+      <section class="w3-ul w3-border-top" id="pipelines">
+        <div v-if="accessGranted">
+          <div class="tools w3-theme-l5">
+            <div class='tool_sc w3-theme-l4 w3-opacity'>
+              <b>Scenario filter:</b>
+              <input v-model="pipelineFilter" placeholder="filter">
+            </div>
+            <div class='tool_timer w3-theme-l4 w3-opacity'>
+              <b>Next update:&nbsp;</b>{{ timer }}&nbsp;/&nbsp;{{ actualRefresh }}
+              <i v-if="! optimizedRefresh">
+                (Please set filter or optimize it to have a better update time)
+              </i>
+            </div>
+            <div class='tool_new w3-theme-l4'>
+              <a v-bind="{ href: newPipelineUrl }" target='_blank'>
+                <div class='fab fa-gitlab w3-text-orange w3-large w3-statusIcon'></div>
+                New pipeline
+              </a>
+            </div>
+          </div>
+          <div v-for="id in sortedPipelinesIds">
+            <div class='pipeline w3-theme-l5'>
+              <div class='pipeline_header w3-center w3-theme-l4 w3-display-container '>
+                <a v-bind="{ href: pipelines[id].url }" target='_blank'>
+                  <div class='pipeline_statusIcon w3-xxlarge statusIcon w3-padding w3-display-middle'
+                      v-bind:class="[ pipelines[id].statusIcon ]"></div></a>
+                <div class='pipeline_scenario w3-opacity'>{{ pipelines[id].scenario }}</div>
+                <div class='pipeline_branch'>{{ pipelines[id].branch }}</div>
+                <div class='pipeline_date'>{{ pipelines[id].date }}</div>
+                <div class='pipeline_time'>{{ pipelines[id].time }}</div>
+                <div class='pipeline_duration'>{{ Math.round(pipelines[id].details.duration/60) }} min</div>
+                <div class='pipeline_user_icon w3-padding'>
+                  <img v-bind="{ src:pipelines[id].userAvatar, alt:pipelines[id].user}"></img>
+                </div>
+              </div>
+              <div class='pipeline_stages w3-theme-l4'>
+                <div v-for="stage in pipelines[id].stages">
+                  <div class='stage'>
+                    <div class='stage_name w3-opacity'>{{ stage.name }}</div>
+                    <div v-for='job in stage.jobs'>
+                      <div class='w3-round w3-theme-l5 w3-btn w3-padding-small w3-block'>
+                        <div v-if="!job.internal">
+                          <div class='job'>
+                            <div class='job_statusIcon w3-large statusIcon'
+                                  v-bind:class="[ job.statusIcon ]"
+                                  @mouseover="mouseOverJob(job)"
+                                  @mouseleave="mouseLeaveJob(job)"
+                                  v-on:click="jobAction(job)"
+                                  ></div>
+                            <div class='job_name'
+                                 v-on:click="jobDetails($event, job)">
+                                 {{ job.shortname }}</div>
+                          </div>
+                        </div>
+                        <div v-if="job.internal">
+                          <div class='job'>
+                            <div class='job_statusIcon w3-large statusIcon'
+                                  v-bind:class="[ job.statusIcon ]"
+                                  @mouseover="mouseOverJob(job)"
+                                  @mouseleave="mouseLeaveJob(job)"
+                                  v-on:click="jobAction(job)"></div>
+                            <div class='job_name'>
+                              <a v-bind="{ href: job.web_url }" target='_blank'>
+                                {{ job.shortname }}</a>
+                            </div>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div>
+            <div class='pipeline w3-theme-l5'>
+              <div class='pipeline_loader w3-theme-l2 w3-center w3-opacity'
+                   v-on:click="loadMore()">
+                Load more pipelines
+              </div>
+            </div>
+          </div>
+        </div>
+      </section>
+
+
+
+      <section id='alert'>
+        <div class='masq'>
+          <modal v-if="showModal" @close="showModal = false">
+            <h3 slot="header">
+              {{ title }}
+            </h3>
+            <div slot="body">
+              {{ message }}
+            </div>
+          </modal>
+        </div>
+      </section>
+
+
+      <section id='task_details'>
+        <div class='masq'>
+          <modal v-if="showModal" @close="showModal = false">
+            <h3 slot="header">
+                <div class='job_statusIcon w3-large statusIcon'
+                      v-bind:class="[ pipeline.statusIcon ]"
+                      @mouseover="mouseOverPipeline(pipeline)"
+                      @mouseleave="mouseLeavePipeline(pipeline)"
+                      v-on:click="jobAction(pipeline)"></div>
+                <a v-bind="{ href: pipeline.url }" target='_blank'>
+                  Pipeline {{ pipeline.name }} {{ pipeline.id }}
+                </a>
+                <a v-bind="{ href: pipeline.console }" target='_blank'>
+                  <div class='fa fa-terminal w3-theme-l2 w3-large w3-statusIcon'></div>
+                </a>
+
+            </h3>
+            <div slot="body">
+              <div v-if="showWaiting">
+                <div class='w3-xxlarge fa fa-sync w3-text-blue-gray statusIcon'>Loading, please wait...</div>
+              </div>
+              <div v-if="showPipeline">
+                <div v-for="stage in pipeline.stages">
+                  <div class='stage'>
+                    <div class='stage_name w3-opacity'>{{ stage.name }}</div>
+                    <div v-for='job in stage.jobs'>
+                      <div class='w3-round w3-theme-l5 w3-btn w3-block'>
+                        <a v-bind="{ href: job.web_url }" target='_blank'>
+                          <div class='job'>
+                            <div class='job_statusIcon w3-large statusIcon'
+                                  v-bind:class="[ job.statusIcon ]"></div>
+                            <div class='job_name'>{{ job.name }}</div>
+                          </div>
+                        </a>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+              <div v-if="chainedCiFailure">
+                <div>The pipeline was probably not triggered, check console:
+                <a v-bind="{ href: pipeline.console }" target='_blank'>
+                  <div class='fa fa-terminal w3-theme-l2 w3-large w3-statusIcon'></div>
+                </a>
+                </div>
+              </div>
+            </div>
+          </modal>
+        </div>
+      </section>
+    </div>
+    <script src="js/index.js"></script>
+  </body>
+</html>
diff --git a/chained-ci-vue/init.sh b/chained-ci-vue/init.sh
new file mode 100755 (executable)
index 0000000..ff308e8
--- /dev/null
@@ -0,0 +1,68 @@
+#!/usr/bin/env sh
+
+export RUN_SCRIPT=${0}
+export INV_FOLDER=${1}
+export ROOT_FOLDER=${PWD}
+
+mkdir ${ROOT_FOLDER}/public
+mkdir -p ${ROOT_FOLDER}/public/${INV_FOLDER}/host_vars
+mkdir -p ${ROOT_FOLDER}/public/${INV_FOLDER}/group_vars
+
+yaml2json(){
+  SRC=$1
+  DEST="public/${SRC%.*}.json";
+  echo "convert ${SRC} to ${DEST}"
+  yq '.' ${SRC} > ${DEST}
+}
+
+updateConf(){
+  ITEM=$1
+  VALUE=$(echo $2| sed 's/:/\\:/g')
+  echo "set $ITEM to $VALUE"
+  sed -i -e "s|^var $ITEM = .*$|var $ITEM = \"$VALUE\";|" ${ROOT_FOLDER}/public/js/config.js
+}
+
+updateConfObj(){
+  ITEM=$1
+  VALUE=$(echo $2| sed 's/:/\\:/g')
+  echo "set $ITEM to $VALUE"
+  sed -i -e "s|^var $ITEM = .*$|var $ITEM = $VALUE;|" ${ROOT_FOLDER}/public/js/config.js
+}
+
+## Install Yq
+pip install --user yq
+include_splitted=$( yq -r '.include' ${CI_CONFIG_PATH})
+# Convert chained_ci files to json
+if [ -z "${include_splitted}" ]; then
+  # Monolitic gitlab ci, doing nothing
+  echo "no need to merge splitted files"
+else
+  # Non monolitic gitlab ci, add the yaml part into main one
+  for part_file in $(yq -r '.include[]' ${CI_CONFIG_PATH}); do
+         sed 's/---//' $part_file >> ${CI_CONFIG_PATH}
+  done
+fi
+yaml2json ${CI_CONFIG_PATH}
+
+for sc in ${INV_FOLDER}/host_vars/*.yml; do
+  yaml2json $sc
+done
+yaml2json ${INV_FOLDER}/group_vars/all.yml
+
+# Copy site
+cp -rf chained-ci-vue/js public/
+cp -rf chained-ci-vue/index.html public/
+cp -rf chained-ci-vue/style.css public/
+
+# Generate config
+updateConf gitlabUrl ${CI_PROJECT_URL%"$CI_PROJECT_PATH"}
+updateConf chainedCiProjectId ${CI_PROJECT_ID}
+updateConf scenarioFolder "${INV_FOLDER}/"
+updateConf chainedCiUrl ${CI_PROJECT_URL}
+updateConf gitlabCiFilename "${CI_CONFIG_PATH%.*}.json"
+updateConf pipelines_size ${pipeline_size:-10}
+updateConf rootUrl "${CI_PROJECT_NAME}/"
+
+# get all gitlab used
+tokenTargets=$(jq -r '.gitlab.git_projects | map(try(.url | split("/")| .[2]))| sort | unique | @csv' ${ROOT_FOLDER}/public/${INV_FOLDER}/group_vars/all.json)
+updateConfObj tokenTargets "[${tokenTargets}]"
diff --git a/chained-ci-vue/js/config.js b/chained-ci-vue/js/config.js
new file mode 100644 (file)
index 0000000..f71ff70
--- /dev/null
@@ -0,0 +1,16 @@
+// Generated by init.sh
+var gitlabUrl = ;
+var chainedCiProjectId = ;
+var scenarioFolder = ;
+var chainedCiUrl = ;
+var rootUrl = ;
+var gitlabCiFilename = ;
+var pipelines_size = ;
+var updateTimer = 60;
+var tokenTargets = [];
+var optimizedRefreshLevel = 5;
+
+// tool url
+var gitlabCiFile = gitlabCiFilename;
+var gitlabApi = gitlabUrl+'api/v4/projects/';
+var gitlabProfileToken = '/profile/personal_access_tokens';
diff --git a/chained-ci-vue/js/index.js b/chained-ci-vue/js/index.js
new file mode 100644 (file)
index 0000000..447a27b
--- /dev/null
@@ -0,0 +1,250 @@
+// Init token we need to fetch
+var privateTokens = [];
+tokenTargets.forEach(function(target){
+  privateTokens.push({'target': target,
+                        'value': '',
+                        'msg': '',
+                        'icon': '',
+                        'accessGranted': false})
+})
+pipelineRefreshId = -1;
+// Start the authentication
+authenticate();
+
+
+/**
+ * VUE for authentication form
+ *
+ * @data {list} privateTokens              list of token objects
+ * @data {list} gitlabProfileToken        list of gitlab token to ask
+ *
+ * @computed {dict} tokensByTarget          dict of tokens by target
+ * @computed {dict} globalAccessGranted   remove the form if all token are verified
+ *
+ * @methods {function} check the form by starting validateTokens
+ */
+var headerVue = new Vue({
+    el: '#header',
+    data: {project: {}}
+});
+
+/**
+ * VUE for pipelines
+ *
+ * @data {bool} loading          lock var to avoid concurrent load
+ * @data {dict} pipelines        dict of pipelines
+ * @data {list} pipelinesIds    list of pipelines
+ * @data {bool} accessGranted   show the pipelines
+ * @data {int} pages             page indication
+ *
+ * @computed {list} sortedPipelinesIds      list of reverse pipelines ids
+ *
+ * @methods {function} job_details     load job details on click
+ * @methods {function} handleScroll    load new pipelines on scroll to bottom
+ */
+var pipelinesVue = new Vue({
+    el: '#pipelines',
+    data: {
+      loading: false,
+      newPipelineUrl: '',
+      pipelines: {},
+      pipelinesIds: [],
+      accessGranted: false,
+      pipelineFilter: '',
+      pages: 1,
+      timer: updateTimer,
+      actualRefresh: updateTimer * 2,
+      optimizedRefresh: false,
+    },
+    computed: {
+      sortedPipelinesIds: function() {
+        filteredList = [];
+        var pipelines = this.pipelines;
+        var filter = this.pipelineFilter;
+        this.pipelinesIds.sort().reverse().forEach(
+          function(pipelineId){
+            if(pipelines[pipelineId].scenario.includes(filter)){
+              filteredList.push(pipelineId)
+            }
+          }
+        );
+        this.optimizedRefresh = (filteredList.length <= optimizedRefreshLevel);
+        if(this.optimizedRefresh){
+          updateLoop(updateTimer/2);
+        }else{
+          updateLoop(updateTimer);
+        }
+        return filteredList;
+      }
+    },
+    methods:{
+      mouseOverJob: function(job){
+        iconMouseOver(job);
+      },
+      mouseLeaveJob: function(job){
+        iconMouseLeave(job);
+      },
+      jobAction: function(job){
+        jobActionSwitch(job.status, job.id)
+      },
+      jobDetails: function(event, job){
+        taskDetailsVue.showModal = true;
+        taskDetailsVue.showWaiting = true;
+        taskDetailsVue.showPipeline = false;
+        loadSubPipeline(job.id, job.name);
+      },
+      loadMore: function() {
+        if (!pipelinesVue.loading){
+          pipelinesVue.loading = true;
+          pipelinesVue.pages += 1;
+          loadPipelines(pipelinesVue.pages);
+        }
+      },
+  }
+});
+
+// Vue.directive('scroll', {
+//   inserted: function(el, binding) {
+//     let f = function(evt) {
+//       if (binding.value(evt, el)) {}
+//     };
+//     window.addEventListener('scroll', f);
+//   },
+// });
+
+/**
+ * VUE for authentication form
+ *
+ * @data {list} privateTokens              list of token objects
+ * @data {list} gitlabProfileToken        list of gitlab token to ask
+ *
+ * @computed {dict} tokensByTarget          dict of tokens by target
+ * @computed {dict} globalAccessGranted   remove the form if all token are verified
+ *
+ * @methods {function} check the form by starting validateTokens
+ */
+var authVue = new Vue({
+    el: '#auth',
+    data: {privateTokens: privateTokens,
+           gitlabProfileToken: gitlabProfileToken},
+    methods:{
+      checkForm: function (e) {
+        validateTokens(this.privateTokens);
+        e.preventDefault();
+      }
+    },
+    computed: {
+      tokensByTarget: function() {
+        tokens = {}
+        this.privateTokens.forEach(function(token){
+          tokens[token.target] = token.value
+        });
+        return tokens
+      },
+      globalAccessGranted: function() {
+        granted = true;
+        this.privateTokens.forEach(function(token){
+          granted = (granted && token.accessGranted)
+        });
+        if (granted){
+          localStorage.setItem("chained_ci_tokens", JSON.stringify(this.privateTokens));
+          load()
+        }
+        pipelinesVue.accessGranted = granted
+        return granted;
+      }
+    }
+});
+
+/**
+ * VUE for the detail of a job (show the sub pipeline)
+ *
+ * @data {bool} showModal          show the modal vue
+ * @data {bool} showPipeline       show the pipeline
+ * @data {bool} showWaiting        show the waiting message
+ * @data {bool} chainedCiFailure prompt a message of chained ci failed
+ * @data {dict} pipeline           pipeline data
+ */
+var taskDetailsVue = new Vue({
+    el: '#task_details',
+    data: {
+      showModal: false,
+      showPipeline: false,
+      showWaiting: false,
+      chainedCiFailure: false,
+      pipeline: {
+        'name': '',
+        'url': '',
+        'id': '',
+        'status': '',
+        'statusIcon': '',
+        'console': '',
+        'stages': [],
+        'parentTaskId': '',
+        'parentTaskName': '',
+      }
+    },
+    methods:{
+      mouseOverPipeline: function(pipeline){
+        iconMouseOver(pipeline);
+      },
+      mouseLeavePipeline: function(pipeline){
+        iconMouseLeave(pipeline);
+      },
+      jobAction: function(pipeline){
+        console.log(pipeline)
+        this.showModal = false;
+        this.showPipeline = false;
+        jobActionSwitch(pipeline.status, pipeline.parentTaskId)
+      }
+    }
+});
+
+/**
+ * VUE for alert
+ *
+ * @data {bool} showModal          show the modal vue
+ * @data {bool} showPipeline       show the pipeline
+ * @data {bool} showWaiting        show the waiting message
+ * @data {bool} chainedCiFailure prompt a message of chained ci failed
+ * @data {dict} pipeline           pipeline data
+ */
+var alertVue = new Vue({
+    el: '#alert',
+    data: {
+      showModal: false,
+      title: '',
+      message: '',
+    }
+});
+
+// Modal template
+Vue.component('modal', {
+  template: '#modal-template',
+  methods:{
+    closeModal: function(event, emit){
+      if(event.target.className == 'modal-wrapper'){
+        // emit('close')
+        alertVue.showModal = false
+        alertVue.title = ''
+        alertVue.message = ''
+        taskDetailsVue.showModal = false
+        taskDetailsVue.showPipeline = false
+        taskDetailsVue.showWaiting = false
+        taskDetailsVue.chainedCiFailure = false
+        taskDetailsVue.pipeline = {
+          'name': '',
+          'url': '',
+          'id': '',
+          'status': '',
+          'statusIcon': '',
+          'console': '',
+          'stages': [],
+          'parentTaskId': '',
+          'parentTaskName': '',
+        }
+        updatePipelines
+      }
+    },
+  },
+})
diff --git a/chained-ci-vue/js/lib.js b/chained-ci-vue/js/lib.js
new file mode 100644 (file)
index 0000000..8ce3e11
--- /dev/null
@@ -0,0 +1,558 @@
+/**
+ * Different functions to load pipelines and jobs from gitlab
+ *
+ * Description. (use period)
+ *
+ * @link   https://gitlab.com/Orange-OpenSource/lfn/ci_cd/chained-ci-vue/blob/master/js/lib.js
+ * @file   lib.js
+ * @author David Blaisonneau
+ */
+
+
+ /**
+  * Load the pipelines
+  */
+function load(){
+  // Load gitlab-ci and save it to configCi var
+  getJson(gitlabCiFile, function(resp) {
+    configCi = resp;
+    // Get Gitlab project name
+    loadTitle();
+    // Load last pipelines
+    pipelinesVue.loading = true;
+    loadPipelines(0);
+
+    setInterval(function (){
+      if(pipelinesVue.timer > 0 ){
+        pipelinesVue.timer = pipelinesVue.timer - 1;
+      }
+    }, 1000);
+    updateLoop(updateTimer)
+    // setInterval(() => {
+    //   console.log("set refresh to "+ (updateTimer * 1000)+ "ms");
+    //   updatePipelines();
+    // }, updateTimer * 1000);
+  });
+}
+
+function updateLoop(timer){
+  // The refresh time has changed, update loop
+  if(timer != pipelinesVue.actualRefresh){
+    // If we had a Visibility loop, stop it
+    if(pipelineRefreshId >= 0){
+      console.log("stop actual refresh loop ["+pipelineRefreshId+"]")
+      Visibility.stop(pipelineRefreshId);
+    }
+    pipelinesVue.timer = timer
+    pipelinesVue.actualRefresh = timer;
+    console.log("set refresh to "+ timer + "s for next update");
+    pipelineRefreshId = Visibility.every(timer * 1000,
+                                         function (){
+      console.log("Update pipelines, then sleep for "+ timer +" seconds")
+      // Update the pipelines
+      updatePipelines();
+      // Update the timer
+      pipelinesVue.timer = timer
+    });
+  }
+
+
+}
+
+/**
+ * Sort jobs by stage
+ *
+ * Get the whole list of jobs in a pipeline and return a list of stages dict
+ * containing the name of the stage and the list of jobs in this stage
+ *
+ * @params {list} jobs    List of jobs sent by the /jobs API
+ *
+ * @return {list} List of {'name': 'stage name', 'jobs': []}
+ */
+function jobsByStages(jobs){
+  stages = {};
+  stagesList = stages2List(jobs)
+  stagesList.forEach(function(stage){
+    stages[stage]={'name': stage, 'jobs': []}})
+  jobs.forEach(function(job){
+    job.statusIcon = getIcon(job.status);
+    stages[job.stage].jobs.push(job)
+  });
+  jobsByStagesList = []
+  // return a list order by stage step
+  stagesList.forEach(function(stage){
+    jobsByStagesList.push(stages[stage])
+  });
+  return jobsByStagesList
+}
+
+/**
+ * Get stages list from jobs list
+ *
+ * Get a list of stages used by the jobs
+ *
+ * @params {list} jobs    List of jobs sent by the /jobs API
+ *
+ * @return {list} List of stage names
+ */
+function stages2List(jobs){
+  stagesList = []
+  jobs.forEach(function(job) {
+    if(stagesList.indexOf(job.stage) < 0){
+      stagesList.push(job.stage)
+    }
+  })
+  return stagesList
+}
+
+/**
+ * Get an icon class
+ *
+ * Get an icon class name from a string
+ *
+ * @params {str} type  name of the icon to get
+ *
+ * @return {str} icon class
+ */
+function getIcon(type){
+  switch(type){
+    case 'failed':
+      return 'fa fa-times-circle w3-text-red'
+      break;
+    case 'success':
+      return 'fa fa-check-circle w3-text-green'
+      break;
+    case 'running':
+      return 'fa fa-play-circle w3-text-blue'
+      break;
+    case 'waiting':
+      return 'fa fa-pause-circle w3-text-orange'
+      break;
+    case 'skipped':
+      return 'fa fa-dot-circle w3-text-blue-gray'
+      break;
+    case 'created':
+      return 'fa fa-circle-notch w3-text-blue-gray'
+      break;
+    case 'canceled':
+      return 'fa fa-stop-circle w3-text-blue-gray'
+      break;
+    case 'retry':
+      return 'fa fa-plus-circle w3-text-orange'
+      break;
+    case 'stop':
+      return 'fa fa-stop-circle w3-text-orange'
+      break;
+    default:
+      return 'fa fa-question-circle'
+  }
+}
+
+
+/**
+ * Wrapper to call gitlab api
+ *
+ * @params {str}      project         gitlab project id
+ * @params {str}      call            api function called
+ * @params {function} reqOnload      function to call on load
+ * @params {str}      api             base gitlab api to call
+ * @params {str}      method          HTTP Method
+ */
+function gitlabCall(project, call, reqOnload,
+                     api = gitlabApi, method = 'GET'){
+  var requestURL = api+project+'/'+call;
+  getJson(requestURL, reqOnload, getToken(api), method);
+}
+
+/**
+ * Get a JSON from an api
+ *
+ * GET a json from an url and run a function on it
+ *
+ * @params {str}      requestURL  gitlab project id
+ * @params {function} reqOnload  function to call on load
+ * @params {str}      token       PRIVATE-TOKEN to add if needed (default: null)
+ * @params {str}      method          HTTP Method
+ */
+function getJson(requestURL, reqOnload, token = null, method = 'GET'){
+  var request = new XMLHttpRequest();
+  request.open(method, requestURL);
+  if (token){
+    request.setRequestHeader('PRIVATE-TOKEN', token);
+  };
+  request.responseType = 'json';
+  request.send();
+  request.onload = function() {
+    reqOnload(request.response);
+  }
+}
+
+/**
+ * Get the token of a gitlab API
+ *
+ * Get API token from auth vector depending of the url to call
+ *
+ * @params {string}  url   the url to call
+ */
+function getToken(url){
+  target = url.split('/')[2]
+  return authVue.tokensByTarget[target]
+}
+
+/**
+ * Validate all API tokens
+ *
+ * Call gitlab API with a token to check it
+ *  - Set VUEJS privateTokens.globalAccessGranted to boolean result of all
+ *    authentications
+ *  - Update privateTokens list to update the vue
+ *
+ * @params {list}  tokens   list of tokens from the form
+ */
+function validateTokens(tokens){
+  tokens.forEach(function(token){
+    if(token.value){
+      getJson(
+        requestURL = 'https://'+ token.target +
+                     '/api/v4/projects/?per_page=1',
+        function(resp){
+          globalSuccess = true;
+          success = (resp.length == 1)
+          privateTokens.forEach(function(globalToken){
+            if(token.target == globalToken.target){
+              globalToken.accessGranted = success;
+              if(success){
+                globalToken.icon = getIcon('success');
+              }else{
+                globalToken.icon = getIcon('failed');
+              }
+            }
+            globalSuccess = (globalSuccess && globalToken.accessGranted)
+          })
+          privateTokens.globalAccessGranted = globalSuccess
+        },
+        token.value);
+      }
+  });
+}
+
+/**
+ * Authenticate at startup
+ *
+ * Recover saved tokens in localStorage and start token validation
+ */
+function authenticate(){
+  // Try to authenticate with local token stored
+  if (typeof(Storage) !== "undefined") {
+    savedTokens = {}
+    savedTokensList = JSON.parse(localStorage.getItem("chained_ci_tokens"));
+    if(savedTokensList){
+      savedTokensList.forEach(function(token){
+        savedTokens[token.target] = token.value;
+      })
+      privateTokens.forEach(function(token){
+        if(token.target in savedTokens){
+          token.value = savedTokens[token.target]
+        }
+      });
+      validateTokens(privateTokens);
+    }
+  } else {
+    authVue.error = "No local storage, must authenticate again"
+  }
+}
+
+/**
+ * Just load the page title from chained-ci project name
+ */
+function loadTitle(){
+  gitlabCall(chainedCiProjectId, '', function(resp) {
+    headerVue.project = resp;
+    pipelinesVue.newPipelineUrl = resp.web_url + '/pipelines/new';
+  });
+}
+
+/**
+ * Load pipelines of the project
+ *
+ * Call gitlab API to get the pipelines and prepare them
+ *   - set global pipelines info
+ *   - load pipeline jobs
+ *   - for each job:
+ *     - get the scenario names
+ *     - clean jobs names
+ *     - set if a job is a sub pipeline
+ *
+ * @params {string}  page   the page of the api to call (to load them by smal bulks)
+ */
+function loadPipelines(page = 1, size = pipelines_size){
+  gitlabCall(chainedCiProjectId, 'pipelines?page='+page+'&per_page='+size, function(resp) {
+    console.log('load page '+page+' with size '+ size)
+    previous_pipelinesIds = Object.keys(pipelinesVue.pipelines);
+    previous_sorted_pipelinesIds = pipelinesVue.sortedPipelinesIds;
+    pipelines = resp;
+    res = {}
+    // Add more info to pipelines
+    pipelines.forEach(function(pipeline) {
+      console.log(pipeline)
+      load_it = false
+      if (previous_pipelinesIds.indexOf(pipeline.id.toString()) < 0 ){
+        console.log("new pipeline " + pipeline.id);
+        load_it = true;
+      }else if (previous_sorted_pipelinesIds.indexOf(pipeline.id.toString()) >= 0 ){
+        console.log("sorted pipeline " + pipeline.id);
+        load_it = true;
+      }else{
+        console.log("filtered existing pipeline " + pipeline.id + ", pass");
+      }
+      if(load_it){
+        res[pipeline.id] = {};
+        res[pipeline.id].id = pipeline.id;
+        res[pipeline.id].status = pipeline.status;
+        res[pipeline.id].statusIcon = getIcon(pipeline.status);
+        res[pipeline.id].scenario = '';
+        res[pipeline.id].branch = pipeline.ref;
+        res[pipeline.id].details = {};
+        res[pipeline.id].stages = [];
+        res[pipeline.id].url = pipeline.web_url;
+        // Add details
+        gitlabCall(chainedCiProjectId, 'pipelines/'+pipeline.id, function(resp) {
+          res[pipeline.id].details = resp;
+          dt = resp.started_at.split('T');
+          res[pipeline.id].date = dt[0];
+          res[pipeline.id].time = dt[1].split('.')[0];
+          res[pipeline.id].user = resp.user.name;
+          res[pipeline.id].userAvatar = resp.user.avatar_url;
+        });
+        // Add jobs
+        gitlabCall(chainedCiProjectId, 'pipelines/'+pipeline.id+'/jobs', function(resp) {
+          jobs = resp;
+          // get scenario name
+          names = []
+          jobs.forEach(function(job){
+            if (job.name in configCi){
+              if ('variables' in configCi[job.name]){
+                if ('pod' in configCi[job.name].variables){
+                  name = configCi[job.name].variables.pod;
+                  if(!names.includes(name)){names.push(name);}
+                }
+              }
+            }
+          });
+          if(names.length){
+            res[pipeline.id].scenario = names.join(' + ')
+          }else{
+            res[pipeline.id].scenario = 'Internal'
+          }
+
+          // test if it trig another pipeline
+          jobs.forEach(function(job){
+            if (job.name in configCi){
+              job.internal = (configCi[job.name].script[0].search('run-ci') < 0)
+            }else{
+              job.internal = true;
+            }
+            // clean jobs names and remove the scenario name in it
+            if(job.name.search(res[pipeline.id].scenario)>=0){
+              job.shortname = job.name.split(':').slice(0,-1).join(':')
+            }else{
+              job.shortname = job.name
+            }
+          });
+
+          res[pipeline.id].stages = jobsByStages(jobs);
+        });
+      }else{
+        console.log("push previous values")
+        res[pipeline.id] = pipelinesVue.pipelines[pipeline.id]
+      }
+    });
+    pipelinesVue.pipelines = Object.assign({}, pipelinesVue.pipelines, res)
+    pipelinesVue.pipelinesIds = Object.keys(pipelinesVue.pipelines);
+    pipelinesVue.loading = false;
+  });
+}
+
+
+/**
+ * Update pipeline
+ *
+ * This function is trigged by a setInterval() and refresh all pipelines
+ *
+ */
+function updatePipelines(){
+  // Update subpipline
+  if(taskDetailsVue.pipeline.status == 'running' ){
+    console.log('update task')
+    loadSubPipeline(taskDetailsVue.pipeline.parentTaskId,
+                    taskDetailsVue.pipeline.parentTaskName)
+  }
+  // Update all piplines
+  loadPipelines(0, pipelinesVue.pages * pipelines_size)
+}
+
+
+/**
+ * Run an action on a pipeline job
+ *
+ * Call gitlab API to get run an action on a pipeline job
+ *   - set global pipelines info
+ *   - load pipeline jobs
+ *   - for each job:
+ *     - get the scenario names
+ *     - clean jobs names
+ *     - set if a job is a sub pipeline
+ *
+ * @params {string}  action   the action to run, in ['cancel', 'retry']
+ * @params {int}  jobId       the job ID
+ */
+function jobAction(action, jobId){
+  gitlabCall(
+    chainedCiProjectId,
+    'jobs/'+jobId+'/'+action,
+    function(resp) {
+      alertVue.showModal = true;
+      alertVue.title = 'Action '+ action +' on job '+ jobId;
+      alertVue.message = 'Status: ' + resp.status;
+      console.log(resp)
+
+    },
+    gitlabApi,
+    'POST'
+  )
+}
+
+/**
+ * Load a sub pipeline
+ *
+ * Load the a pipeline trigged by chained ci
+ *   - Call the job logs and recover the subpipeline url
+ *   - Load the pipeline info
+ *   - Load the pipeline jobs
+ *
+ * @params {string}  jobId     The job ID inside a chained ci pipeline
+ * @params {string}  jobName   The job Name inside a chained ci pipeline
+ */
+function loadSubPipeline(jobId, originalJobName){
+
+  // Get project URL from static config
+  pod = configCi[originalJobName].variables.pod;
+  jobName = originalJobName.split(":")[0]
+  // Load the config of this scenario
+  getJson(scenarioFolder+'/host_vars/'+pod+'.json', function(scenario) {
+    project = scenario.scenario_steps[jobName].project;
+    // Load top config
+    getJson(scenarioFolder+'/group_vars/all.json', function(all) {
+      subprojectApi = all.gitlab.git_projects[project].api;
+      subprojectUrl = all.gitlab.git_projects[project].url;
+      // console.log(project_url);
+
+      // Load the job log and search for the pipeline string
+      var request = new XMLHttpRequest();
+      requestURL = gitlabApi+chainedCiProjectId+'/jobs/'+jobId+'/trace';
+      request.open('GET', requestURL);
+      request.setRequestHeader('PRIVATE-TOKEN', getToken(gitlabApi) );
+      request.send();
+      request.onload = function() {
+        log = request.response;
+        regex = '\\* ' + subprojectUrl +'/pipelines/\\d+';
+        regex = regex.replace(/\//g,'\\/');
+        regex = regex.replace(/\:/g,'\\:');
+        regex = regex.replace(/\./g,'\\.');
+        regex = regex.replace(/\-/g,'\\-');
+        filter = new RegExp(regex, 'm');
+        m = log.match(filter);
+        if (m){
+          subpipelineId = m[0].split('/').slice(-1)[0];
+          // List subpipeline jobs
+          gitlabCall(
+            '',
+            'pipelines/'+ subpipelineId,
+            function(pipeline) {
+              taskDetailsVue.pipeline.name = project;
+              taskDetailsVue.pipeline.id = subpipelineId;
+              taskDetailsVue.pipeline.parentTaskId = jobId;
+              taskDetailsVue.pipeline.parentTaskName = originalJobName;
+              taskDetailsVue.pipeline.chainedCiFailure = false;
+              taskDetailsVue.pipeline.status = pipeline.status;
+              taskDetailsVue.pipeline.statusIcon = getIcon(pipeline.status);
+              taskDetailsVue.pipeline.url = subprojectUrl+'/pipelines/'+subpipelineId;
+              taskDetailsVue.pipeline.console = chainedCiUrl+'/-/jobs/'+jobId;
+            },
+            subprojectApi);
+          gitlabCall(
+            '',
+            'pipelines/'+ subpipelineId +'/jobs',
+            function(jobs) {
+              jobs.forEach(function(job){
+                if(job.name.search('triggered')>=0){
+                  job.name = job.name.split(':').slice(0,-1).join(':')
+                }
+              });
+              stages = jobsByStages(jobs);
+              // console.log(stages);
+              taskDetailsVue.pipeline.stages = stages;
+              taskDetailsVue.showWaiting = false;
+              taskDetailsVue.showPipeline = true;
+              taskDetailsVue.chainedCiFailure = false;
+            },
+            subprojectApi);
+          }else{
+            taskDetailsVue.showWaiting = false;
+            taskDetailsVue.showPipeline = false;
+            taskDetailsVue.chainedCiFailure = true;
+            taskDetailsVue.pipeline.name = project;
+            taskDetailsVue.pipeline.status = 'failed';
+            taskDetailsVue.pipeline.statusIcon = getIcon('failed');
+            taskDetailsVue.pipeline.url = chainedCiUrl+'/-/jobs/'+jobId;
+            taskDetailsVue.pipeline.console = chainedCiUrl+'/-/jobs/'+jobId;
+          }
+      }
+    });
+  });
+}
+
+/**
+ * Change icon on mouse over
+ *
+ * @params {object}  target     The target object
+ */
+function iconMouseOver(target){
+  switch(target.status){
+    case 'failed':
+    case 'success':
+      target.statusIcon = getIcon('retry');
+      break;
+    case 'running':
+      target.statusIcon = getIcon('stop');
+      break;
+  }
+}
+
+/**
+ * Change icon on mouse leave
+ *
+ * @params {object}  target     The target object
+ */
+function iconMouseLeave(target){
+  target.statusIcon = getIcon(target.status);
+}
+
+
+/**
+ * Action depending on job status
+ *
+ * @params {string}  status     The job status
+ * @params {int}  target     The job id
+ */
+function jobActionSwitch(status, jobId){
+    switch(status){
+      case 'failed':
+      case 'success':
+        jobAction('retry', jobId)
+        break;
+      case 'running':
+        jobAction('cancel', jobId)
+        break;
+    }
+}
diff --git a/chained-ci-vue/js/visibility.LICENSE b/chained-ci-vue/js/visibility.LICENSE
new file mode 100644 (file)
index 0000000..9ef56c8
--- /dev/null
@@ -0,0 +1,3 @@
+source: https://github.com/ai/visibilityjs
+LICENSE: MIT - https://github.com/ai/visibilityjs/blob/master/LICENSE
+author: Andrey Sitnik - https://github.com/ai
diff --git a/chained-ci-vue/js/visibility.core.js b/chained-ci-vue/js/visibility.core.js
new file mode 100644 (file)
index 0000000..6dda095
--- /dev/null
@@ -0,0 +1,189 @@
+;(function (global) {
+    var lastId = -1;
+
+    // Visibility.js allow you to know, that your web page is in the background
+    // tab and thus not visible to the user. This library is wrap under
+    // Page Visibility API. It fix problems with different vendor prefixes and
+    // add high-level useful functions.
+    var self = {
+
+        // Call callback only when page become to visible for user or
+        // call it now if page is visible now or Page Visibility API
+        // doesn’t supported.
+        //
+        // Return false if API isn’t supported, true if page is already visible
+        // or listener ID (you can use it in `unbind` method) if page isn’t
+        // visible now.
+        //
+        //   Visibility.onVisible(function () {
+        //       startIntroAnimation();
+        //   });
+        onVisible: function (callback) {
+            var support = self.isSupported();
+            if ( !support || !self.hidden() ) {
+                callback();
+                return support;
+            }
+
+            var listener = self.change(function (e, state) {
+                if ( !self.hidden() ) {
+                    self.unbind(listener);
+                    callback();
+                }
+            });
+            return listener;
+        },
+
+        // Call callback when visibility will be changed. First argument for
+        // callback will be original event object, second will be visibility
+        // state name.
+        //
+        // Return listener ID to unbind listener by `unbind` method.
+        //
+        // If Page Visibility API doesn’t supported method will be return false
+        // and callback never will be called.
+        //
+        //   Visibility.change(function(e, state) {
+        //       Statistics.visibilityChange(state);
+        //   });
+        //
+        // It is just proxy to `visibilitychange` event, but use vendor prefix.
+        change: function (callback) {
+            if ( !self.isSupported() ) {
+                return false;
+            }
+            lastId += 1;
+            var number = lastId;
+            self._callbacks[number] = callback;
+            self._listen();
+            return number;
+        },
+
+        // Remove `change` listener by it ID.
+        //
+        //   var id = Visibility.change(function(e, state) {
+        //       firstChangeCallback();
+        //       Visibility.unbind(id);
+        //   });
+        unbind: function (id) {
+            delete self._callbacks[id];
+        },
+
+        // Call `callback` in any state, expect “prerender”. If current state
+        // is “prerender” it will wait until state will be changed.
+        // If Page Visibility API doesn’t supported, it will call `callback`
+        // immediately.
+        //
+        // Return false if API isn’t supported, true if page is already after
+        // prerendering or listener ID (you can use it in `unbind` method)
+        // if page is prerended now.
+        //
+        //   Visibility.afterPrerendering(function () {
+        //       Statistics.countVisitor();
+        //   });
+        afterPrerendering: function (callback) {
+            var support   = self.isSupported();
+            var prerender = 'prerender';
+
+            if ( !support || prerender != self.state() ) {
+                callback();
+                return support;
+            }
+
+            var listener = self.change(function (e, state) {
+                if ( prerender != state ) {
+                    self.unbind(listener);
+                    callback();
+                }
+            });
+            return listener;
+        },
+
+        // Return true if page now isn’t visible to user.
+        //
+        //   if ( !Visibility.hidden() ) {
+        //       VideoPlayer.play();
+        //   }
+        //
+        // It is just proxy to `document.hidden`, but use vendor prefix.
+        hidden: function () {
+            return !!(self._doc.hidden || self._doc.webkitHidden);
+        },
+
+        // Return visibility state: 'visible', 'hidden' or 'prerender'.
+        //
+        //   if ( 'prerender' == Visibility.state() ) {
+        //       Statistics.pageIsPrerendering();
+        //   }
+        //
+        // Don’t use `Visibility.state()` to detect, is page visible, because
+        // visibility states can extend in next API versions.
+        // Use more simpler and general `Visibility.hidden()` for this cases.
+        //
+        // It is just proxy to `document.visibilityState`, but use
+        // vendor prefix.
+        state: function () {
+            return self._doc.visibilityState       ||
+                   self._doc.webkitVisibilityState ||
+                   'visible';
+        },
+
+        // Return true if browser support Page Visibility API.
+        // refs: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
+        //
+        //   if ( Visibility.isSupported() ) {
+        //       Statistics.startTrackingVisibility();
+        //       Visibility.change(function(e, state)) {
+        //           Statistics.trackVisibility(state);
+        //       });
+        //   }
+        isSupported: function () {
+            return self._doc.hidden !== undefined || self._doc.webkitHidden !== undefined;
+        },
+
+        // Link to document object to change it in tests.
+        _doc: document || {},
+
+        // Callbacks from `change` method, that wait visibility changes.
+        _callbacks: { },
+
+        // Listener for `visibilitychange` event.
+        _change: function(event) {
+            var state = self.state();
+
+            for ( var i in self._callbacks ) {
+                self._callbacks[i].call(self._doc, event, state);
+            }
+        },
+
+        // Set listener for `visibilitychange` event.
+        _listen: function () {
+            if ( self._init ) {
+                return;
+            }
+
+            var event = 'visibilitychange';
+            if ( self._doc.webkitVisibilityState ) {
+                event = 'webkit' + event;
+            }
+
+            var listener = function () {
+                self._change.apply(self, arguments);
+            };
+            if ( self._doc.addEventListener ) {
+                self._doc.addEventListener(event, listener);
+            } else {
+                self._doc.attachEvent(event, listener);
+            }
+            self._init = true;
+        }
+
+    };
+
+    if ( typeof(module) != 'undefined' && module.exports ) {
+        module.exports = self;
+    } else {
+        global.Visibility = self;
+    }
+
+})(this);
diff --git a/chained-ci-vue/js/visibility.timers.js b/chained-ci-vue/js/visibility.timers.js
new file mode 100644 (file)
index 0000000..546c24e
--- /dev/null
@@ -0,0 +1,161 @@
+;(function (window) {
+    var lastTimer = -1;
+
+    var install = function (Visibility) {
+
+        // Run callback every `interval` milliseconds if page is visible and
+        // every `hiddenInterval` milliseconds if page is hidden.
+        //
+        //   Visibility.every(60 * 1000, 5 * 60 * 1000, function () {
+        //       checkNewMails();
+        //   });
+        //
+        // You can skip `hiddenInterval` and callback will be called only if
+        // page is visible.
+        //
+        //   Visibility.every(1000, function () {
+        //       updateCountdown();
+        //   });
+        //
+        // It is analog of `setInterval(callback, interval)` but use visibility
+        // state.
+        //
+        // It return timer ID, that you can use in `Visibility.stop(id)` to stop
+        // timer (`clearInterval` analog).
+        // Warning: timer ID is different from interval ID from `setInterval`,
+        // so don’t use it in `clearInterval`.
+        //
+        // On change state from hidden to visible timers will be execute.
+        Visibility.every = function (interval, hiddenInterval, callback) {
+            Visibility._time();
+
+            if ( !callback ) {
+                callback = hiddenInterval;
+                hiddenInterval = null;
+            }
+
+            lastTimer += 1;
+            var number = lastTimer;
+
+            Visibility._timers[number] = {
+                visible:  interval,
+                hidden:   hiddenInterval,
+                callback: callback
+            };
+            Visibility._run(number, false);
+
+            if ( Visibility.isSupported() ) {
+                Visibility._listen();
+            }
+            return number;
+        };
+
+        // Stop timer from `every` method by it ID (`every` method return it).
+        //
+        //   slideshow = Visibility.every(5 * 1000, function () {
+        //       changeSlide();
+        //   });
+        //   $('.stopSlideshow').click(function () {
+        //       Visibility.stop(slideshow);
+        //   });
+        Visibility.stop = function(id) {
+            if ( !Visibility._timers[id] ) {
+                return false;
+            }
+            Visibility._stop(id);
+            delete Visibility._timers[id];
+            return true;
+        };
+
+        // Callbacks and intervals added by `every` method.
+        Visibility._timers = { };
+
+        // Initialize variables on page loading.
+        Visibility._time = function () {
+            if ( Visibility._timed ) {
+                return;
+            }
+            Visibility._timed     = true;
+            Visibility._wasHidden = Visibility.hidden();
+
+            Visibility.change(function () {
+                Visibility._stopRun();
+                Visibility._wasHidden = Visibility.hidden();
+            });
+        };
+
+        // Try to run timer from every method by it’s ID. It will be use
+        // `interval` or `hiddenInterval` depending on visibility state.
+        // If page is hidden and `hiddenInterval` is null,
+        // it will not run timer.
+        //
+        // Argument `runNow` say, that timers must be execute now too.
+        Visibility._run = function (id, runNow) {
+            var interval,
+                timer = Visibility._timers[id];
+
+            if ( Visibility.hidden() ) {
+                if ( null === timer.hidden ) {
+                    return;
+                }
+                interval = timer.hidden;
+            } else {
+                interval = timer.visible;
+            }
+
+            var runner = function () {
+                timer.last = new Date();
+                timer.callback.call(window);
+            }
+
+            if ( runNow ) {
+                var now  = new Date();
+                var last = now - timer.last ;
+
+                if ( interval > last ) {
+                    timer.delay = setTimeout(function () {
+                        timer.id = setInterval(runner, interval);
+                        runner();
+                    }, interval - last);
+                } else {
+                    timer.id = setInterval(runner, interval);
+                    runner();
+                }
+
+            } else {
+              timer.id = setInterval(runner, interval);
+            }
+        };
+
+        // Stop timer from `every` method by it’s ID.
+        Visibility._stop = function (id) {
+            var timer = Visibility._timers[id];
+            clearInterval(timer.id);
+            clearTimeout(timer.delay);
+            delete timer.id;
+            delete timer.delay;
+        };
+
+        // Listener for `visibilitychange` event.
+        Visibility._stopRun = function (event) {
+            var isHidden  = Visibility.hidden(),
+                wasHidden = Visibility._wasHidden;
+
+            if ( (isHidden && !wasHidden) || (!isHidden && wasHidden) ) {
+                for ( var i in Visibility._timers ) {
+                    Visibility._stop(i);
+                    Visibility._run(i, !isHidden);
+                }
+            }
+        };
+
+        return Visibility;
+    }
+
+    if ( typeof(module) != 'undefined' && module.exports ) {
+        module.exports = install(require('./visibility.core'));
+    } else {
+        install(window.Visibility || require('./visibility.core'))
+    }
+
+})(window);
diff --git a/chained-ci-vue/logo.svg b/chained-ci-vue/logo.svg
new file mode 100644 (file)
index 0000000..1eb9231
--- /dev/null
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="19.586046mm"
+   height="19.586046mm"
+   viewBox="0 0 19.586046 19.586046"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)"
+   sodipodi:docname="logo.svg"
+   inkscape:export-filename="/home/edby8475/Dev/chained-ci-vue/logo.png"
+   inkscape:export-xdpi="98"
+   inkscape:export-ydpi="98">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="6.5333333"
+     inkscape:cx="7.8188803"
+     inkscape:cy="30.617961"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="2560"
+     inkscape:window-height="1403"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Calque 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-76.099242,-143.22085)">
+    <circle
+       style="opacity:1;vector-effect:none;fill:#abc837;fill-opacity:1;stroke:#4d4d4d;stroke-width:0.50260705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="path921"
+       cx="85.892265"
+       cy="153.01387"
+       r="9.5417194" />
+    <g
+       id="g933">
+      <path
+         sodipodi:nodetypes="cccc"
+         inkscape:connector-curvature="0"
+         id="path919"
+         d="m 87.755086,149.65335 h -3.722685 c 3.722685,0 0.03786,6.84943 3.737003,6.84401 v 0"
+         style="fill:none;stroke:#f2f2f2;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <g
+         transform="translate(-2.1166667)"
+         id="g862">
+        <circle
+           style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#44aa00;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+           id="path848"
+           cx="83.371094"
+           cy="149.57428"
+           r="2.7487407" />
+        <path
+           style="fill:none;stroke:#44aa00;stroke-width:0.62900001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 82.386279,149.28284 0.715902,0.81612 1.267143,-1.24567"
+           id="path850"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         transform="translate(-0.03559777,6.879167)"
+         id="g858">
+        <circle
+           r="2.7487407"
+           cy="149.57428"
+           cx="90.565697"
+           id="circle852"
+           style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#2a7fff;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+        <path
+           style="opacity:1;vector-effect:none;fill:#2a7fff;fill-opacity:1;stroke:none;stroke-width:0.36824697;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+           id="circle854"
+           sodipodi:type="arc"
+           sodipodi:cx="90.415558"
+           sodipodi:cy="149.57428"
+           sodipodi:rx="1.9128479"
+           sodipodi:ry="1.9128479"
+           sodipodi:start="4.7106229"
+           sodipodi:end="2.5736704"
+           d="m 90.41218,147.66143 a 1.9128479,1.9128479 0 0 1 1.881627,1.55068 1.9128479,1.9128479 0 0 1 -1.17015,2.13913 1.9128479,1.9128479 0 0 1 -2.320669,-0.74807 l 1.61257,-1.02889 z" />
+      </g>
+      <g
+         transform="translate(7.1590052)"
+         id="g868">
+        <circle
+           r="2.7487407"
+           cy="149.57428"
+           cx="83.371094"
+           id="circle864"
+           style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#44aa00;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path866"
+           d="m 82.386279,149.28284 0.715902,0.81612 1.267143,-1.24567"
+           style="fill:none;stroke:#44aa00;stroke-width:0.62900001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/chained-ci-vue/style.css b/chained-ci-vue/style.css
new file mode 100644 (file)
index 0000000..d2c7f8b
--- /dev/null
@@ -0,0 +1,219 @@
+a {
+    text-decoration: none;
+}
+
+html, body {
+    margin: 5px;
+    padding: 0;
+}
+
+.main{
+  margin: auto;
+  width: 70%;
+  min-width: 1200px;
+}
+
+.tools {
+  margin: 10px auto;
+}
+.tools > div {
+  padding: 0.3em;
+  margin: 1em;
+}.tools {
+  display: grid;
+  text-align: center;
+  grid-template-columns: repeat(8, 1fr );
+  grid-gap: 5px;
+}
+.tool_sc {
+  grid-column: 1 / 3;
+}
+.tool_timer {
+  grid-column: 3 / 8;
+}
+.tool_new {
+  grid-column: 8;
+}
+
+
+.pipeline {
+  margin: 10px auto;
+}
+.pipeline > div {
+  padding: 1em;
+  margin: 1em;
+}.pipeline {
+  display: grid;
+  grid-template-columns: repeat( 6, 1fr );
+  grid-gap: 5px;
+}
+
+
+.pipeline_header {
+  grid-column: 1;
+  grid-row: 1;
+}.pipeline_header {
+  display: grid;
+  grid-template-columns: repeat( 2, 1fr );
+  grid-template-rows: repeat(5, 3fr);
+  grid-gap: 5px;
+}
+
+.pipeline_user_icon {
+  grid-column: 1 ;
+  grid-row: 4 / 6;
+}
+
+.pipeline_statusIcon {
+  grid-column: 2 ;
+  grid-row: 4 / 6;
+} img{
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+}
+
+.pipeline_scenario {
+  font-weight: bold;
+  grid-column: 1 / 3;
+  grid-row:1;
+}
+.pipeline_branch {
+  grid-column: 1;
+  grid-row:2;
+}
+.pipeline_date {
+  grid-column: 1;
+  grid-row:3;
+}
+.pipeline_time {
+  grid-column: 2;
+  grid-row:3;
+}
+
+.pipeline_duration {
+  grid-column: 2;
+  grid-row: 2;
+}
+
+.pipeline_stages {
+  grid-column: 2 / 7;
+  grid-row: 1;
+}
+
+.pipeline_loader {
+  grid-column: 1 / 7;
+  grid-row: 1;
+}
+
+.stage {
+}
+.stage > div {
+  padding: 1px;
+  width: 100%;
+}.stage {
+  float:left;
+  display: table;
+}
+
+.stage_name{
+  display: inline-block;
+  text-align: center;
+  padding: 2px;
+}
+
+.job{
+  display: inline-block;
+} > div {
+}.job{
+  display: grid;
+  grid-template-columns: repeat(6, 1fr);
+  grid-gap: 0px;
+}
+
+.job_statusIcon{
+  grid-column: 1;
+  grid-row: 1;
+}
+
+.job_name{
+  padding: 2px;
+  grid-column: 2 / 7;
+  grid-row: 1;
+}
+
+.statusIcon{
+  padding: 2px;
+}
+
+
+
+
+
+
+
+
+
+.modal-mask {
+  position: fixed;
+  z-index: 9998;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, .5);
+  display: table;
+  transition: opacity .3s ease;
+}
+
+.modal-wrapper {
+  display: table-cell;
+  vertical-align: middle;
+}
+
+.modal-container {
+  width: 60%;
+  min-height: 600px;;
+  margin: 0px auto;
+  padding: 20px 30px;
+  background-color: #fff;
+  border-radius: 2px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
+  transition: all .3s ease;
+}
+
+.modal-header h3 {
+  margin-top: 0;
+  color: #42b983;
+}
+
+.modal-body {
+  margin: 20px 0;
+}
+
+.modal-default-button {
+  float: right;
+}
+
+/*
+ * The following styles are auto-applied to elements with
+ * transition="modal" when their visibility is toggled
+ * by Vue.js.
+ *
+ * You can easily play with the modal transition by editing
+ * these styles.
+ */
+
+.modal-enter {
+  opacity: 0;
+}
+
+.modal-leave-active {
+  opacity: 0;
+}
+
+.modal-enter .modal-container,
+.modal-leave-active .modal-container {
+  -webkit-transform: scale(1.1);
+  transform: scale(1.1);
+}
diff --git a/pod_config/config/artifacts/azure.zip b/pod_config/config/artifacts/azure.zip
new file mode 100644 (file)
index 0000000..44f96a9
Binary files /dev/null and b/pod_config/config/artifacts/azure.zip differ
diff --git a/pod_config/config/az14-gating3.yaml b/pod_config/config/az14-gating3.yaml
new file mode 100644 (file)
index 0000000..9310f8c
--- /dev/null
@@ -0,0 +1,168 @@
+##############################################################################
+# Copyright (c) 2017 Orange and others.
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+---
+### vPOD descriptor file ###
+
+details:
+  pod_owner: OPNFV
+  contact: N/A
+  lab: OPNFV LaaS
+  location: N/A
+  type: production
+  link: http://wiki.opnfv.org/
+##############################################################################
+
+nodes:
+  - name: nfs01
+    node:
+      type: virtual
+      vendor: azure
+      model:
+        offer: debian-10
+        version: latest
+        publisher: Debian
+        sku: "10"
+      arch: x86_64
+      cpus: 1
+      cpu_cflags: host-model
+      cores: 1
+      memory: 1G
+      flavor: Standard_B2s
+    disks:
+      - name: disk1
+        disk_capacity: 27G
+        disk_type: qcow2
+        disk_interface: ide
+        disk_rotation:
+      - name: disk-nfs
+        disk_capacity: 100G
+        disk_type: qcow2
+        disk_interface: ide
+        disk_rotation:
+    remote_management: &remote_management
+      type:
+        - ipmi: NA
+      user: NA
+      pass: NA
+      address: NA
+      mac_address: NA
+    interfaces:
+      - mac_address:
+        name: nic1
+        speed:
+        features:
+        network: internal
+  - name: devstack01
+    node:
+      type: virtual
+      vendor: azure
+      model:
+        #offer: "0001-com-ubuntu-server-focal"
+        #sku: "20_04-lts-gen2"
+        offer: UbuntuServer
+        version: latest
+        publisher: Canonical
+        sku: "18_04-lts-gen2"
+      arch: x86_64
+      cpus: 16
+      cpu_cflags: host-model
+      cores: 16
+      memory: 64G
+      flavor: Standard_D16s_v3
+    disks:
+      - name: disk1
+        disk_capacity: 32G
+        disk_type: qcow2
+        disk_interface: ide
+        disk_rotation:
+    remote_management: *remote_management
+    interfaces:
+      - mac_address:
+        name: nic1
+        speed:
+        features:
+        network: internal
+  - name: worker01
+    node: &workerparams
+      type: virtual
+      vendor: aks
+      model: worker
+      arch: x86_64
+      cpus: 4
+      cpu_cflags: host-model
+      cores: 4
+      memory: 16G
+      flavor: Standard_D4s_v3
+    disks: &workerdisks
+      - name: disk1
+        disk_capacity: 8G
+        disk_type: qcow2
+        disk_interface: ide
+        disk_rotation:
+    remote_management: *remote_management
+    interfaces: &interfaces
+      - mac_address:
+        name: nic1
+        speed:
+        features:
+        network: kubernetes
+  - name: worker02
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
+  - name: worker03
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
+  - name: worker04
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
+  - name: worker05
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
+  - name: worker06
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
+  - name: worker07
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
+  - name: worker08
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
+  - name: worker09
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
+  - name: worker10
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
+  - name: worker11
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
+  - name: worker12
+    node: *workerparams
+    disks: *workerdisks
+    remote_management: *remote_management
+    interfaces: *interfaces
diff --git a/pod_config/config/idf-az14-gating3.yaml b/pod_config/config/idf-az14-gating3.yaml
new file mode 100644 (file)
index 0000000..e63289a
--- /dev/null
@@ -0,0 +1,171 @@
+---
+idf:
+  net_config: &net_config
+    internal:
+      interface: 0
+      network: 192.168.65.0
+      mask: 24
+      gateway: 192.168.65.1
+      dns: 192.168.65.1
+
+aks:
+  peering_network: internal
+
+az_infra:
+  jumphost_network: admin
+
+openstack:
+    release: stable/victoria
+
+os_infra:
+  net_config: *net_config
+  user:
+    name: cloud
+    is_admin: false
+  tenant:
+    name: Gating
+  images_to_push:
+  image_default: debian-10
+  image2user_mapping:
+    debian-10: debian
+  nodes_roles:
+    nfs01: [kube-master, nfs-server]
+    devstack01: [devstack]
+  roles_group:
+    k8s-cluster:
+      - kube-master
+      - kube-node
+    k8s-full-cluster:
+      - k8s-cluster
+      - jumphost
+      - devstack
+
+  onap:
+    global_storage:
+      enabled: true
+      rwx_class: nfs
+      class: default
+
+  dns:
+    update: true
+    provider: gandiv5
+    zone: onap.eu
+    name: azure3
+    master: nfs01
+
+  kubernetes:
+    dns_prefix: onap-gating
+    name: cluster
+    kubernetes_version: 1.23.15
+    helm_version: v3.8.2
+    worker_role: kube-node
+    helm:
+      repositories:
+        - name: jetstack
+          url: https://charts.jetstack.io
+        - name: minio
+          url: https://helm.min.io/
+    charts:
+      cert-manager:
+        chart: jetstack/cert-manager
+        namespace: cert-manager
+        istioEnabled: true
+        content: |
+          ---
+          installCRDs: true
+          prometheus:
+            servicemonitor:
+              enabled: true
+      minio:
+        chart: minio/minio
+        namespace: minio
+        content: |
+          ---
+          accessKey: "{{ lookup('env','S3_ACCESS_KEY') }}"
+          secretKey: "{{ lookup('env','S3_SECRET_KEY') }}"
+          replicas: 1
+          persistence:
+            size: 100Gi
+          service:
+            type: LoadBalancer
+            nodePort: 30345
+          metrics:
+            serviceMonitor:
+              enabled: true
+      nfs-server-provisioner:
+        chart: stable/nfs-server-provisioner
+        namespace: nfs-server
+        enabled: true
+
+  ssh_pub_key_default: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCqbo9WQwjZp6Op6a\
+    wsgRaBdR+gJhbmMHOm+2Ol7TJlXe2wV5r2vFQhbmd0GHTdz4g8MH+y7oO5637RoAMZlY4ieZdS\
+    VGZZ5HvIoMqoS7FVT6u1kvVTVBmlpEgF4gXLsJi3s4bGlXqVnYA42SoKX65DhvHgLIScL1E6uS\
+    63MLRP+clFchNASyhz8CgRPdazE7vy4LJJNE8C6hj0MVSOVasJKMZQAmqjSFkuMy0ECNDj5sQm\
+    oPYozpvM9xsP/UWiW8MCWuTh33JRBPH/06OIUMVN0dD+xhfVgYY+aueFRjs4oaMm2kfgiTJsQw\
+    uAziLrvnusSse+idq9t49YeiAPGpuZ sylvain@MacBook-Pro-de-Sylvain.local
+
+    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBlukmibuf0LMrK5NmZkxIziYrOovBzYG8JmQ\
+    KONN3KtRZBy9ooL5JBFfmdWMvdcWoLee23R024DwWGF1ooR9SyolXQ+K7lbsSzw0NR/+feRs3of\
+    asxj0bXJsWjC48C7eD8v48Y2PMs5uP5Yp/RMOThHVN7w2gbm2OZGG5MIV1ixML/coqo39clm8dC\
+    pFhSZmPfmVFliOBW8dFk+DSoKClVy7FH56qEzKoenLJy/+j0B10/5t6T7Sk1QUWDOfIikBomGa2\
+    gZAozgGFnuoK9b7FBwlyZiYbbbL4EShiqkEaY938zYcXr1/bqcZWj/F30hK5ddo6f1IDwr57U6l\
+    1W5Vd thierry.hardy@orange.com
+
+    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDB22+oh+TaQs5XbJtDY3elDWHnTlPCOwqr0kh\
+    wYGn/teOr9hPDtqADPjaymoowNDbFIB9CZleZuLFAIryCplb563EwHoaGllRaBom/Alzj3QRIhx\
+    FjhWCBzes+ix6NRqKQZ7wPGB7u5QuQCvqlMgLKDNz2UmPoIoYLrhrFyUE5cxWtZ2vmSMTkOpLwO\
+    Aw+9UWMww6jzCLNcQjl25zyfSLXVCLLbuyBUza0SvcyxH4glbjTx3BjdCp6ovDDhO2z9tvofvzF\
+    j5ceGlohD40ETiROT9C7hKj7Bn2MPDCKPEEjBVjkh9YkpUD9W6uW/t1loGoajLZeJEbN946L3Lt\
+    Yi0wz morgan.richomme@orange.com
+
+    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDRslHbwKe4AzMa9dRXnpEDjrbqMvPuGxG2zzZ\
+    KbKWsqtzUhgIAMhUL6vw4ePI34bN6uqq8qsQDDu5JYJ3sO1WwpFhX0ZEvWohQK55oe94kfXfnbu\
+    sOwRfd8uiWh2s8lfCL7BlbruKGIJcueX+bMLJfUPo3vo5Ae3bBn+J4sG1tLLpf9UxvuXmnZiRyv\
+    QJjCy2f6BOeS2AzRhezkC23IS2gylzG3I3MT1acw3i9wAF2yZ2ca++6MdLb6e6Bu30Y9rO7PAj8\
+    76myts4GaBhbXmi1+rjb0HbUm3V9wxG7drKv7C0y0nftBS0Q9yCKqld96WQgseYL3CcwEe4qVqn\
+    a7DxWQCJROJRi9/DcSSoL73jPcfSWyRqeutWp55S/ajvseAG2yZExbMiGOmW5qsyAGMigjGik59\
+    H7gSJR0RitQtGpoqBzsuFaGgYnpfUkAgnjdqqlLT7iRO6OZAXVVSZODbb6LE+2av4B0Yp7SHx8U\
+    Rps4CGBS8HyGHQNelnOmra8SnVmXtOhYU87kdOoPaTU7GkuYiy8ctewSoHdgX40tRbB45AMlc+L\
+    avf+4hx/5K/u50gei19tUM9aKf6TmdH8LFOPtwUtMd/4nNVYmuwdEFqTVTkVPYdpIF75N5NDF7d\
+    uDRyYakTPG/rvj9l0zT5V7sfc3QA0TVNdHu6NLPHHqvkjsQ== k.opasiak@samsung.com
+
+    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDHfYKtwwhaiw+uq8G4G6TVFRsvq9kbaN+vZ/H\
+    cToLN1LfIJVKFCGykbnrm+f4LrGM6CTvCxt6YU2wpszHoCIVQc+9FTe58O/fuSrrSPVS3JuwIqO\
+    /QT4pTnxGrP/FrEEkefoEUD1kLFxYLKy0z4+d0uLee1aBRlSwbk+dWkVoedkoXQmUfTugsORiuk\
+    beuFEJR77MQDAGYC0AVbl7Eb0Fv/DNNV/SLNWHh0OP5ilLeSNpqC5s51u7iALhbOzI1kRHZrc/8\
+    SExyXjeBNy01l4cNfOse3/3AWo8gOVqI49b5UggQPh1fuBQ7QZkzbRp9/U7+t4v6b+JJg6eBeoj\
+    A/yuL0uMrKuX4hrT5euTOBup4GyTv8GHS8EN4jRWbWMz6hQ1FYwssnAIpVWXyl/4xD2jJ0Av9mG\
+    /W0htB/wnNeOss+RokiWA5HQ+Dupbcp2GucMhs2atRPD0V30HbvcUMxPE/BC15R1uqQapV5hkz1\
+    WrP5KQTHdYmDRvKU5ZxBNBAkR8= j.latusek@samsung.com
+
+    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCSNiMbJEDhn32OXdWxRwftB8LRvF9X1npSlfB\
+    2M3ezxKqzm3bx4yhBMm7sCBhS6f1dQuIiLz4BE6h1P+AdXfg+zkqkV44lhGLGAu7L2ysRRCUGoU\
+    +h4LwRVBuKyd5WL3u3jzwBK/tp1uHNqSVEoRPFgfkBNZwALoclymw/2W7gfQ2OAVeu6pFycLF9e\
+    oUSkAWQxlZcYaVXSIL7EhSVs7pP41BzQ3DIHv1fN/bVzbss8bePUa5j8gHJiSrKfU/gslnCN8m7\
+    yJuj1IpmfK/XJl+JAU9Y0+GSqvN6mitIe6m+qhkCK75A6+pXdycbZlYT/fUlFBv1rZCuzapb3NC\
+    b5V8gO7B1Rx3W2lu50m5lmRjLjRichyRsZCtWQNAOUi7I7BJcq8voEqb5sjuIuZThezMT5TW+d4\
+    1+JlbD2bQjAiFz5TEtnrHDXERW5PmsVqlqhQnMc/lSrLmHQaQPFTZtyI0jfB5Jvehp0y7PdC5Li\
+    rz8wSuWB2cXLToxhGqxz3xgZ4s= samuel.liard@orange.com
+
+    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCx/xA4zpJOXj03X3H8jNAZynKGOJvOkqnoRwq\
+    1gEw46WDnZoElAkTojjCZSqBXYsmK48uLNMsys0FufL4XPjtYEvjrApooQBXsH+JdGKjm9M16pv\
+    MSPjcxov0IQ+GuTrBFEbg6ismmLNgAGdXBvJa4q+Ne0yaPEh3WbffbPEShIUj6wiFFI7pdd4je2\
+    Dka2kPrFBQUsJe6qUrQ3nbpXpNg3XGnKm3fqNfKWSw5Lc5UfvKxgLxU+9ur46O63LwgAyako2FY\
+    J5dC6RkAe91fpqUNcM4JhjuqmeTrqpi8QEjX4t22Zr6W26Ueepd7uuKyiRnXYd27uwjvHCgC41V\
+    0BGiB krzysztof.kuzmicki@nokia.com
+
+    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCwJS4K/mt2MfH2VOlOWwEqFW4T/oTIzM8lcz8\
+    e3QeKMBJJa5jWiviEruXhbc3kG4nHuq1xkaQLxqbOUN4TF1GH4AHkCl1mLtb9TnBWtihV4A9zQD\
+    bjluNmsT0lotOOK3Qfqee35BXkoDX4IXjjqjHXmUM2n4DDajJUd38H7Owd28nlpDhzXpFhfDzM5\
+    8wO586frKs0UGYM6gc8JyaLRPACum0wFz9HzXb4NShDQh+smW+v6fIeKe9d4HC8t/b8XZFChhGe\
+    pLN/i0EGLZqwX5uHrGoq3/6i7gAmHBmNs1wR8qX5nVl28VKdz4Dbum9hJjoxSNC54K8Yyh5JbCB\
+    G7ft/eXLGY+Dm5moPaoPBkpOHcIk5aP/MVNgOBmAETxnujlaXYmrorrH0aTB9UGE0zHezzXfq0a\
+    VjuSsFFmXDFOoKJLyc/C8WnMDdB+GD3UBnCdhlKPjbod8OAPjq1OdFpLgR0APTb3MB/v1Nfdhf3\
+    hqYatkG0P9TCVvjfLqzeHqHhfs= ahmad@Ahmads-MBP
+
+    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDA/6kqp3MNvYCajLYIunRG4w8aZFbfNZqFmhO\
+    nvR/8h0vrTJAxSVPMZVETlEkIdZMB8wKHnvj75TepQVxP2dXtqlgrmVnCOrKDYNgCQsMoDr/EaY\
+    Xbv+ph/asWWFuIQ9wPVZG3+Qlf+Y5ne88WVnjlLOvqoB7UJYEqNnhqDR4OVgBVhnROlcnPZE4mh\
+    3TOxFXMDNctOQUr+4h4Cp7hBzXW7SbbpDMiQizIGSEFrzA/L0/peSK1f105KDdenWSfSxsbqABu\
+    KL+2/68BheRqcXxhiXf3AUGMy2awhLRLPCmTbE10J2Ky6z1bNgB0Wnqb0bwWLerow0B3Urb+c/0\
+    Lv9EZ Michal.Jagiello@t-mobile.pl"
index f31df15..7257da5 100644 (file)
@@ -14,7 +14,7 @@ runner:
   tags:
     - "$RUNNER_TAG"
   env_vars:
-    CHAINED_CI_SRC: https://gitlab.devops.telekom.de/tnap/onapcommunity/integrationproject/onapdeployment/chained-ci.git  #TODO: Chained CI on onap gitlab
+    CHAINED_CI_SRC: https://gitlab.com/onap/integration/pipelines/chained-ci.git  #TODO: Chained CI on onap gitlab
   docker_proxy:
   image: registry.gitlab.com/orange-opensource/lfn/ci_cd/docker_ansible
   image_tag: 2.7.10-alpine
@@ -22,31 +22,39 @@ runner:
 gitlab:
   pipeline:
     delay: 15
-  base_url: https://gitlab.devops.telekom.de
-  api_url: https://gitlab.devops.telekom.de/api/v4
+  base_url: https://gitlab.com
+  api_url: https://gitlab.com/api/v4
   private_token: "{{ lookup('env','CI_private_token') }}"
 
   git_projects:
     config:
       stage: config
-      url: https://gitlab.devops.telekom.de/tnap/onapcommunity/integrationproject/onapdeployment/chained-ci.git #TODO Chained CI on onap gitlab
-      api: https://gitlab.devops.telekom.de/api/v4/projects/36215
+      url: https://gitlab.com/onap/integration/pipelines/chained-ci.git #TODO Chained CI on onap gitlab
+      api: https://gitlab.com/api/v4/projects/39992873
       branch: "{{ lookup('env','config_branch')|default('master', true) }}"
       path: pod_config
 
-    trigger:
-      stage: apps
+    build_integration:
+      stage: infra_install
+      api: https://gitlab.com/api/v4/projects/24365265
+      url: https://gitlab.com/Orange-OpenSource/lfn/onap/build-integration
       trigger_token: !vault |
         $ANSIBLE_VAULT;1.1;AES256
-        64386138616464653132353964363032346464373363323366616436346263323230353961363263
-        3562653664303631323134313864393364636538643430640a363766316230633932376466643333
-        64386331633737623164313831633537666638623534663736313331313266396438306266636632
-        3532313263396532300a306661393438613734323064313064343361363763636664393231363934
-        37633335396563623462653935393236356139303864646135303935373937623739
-      branch: "{{ lookup('env','CI_BUILD_REF_NAME')|default('master', true) }}"
+        61313463643234303366353965653038363162386565613266326237373634326465
+        3365323331306531363834326264613736393836633362323635323365300a333963
+        30346666663636653238306265393833663463393538613466633831383234336332
+        3239613634383063386635653836626634633136623831396362640a363332666465
+        66396131663861326163666536346336356430303933363035373830363162373036
+        323433383436616461373231386464666232353932383162
+      branch: "{{ lookup('env','build_integration_branch')|default('master', true) }}"
+      pull_artifacts:
+      timeout: 1400
+      get_encrypt: true
+      get_bin: true
       parameters:
-        ansible_verbose: "{{ lookup('env','ansible_verbose') }}"
-        RUNNER_TAG: "{{ lookup('env','RUNNER_TAG') }}"
+        GERRIT_REVIEW: "{{ lookup('env','GERRIT_REVIEW') }}"
+        GERRIT_PATCHSET: "{{ lookup('env','GERRIT_PATCHSET') }}"
+        PROJECT: "{{ lookup('env','PROJECT') }}"
 
     cloud-infra:
       stage: infra_install
@@ -69,53 +77,6 @@ gitlab:
         RUNNER_TAG: "{{ lookup('env','RUNNER_TAG') }}"
         USER_ROLE: "{{ lookup('env','USER_ROLE') }}"
 
-    # rke-install:
-    #   stage: virt_install
-    #   api: https://gitlab.devops.telekom.de/api/v4/projects/36232
-    #   url: https://gitlab.devops.telekom.de/tnap/onapcommunity/integrationproject/onapdeployment/rke-install.git
-    #   trigger_token: !vault |
-    #     $ANSIBLE_VAULT;1.1;AES256
-    #     31366336336230663530613735643963626665633538643838353762386364363632393039623165
-    #     3939326531333765393964373431633961656663303933340a393934326464646436313839666662
-    #     33313662333662386362316666316232623364346134646165326562303439373861616162653938
-    #     6130363366366634320a363231326234346530333665353134616435643136353638613332313061
-    #     34353035336437306130396366343566376362366630613233613464663962626539
-    #   branch: "{{ lookup('env','vim_branch')|default('master', true) }}"
-    #   get_artifacts:
-    #   pull_artifacts: "postconfigure"
-    #   timeout: 900
-    #   parameters:
-    #     ANSIBLE_VERBOSE: "{{ lookup('env','ansible_verbose') }}"
-    #     docker_version: "{{ lookup('env','docker_version') }}"
-    #     kubernetes_release: "{{ lookup('env','kubernetes_release') }}"
-    #     kubespray_version: "{{ lookup('env','kubespray_version') }}"
-    #     helm_version: "{{ lookup('env','helm_version') }}"
-    #     RUNNER_TAG: "{{ lookup('env','RUNNER_TAG') }}"
-
-    kubespray-install:
-      stage: virt_install
-      api: https://gitlab.devops.telekom.de/api/v4/projects/36231 #TODO k8s install on onap gitlab
-      url: https://gitlab.devops.telekom.de/tnap/onapcommunity/integrationproject/onapdeployment/k8s-install.git #TODO k8s install on onap gitlab
-      trigger_token: !vault |
-        $ANSIBLE_VAULT;1.1;AES256
-        64376532616532636231396662336134396561643133323432393330623132353063643635336665
-        6232643832343133366333656438616463616336613331320a313166366234356537383639303133
-        64353338653639623034313735653561356362366236636363376431336264653332356134616335
-        3539626665613336350a313035636665383939656461306664386135623139346531343935373763
-        34323539663261363634373031383838376362623135386565643465376661616534
-      branch: "{{ lookup('env','vim_branch')|default('master', true) }}"
-      get_artifacts:
-      pull_artifacts: "postconfigure"
-      timeout: 600
-      get_bin: true
-      parameters:
-        ANSIBLE_VERBOSE: "{{ lookup('env','ansible_verbose') }}"
-        docker_version: "{{ lookup('env','docker_version') }}"
-        kubernetes_release: "{{ lookup('env','kubernetes_release') }}"
-        kubespray_version: "{{ lookup('env','kubespray_version') }}"
-        helm_version: "{{ lookup('env','helm_version') }}"
-        RUNNER_TAG: "{{ lookup('env','RUNNER_TAG') }}"
-
     onap-install:
       stage: apps
       api: https://gitlab.devops.telekom.de/api/v4/projects/36235 #TODO onap oom install on onap gitlab
@@ -171,23 +132,96 @@ gitlab:
         PROJECT: "{{ lookup('env','PROJECT') }}"
         DEBUG: true
         RUNNER_TAG: "{{ lookup('env','RUNNER_TAG') }}"
-        #EXT_NET: "admin-daily-{{ lookup('env','onap-testing_branch')|default('master', true) }}"
 
-    # onap-operate:
-    #   stage: check
-    #   api: https://gitlab.devops.telekom.de/api/v4/projects/36337
-    #   url: https://gitlab.devops.telekom.de/tnap/onapcommunity/labtools/tenantaccess.git
-    #   trigger_token: !vault |
-    #     $ANSIBLE_VAULT;1.1;AES256
-    #     31646539393535313462666661336239336234333436376438333165383264613535323534373665
-    #     3737313238313139613564326639393239333839636531350a323735343565656665386666346237
-    #     39663539393636653739343762613233363862393630336135656633333565393535366561613735
-    #     3934393735383266650a326530636434633163363631316634323966383662623664316331343465
-    #     32636161376133393765633130326134333661666239323835633164316433636431
-    #   branch: "{{ lookup('env','onap-operate_branch')|default('master', true) }}"
-    #   pull_artifacts:
-    #   timeout: 300
-    #   parameters:
-    #     ANSIBLE_VERBOSE: "{{ lookup('env','ansible_verbose') }}"
-    #     RUNNER_TAG: "{{ lookup('env','RUNNER_TAG') }}"
+    kubespray-install:
+      stage: virt_install
+      api: https://gitlab.devops.telekom.de/api/v4/projects/36231 #TODO k8s install on onap gitlab
+      url: https://gitlab.devops.telekom.de/tnap/onapcommunity/integrationproject/onapdeployment/k8s-install.git #TODO k8s install on onap gitlab
+      trigger_token: !vault |
+        $ANSIBLE_VAULT;1.1;AES256
+        64376532616532636231396662336134396561643133323432393330623132353063643635336665
+        6232643832343133366333656438616463616336613331320a313166366234356537383639303133
+        64353338653639623034313735653561356362366236636363376431336264653332356134616335
+        3539626665613336350a313035636665383939656461306664386135623139346531343935373763
+        34323539663261363634373031383838376362623135386565643465376661616534
+      branch: "{{ lookup('env','vim_branch')|default('master', true) }}"
+      get_artifacts:
+      pull_artifacts: "postconfigure"
+      timeout: 600
+      get_bin: true
+      parameters:
+        ANSIBLE_VERBOSE: "{{ lookup('env','ansible_verbose') }}"
+        docker_version: "{{ lookup('env','docker_version') }}"
+        kubernetes_release: "{{ lookup('env','kubernetes_release') }}"
+        kubespray_version: "{{ lookup('env','kubespray_version') }}"
+        helm_version: "{{ lookup('env','helm_version') }}"
+        RUNNER_TAG: "{{ lookup('env','RUNNER_TAG') }}"
+
+    oom:
+      stage: apps
+      api: https://gitlab.com/api/v4/projects/6550110
+      url: https://gitlab.com/Orange-OpenSource/lfn/onap/onap_oom_automatic_installation/
+      trigger_token: !vault |
+        $ANSIBLE_VAULT;1.1;AES256
+        363633373665306138306339646434343366613963393165346661373436633032643430
+        326536646361313061663837633137663134306331346439313638390a38623034643463
+        626666366366633764373132373634626436666132333031303033653133613464363632
+        6363366466396136303232356639623961653637340a3136666438333263636436326463
+        616462646239323066316231346131623237646238393361643634366436356639386533
+        3632353462663933643835656364
+      branch: "{{ lookup('env','oom_deploy_branch')|default('master', true) }}"
+      pull_artifacts: postinstallation
+      timeout: 1400
+      get_encrypt: true
+      get_bin: true
+      parameters:
+        GERRIT_REVIEW: "{{ lookup('env','GERRIT_REVIEW') }}"
+        GERRIT_PATCHSET: "{{ lookup('env','GERRIT_PATCHSET') }}"
+        OOM_BRANCH: "{{ lookup('env','OOM_BRANCH') }}"
+        OOM_GIT_REPO: "{{ lookup('env','OOM_GIT_REPO') }}"
+        OOM_ON_GITLAB: "{{ lookup('env','OOM_ON_GITLAB') }}"
+        OOM_VERSION: "{{ lookup('env','OOM_VERSION') }}"
+        PROJECT: "{{ lookup('env','PROJECT') }}"
+        ANSIBLE_VERBOSE: "{{ lookup('env','ansible_verbose') }}"
+        TEST_RESULT_DB_URL: "http://onap.api.testresults.opnfv.fr/api/v1/results"
 
+    xtesting-onap:
+      stage: check
+      api: https://gitlab.com/api/v4/projects/10614465
+      url: https://gitlab.com/Orange-OpenSource/lfn/onap/xtesting-onap
+      trigger_token: !vault |
+        $ANSIBLE_VAULT;1.1;AES256
+        376564383532616465343061313138373763333833653463333165313062623262303930
+        626531653332333063663134393038623661646430633335393266360a35653732613063
+        333338356136656332323337623534663964653234613836336530303564653463613838
+        3566306635613566373036356135646364613034660a3037323932396165363334616264
+        393938316636316437303261323066326530393363303365623036316463613032343533
+        3234633838343731333166616632
+      branch: "{{ lookup('env','xtesting-onap_branch')|default('master', true) }}"
+      get_artifacts: vim
+      pull_artifacts:
+      timeout: 1400
+      get_encrypt: true
+      get_bin: true
+      parameters:
+        GERRIT_REVIEW: "{{ lookup('env','GERRIT_REVIEW') }}"
+        GERRIT_PATCHSET: "{{ lookup('env','GERRIT_PATCHSET') }}"
+        EXPERIMENTAL: "{{ lookup('env','EXPERIMENTAL') }}"
+        ONAP_VERSION: "{{ lookup('env','OOM_BRANCH') }}"
+        DEPLOY_SCENARIO: os-nosdn-nofeature-ha
+        TEST_RESULT_DB_URL: "http://testresults.opnfv.org/onap/api/v1/results"
+        PROJECT: "{{ lookup('env','PROJECT') }}"
+
+    trigger:
+      stage: apps
+      trigger_token: !vault |
+        $ANSIBLE_VAULT;1.1;AES256
+        64386138616464653132353964363032346464373363323366616436346263323230353961363263
+        3562653664303631323134313864393364636538643430640a363766316230633932376466643333
+        64386331633737623164313831633537666638623534663736313331313266396438306266636632
+        3532313263396532300a306661393438613734323064313064343361363763636664393231363934
+        37633335396563623462653935393236356139303864646135303935373937623739
+      branch: "{{ lookup('env','CI_BUILD_REF_NAME')|default('master', true) }}"
+      parameters:
+        ansible_verbose: "{{ lookup('env','ansible_verbose') }}"
+        RUNNER_TAG: "{{ lookup('env','RUNNER_TAG') }}"
diff --git a/pod_inventory/host_vars/onap_oom_gating_azure_3.yml b/pod_inventory/host_vars/onap_oom_gating_azure_3.yml
new file mode 100644 (file)
index 0000000..545f0bb
--- /dev/null
@@ -0,0 +1,85 @@
+---
+jumphost:
+  server: rebond.francecentral.cloudapp.azure.com
+  user: !vault |
+    $ANSIBLE_VAULT;1.1;AES256
+    3530396436393930316538613463623538376563633364623938623136353961393336326538
+    39363265373139623432353864306265666437663433363338630a3638386530613233326439
+    3932303435363761346338373534613363336633616537636361396466376435626335333462
+    3234633631396331323639393435370a35646638343365616234383835303932323734613739
+    61313533373262363530
+environment: azure/onap_gating_3/oom_gating
+scenario_steps:
+  config:
+    project: config
+    get_artifacts:
+      - name: azure
+        static_src: true
+    infra: az14-gating3
+    ssh_access: azure.eyml
+  build_integration:
+    project: build_integration
+    get_artifacts: config
+    only:
+      - "PROJECT == testsuite/pythonsdk-tests"
+  onap_deploy:
+    branch: add_strimzi_chartmuseum
+    extra_parameters:
+      CLEAN: True
+      WORKAROUND: False
+      ONAP_REPOSITORY: docker.nexus.azure.onap.eu
+      ONAP_FLAVOR: unlimited
+      SERVICEMESH: true
+      INGRESS: true
+      DOCKER_HUB_PROXY: docker.nexus.azure.onap.eu
+      ELASTIC_PROXY: docker.nexus.azure.onap.eu
+      K8S_GCR_PROXY: docker.nexus.azure.onap.eu
+      GATHER_NODE_FACTS: False
+      METRICS_CRD: True
+    get_artifacts:
+      - name: vim_deploy:onap_oom_gating_k8s_azure_3
+        in_pipeline: false
+        limit_to:
+          - vars/clouds.yaml: vars/user_cloud.yml
+      - name: k8s_deploy:onap_oom_gating_k8s_azure_3
+        in_pipeline: false
+        limit_to:
+          - inventory/infra: inventory/infra
+          - vars/pdf.yml: vars/pdf.yml
+          - vars/idf.yml: vars/idf.yml
+      - name: config
+        limit_to:
+          - vars/ssh_gateways.yml: vars/ssh_gateways.yml
+          - vars/vaulted_ssh_credentials.yml: vars/vaulted_ssh_credentials.yml
+    project: oom
+  onap_test:
+    project: xtesting-onap
+    branch: master
+    get_artifacts:
+      - name: k8s_deploy:onap_oom_gating_k8s_azure_3
+        in_pipeline: false
+        limit_to:
+          - vars/kube-config: vars/kube-config
+          - inventory/infra: inventory/infra
+          - vars/pdf.yml: vars/pdf.yml
+      - name: config
+        limit_to:
+          - vars/ssh_gateways.yml: vars/ssh_gateways.yml
+          - vars/vaulted_ssh_credentials.yml: vars/vaulted_ssh_credentials.yml
+      - name: onap_deploy
+        limit_to:
+          - vars/cluster.yml: vars/cluster.yml
+    extra_parameters:
+      DEPLOYMENT: oom
+      INFRA_DEPLOYMENT: aks
+      DEPLOYMENT_TYPE: full
+      DEPLOY_SCENARIO: onap-ftw
+      INGRESS: true
+      POD: azure-gating-3
+      OS_TEST_CLOUD: admin
+      TEST_RESULT_DB_URL: http://testresults.opnfv.org/onap/api/v1/results
+      S3_ENDPOINT_URL: http://minio.azure3.onap.eu:9000
+      S3_INTERNAL_ENDPOINT_URL: http://minio.minio:9000
+      S3_HTTP_DST_URL: http://minio.minio:8181
+      RANDOM_WAIT: True
+
index 34b2e74..c5c627d 100644 (file)
@@ -1,2 +1,4 @@
 [LF-UNH]
-onap-daily-unh-oom-master
\ No newline at end of file
+onap-daily-unh-oom-master
+[LF-GATING]
+onap_oom_gating_azure_3
\ No newline at end of file
diff --git a/roles/LICENSE b/roles/LICENSE
new file mode 100644 (file)
index 0000000..3be62f8
--- /dev/null
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2018 Orange-OpenSource / lfn / ci_cd
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/roles/README.md b/roles/README.md
new file mode 100644 (file)
index 0000000..9002afc
--- /dev/null
@@ -0,0 +1,15 @@
+Chained-CI-roles
+==========
+
+Role
+----
+Chained-CI is a way to run a set of projects, each one as a job in a top level
+pipeline.
+
+This project, running can run on a gitlab CE, is triggering configured projects
+one after the other, or in parallele, sharing configuration through artifacts.
+This allow to integrate projects managed by third parties, or running together
+independant projects.
+
+___This project is hosting the roles needed to run the pipelines. The running
+project hosting pipelines and the inventory is not yet public___
diff --git a/roles/artifact_init/defaults/main.yaml b/roles/artifact_init/defaults/main.yaml
new file mode 100644 (file)
index 0000000..c1ccbb9
--- /dev/null
@@ -0,0 +1,2 @@
+---
+step: "{{ lookup('env', 'CONFIG_NAME') | default('config', true )}}"
diff --git a/roles/artifact_init/filter_plugins/filters.py b/roles/artifact_init/filter_plugins/filters.py
new file mode 100644 (file)
index 0000000..db38fc6
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+
+sys.path.append(os.path.normpath(os.path.join(os.path.dirname(__file__),'../../')))
+
+from library.filepath import FilterModule
diff --git a/roles/artifact_init/tasks/main.yml b/roles/artifact_init/tasks/main.yml
new file mode 100644 (file)
index 0000000..e4e4fb6
--- /dev/null
@@ -0,0 +1,180 @@
+---
+##
+# Warn if log level is high
+##
+- name: Warn if log level is high
+  debug:
+    msg: "{{ msg.split('\n') }}"
+    verbosity: 3
+  vars:
+    msg: |
+      !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+      !! Log level is HIGH  !                                                 !!
+      !! Some sensitive data may be visible to everyone.                      !!
+      !! Don't forget to clean the task output !                              !!
+      !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+##
+# get the config
+##
+
+- name: get artifact_src if we refer to a previous one
+  when: artifacts_src is defined
+  uri:
+    url: "{{ artifacts_src }}"
+    headers:
+      PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+    dest: "{{ playbook_dir }}/artifacts.zip"
+
+- name: unzip get_artifact archive
+  when: artifacts_src is defined or artifacts_bin is defined
+  unarchive:
+    src: "{{ playbook_dir }}/artifacts.zip"
+    dest: "{{ playbook_dir }}"
+    remote_src: "yes"
+
+- name: delete archive
+  file:
+    path: "{{ playbook_dir }}/artifacts.zip"
+    state: absent
+
+- name: create artifacts folders
+  file:
+    path: "{{ item }}"
+    state: directory
+    mode: 0775
+  when: item[-1] == '/'
+  with_items: "{{ vars[lookup( 'env', 'CI_JOB_NAME')].artifacts.paths }}"
+
+- name: ensure configs can be written
+  file:
+    path: "{{ playbook_dir }}/{{ item }}"
+    mode: 0660
+  ignore_errors: true
+  with_items:
+    - vars/pdf.yml
+    - vars/idf.yml
+    - vars/certificates.yml
+    - vars/vaulted_ssh_credentials.yml
+    - vars/ssh_gateways.yml
+
+- name: get the infra config name
+  set_fact:
+    infra_config: "{{ config.infra | default(inventory_hostname) }}"
+
+- name: get the infra PDF/IDF
+  when: infra_config != 'NONE'
+  block:
+    - name: get PDF configs
+      uri:
+        url: >-
+          {{ config.api }}/repository/files/{{
+          [config.path | default(''), 'config'] |
+          filepath(infra_config, '.yaml') }}?ref={{ config.branch }}
+        headers:
+          PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+        status_code: 200
+        return_content: yes
+      register: pdf_get
+
+    - name: save PDF config
+      copy:
+        content: "{{ pdf_get.json.content | b64decode }}"
+        dest: "{{ playbook_dir }}/vars/pdf.yml"
+        force: true
+        mode: 0660
+        decrypt: false
+
+    - name: get IDF configs
+      uri:
+        url: >-
+          {{ config.api }}/repository/files/{{ [config.path | default(''),
+          'config'] | filepath('idf-', infra_config, '.yaml')
+          }}?ref={{ config.branch }}
+        headers:
+          PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+        status_code: 200
+        return_content: yes
+      register: idf_get
+
+    - name: save IDF config
+      copy:
+        content: "{{ idf_get.json.content | b64decode }}"
+        dest: "{{ playbook_dir }}/vars/idf.yml"
+        force: true
+        mode: 0660
+        decrypt: false
+
+- name: get certificate
+  uri:
+    url: >-
+      {{ config.api }}/repository/files/{{
+      [config.path | default(''), 'certificats']
+      | filepath(config.certificates) }}?ref={{ config.branch }}
+    headers:
+      PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+    status_code: 200
+    return_content: yes
+  register: certs_get
+  when: config.certificates is defined
+
+- name: save certificate
+  copy:
+    content: "{{ certs_get.json.content | b64decode }}"
+    dest: "{{ playbook_dir }}/vars/certificates.yml"
+    force: true
+    mode: 0660
+    decrypt: false
+  when: config.certificates is defined
+
+- name: get ssh credentials
+  uri:
+    url: >-
+      {{ config.api }}/repository/files/{{
+      [config.path | default(''), 'ssh_creds'] |
+      filepath(config.ssh_creds | default(ansible_ssh_creds))
+      }}?ref={{ config.branch }}
+    headers:
+      PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+    status_code: 200
+    return_content: yes
+  register: ssh_creds_get
+  when: config.ansible_ssh_creds is defined or ansible_ssh_creds is defined
+
+- name: save ssh credentials
+  copy:
+    content: "{{ ssh_creds_get.json.content | b64decode }}"
+    dest: "{{ playbook_dir }}/vars/vaulted_ssh_credentials.yml"
+    force: true
+    mode: 0660
+    decrypt: false
+  when: config.ansible_ssh_creds is defined or ansible_ssh_creds is defined
+
+- name: set ssh gateways config
+  uri:
+    url: >-
+      {{ config.api }}/repository/files/{{
+      [config.path | default(''), 'config/ssh_gateways']
+      | filepath(config.ssh_access) }}?ref={{ config.branch }}
+    headers:
+      PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+    status_code: 200
+    return_content: yes
+  register: ssh_gw_get
+  when: config.ssh_access is defined
+
+- name: save ssh gateways config
+  copy:
+    content: "{{ ssh_gw_get.json.content | b64decode }}"
+    dest: "{{ playbook_dir }}/vars/ssh_gateways.yml"
+    force: true
+    mode: 0660
+    decrypt: false
+  when: config.ssh_access is defined
+
+- name: set basic inventory
+  copy:
+    dest: "{{ playbook_dir }}/inventory/inventory"
+    content: >
+      jumphost ansible_host={{ jumphost.server }}
+      ansible_user={{ jumphost.user }} pod={{ inventory_hostname }}
diff --git a/roles/get_artifacts/defaults/main.yml b/roles/get_artifacts/defaults/main.yml
new file mode 100644 (file)
index 0000000..112aa4a
--- /dev/null
@@ -0,0 +1,7 @@
+---
+previous_artifacts_folder: "{{ playbook_dir }}/previous_artifacts"
+final_artifacts_folder: "{{  playbook_dir }}/FINAL_ARTIFACT"
+
+job_id_fetch:
+  max_page: 100
+  per_page: 100
diff --git a/roles/get_artifacts/filter_plugins/filters.py b/roles/get_artifacts/filter_plugins/filters.py
new file mode 100644 (file)
index 0000000..db38fc6
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env python3
+
+import os
+import sys
+
+sys.path.append(os.path.normpath(os.path.join(os.path.dirname(__file__),'../../')))
+
+from library.filepath import FilterModule
diff --git a/roles/get_artifacts/tasks/binary.yml b/roles/get_artifacts/tasks/binary.yml
new file mode 100644 (file)
index 0000000..99ba930
--- /dev/null
@@ -0,0 +1,244 @@
+---
+##
+# Handle different get_artifacts types
+##
+- name: value change for coherency
+  set_fact:
+    config: >-
+      {{ config|combine({'get_artifacts': [] }) }}
+  when: config.get_artifacts is not defined
+- name: value change for coherency
+  set_fact:
+    config: >-
+      {{ config|combine({'get_artifacts':
+            [{ 'name': config.get_artifacts }] }) }}
+  when: config.get_artifacts is string
+
+- debug:
+    var: config
+    verbosity: 3
+##
+# Prepare a folder for
+##
+
+- name: set previous_artifacts_folder
+  file:
+    path: "{{ item }}"
+    state: directory
+  loop:
+    - "{{ previous_artifacts_folder }}"
+    - "{{ final_artifacts_folder }}"
+
+- name: create dest folders for the jobs artifacts
+  file:
+    path: "{{ previous_artifacts_folder }}/{{ item.name }}"
+    state: directory
+  loop: "{{ config.get_artifacts }}"
+  loop_control:
+    label: "{{ item.name }}"
+
+##
+# Get all artifacts job ids
+##
+- name: loop on get_artifacts
+  include_tasks: get_one_artifact.yml
+  vars:
+    artifact_job_name: "{{ item.name }}"
+    artifact_in_pipeline: "{{ item.in_pipeline | default(true) }}"
+  when: not (item.static_src | default(false))
+  loop: "{{ config.get_artifacts }}"
+  loop_control:
+    label: "{{ artifact_job_name }}"
+
+- name: download all job artifacts
+  uri:
+    url: >-
+      {{ gitlab.api_url }}/projects/{{ lookup('env', 'CI_PROJECT_ID')
+      }}/jobs/{{ artifact_job_ids[idx] }}/artifacts
+    headers:
+      PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+    status_code: 200
+    dest: >-
+      {{ previous_artifacts_folder }}/{{ item.name }}/artifacts.zip
+  when: not (item.static_src | default(false))
+  loop: "{{ config.get_artifacts }}"
+  loop_control:
+    index_var: idx
+    label: "{{ item.name }}"
+
+- name: download all static artifacts on public projects
+  uri:
+    url: >-
+      {{ config.url }}/raw/{{ config.branch }}/{{
+      config.path | default('') }}/config/artifacts/{{
+      item.name }}.zip?inline=false
+    status_code: 200
+    dest: >-
+      {{ previous_artifacts_folder }}/{{ item.name }}/artifacts.zip
+  when: (item.static_src | default(false)) and (config.api is not defined)
+  loop: "{{ config.get_artifacts }}"
+  loop_control:
+    label: "{{ item.name }}"
+
+- name: download all static artifacts using api
+  uri:
+    url: >-
+      {{ config.api }}/repository/files/{{
+      [config.path | default('') , 'config/artifacts'] |
+      filepath(item.name, '.zip')
+      }}/raw?ref={{ config.branch }}
+    headers:
+      PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+    status_code: 200
+    dest: >-
+      {{ previous_artifacts_folder }}/{{ item.name }}/artifacts.zip
+  when: (item.static_src | default(false)) and (config.api is defined)
+  loop: "{{ config.get_artifacts }}"
+  loop_control:
+    label: "{{ item.name }}"
+
+- name: unarchive all artifacts
+  unarchive:
+    src: "{{ previous_artifacts_folder }}/{{ item.name }}/artifacts.zip"
+    dest: "{{ previous_artifacts_folder }}/{{ item.name }}/"
+    remote_src: "yes"
+  loop: "{{ config.get_artifacts }}"
+  loop_control:
+    label: "{{ item.name }}"
+
+- name: remove all artifacts archives
+  file:
+    path: "{{ previous_artifacts_folder }}/{{ item.name }}/artifacts.zip"
+    state: absent
+  loop: "{{ config.get_artifacts }}"
+  loop_control:
+    label: "{{ item.name }}"
+
+- name: create artifacts folders
+  file:
+    path: "{{ final_artifacts_folder }}/{{ item }}"
+    state: directory
+    recurse: true
+    mode: 0775
+  when: item[-1] == '/'
+  with_items: "{{ vars['.artifacts_root'].paths }}"
+
+- name: copy all files if no filters
+  copy:
+    decrypt: false
+    src: "{{ previous_artifacts_folder }}/{{ item.name }}/"
+    dest: "{{ final_artifacts_folder }}/"
+  when: item.limit_to is not defined or item.limit_to == None
+  loop: "{{ config.get_artifacts }}"
+  loop_control:
+    label: "{{ item.name }}"
+
+- name: copy filtered files if filters
+  include_tasks: limit_to.yml
+  when: item.limit_to is defined
+  loop: "{{ config.get_artifacts }}"
+  vars:
+    job_name: "{{ item.name }}"
+    limit_to: "{{ item.limit_to }}"
+  loop_control:
+    label: "{{ item.name }}"
+
+##
+# get list of files to archive
+##
+- name: get list of files to encrypt
+  find:
+    paths: "{{ final_artifacts_folder }}"
+    recurse: true
+  register: artifacts_files
+
+- name: set file list
+  set_fact:
+    files_list: "{{ artifacts_files.files | map(attribute='path')| list }}"
+
+##
+# If we encode file via ansible vault
+##
+- name: encrypt files
+  shell: >
+    ansible-vault encrypt --vault-password-file {{
+    lookup( 'env', 'VAULT_FILE') }} {{ item }}
+  register: res
+  loop: "{{ files_list }}"
+  failed_when:
+    res.rc == 1 and res.stderr != "ERROR! input is already encrypted"
+  when:
+    config.get_encrypt is defined and (config.get_encrypt | bool)
+
+
+##
+# Add ssh_gateways file if needed
+##
+
+- name: get config step parameters
+  set_fact:
+    config_step: >-
+      {{ gitlab.git_projects[
+           hostvars[inventory_hostname].scenario_steps['config'].project] |
+         combine(hostvars[inventory_hostname].scenario_steps['config']) }}
+
+- name: get ssh gateways config
+  uri:
+    url: >-
+      {{ config_step.api }}/repository/files/{{
+      [config_step.path | default(''), 'config/ssh_gateways'] |
+      filepath(config.ssh_access)
+      }}?ref={{ config_step.branch }}
+    headers:
+      PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+    status_code: 200
+    return_content: yes
+  register: ssh_gw_get
+  when: config.ssh_access is defined
+
+- name: save ssh gateways config
+  copy:
+    content: "{{ ssh_gw_get.json.content | b64decode }}"
+    dest: "{{ final_artifacts_folder }}/vars/ssh_gateways.yml"
+    force: true
+    mode: 0660
+  when: config.ssh_access is defined
+
+##
+# get list of files and folders to archive
+##
+- name: set file list
+  set_fact:
+    arch_files:
+      "{{ (arch_files | default([])) +
+          [ final_artifacts_folder + '/' + item ] }}"
+  loop: "{{ vars['.artifacts_root'].paths }}"
+
+- name: Prepare artifact archive for binary transmission
+  archive:
+    path: "{{ arch_files }}"
+    dest: "{{ playbook_dir }}/artifacts.zip"
+    format: zip
+
+##
+# Set the artifact to send
+##
+- name: "Prepare artifact archive for binary transmission"
+  slurp:
+    src: artifacts.zip
+  register: slurped_artifact
+
+- name: Add artifacts bin if requested
+  set_fact:
+    artifacts_bin: "{{ slurped_artifact.content }}"
+
+##
+# Clean
+##
+- name: delete temporary folders
+  file:
+    path: "{{ item }}"
+    state: absent
+  loop:
+    - "{{ previous_artifacts_folder }}"
+    - "{{ final_artifacts_folder }}"
diff --git a/roles/get_artifacts/tasks/get_one_artifact.yml b/roles/get_artifacts/tasks/get_one_artifact.yml
new file mode 100644 (file)
index 0000000..ccbdc48
--- /dev/null
@@ -0,0 +1,49 @@
+---
+##
+# Search for a job id
+#    with name: artifact_job_name
+#    limit to pipeline if artifact_in_pipeline (default: true)
+##
+
+- name: set empty fact for job
+  set_fact:
+    job: {}
+    artifact_in_pipeline: "{{ artifact_in_pipeline | default(true) }}"
+
+- name: get job id in this pipeline
+  when: artifact_in_pipeline | bool
+  block:
+    - name: "Get job successful job ids of the pipeline"
+      uri:
+        url: >-
+          {{ gitlab.api_url }}/projects/{{
+          lookup( 'env', 'CI_PROJECT_ID') }}/pipelines/{{
+          lookup( 'env', 'CI_PIPELINE_ID') }}/jobs?scope[]=success
+        method: GET
+        headers:
+          PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+      register: pipeline_success_jobs
+    - name: get the job id
+      set_fact:
+        job: >-
+          {{ { 'id':
+                  pipeline_success_jobs.json |json_query(
+                    '[?name==`'+ artifact_job_name + ':' +
+                    inventory_hostname +'`].id') | last
+              } }}
+
+- name: fetch the job id corresponding to get_artifact value if not in pipeline
+  include_tasks: job_id_fetch.yml
+  loop: "{{ range(0, job_id_fetch.max_page)| list }}"
+  when: not (artifact_in_pipeline | bool )
+  loop_control:
+    loop_var: page
+
+- name: check we found an artifact job id
+  fail:
+    msg: 'We can not found a correct job id'
+  when: job.id is not defined
+
+- name: get last successful job id
+  set_fact:
+    artifact_job_ids: "{{ (artifact_job_ids|default([])) + [job.id] }}"
diff --git a/roles/get_artifacts/tasks/job_id_fetch.yml b/roles/get_artifacts/tasks/job_id_fetch.yml
new file mode 100644 (file)
index 0000000..cab4bcb
--- /dev/null
@@ -0,0 +1,20 @@
+---
+
+- block:
+  - name: "Get successful job ids if artifact fetching"
+    uri:
+      url: >-
+        {{ gitlab.api_url }}/projects/{{ lookup( 'env', 'CI_PROJECT_ID')
+        }}/jobs?scope[]=success&per_page={{ job_id_fetch.per_page
+        }}&page={{ page }}
+      method: GET
+      headers:
+        PRIVATE-TOKEN: "{{ gitlab.private_token }}"
+    register: successful_jobs
+  - name: save successful job
+    set_fact:
+      job: >-
+        {{ successful_jobs.json|
+           selectattr('name', 'equalto', artifact_job_name)| list |
+           first | default({}) }}
+  when: job.id is not defined
diff --git a/roles/get_artifacts/tasks/limit_to.yml b/roles/get_artifacts/tasks/limit_to.yml
new file mode 100644 (file)
index 0000000..2e1b782
--- /dev/null
@@ -0,0 +1,20 @@
+---
+
+- debug:
+    var: limit_to
+    verbosity: 3
+- debug:
+    var: job_name
+    verbosity: 3
+- name: copy all files if filters and rename if needed
+  copy:
+    decrypt: false
+    src: "{{ previous_artifacts_folder }}/{{ job_name }}/{{ original }}"
+    dest: "{{ final_artifacts_folder }}/{{ renamed }}"
+  loop: "{{ limit_to }}"
+  vars:
+    original: "{{ file.keys()|first }}"
+    renamed: "{{ file.values()|first }}"
+  loop_control:
+    loop_var: file
+    label: "{{ original }}"
diff --git a/roles/get_artifacts/tasks/main.yml b/roles/get_artifacts/tasks/main.yml
new file mode 100644 (file)
index 0000000..605521c
--- /dev/null
@@ -0,0 +1,34 @@
+---
+##
+# Check config is prepared
+##
+- name: check 'step' is set
+  fail:
+    msg: 'Prepare role must be run before'
+  when: config is not defined
+
+
+- name: recover previous artifacts
+  when:
+    config.get_artifacts is defined and
+    config.get_artifacts
+  block:
+    ##
+    # If we get previous artifacts via url
+    ##
+    - name: Add artifacts via source
+      include_tasks: url.yml
+      when:
+        (config.get_bin is not defined or not (config.get_bin | bool))
+        and (config.ssh_access is not defined)
+        and (config.get_artifacts is string)
+
+    ##
+    # If we get previous artifacts via url
+    ##
+    - name: Add artifacts via binary
+      include_tasks: binary.yml
+      when:
+        (config.get_bin is defined and (config.get_bin | bool))
+        or (config.ssh_access is defined)
+        or (config.get_artifacts is not string)
diff --git a/roles/get_artifacts/tasks/url.yml b/roles/get_artifacts/tasks/url.yml
new file mode 100644 (file)
index 0000000..a2b5a91
--- /dev/null
@@ -0,0 +1,13 @@
+---
+
+- name: get_artifacts with just one value
+  include_tasks: get_one_artifact.yml
+  vars:
+    artifact_job_name: "{{ config.get_artifacts }}"
+
+- name: get the url of the artifact
+  set_fact:
+    artifacts_src: >-
+      {{ gitlab.api_url }}/projects/{{
+      lookup( 'env', 'CI_PROJECT_ID') }}/jobs/{{
+      artifact_job_ids[0] }}/artifacts
diff --git a/roles/gitlab-ci-generator/defaults/main.yml b/roles/gitlab-ci-generator/defaults/main.yml
new file mode 100644 (file)
index 0000000..11b8726
--- /dev/null
@@ -0,0 +1,3 @@
+---
+ci_file: "{{ lookup('env', 'CI_FILE')
+             | default(playbook_dir +'/.gitlab-ci.yml', true)}}"
diff --git a/roles/gitlab-ci-generator/tasks/main.yml b/roles/gitlab-ci-generator/tasks/main.yml
new file mode 100644 (file)
index 0000000..a96ae7c
--- /dev/null
@@ -0,0 +1,45 @@
+---
+##
+# Warn if log level is high
+##
+- name: Warn if log level is high
+  debug:
+    msg: "{{ msg.split('\n') }}"
+    verbosity: 3
+  vars:
+    msg: |
+      !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+      !! Log level is HIGH  !                                                 !!
+      !! Some sensitive data may be visible to everyone.                      !!
+      !! Don't forget to clean the task output !                              !!
+      !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+##
+# Generate the CI file
+##
+- name: generate the new gitlab-ci file from inventory
+  run_once: true
+  block:
+    - name: create a tempfile
+      tempfile:
+        state: file
+        suffix: temp
+      register: tmp_file
+    - copy:
+        src: "{{ ci_file }}"
+        dest: "{{ tmp_file.path }}"
+      ignore_errors: true
+    - name: generate the gitlab-ci.yml
+      template:
+        src: gitlab-ci.yml
+        dest: "{{ ci_file }}"
+  rescue:
+    - name: restore gitlab-ci
+      copy:
+        src: "{{ tmp_file.path }}"
+        dest: "{{ ci_file }}"
+  always:
+    - name: destroy temp file
+      file:
+        path: "{{ tmp_file.path }}"
+        state: absent
diff --git a/roles/gitlab-ci-generator/templates/gitlab-ci.yml b/roles/gitlab-ci-generator/templates/gitlab-ci.yml
new file mode 100644 (file)
index 0000000..51ceb05
--- /dev/null
@@ -0,0 +1,204 @@
+---
+################################################################################
+#
+# !! DO NOT EDIT MANUALLY !!
+#
+# This file is generated by gitlab-ci-generator
+#
+################################################################################
+
+stages:
+{% for stage in stages %}
+  - {{ stage }}
+{% endfor %}
+
+variables:
+  GIT_SUBMODULE_STRATEGY: recursive
+  VAULT_FILE: .vault
+
+################################################################################
+# Shared parameters
+################################################################################
+.runner_tags: &runner_tags
+  tags:
+{% for tag in runner.tags %}
+    - {{ tag }}
+{% endfor %}
+
+.syntax_checking: &syntax_checking
+  only:
+    - pushes
+  stage: lint
+
+.artifacts_root: &artifacts_root
+  name: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
+  paths:
+    - vars/
+    - inventory/
+
+.artifacts: &artifacts
+  artifacts:
+    <<: *artifacts_root
+    expire_in: 15 days
+
+.artifacts_longexpire: &artifacts_longexpire
+  artifacts:
+    <<: *artifacts_root
+    expire_in: 1 yrs
+
+.runner_env: &runner_env
+{% for var_name, var_value in runner.env_vars.items()|default({'foo': 'bar'}) %}
+  {{ var_name }}: "{{ var_value }}"
+{% endfor %}
+
+################################################################################
+# Linting
+################################################################################
+
+yaml_checking:
+  <<: *syntax_checking
+  <<: *runner_tags
+  variables:
+    <<: *runner_env
+  image: {{ runner.docker_proxy }}sdesbure/yamllint:latest
+  script:
+    - >
+      yamllint -d "line-length: {
+      max: 80,
+      allow-non-breakable-words: true,
+      allow-non-breakable-inline-mappings: true}"
+      .gitlab-ci.yml
+    - yamllint *.yml
+
+ansible_linting:
+  <<: *syntax_checking
+  <<: *runner_tags
+  variables:
+    <<: *runner_env
+  image: {{ runner.docker_proxy }}sdesbure/ansible-lint:latest
+  script:
+    - ansible-lint -x ANSIBLE0010,ANSIBLE0013 run-ci.yml
+
+{% if not (disable_pages | default(false)) %}
+################################################################################
+# Pages
+################################################################################
+
+pages:
+  image: {{ runner.docker_proxy }}{{ runner.image }}:{{ runner.image_tag }}
+  stage: lint
+  <<: *runner_tags
+  variables:
+    <<: *runner_env
+  script:
+    - ./chained-ci-vue/init.sh ./pod_inventory
+  artifacts:
+    paths:
+      - public
+  only:
+    - master
+  except:
+    - triggers
+    - api
+    - external
+    - pipelines
+    - schedules
+    - web
+
+{% endif %}
+
+################################################################################
+# Jobs
+################################################################################
+
+.vault_mgmt: &vault_mgmt
+  before_script:
+    - echo ${ANSIBLE_VAULT_PASSWORD} > ${PWD}/${VAULT_FILE}
+  after_script:
+    - rm -f $PWD/.vault
+
+.set_config: &set_config
+  <<: *runner_tags
+  <<: *vault_mgmt
+  image: {{ runner.docker_proxy }}{{ runner.image }}:{{ runner.image_tag }}
+  script:
+    - >
+      ansible-playbook -i pod_inventory/inventory --limit ${pod}
+      --vault-password-file ${PWD}/${VAULT_FILE}
+      ${ansible_verbose} artifacts_init.yml
+
+.run_ci: &run_ci
+  <<: *runner_tags
+  <<: *vault_mgmt
+  image: {{ runner.docker_proxy }}{{ runner.image }}:{{ runner.image_tag }}
+  script:
+    - >
+      ansible-playbook -i pod_inventory/inventory --limit ${pod}
+      --extra-vars "step=${CI_JOB_NAME%:*}"
+      --vault-password-file ${PWD}/${VAULT_FILE}
+      ${ansible_verbose} run-ci.yml
+
+.trigger: &trigger
+  <<: *runner_tags
+  <<: *vault_mgmt
+  image: {{ runner.docker_proxy }}{{ runner.image }}:{{ runner.image_tag }}
+  script:
+    - >
+      ansible-playbook -i pod_inventory/inventory --limit ${pod}
+      --vault-password-file ${PWD}/${VAULT_FILE}
+      ${ansible_verbose} --extra-vars "step=trigger" trigger_myself.yml
+
+{% for pipeline in groups['all'] %}
+################################################################################
+# {{ pipeline }}
+################################################################################
+
+.{{ pipeline }}_global: &{{ pipeline }}_global
+  variables:
+    pod: {{ pipeline }}
+    <<: *runner_env
+{% if hostvars[pipeline].environment is defined %}
+  environment:
+    name: {{ hostvars[pipeline].environment }}
+{% endif %}
+  only:
+    variables:
+      - $POD == "{{ pipeline }}"
+{% if hostvars[pipeline].inpod is defined %}
+      - $INPOD == "{{ hostvars[pipeline].inpod }}"
+{% endif %}
+    refs:
+      - web
+      - schedules
+      - triggers
+
+{% for stage in stages %}
+{% for task in hostvars[pipeline].scenario_steps %}
+{% if hostvars[pipeline].scenario_steps[task].stage | default(
+    gitlab.git_projects[hostvars[pipeline].scenario_steps[task].project].stage
+  ) == stage %}
+{{ task }}:{{ pipeline }}:
+  stage: {{ stage }}
+  <<: *{{ pipeline }}_global
+{% if hostvars[pipeline].scenario_steps[task].project == 'config' %}
+  <<: *set_config
+{% elif hostvars[pipeline].scenario_steps[task].project == 'trigger' %}
+  <<: *trigger
+{% else %}
+  <<: *run_ci
+{% endif %}
+{% if (hostvars[pipeline].scenario_steps[task].pull_artifacts
+    | default(gitlab.git_projects[hostvars[pipeline].scenario_steps[task].project].pull_artifacts)
+    | default(false))
+    or task == 'config' %}
+  <<: *artifacts{% if hostvars[pipeline].longlife_artifact | default(false) | bool %}_longexpire{% endif %}
+
+{% endif %}
+{% endif %}
+{% endfor %}
+{% endfor %}
+
+{% endfor %}
+##
+# End of generated file
+##
diff --git a/roles/library/filepath.py b/roles/library/filepath.py
new file mode 100644 (file)
index 0000000..356f5fb
--- /dev/null
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+import os
+from urllib.parse import quote
+
+class FilterModule(object):
+    def filters(self):
+        return {
+            'filepath': self.filepath
+        }
+
+    def filepath(self, path, *filename):
+        #
+        # path: a string or a list of string that contains successive parts
+        #       of the path. Nul or empty parts are removed
+        # filename: the optionnal filename to be used after the path. It may
+        #       be specified using multiple args to be concatenate (useful
+        #       when building dynamic names in ansible/jinja templates)
+        #
+        '''build a gitlab filepath given `path' and `filename'.'''
+
+        if path is not None:
+            if not isinstance(path, list):
+                path = [path]
+            path = list(filter(None, path))
+            if path:
+                path = os.path.normpath(os.path.join(path[0], *path[1:]))
+
+        if filename:
+            filename = ''.join(list(filter(None, filename)))
+
+        if path and filename:
+            path = os.path.join(path, filename)
+        elif filename:
+            path = filename
+
+        if path:
+            return quote(path, safe='')
+
+        return None
diff --git a/roles/logo.png b/roles/logo.png
new file mode 100644 (file)
index 0000000..1fc981e
Binary files /dev/null and b/roles/logo.png differ
diff --git a/roles/logo.svg b/roles/logo.svg
new file mode 100644 (file)
index 0000000..b2e6875
--- /dev/null
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="19.586046mm"
+   height="19.586046mm"
+   viewBox="0 0 19.586046 19.586046"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)"
+   sodipodi:docname="logo.svg"
+   inkscape:export-filename="/home/edby8475/Dev/chained-ci-roles/logo.png"
+   inkscape:export-xdpi="98"
+   inkscape:export-ydpi="98">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="6.5333333"
+     inkscape:cx="7.8188803"
+     inkscape:cy="30.617961"
+     inkscape:document-units="mm"
+     inkscape:current-layer="g933"
+     showgrid="false"
+     inkscape:window-width="2560"
+     inkscape:window-height="1403"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Calque 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-76.099242,-143.22085)">
+    <circle
+       style="opacity:1;vector-effect:none;fill:#4d4d4d;fill-opacity:1;stroke:#4d4d4d;stroke-width:0.50260705;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+       id="path921"
+       cx="85.892265"
+       cy="153.01387"
+       r="9.5417194" />
+    <g
+       id="g933">
+      <path
+         sodipodi:nodetypes="cccc"
+         inkscape:connector-curvature="0"
+         id="path919"
+         d="m 87.755086,149.65335 h -3.722685 c 3.722685,0 0.03786,6.84943 3.737003,6.84401 v 0"
+         style="fill:none;stroke:#f2f2f2;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <g
+         transform="translate(-2.1166667)"
+         id="g862">
+        <circle
+           style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#44aa00;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+           id="path848"
+           cx="83.371094"
+           cy="149.57428"
+           r="2.7487407" />
+        <path
+           style="fill:none;stroke:#44aa00;stroke-width:0.62900001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+           d="m 82.386279,149.28284 0.715902,0.81612 1.267143,-1.24567"
+           id="path850"
+           inkscape:connector-curvature="0" />
+      </g>
+      <g
+         transform="translate(-0.03559777,6.879167)"
+         id="g858">
+        <circle
+           r="2.7487407"
+           cy="149.57428"
+           cx="90.565697"
+           id="circle852"
+           style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#2a7fff;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+        <path
+           style="opacity:1;vector-effect:none;fill:#2a7fff;fill-opacity:1;stroke:none;stroke-width:0.36824697;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+           id="circle854"
+           sodipodi:type="arc"
+           sodipodi:cx="90.415558"
+           sodipodi:cy="149.57428"
+           sodipodi:rx="1.9128479"
+           sodipodi:ry="1.9128479"
+           sodipodi:start="4.7106229"
+           sodipodi:end="2.5736704"
+           d="m 90.41218,147.66143 a 1.9128479,1.9128479 0 0 1 1.881627,1.55068 1.9128479,1.9128479 0 0 1 -1.17015,2.13913 1.9128479,1.9128479 0 0 1 -2.320669,-0.74807 l 1.61257,-1.02889 z" />
+      </g>
+      <g
+         transform="translate(7.1590052)"
+         id="g868">
+        <circle
+           r="2.7487407"
+           cy="149.57428"
+           cx="83.371094"
+           id="circle864"
+           style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#44aa00;stroke-width:0.5291667;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+        <path
+           inkscape:connector-curvature="0"
+           id="path866"
+           d="m 82.386279,149.28284 0.715902,0.81612 1.267143,-1.24567"
+           style="fill:none;stroke:#44aa00;stroke-width:0.62900001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/roles/prepare/README.md b/roles/prepare/README.md
new file mode 100644 (file)
index 0000000..2cdcd0e
--- /dev/null
@@ -0,0 +1,28 @@
+# Chained CI Prepare role
+
+This role prepare the settings before getting artifacts and run the playbook.
+It:
+  - Warn if log level is HIGH to avoid data leaking
+  - Check the step parameter is set
+  - prepare the `config` fact
+  - test `only` and `except` step parameters to limit when jobs are runned.
+    This will __SKIP__ this job if __ONE of__ the `except` condition is
+    successful __AND__ if __ALL__ the `only` conditions are failing. Those
+    conditions are testing environment variables like this:
+    - `VAR`: this test the presence of a variable that is not empty
+    - `VAR == value`: this test the exact value of a variable
+    - `VAR != value`: this test the exact difference of a variable.
+    - `VAR in [value1, value2]`: this test the exact value of a variable is a
+      set of possibilities
+
+## Example
+
+```
+except:
+  - "XXX in [aaa, aab]"
+  - "YYY"
+only:
+  - "AAA == yes"
+  - "BBB != no"
+  - "CCC in [pitet, possible]"
+```
diff --git a/roles/prepare/tasks/continue.yml b/roles/prepare/tasks/continue.yml
new file mode 100644 (file)
index 0000000..5d664c7
--- /dev/null
@@ -0,0 +1,15 @@
+---
+
+- name: we have to continue this role
+  debug:
+    msg: "{{ msg.split('\n') }}"
+  vars:
+    msg: |
+      **************************************************************************
+      ** We continue the play
+      **   REASON = '{{ condition }}'
+      **************************************************************************
+
+- name: Do not skip the run of the play
+  set_fact:
+    skip_run: false
diff --git a/roles/prepare/tasks/except.yml b/roles/prepare/tasks/except.yml
new file mode 100644 (file)
index 0000000..8d8abff
--- /dev/null
@@ -0,0 +1,55 @@
+---
+# in this file, default variable value is '-666-', I hope no one will ever
+# test the number of the beast :)
+
+
+- name: Testing 'EXCEPT' condition
+  debug:
+    var: condition
+
+- name: if condition is only one word
+  block:
+    - name: check variable is present
+      include_tasks: exit.yml
+      when: lookup('env', condition)| default(False, true)
+  when: condition.split()| length == 1
+
+- name: if condition contains '=='
+  block:
+    - name: split condition with '=='
+      set_fact:
+        cond: "{{ (condition|replace(' == ', '==')).split('==') }}"
+    - debug: msg="{{ cond[1:]| join('==') }}"
+    - name: test condition
+      include_tasks: exit.yml
+      when: (lookup('env', cond[0])| default('-666-', true)) == (
+            cond[1:]| join('=='))
+  when: condition is search('==')
+
+- name: if condition contains '!='
+  block:
+    - name: split condition with '!='
+      set_fact:
+        cond: "{{ (condition|replace(' != ', '!=')).split('!=') }}"
+    - name: test condition
+      include_tasks: exit.yml
+      when: (lookup('env', cond[0])| default('-666-', true)) != (
+            cond[1:]| join('!='))
+  when: condition is search('!=')
+
+- name: if condition contains 'in'
+  block:
+    - name: split condition with ' in '
+      set_fact:
+        cond: "{{ condition.split(' in ') }}"
+    - name: split list
+      set_fact:
+        inlist: |
+          {{ (cond[1]|
+              replace(', ', ',')| replace(' ,', ',')|
+              replace(' ]', '') | replace(']', '')|
+              replace('[ ', '') | replace('[', '')).split(',') }}
+    - name: test condition
+      include_tasks: exit.yml
+      when: (lookup('env', cond[0])| default('-666-', true)) in inlist
+  when: condition is search(' in ')
diff --git a/roles/prepare/tasks/exit.yml b/roles/prepare/tasks/exit.yml
new file mode 100644 (file)
index 0000000..58fb43d
--- /dev/null
@@ -0,0 +1,13 @@
+---
+
+- name: we have to end this role
+  debug:
+    msg: "{{ msg.split('\n') }}"
+  vars:
+    msg: |
+      **************************************************************************
+      ** We finish the play here
+      **   REASON = '{{ condition }}'
+      **************************************************************************
+
+- meta: end_play
diff --git a/roles/prepare/tasks/main.yml b/roles/prepare/tasks/main.yml
new file mode 100644 (file)
index 0000000..ce08540
--- /dev/null
@@ -0,0 +1,93 @@
+---
+##
+# Warn if log level is high
+##
+- name: Echo running pipeline link
+  debug:
+    msg: "{{ msg.split('\n') }}"
+    verbosity: 3
+  vars:
+    msg: |
+      !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+      !! Log level is HIGH  !                                               !!
+      !! Some sensitive data may be visible to everyone.                    !!
+      !! Don't forget to clean the task output !                            !!
+      !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+##
+# Check Step parameters
+##
+- name: check 'step' is set
+  fail:
+    msg: 'Step must be defined ! (use --extra-vars "step=test1")'
+  when: step is not defined
+
+##
+# Check the pod is not protected
+##
+- name: clean var
+  set_fact:
+    protected_pods: []
+  when: protected_pods|default() == None
+
+- name: check pod protection
+  fail:
+    msg: 'This pod is protected'
+  when:
+    inventory_hostname in protected_pods and
+    lookup( 'env', 'AREYOUSURE') != 'MAIS OUI !!!'
+
+##
+# Prepare the step config
+##
+- name: get default step parameters
+  set_fact:
+    config: >-
+      {{ gitlab.git_projects[
+           hostvars[inventory_hostname].scenario_steps[step].project] |
+         combine(hostvars[inventory_hostname].scenario_steps[step]) }}
+
+- name: merge step parameters
+  set_fact:
+    config: >-
+      {{ config| combine(
+         {'parameters': config.parameters|
+          combine(config.extra_parameters)}) }}
+  when: config.extra_parameters is defined
+
+##
+# Check if we must run this step - Must be run at the end of this role
+##
+
+- name: Set default skip_run value
+  set_fact:
+    skip_run: false
+
+- name: run except parameter
+  include_tasks: except.yml
+  loop: "{{ config.except }}"
+  loop_control:
+    loop_var: condition
+    label: "{{ condition }}"
+  when: config.except is defined
+
+- name: Set default skip_run value
+  set_fact:
+    skip_run: true
+  when: config.only is defined
+
+- name: run only parameter
+  include_tasks: only.yml
+  loop: "{{ config.only }}"
+  vars:
+    skip_all: false
+  loop_control:
+    loop_var: condition
+    label: "{{ condition }}"
+  when: config.only is defined
+
+- name: Skip if none of ONLY is successful
+  include_tasks: exit.yml
+  vars:
+    condition: "None of ONLY conditions are successful"
+  when: config.only is defined and skip_run
diff --git a/roles/prepare/tasks/only.yml b/roles/prepare/tasks/only.yml
new file mode 100644 (file)
index 0000000..893d32b
--- /dev/null
@@ -0,0 +1,57 @@
+---
+# in this file, default variable value is '-666-', I hope no one will ever
+# test the number of the beast :)
+
+- name: test condition only if the previous failed
+  when: skip_run
+  block:
+    - name: Testing 'ONLY' condition
+      debug:
+        var: condition
+
+    - name: if condition is only one word
+      block:
+        - name: check variable is present
+          include_tasks: continue.yml
+          when: lookup('env', condition)| default(False, true)
+      when: condition.split()| length == 1
+
+    - name: if condition contains '=='
+      block:
+        - name: split condition with '=='
+          set_fact:
+            cond: "{{ (condition|replace(' == ', '==')).split('==') }}"
+        - debug: msg="{{ cond[1:]| join('==') }}"
+        - name: test condition
+          include_tasks: continue.yml
+          when: (lookup('env', cond[0])| default('-666-', true)) == (
+                cond[1:]| join('=='))
+      when: condition is search('==')
+
+    - name: if condition contains '!='
+      block:
+        - name: split condition with '!='
+          set_fact:
+            cond: "{{ (condition|replace(' != ', '!=')).split('!=') }}"
+        - name: test condition
+          include_tasks: continue.yml
+          when: (lookup('env', cond[0])| default('-666-', true)) != (
+                cond[1:]| join('!='))
+      when: condition is search('!=')
+
+    - name: if condition contains 'in'
+      block:
+        - name: split condition with ' in '
+          set_fact:
+            cond: "{{ condition.split(' in ') }}"
+        - name: split list
+          set_fact:
+            inlist: |
+              {{ (cond[1]|
+                  replace(', ', ',')| replace(' ,', ',')|
+                  replace(' ]', '') | replace(']', '')|
+                  replace('[ ', '') | replace('[', '')).split(',') }}
+        - name: test condition
+          include_tasks: continue.yml
+          when: (lookup('env', cond[0])| default('-666-', true)) in inlist
+      when: condition is search(' in ')
diff --git a/roles/run-ci/tasks/grafana_start.yml b/roles/run-ci/tasks/grafana_start.yml
new file mode 100644 (file)
index 0000000..183a439
--- /dev/null
@@ -0,0 +1,42 @@
+---
+- block:
+    - name: get start time (epoch+milliseconds)
+      set_fact:
+        time_start: "{{ lookup('pipe', 'date +%s%N | head -c 13' ) | int }}"
+
+    - name: set tags
+      set_fact:
+        grafana_tags: "{{ [ inventory_hostname ] }}"
+
+    - name: add inpod in tags
+      set_fact:
+        grafana_tags: "{{ grafana_tags + [ inpod ] }}"
+      when: inpod is defined
+
+    - name: "Create a grafana annotation"
+      uri:
+        url: "{{ grafana.api | regex_replace('\\/$', '') }}/annotations"
+        method: POST
+        status_code: 200
+        body_format: "json"
+        body: "{{
+            {
+              'time': time_start | int,
+              'isRegion': true,
+              'timeEnd': (time_start | int + 10000000),
+              'tags': grafana_tags,
+              'title': step,
+              'text': text
+            }
+          }}"
+        headers:
+          Content-Type: "application/json"
+          Accept: "application/json"
+          Authorization: "Bearer {{ grafana.token }}"
+      register: grafana_events
+      vars:
+        text:
+          "<a href=\"{{ pipeline_url }}\">{{ step }}</a> running"
+
+  delegate_to: "{{ grafana.jumphost }}"
+  ignore_errors: true
diff --git a/roles/run-ci/tasks/grafana_stop.yml b/roles/run-ci/tasks/grafana_stop.yml
new file mode 100644 (file)
index 0000000..4d5a7e7
--- /dev/null
@@ -0,0 +1,53 @@
+---
+- block:
+    - name: get end time
+      set_fact:
+        time_end: "{{ lookup('pipe', 'date +%s%N | head -c 13' ) | int }}"
+
+    - name: calculate duration
+      set_fact:
+        duration:
+          "{{ ((time_end|int) - (time_start|int))/1000 }}"
+
+    - name: "update a grafana annotation start"
+      uri:
+        url:
+          "{{ grafana.api | regex_replace('\\/$', '') }}/annotations/{{
+          grafana_events.json.id }}"
+        method: PUT
+        status_code: 200
+        body_format: "json"
+        body: "{{
+          {
+            'time': time_start | int,
+            'tags': grafana_tags + [ result ],
+            'text': text + '<br/>Duration (s): ' + duration
+          }
+          }}"
+        headers:
+          Content-Type: "application/json"
+          Accept: "application/json"
+          Authorization: "Bearer {{ grafana.token }}"
+
+    - name: "update a grafana annotation end"
+      uri:
+        url:
+          "{{ grafana.api | regex_replace('\\/$', '') }}/annotations/{{
+          grafana_events.json.endId }}"
+        method: PUT
+        status_code: 200
+        body_format: "json"
+        body: "{{
+            {
+              'time': time_end | int,
+              'tags': grafana_tags + [ result ],
+              'text': text + '<br/>Duration (s): ' + duration
+            }
+          }}"
+        headers:
+          Content-Type: "application/json"
+          Accept: "application/json"
+          Authorization: "Bearer {{ grafana.token }}"
+
+  delegate_to: "{{ grafana.jumphost }}"
+  ignore_errors: true
diff --git a/roles/run-ci/tasks/main.yml b/roles/run-ci/tasks/main.yml
new file mode 100644 (file)
index 0000000..eed27f8
--- /dev/null
@@ -0,0 +1,270 @@
+---
+
+##
+# Prepare base of variables to send
+##
+- name: prepare variables to sent
+  set_fact:
+    params:
+      {
+        'token': "{{ config.trigger_token }}",
+        'ref': "{{ config.branch }}",
+        'variables[source_job_name]': "{{ step }}",
+        'variables[pod]': "{{ inventory_hostname }}",
+        'variables[jumphost]': "{{ jumphost }}",
+      }
+
+##
+# Prepare the artifacts to get
+##
+
+- name: add bin artifacts param
+  when: artifacts_bin is defined
+  set_fact:
+    params:
+      "{{ params|combine({'variables[artifacts_bin]': artifacts_bin }) }}"
+
+- name: add src artifacts param
+  when: artifacts_src is defined
+  set_fact:
+    params:
+      "{{ params|combine({'variables[artifacts_src]': artifacts_src }) }}"
+
+- name: ensure artifacts.zip is not present
+  file:
+    path: "{{ playbook_dir }}/artifacts.zip"
+    state: absent
+
+- name: set healthchecks base url
+  set_fact:
+    base_url: "{{ gitlab.healthchecks_url }}/ping/{{ healthchecks_id }}"
+  when: healthchecks_id is defined
+
+##
+# Run the step
+##
+- name: Run step
+  block:
+    ##
+    # add step parameters in the parameters to send
+    ##
+    - name: Add step parameters
+      set_fact:
+        params: "{{ params|combine({key: value}) }}"
+      vars:
+        key: "variables[{{ item.key }}]"
+        value: "{{ item.value }}"
+      with_dict: "{{ config.parameters }}"
+      when: config.parameters is defined and config.parameters != None
+
+    ##
+    # add NOVAULT_LIST parameter in the parameters to send
+    ##
+    - name: Add NOVAULT_LIST parameter
+      set_fact:
+        params: "{{ params|combine({key: value}) }}"
+      vars:
+        key: "variables[NOVAULT_LIST]"
+        value: "{{ config.novault |join(\"\n\") }}"
+      when: config.novault is defined
+
+    ##
+    # Trigger the pipeline
+    ##
+    - name: "Trigger a new pipeline for step {{ step }}"
+      uri:
+        url: "{{ config.api }}/trigger/pipeline"
+        method: POST
+        status_code: 201
+        body_format: raw
+        body: "{{ params| urlencode }}"
+        headers:
+          Content-Type: "application/x-www-form-urlencoded"
+      register: trigger_out
+
+    - name: set pipeline url
+      set_fact:
+        pipeline_url: "{{ config.url }}/pipelines/{{ trigger_out.json.id }}"
+        api_pipeline_url: "{{ config.api }}/pipelines/{{ trigger_out.json.id }}"
+
+    - name: Echo running pipeline link
+      debug:
+        msg: "{{ msg.split('\n') }}"
+      vars:
+        msg: |
+          ******************************************************************
+          * Pipeline triggered for step '{{ step }}'
+          * {{ pipeline_url }}
+          ******************************************************************
+
+    - name: set grafana start point
+      include_tasks: grafana_start.yml
+      when: grafana is defined
+
+    - name: "Wait for pipeline result {{ step }}"
+      uri:
+        url: "{{ config.api }}/pipelines/{{ trigger_out.json.id }}"
+        method: GET
+        status_code: 200
+        return_content: 'yes'
+        headers:
+          PRIVATE-TOKEN:
+            "{{ config.api_token|default(gitlab.private_token, 'true') }}"
+      register: pipeline_out
+      retries: "{{ config.timeout }}"
+      delay: "{{ gitlab.pipeline.delay }}"
+      until: (((pipeline_out.json
+             |default({'status':'unknown'})).status
+             |default('unknown'))
+             not in ['created', 'waiting_for_resource', 'preparing',
+             'pending', 'running', 'unknown']) or (
+             pipeline_out.status == 401
+             )
+
+
+    - name: Exit -1
+      fail:
+      when: pipeline_out.json.status not in ['success']
+
+    ##
+    # When finished, recover an artifact if requested
+    ##
+    - name: pull artifacts_src
+      when:
+        config.pull_artifacts is defined and config.pull_artifacts != None
+      block:
+        - name: "Get job id for the artifact to get"
+          uri:
+            url: >-
+              {{ config.api
+              }}/pipelines/{{ trigger_out.json.id }}/jobs?scope[]=success
+            method: GET
+            headers:
+              PRIVATE-TOKEN:
+                "{{ config.api_token|default(gitlab.private_token, 'true') }}"
+          register: pipeline_success_jobs
+
+        - name: download job artifact
+          uri:
+            url: "{{ config.api }}/jobs/{{ job_id[0] }}/artifacts"
+            headers:
+              PRIVATE-TOKEN:
+                "{{ config.api_token|default(gitlab.private_token, 'true') }}"
+            dest: "{{ playbook_dir }}/artifacts.zip"
+          vars:
+            job_id: >-
+              {{ pipeline_success_jobs.json |json_query(
+              '[?name==`'+ config.pull_artifacts +'`].id') }}
+
+        - name: remove actual artifacts
+          file:
+            path: "{{ item }}"
+            state: absent
+          when: item[-1] == '/'
+          with_items:
+            "{{ vars[lookup( 'env', 'CI_JOB_NAME')].artifacts.paths }}"
+
+        - name: create artifacts folders
+          file:
+            path: "{{ item }}"
+            state: directory
+            recurse: true
+            mode: 0775
+          when: item[-1] == '/'
+          with_items:
+            "{{ vars[lookup( 'env', 'CI_JOB_NAME')].artifacts.paths }}"
+
+        - name: unarchive artifacts
+          unarchive:
+            src: "{{ playbook_dir }}/artifacts.zip"
+            dest: "{{ playbook_dir }}"
+            remote_src: "yes"
+
+        - name: trigger OK healthchecks
+          uri:
+            url: "{{ base_url }}"
+          when: healthchecks_id is defined
+          ignore_errors: true
+
+        - name: update grafana stop point
+          include_tasks: grafana_stop.yml
+          vars:
+            result: "{{ pipeline_out.json.status }}"
+            text: "<a href={{ pipeline_url }}>{{ step }}</a> succeeded"
+          when: grafana is defined and grafana_events is defined
+
+  ##
+  # If something failed, print the jobs that failed
+  ##
+  rescue:
+    - name: print last pipeline result for forensic
+      debug:
+        var: pipeline_out
+        verbosity: 3
+
+    - name: update grafana stop point
+      include_tasks: grafana_stop.yml
+      vars:
+        result: "{{ pipeline_out.json.status }}"
+        text: "<a href={{ pipeline_url }}>{{ step }}</a> failed"
+      when: grafana is defined and grafana_events is defined
+
+    - name: trigger Failed healthcheck
+      uri:
+        url: "{{ base_url }}/fail"
+      when: healthchecks_id is defined
+
+    - name: "Show last pipeline_out value"
+      debug:
+        msg: "{{ pipeline_out.json | default('No pipeline out') }}"
+        verbosity: 3
+
+    - name: "RESCUE - Get jobs list that failed"
+      uri:
+        url: "{{ config.api }}/pipelines/{{ trigger_out.json.id }}/jobs/"
+        method: GET
+        status_code: 200
+        return_content: 'yes'
+        headers:
+          PRIVATE-TOKEN:
+            "{{ config.api_token|default(gitlab.private_token, 'true') }}"
+      register: jobs_list
+
+    - name: RESCUE - filter failed jobs
+      set_fact:
+        failed_jobs:
+          "{{ failed_jobs | default({}) | combine({ item.id:
+             {'stage': item.stage,
+              'name': item.name,
+              'status': item.status,
+              'duration': item.duration,
+              'url': url
+             }})}}"
+      vars:
+        url: "{{ config.url }}/-/jobs/{{ item.id }}"
+      when: item.status not in ['success', 'skipped']
+      with_items: "{{ jobs_list.json }}"
+
+    - name: RESCUE - run failed !
+      when: true
+      fail:
+        msg: "{{ msg.split('\n') }}"
+      vars:
+        msg: |
+          ******************************************************************
+          * Oh ! NO !!! Pipeling of the project failed !!!
+          * -----------------------
+          * Step: {{ step }}
+          * Project: {{ config.project }}
+          * Status: {{ pipeline_out.json.status }}
+          * Pipeline: '{{ pipeline_url }}'
+          * API pipeline url: '{{ api_pipeline_url }}'
+          * Failed jobs:
+          {% for job_id, job_status in failed_jobs.items() -%}
+          *   - id: {{ job_id }}
+          *     name: {{ job_status.stage }}/{{ job_status.name }}
+          *     status: {{ job_status.status }}
+          *     duration: {{ job_status.duration }}
+          *     link: {{ job_status.url }}
+          {% endfor %}
+          ******************************************************************
diff --git a/roles/trigger_myself/tasks/main.yml b/roles/trigger_myself/tasks/main.yml
new file mode 100644 (file)
index 0000000..bbea974
--- /dev/null
@@ -0,0 +1,75 @@
+---
+- name: check 'step' is set
+  fail:
+    msg: 'Step must be defined ! (use --extra-vars "step=test1")'
+  when: step is not defined
+
+- name: get default step parameters
+  set_fact:
+    config: >-
+      {{ gitlab.git_projects[
+           hostvars[inventory_hostname].scenario_steps[step].project] |
+         combine(hostvars[inventory_hostname].scenario_steps[step]) }}
+
+- name: merge step parameters
+  set_fact:
+    config: >-
+      {{ config| combine(
+         {'parameters': config.parameters|
+          combine(config.extra_parameters)}) }}
+  when: config.extra_parameters is defined
+
+
+##
+# Prepare base of variables to send
+##
+- name: prepare variables to sent
+  set_fact:
+    params:
+      {
+        'token': "{{ config.trigger_token}}",
+        'ref': "{{ config.branch }}",
+        'variables[source_job_name]': "{{ step }}",
+        'variables[triggered_from]': "{{ lookup('env','CI_JOB_NAME') }}",
+        'variables[INPOD]': "{{ inventory_hostname }}",
+        'variables[jumphost]': "{{ jumphost }}",
+      }
+
+- name: Add step parameters
+  set_fact:
+    params: "{{ params|combine({key: value}) }}"
+  vars:
+    key: "variables[{{ item.key }}]"
+    value: "{{ item.value }}"
+  with_dict: "{{ config.parameters }}"
+  when: config.parameters is defined
+
+
+##
+# Trigger the pipeline
+##
+- name: "Trigger a new pipeline for step {{ step }}"
+  uri:
+    url: >-
+      {{ gitlab.api_url}}/projects/{{ lookup( 'env', 'CI_PROJECT_ID')
+      }}/trigger/pipeline
+    method: POST
+    status_code: 201
+    body_format: raw
+    body: "{{params| urlencode}}"
+    headers:
+      Content-Type: "application/x-www-form-urlencoded"
+  register: trigger_out
+
+- name: Echo running pipeline link
+  debug:
+    msg: "{{ msg.split('\n') }}"
+  vars:
+    url: >-
+      {{ lookup('env','CI_PROJECT_URL') }}/pipelines/{{
+      trigger_out.json.id }}"
+    msg: |
+      ******************************************************************
+      * Pipeline triggered for step '{{ step }}'
+      * {{ url }}
+      ******************************************************************