From 70fa03898ee412e30b6b87cf961004bf16ccaef4 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Marek=20Szwa=C5=82kiewicz?= Date: Wed, 1 Mar 2023 12:27:28 +0100 Subject: [PATCH] [GATING] Add configuration for Azure3 gating in the fork of chained-ci MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 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 Change-Id: Idb475c166d78f10ed4204153ab634110aa9093f6 --- .gitlab-ci.yml | 87 +++- .gitmodules | 6 - chained-ci-vue/LICENSE | 201 ++++++++ chained-ci-vue/README.md | 4 + chained-ci-vue/favicon.png | Bin 0 -> 5898 bytes chained-ci-vue/index.html | 238 +++++++++ chained-ci-vue/init.sh | 68 +++ chained-ci-vue/js/config.js | 16 + chained-ci-vue/js/index.js | 250 +++++++++ chained-ci-vue/js/lib.js | 558 +++++++++++++++++++++ chained-ci-vue/js/visibility.LICENSE | 3 + chained-ci-vue/js/visibility.core.js | 189 +++++++ chained-ci-vue/js/visibility.timers.js | 161 ++++++ chained-ci-vue/logo.svg | 130 +++++ chained-ci-vue/style.css | 219 ++++++++ pod_config/config/artifacts/azure.zip | Bin 0 -> 1132 bytes pod_config/config/az14-gating3.yaml | 168 +++++++ pod_config/config/idf-az14-gating3.yaml | 171 +++++++ pod_inventory/group_vars/all.yml | 194 ++++--- .../host_vars/onap_oom_gating_azure_3.yml | 85 ++++ pod_inventory/inventory | 4 +- roles/LICENSE | 201 ++++++++ roles/README.md | 15 + roles/artifact_init/defaults/main.yaml | 2 + roles/artifact_init/filter_plugins/filters.py | 8 + roles/artifact_init/tasks/main.yml | 180 +++++++ roles/get_artifacts/defaults/main.yml | 7 + roles/get_artifacts/filter_plugins/filters.py | 8 + roles/get_artifacts/tasks/binary.yml | 244 +++++++++ roles/get_artifacts/tasks/get_one_artifact.yml | 49 ++ roles/get_artifacts/tasks/job_id_fetch.yml | 20 + roles/get_artifacts/tasks/limit_to.yml | 20 + roles/get_artifacts/tasks/main.yml | 34 ++ roles/get_artifacts/tasks/url.yml | 13 + roles/gitlab-ci-generator/defaults/main.yml | 3 + roles/gitlab-ci-generator/tasks/main.yml | 45 ++ roles/gitlab-ci-generator/templates/gitlab-ci.yml | 204 ++++++++ roles/library/filepath.py | 40 ++ roles/logo.png | Bin 0 -> 4723 bytes roles/logo.svg | 130 +++++ roles/prepare/README.md | 28 ++ roles/prepare/tasks/continue.yml | 15 + roles/prepare/tasks/except.yml | 55 ++ roles/prepare/tasks/exit.yml | 13 + roles/prepare/tasks/main.yml | 93 ++++ roles/prepare/tasks/only.yml | 57 +++ roles/run-ci/tasks/grafana_start.yml | 42 ++ roles/run-ci/tasks/grafana_stop.yml | 53 ++ roles/run-ci/tasks/main.yml | 270 ++++++++++ roles/trigger_myself/tasks/main.yml | 75 +++ 50 files changed, 4563 insertions(+), 113 deletions(-) delete mode 100644 .gitmodules create mode 100644 chained-ci-vue/LICENSE create mode 100644 chained-ci-vue/README.md create mode 100644 chained-ci-vue/favicon.png create mode 100644 chained-ci-vue/index.html create mode 100755 chained-ci-vue/init.sh create mode 100644 chained-ci-vue/js/config.js create mode 100644 chained-ci-vue/js/index.js create mode 100644 chained-ci-vue/js/lib.js create mode 100644 chained-ci-vue/js/visibility.LICENSE create mode 100644 chained-ci-vue/js/visibility.core.js create mode 100644 chained-ci-vue/js/visibility.timers.js create mode 100644 chained-ci-vue/logo.svg create mode 100644 chained-ci-vue/style.css create mode 100644 pod_config/config/artifacts/azure.zip create mode 100644 pod_config/config/az14-gating3.yaml create mode 100644 pod_config/config/idf-az14-gating3.yaml create mode 100644 pod_inventory/host_vars/onap_oom_gating_azure_3.yml create mode 100644 roles/LICENSE create mode 100644 roles/README.md create mode 100644 roles/artifact_init/defaults/main.yaml create mode 100644 roles/artifact_init/filter_plugins/filters.py create mode 100644 roles/artifact_init/tasks/main.yml create mode 100644 roles/get_artifacts/defaults/main.yml create mode 100644 roles/get_artifacts/filter_plugins/filters.py create mode 100644 roles/get_artifacts/tasks/binary.yml create mode 100644 roles/get_artifacts/tasks/get_one_artifact.yml create mode 100644 roles/get_artifacts/tasks/job_id_fetch.yml create mode 100644 roles/get_artifacts/tasks/limit_to.yml create mode 100644 roles/get_artifacts/tasks/main.yml create mode 100644 roles/get_artifacts/tasks/url.yml create mode 100644 roles/gitlab-ci-generator/defaults/main.yml create mode 100644 roles/gitlab-ci-generator/tasks/main.yml create mode 100644 roles/gitlab-ci-generator/templates/gitlab-ci.yml create mode 100644 roles/library/filepath.py create mode 100644 roles/logo.png create mode 100644 roles/logo.svg create mode 100644 roles/prepare/README.md create mode 100644 roles/prepare/tasks/continue.yml create mode 100644 roles/prepare/tasks/except.yml create mode 100644 roles/prepare/tasks/exit.yml create mode 100644 roles/prepare/tasks/main.yml create mode 100644 roles/prepare/tasks/only.yml create mode 100644 roles/run-ci/tasks/grafana_start.yml create mode 100644 roles/run-ci/tasks/grafana_stop.yml create mode 100644 roles/run-ci/tasks/main.yml create mode 100644 roles/trigger_myself/tasks/main.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6c4b312..c953d1f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,15 +10,14 @@ 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: ################################################################################ # Shared parameters @@ -49,8 +48,7 @@ variables: expire_in: 1 yrs .runner_env: &runner_env - CHAINED_CI_SRC: "" # 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 index b0a82e6..0000000 --- a/.gitmodules +++ /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 index 0000000..3be62f8 --- /dev/null +++ b/chained-ci-vue/LICENSE @@ -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 index 0000000..e4baa04 --- /dev/null +++ b/chained-ci-vue/README.md @@ -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 index 0000000000000000000000000000000000000000..6e07930873b1ef18282275143f1f0f1f9f74c1f8 GIT binary patch literal 5898 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 + + + + + Pipelines + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+

Please set your gitlabs + private tokens +

+
+
+
+ + +
+ + +
+ +
+
+
+
this is required and can be generated on your user profile like: + {{gitlabProfileToken}} + (Only API option is needed)
+
+
+
+
+ +
+
+
+
+ Scenario filter: + +
+
+ Next update: {{ timer }} / {{ actualRefresh }} + + (Please set filter or optimize it to have a better update time) + +
+ +
+
+
+
+ +
+
{{ pipelines[id].scenario }}
+
{{ pipelines[id].branch }}
+
{{ pipelines[id].date }}
+
{{ pipelines[id].time }}
+
{{ Math.round(pipelines[id].details.duration/60) }} min
+
+ +
+
+
+
+
+
{{ stage.name }}
+
+
+
+
+
+
+ {{ job.shortname }}
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Load more pipelines +
+
+
+
+
+ + + +
+
+ +

+ {{ title }} +

+
+ {{ message }} +
+
+
+
+ + +
+
+ +

+
+ + Pipeline {{ pipeline.name }} {{ pipeline.id }} + + +
+
+ +

+
+
+
Loading, please wait...
+
+
+
+
+
{{ stage.name }}
+ +
+
+
+
+
The pipeline was probably not triggered, check console: + +
+
+
+
+
+
+
+
+
+ + + diff --git a/chained-ci-vue/init.sh b/chained-ci-vue/init.sh new file mode 100755 index 0000000..ff308e8 --- /dev/null +++ b/chained-ci-vue/init.sh @@ -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 index 0000000..f71ff70 --- /dev/null +++ b/chained-ci-vue/js/config.js @@ -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 index 0000000..447a27b --- /dev/null +++ b/chained-ci-vue/js/index.js @@ -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 index 0000000..8ce3e11 --- /dev/null +++ b/chained-ci-vue/js/lib.js @@ -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 index 0000000..9ef56c8 --- /dev/null +++ b/chained-ci-vue/js/visibility.LICENSE @@ -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 index 0000000..6dda095 --- /dev/null +++ b/chained-ci-vue/js/visibility.core.js @@ -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 index 0000000..546c24e --- /dev/null +++ b/chained-ci-vue/js/visibility.timers.js @@ -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 index 0000000..1eb9231 --- /dev/null +++ b/chained-ci-vue/logo.svg @@ -0,0 +1,130 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chained-ci-vue/style.css b/chained-ci-vue/style.css new file mode 100644 index 0000000..d2c7f8b --- /dev/null +++ b/chained-ci-vue/style.css @@ -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 index 0000000000000000000000000000000000000000..44f96a94aa371357696cbd03e926779413da2443 GIT binary patch literal 1132 zcmWIWW@Zs#U|`^2VC?q@{mzIoi9i)Y3vs*9gk z=rF9>^h!l`UY1B# z#KK*!r{2Wh(fWIBXB zo3cyf<|%nTkHCQU@$c?S9>`o+a;SY`eAF4on|TT`b2_%nQn+$YCdXhXE&sNigu`ZWWKadu}#t__}if`cDY~HE=!yj5zw5!{N%!#pSeQs z(vJFx|C05zncKU&?_%HW11B;hVhpwYoMSe;Y2udGv-e28?f%q?Wlo`||MJEiyE%n< zf{k^&Zf$%Jb})CD#PdC@_fMRAZEpLw=3lHx)$LmW2Cx74Mr7`u7=E53@SNtd>EiFK zvR?bOZ22DmPh6?t?tv{Y`)of0liCwtIuQmYuFSkLV1mjos?^WPPfpCq$S*Ddr>eN- z+IW*pj8vsN@u1%k10I+6Kl#pS^a*djIZ=yB&lzUkCpk)84i;|k_tE$m`E5(`{$J~#n7L(d_MDn@Fx@2Rv`mRnwBW?jQ(Vbk z`Bv0)-|^nEaF2k`-pP?!i`#fNXLfq=u_oR)k>s%Rqw4aDmnvR8dUz|T!EaS$-K7?Z zpb0a4sO$_8>06A- + {{ 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 index 0000000..112aa4a --- /dev/null +++ b/roles/get_artifacts/defaults/main.yml @@ -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 index 0000000..db38fc6 --- /dev/null +++ b/roles/get_artifacts/filter_plugins/filters.py @@ -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 index 0000000..99ba930 --- /dev/null +++ b/roles/get_artifacts/tasks/binary.yml @@ -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 index 0000000..ccbdc48 --- /dev/null +++ b/roles/get_artifacts/tasks/get_one_artifact.yml @@ -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 index 0000000..cab4bcb --- /dev/null +++ b/roles/get_artifacts/tasks/job_id_fetch.yml @@ -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 index 0000000..2e1b782 --- /dev/null +++ b/roles/get_artifacts/tasks/limit_to.yml @@ -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 index 0000000..605521c --- /dev/null +++ b/roles/get_artifacts/tasks/main.yml @@ -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 index 0000000..a2b5a91 --- /dev/null +++ b/roles/get_artifacts/tasks/url.yml @@ -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 index 0000000..11b8726 --- /dev/null +++ b/roles/gitlab-ci-generator/defaults/main.yml @@ -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 index 0000000..a96ae7c --- /dev/null +++ b/roles/gitlab-ci-generator/tasks/main.yml @@ -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 index 0000000..51ceb05 --- /dev/null +++ b/roles/gitlab-ci-generator/templates/gitlab-ci.yml @@ -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 index 0000000..356f5fb --- /dev/null +++ b/roles/library/filepath.py @@ -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 index 0000000000000000000000000000000000000000..1fc981e37b4d7a13af7fc446f9066d52e8f95075 GIT binary patch literal 4723 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 B6WRa( literal 0 HcmV?d00001 diff --git a/roles/logo.svg b/roles/logo.svg new file mode 100644 index 0000000..b2e6875 --- /dev/null +++ b/roles/logo.svg @@ -0,0 +1,130 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/roles/prepare/README.md b/roles/prepare/README.md new file mode 100644 index 0000000..2cdcd0e --- /dev/null +++ b/roles/prepare/README.md @@ -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 index 0000000..5d664c7 --- /dev/null +++ b/roles/prepare/tasks/continue.yml @@ -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 index 0000000..8d8abff --- /dev/null +++ b/roles/prepare/tasks/except.yml @@ -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 index 0000000..58fb43d --- /dev/null +++ b/roles/prepare/tasks/exit.yml @@ -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 index 0000000..ce08540 --- /dev/null +++ b/roles/prepare/tasks/main.yml @@ -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 index 0000000..893d32b --- /dev/null +++ b/roles/prepare/tasks/only.yml @@ -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 index 0000000..183a439 --- /dev/null +++ b/roles/run-ci/tasks/grafana_start.yml @@ -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: + "{{ step }} 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 index 0000000..4d5a7e7 --- /dev/null +++ b/roles/run-ci/tasks/grafana_stop.yml @@ -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 + '
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 + '
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 index 0000000..eed27f8 --- /dev/null +++ b/roles/run-ci/tasks/main.yml @@ -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: "{{ step }} 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: "{{ step }} 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 index 0000000..bbea974 --- /dev/null +++ b/roles/trigger_myself/tasks/main.yml @@ -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 }} + ****************************************************************** -- 2.16.6