From caab75121529d8068b4dcc83f2b2181dbb3e699e Mon Sep 17 00:00:00 2001 From: JoelKle <34544090+JoelKle@users.noreply.github.com> Date: Tue, 19 May 2020 09:26:03 +0200 Subject: [PATCH 001/188] Added the ability, to set the broadcast_websocket_secret variable. This is nessesary if you would like to rerun the playbook. Signed-off-by: JoelKle <34544090+JoelKle@users.noreply.github.com> --- installer/inventory | 5 +++++ installer/roles/kubernetes/tasks/main.yml | 1 + installer/roles/local_docker/tasks/main.yml | 1 + 3 files changed, 7 insertions(+) diff --git a/installer/inventory b/installer/inventory index 85a875eb0a..de001730eb 100644 --- a/installer/inventory +++ b/installer/inventory @@ -117,6 +117,11 @@ create_preload_data=True # your credentials secret_key=awxsecret +# By default a broadcast websocket secret will be generated. +# If you would like to *rerun the playbook*, you need to set a unique password. +# Otherwise it would generate a new one every playbook run. +# broadcast_websocket_secret= + # Build AWX with official logos # Requires cloning awx-logos repo as a sibling of this project. # Review the trademark guidelines at https://github.com/ansible/awx-logos/blob/master/TRADEMARKS.md diff --git a/installer/roles/kubernetes/tasks/main.yml b/installer/roles/kubernetes/tasks/main.yml index bb4065f211..6a9d9a2deb 100644 --- a/installer/roles/kubernetes/tasks/main.yml +++ b/installer/roles/kubernetes/tasks/main.yml @@ -4,6 +4,7 @@ broadcast_websocket_secret: "{{ lookup('password', '/dev/null', length=128) }}" run_once: true no_log: true + when: broadcast_websocket_secret is not defined - fail: msg: "Only set one of kubernetes_context or openshift_host" diff --git a/installer/roles/local_docker/tasks/main.yml b/installer/roles/local_docker/tasks/main.yml index ad87f16fb4..aab1260a36 100644 --- a/installer/roles/local_docker/tasks/main.yml +++ b/installer/roles/local_docker/tasks/main.yml @@ -4,6 +4,7 @@ broadcast_websocket_secret: "{{ lookup('password', '/dev/null', length=128) }}" run_once: true no_log: true + when: broadcast_websocket_secret is not defined - import_tasks: upgrade_postgres.yml when: From 699f1868904a6def505bf8b825c63b1dc3bc3c3e Mon Sep 17 00:00:00 2001 From: JoelKle <34544090+JoelKle@users.noreply.github.com> Date: Tue, 19 May 2020 09:26:30 +0200 Subject: [PATCH 002/188] Fixed a bug, where the redis.conf first would be stored with mod 0600 and in the next task changed to 0666. This has broke the ability to rerun the playbook. Signed-off-by: JoelKle <34544090+JoelKle@users.noreply.github.com> --- .../roles/local_docker/tasks/compose.yml | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/installer/roles/local_docker/tasks/compose.yml b/installer/roles/local_docker/tasks/compose.yml index 120b81cc1a..9a95ddabc3 100644 --- a/installer/roles/local_docker/tasks/compose.yml +++ b/installer/roles/local_docker/tasks/compose.yml @@ -12,22 +12,22 @@ - name: Create Docker Compose Configuration template: - src: "{{ item }}.j2" - dest: "{{ docker_compose_dir }}/{{ item }}" - mode: 0600 - with_items: - - environment.sh - - credentials.py - - docker-compose.yml - - nginx.conf - - redis.conf + src: "{{ item.file }}.j2" + dest: "{{ docker_compose_dir }}/{{ item.file }}" + mode: "{{ item.mode }}" + loop: + - file: environment.sh + mode: "0600" + - file: credentials.py + mode: "0600" + - file: docker-compose.yml + mode: "0600" + - file: nginx.conf + mode: "0600" + - file: redis.conf + mode: "0664" register: awx_compose_config -- name: Set redis config to other group readable to satisfy redis-server - file: - path: "{{ docker_compose_dir }}/redis.conf" - mode: 0666 - - name: Render SECRET_KEY file copy: content: "{{ secret_key }}" From 58da3df03ea20215a1cd445b3e6963ec79d41b09 Mon Sep 17 00:00:00 2001 From: Stefan Jakobs Date: Wed, 22 Jul 2020 14:00:05 +0200 Subject: [PATCH 003/188] Change Dockerfile to copy custom venv --- docs/custom_virtualenvs.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/custom_virtualenvs.md b/docs/custom_virtualenvs.md index 9b47c5d5ca..78bde90758 100644 --- a/docs/custom_virtualenvs.md +++ b/docs/custom_virtualenvs.md @@ -83,15 +83,17 @@ index aa8b304..eb05f91 100644 + virtualenv /opt/my-envs/my-custom-env + /opt/my-envs/my-custom-env/bin/pip install psutil + -diff --git a/installer/image_build/templates/Dockerfile.j2 b/installer/image_build/templates/Dockerfile.j2 -index d69e2c9..a08bae5 100644 ---- a/installer/image_build/templates/Dockerfile.j2 -+++ b/installer/image_build/templates/Dockerfile.j2 -@@ -34,6 +34,7 @@ RUN yum -y install epel-release && \ - pip install virtualenv supervisor && \ - VENV_BASE=/var/lib/awx/venv make requirements_ansible && \ - VENV_BASE=/var/lib/awx/venv make requirements_awx && \ -+ VENV_BASE=/var/lib/awx/venv make requirements_custom && \ +diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 +index d3b582ffcb..220ac760a3 100644 +--- a/installer/roles/image_build/templates/Dockerfile.j2 ++++ b/installer/roles/image_build/templates/Dockerfile.j2 +@@ -165,6 +165,7 @@ RUN openssl req -nodes -newkey rsa:2048 -keyout /etc/nginx/nginx.key -out /etc/n + chmod 640 /etc/nginx/nginx.{csr,key,crt} + {% else %} + COPY --from=builder /var/lib/awx /var/lib/awx ++COPY --from=builder /opt/my-envs /opt/my-envs + RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage + {% endif %} ``` Once the AWX API is available, update the `CUSTOM_VENV_PATHS` setting as described in `Preparing a New Custom Virtualenv`. From 02252f3f97083891ae3f2d91c0e8b3c9f5130e11 Mon Sep 17 00:00:00 2001 From: Andrew Gaffney Date: Sat, 11 Jul 2020 13:53:30 +0000 Subject: [PATCH 004/188] ARM image build support * upgrade `chromedriver` for ARM support * upgrade `pynacl` to fix `libsodium` build issue on ARM * remove unnecessary i686-specific `libstdc++.so.6` package * install `kubectl` and `tini` from upstream binaries for ARM support * use upstream `postgres` and `alpine` docker images for `postgresql` helm chart Fixes #7051 --- awx/ui/package-lock.json | 340 +++++++++++++++--- awx/ui/package.json | 2 +- installer/roles/image_build/defaults/main.yml | 4 + .../roles/image_build/templates/Dockerfile.j2 | 15 +- .../templates/postgresql-values.yml.j2 | 12 +- requirements/requirements_ansible.txt | 2 +- 6 files changed, 314 insertions(+), 61 deletions(-) diff --git a/awx/ui/package-lock.json b/awx/ui/package-lock.json index 24b7837312..9f6c2322e1 100644 --- a/awx/ui/package-lock.json +++ b/awx/ui/package-lock.json @@ -4,6 +4,28 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "@types/node": { + "version": "14.0.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.26.tgz", + "integrity": "sha512-W+fpe5s91FBGE0pEa0lnqGLL4USgpLgs4nokw16SrBBco/gQxuua7KnArSEOd5iaMqbbSHV10vUDkJYJJqpXKA==", + "dev": true + }, "@uirouter/angularjs": { "version": "1.0.18", "resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.18.tgz", @@ -740,7 +762,8 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", - "dev": true + "dev": true, + "optional": true }, "axios": { "version": "0.16.2", @@ -2299,16 +2322,215 @@ "dev": true }, "chromedriver": { - "version": "2.40.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-2.40.0.tgz", - "integrity": "sha512-ewvRQ1HMk0vpFSWYCk5hKDoEz5QMPplx5w3C6/Me+03y1imr67l3Hxl9U0jn3mu2N7+c7BoC7JtNW6HzbRAwDQ==", + "version": "77.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-77.0.0.tgz", + "integrity": "sha512-mZa1IVx4HD8rDaItWbnS470mmypgiWsDiu98r0NkiT4uLm3qrANl4vOU6no6vtWtLQiW5kt1POcIbjeNpsLbXA==", "dev": true, "requires": { - "del": "^3.0.0", + "del": "^4.1.1", "extract-zip": "^1.6.7", - "kew": "^0.7.0", "mkdirp": "^0.5.1", - "request": "^2.87.0" + "request": "^2.88.0", + "tcp-port-used": "^1.0.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "aws4": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "dev": true + }, + "del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "requires": { + "is-path-inside": "^2.1.0" + } + }, + "is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "requires": { + "mime-db": "1.44.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "cipher-base": { @@ -6458,16 +6680,6 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", "dev": true }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "dev": true, - "requires": { - "ajv": "^5.1.0", - "har-schema": "^2.0.0" - } - }, "hard-source-webpack-plugin": { "version": "0.5.18", "resolved": "https://registry.npmjs.org/hard-source-webpack-plugin/-/hard-source-webpack-plugin-0.5.18.tgz", @@ -7349,6 +7561,12 @@ "integrity": "sha1-x+NWzeoiWucbNtcPLnGpK6TkJZA=", "dev": true }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, "ipaddr.js": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", @@ -7658,6 +7876,12 @@ "unc-path-regex": "^0.1.0" } }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -7676,6 +7900,17 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "dev": true }, + "is2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", + "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "ip-regex": "^2.1.0", + "is-url": "^1.2.2" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -8173,12 +8408,6 @@ } } }, - "kew": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", - "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", - "dev": true - }, "killable": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", @@ -9826,7 +10055,8 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -11196,6 +11426,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, "public-encrypt": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", @@ -11816,34 +12052,6 @@ "is-finite": "^1.0.0" } }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", - "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", - "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -13171,6 +13379,33 @@ "xtend": "^4.0.0" } }, + "tcp-port-used": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.1.tgz", + "integrity": "sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q==", + "dev": true, + "requires": { + "debug": "4.1.0", + "is2": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", + "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "test-exclude": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", @@ -13355,6 +13590,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", "dev": true, + "optional": true, "requires": { "punycode": "^1.4.1" } diff --git a/awx/ui/package.json b/awx/ui/package.json index 59cb964b5c..e546bfc891 100644 --- a/awx/ui/package.json +++ b/awx/ui/package.json @@ -43,7 +43,7 @@ "babel-loader": "^7.1.2", "babel-plugin-istanbul": "^4.1.5", "babel-preset-env": "^1.6.0", - "chromedriver": "^2.35.0", + "chromedriver": "^77.0.0", "clean-webpack-plugin": "^0.1.16", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.5", diff --git a/installer/roles/image_build/defaults/main.yml b/installer/roles/image_build/defaults/main.yml index 2618c9b40d..ab152975ce 100644 --- a/installer/roles/image_build/defaults/main.yml +++ b/installer/roles/image_build/defaults/main.yml @@ -1,3 +1,7 @@ --- create_preload_data: true build_dev: false + +# Helper vars to construct the proper download URL for the current architecture +tini_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm" }[ansible_facts.architecture] }}' +kubectl_architecture: '{{ { "x86_64": "amd64", "aarch64": "arm64", "armv7": "arm" }[ansible_facts.architecture] }}' diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 index ecded902ea..c16b4b4a2c 100644 --- a/installer/roles/image_build/templates/Dockerfile.j2 +++ b/installer/roles/image_build/templates/Dockerfile.j2 @@ -35,7 +35,6 @@ RUN dnf -y update && \ glibc-langpack-en \ libcurl-devel \ libffi-devel \ - libstdc++.so.6 \ libtool-ltdl-devel \ make \ nodejs \ @@ -115,7 +114,6 @@ RUN dnf -y install \ # Install runtime requirements RUN dnf -y update && \ - dnf -y install https://github.com/krallin/tini/releases/download/v0.18.0/tini_0.18.0.rpm && \ dnf -y install epel-release 'dnf-command(config-manager)' && \ dnf module -y enable 'postgresql:10' && \ dnf config-manager --set-enabled PowerTools && \ @@ -140,12 +138,17 @@ RUN dnf -y update && \ vim-minimal \ which \ xmlsec1-openssl && \ - dnf -y --repofrompath gcloud,https://packages.cloud.google.com/yum/repos/cloud-sdk-el8-x86_64 \ - --setopt gcloud.gpgkey=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg \ - install kubectl && \ dnf -y install centos-release-stream && dnf -y install "rsyslog >= 8.1911.0" && dnf -y remove centos-release-stream && \ dnf -y clean all +# Install kubectl +RUN curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.17.8/bin/linux/{{ kubectl_architecture | default('amd64') }}/kubectl && \ + chmod a+x /usr/bin/kubectl + +# Install tini +RUN curl -L -o /usr/bin/tini https://github.com/krallin/tini/releases/download/v0.19.0/tini-{{ tini_architecture | default('amd64') }} && \ + chmod +x /usr/bin/tini + RUN python3 -m ensurepip && pip3 install "virtualenv < 20" supervisor {% if build_dev|bool %}flake8{% endif %} RUN rm -rf /root/.cache && rm -rf /tmp/* @@ -244,7 +247,7 @@ CMD ["/bin/bash"] USER 1000 EXPOSE 8052 -ENTRYPOINT ["tini", "--"] +ENTRYPOINT ["/usr/bin/tini", "--"] CMD /usr/bin/launch_awx.sh VOLUME /var/lib/nginx {% endif %} diff --git a/installer/roles/kubernetes/templates/postgresql-values.yml.j2 b/installer/roles/kubernetes/templates/postgresql-values.yml.j2 index 658b898505..ea6ba29230 100644 --- a/installer/roles/kubernetes/templates/postgresql-values.yml.j2 +++ b/installer/roles/kubernetes/templates/postgresql-values.yml.j2 @@ -34,12 +34,22 @@ master: {{ affinity | to_nice_yaml(indent=2) | indent(width=4, indentfirst=True) }} {% endif %} {% endif %} -{% if pg_image_registry is defined %} image: +{% if pg_image_registry is defined %} registry: {{ pg_image_registry }} +{% endif %} + # The default bitnami image from the chart doesn't work on ARM + repository: postgres + tag: '11' volumePermissions: image: +{% if pg_image_registry is defined %} registry: {{ pg_image_registry }} +{% endif %} + # The default bitnami image from the chart doesn't work on ARM + repository: alpine + tag: '3' +{% if pg_image_registry is defined %} metrics: image: registry: {{ pg_image_registry }} diff --git a/requirements/requirements_ansible.txt b/requirements/requirements_ansible.txt index 55db209e4e..5bdbaadd67 100644 --- a/requirements/requirements_ansible.txt +++ b/requirements/requirements_ansible.txt @@ -92,7 +92,7 @@ pycurl==7.43.0.1 # via -r /awx_devel/requirements/requirements_ansible. pygments==2.5.2 # via azure-cli-core, knack pyjwt==1.7.1 # via adal, azure-cli-core pykerberos==1.2.1 # via requests-kerberos -pynacl==1.3.0 # via paramiko +pynacl==1.4.0 # via paramiko pyopenssl==19.1.0 # via azure-cli-core, requests-credssp pyparsing==2.4.5 # via packaging python-dateutil==2.8.1 # via adal, azure-storage, botocore, kubernetes From 579604d2c666c9fc24ec9065b18fb74a6bf4f138 Mon Sep 17 00:00:00 2001 From: Rigel Di Scala Date: Wed, 29 Jul 2020 15:27:45 +0200 Subject: [PATCH 005/188] Allow YAML as a CLI import format This changset allows the import of YAML formatted resources. The CLI user can indicate which format to use with the `-f, --format` option. The CLI help text has been amended to reflect the new feature. The AWX CLI `export` subcommand offers the option of formatting the output as YAML or JSON, so it makes sense that the `import` subcommand reflects this. A simple test is also provided. In order to ease the task of testing commands that import resources by reading the stdin, the CLI has been extended to allow specifying an alternative file descriptor for stdin, similarly to stdout and stderr. --- awxkit/awxkit/cli/client.py | 3 ++- awxkit/awxkit/cli/format.py | 4 ++-- awxkit/awxkit/cli/resource.py | 11 +++++++++-- awxkit/test/cli/test_format.py | 26 ++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/awxkit/awxkit/cli/client.py b/awxkit/awxkit/cli/client.py index 8013523921..3feded89dc 100755 --- a/awxkit/awxkit/cli/client.py +++ b/awxkit/awxkit/cli/client.py @@ -70,9 +70,10 @@ class CLI(object): subparsers = {} original_action = None - def __init__(self, stdout=sys.stdout, stderr=sys.stderr): + def __init__(self, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin): self.stdout = stdout self.stderr = stderr + self.stdin = stdin def get_config(self, key): """Helper method for looking up the value of a --conf.xyz flag""" diff --git a/awxkit/awxkit/cli/format.py b/awxkit/awxkit/cli/format.py index e8c5a6903e..d35c61efbb 100644 --- a/awxkit/awxkit/cli/format.py +++ b/awxkit/awxkit/cli/format.py @@ -40,7 +40,7 @@ def add_authentication_arguments(parser, env): def add_output_formatting_arguments(parser, env): - formatting = parser.add_argument_group('output formatting') + formatting = parser.add_argument_group('input/output formatting') formatting.add_argument( '-f', @@ -49,7 +49,7 @@ def add_output_formatting_arguments(parser, env): choices=FORMATTERS.keys(), default=env.get('TOWER_FORMAT', 'json'), help=( - 'specify an output format' + 'specify a format for the input and output' ), ) formatting.add_argument( diff --git a/awxkit/awxkit/cli/resource.py b/awxkit/awxkit/cli/resource.py index f22795fab2..0f9f24dbae 100644 --- a/awxkit/awxkit/cli/resource.py +++ b/awxkit/awxkit/cli/resource.py @@ -1,8 +1,9 @@ +import yaml import json import os -import sys from awxkit import api, config +from awxkit.exceptions import ImportExportError from awxkit.utils import to_str from awxkit.api.pages import Page from awxkit.api.pages.api import EXPORTABLE_RESOURCES @@ -135,7 +136,13 @@ class Import(CustomCommand): parser.print_help() raise SystemExit() - data = json.load(sys.stdin) + format = getattr(client.args, 'conf.format') + if format == 'json': + data = json.load(client.stdin) + elif format == 'yaml': + data = yaml.safe_load(client.stdin) + else: + raise ImportExportError("Unsupported format for Import: " + format) client.authenticate() client.v2.import_assets(data) diff --git a/awxkit/test/cli/test_format.py b/awxkit/test/cli/test_format.py index 7166fb841c..5ab6e55d6c 100644 --- a/awxkit/test/cli/test_format.py +++ b/awxkit/test/cli/test_format.py @@ -1,10 +1,13 @@ +import io import json import yaml from awxkit.api.pages import Page from awxkit.api.pages.users import Users, User +from awxkit.cli import CLI from awxkit.cli.format import format_response +from awxkit.cli.resource import Import def test_json_empty_list(): @@ -44,3 +47,26 @@ def test_yaml_list(): page = Users.from_json(users) formatted = format_response(page, fmt='yaml') assert yaml.safe_load(formatted) == users + + +def test_yaml_import(): + class MockedV2: + def import_assets(self, data): + self._parsed_data = data + + def _dummy_authenticate(): + pass + + yaml_fd = io.StringIO( + """ + workflow_job_templates: + - name: Workflow1 + """ + ) + cli = CLI(stdin=yaml_fd) + cli.parse_args(['--conf.format', 'yaml']) + cli.v2 = MockedV2() + cli.authenticate = _dummy_authenticate + + Import().handle(cli, None) + assert cli.v2._parsed_data['workflow_job_templates'][0]['name'] From 5e9d372db2ad8218266dc18abf579cae71b9d96e Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 11 Aug 2020 09:32:09 -0400 Subject: [PATCH 006/188] update to a newer python-ldap to address a bug see: https://github.com/ansible/awx/issues/7868 --- CHANGELOG.md | 3 +++ requirements/requirements.in | 1 + requirements/requirements.txt | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a53e50174..63473b431e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/`. +## 14.1.0 (TBD) +- Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 + ## 14.0.0 (Aug 6, 2020) - As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to: * https://github.com/ansible/awx/commit/78229f58715fbfbf88177e54031f532543b57acc diff --git a/requirements/requirements.in b/requirements/requirements.in index cc194454a7..f8126fb081 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -36,6 +36,7 @@ pygerduty pyparsing python-radius python3-saml +python-ldap>=3.3.1 # https://github.com/python-ldap/python-ldap/issues/270 pyyaml>=5.3.1 # minimum version to pull in new pyyaml for CVE-2017-18342 schedule==0.6.0 social-auth-core==3.3.1 # see UPGRADE BLOCKERs diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8820e46b29..8408960f28 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -93,7 +93,7 @@ pyrad==2.3 # via django-radius pyrsistent==0.15.7 # via jsonschema python-daemon==2.2.4 # via ansible-runner python-dateutil==2.8.1 # via adal, kubernetes -python-ldap==3.2.0 # via django-auth-ldap +python-ldap==3.3.1 # via -r /awx_devel/requirements/requirements.in, django-auth-ldap python-radius==1.0 # via -r /awx_devel/requirements/requirements.in python-string-utils==1.0.0 # via openshift python3-openid==3.1.0 # via social-auth-core From 987c7d48a05de967f3dc4110afa070d74f902fc3 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 11 Aug 2020 09:37:12 -0400 Subject: [PATCH 007/188] minor cleanup up CLI import -f yaml support --- awxkit/awxkit/cli/resource.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/awxkit/awxkit/cli/resource.py b/awxkit/awxkit/cli/resource.py index 0f9f24dbae..8e30accad2 100644 --- a/awxkit/awxkit/cli/resource.py +++ b/awxkit/awxkit/cli/resource.py @@ -136,13 +136,13 @@ class Import(CustomCommand): parser.print_help() raise SystemExit() - format = getattr(client.args, 'conf.format') - if format == 'json': + fmt = client.get_config('format') + if fmt == 'json': data = json.load(client.stdin) - elif format == 'yaml': + elif fmt == 'yaml': data = yaml.safe_load(client.stdin) else: - raise ImportExportError("Unsupported format for Import: " + format) + raise ImportExportError("Unsupported format for Import: " + fmt) client.authenticate() client.v2.import_assets(data) From 2a0c61de630b35fd952f7574321eead45b700c83 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Tue, 11 Aug 2020 10:04:14 -0400 Subject: [PATCH 008/188] Change regex to match what is in source --- awx_collection/tools/roles/template_galaxy/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tools/roles/template_galaxy/tasks/main.yml b/awx_collection/tools/roles/template_galaxy/tasks/main.yml index 414f55b7de..96eb26413c 100644 --- a/awx_collection/tools/roles/template_galaxy/tasks/main.yml +++ b/awx_collection/tools/roles/template_galaxy/tasks/main.yml @@ -2,7 +2,7 @@ - name: Set the collection version in the tower_api.py file replace: path: "{{ collection_path }}/plugins/module_utils/tower_api.py" - regexp: '^ _COLLECTION_VERSION = "devel"' + regexp: '^ _COLLECTION_VERSION = "0.0.1-devel"' replace: ' _COLLECTION_VERSION = "{{ collection_version }}"' when: - "awx_template_version | default(True)" From e3fe680d14100e8bc9d54b7547b2ae9eac044609 Mon Sep 17 00:00:00 2001 From: nixocio Date: Mon, 10 Aug 2020 14:47:01 -0400 Subject: [PATCH 009/188] Add feature to edit instance group Add feature to edit instance group. See: https://github.com/ansible/awx/issues/7767 --- .../InstanceGroupEdit/InstanceGroupEdit.jsx | 40 ++++- .../InstanceGroupEdit.test.jsx | 140 ++++++++++++++++++ .../shared/InstanceGroupForm.jsx | 9 +- 3 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx index 2f724479ee..b2f9bbaa9a 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.jsx @@ -1,13 +1,37 @@ -import React from 'react'; -import { Card, PageSection } from '@patternfly/react-core'; +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { CardBody } from '../../../components/Card'; +import { InstanceGroupsAPI } from '../../../api'; +import InstanceGroupForm from '../shared/InstanceGroupForm'; + +function InstanceGroupEdit({ instanceGroup }) { + const history = useHistory(); + const [submitError, setSubmitError] = useState(null); + const detailsUrl = `/instance_groups/${instanceGroup.id}/details`; + + const handleSubmit = async values => { + try { + await InstanceGroupsAPI.update(instanceGroup.id, values); + history.push(detailsUrl); + } catch (error) { + setSubmitError(error); + } + }; + + const handleCancel = () => { + history.push(detailsUrl); + }; -function InstanceGroupEdit() { return ( - - -
Edit instance group
-
-
+ + + ); } diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx new file mode 100644 index 0000000000..45b94bd17d --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupEdit/InstanceGroupEdit.test.jsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { InstanceGroupsAPI } from '../../../api'; + +import InstanceGroupEdit from './InstanceGroupEdit'; + +jest.mock('../../../api'); + +const instanceGroupData = { + id: 42, + type: 'instance_group', + url: '/api/v2/instance_groups/42/', + related: { + jobs: '/api/v2/instance_groups/42/jobs/', + instances: '/api/v2/instance_groups/7/instances/', + }, + name: 'Foo', + created: '2020-07-21T18:41:02.818081Z', + modified: '2020-07-24T20:32:03.121079Z', + capacity: 24, + committed_capacity: 0, + consumed_capacity: 0, + percent_capacity_remaining: 100.0, + jobs_running: 0, + jobs_total: 0, + instances: 1, + controller: null, + is_controller: false, + is_isolated: false, + is_containerized: false, + credential: null, + policy_instance_percentage: 46, + policy_instance_minimum: 12, + policy_instance_list: [], + pod_spec_override: '', + summary_fields: { + user_capabilities: { + edit: true, + delete: true, + }, + }, +}; + +const updatedInstanceGroup = { + name: 'Bar', + policy_instance_percentage: 42, +}; + +describe('', () => { + let wrapper; + let history; + + beforeAll(async () => { + history = createMemoryHistory(); + await act(async () => { + wrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('tower instance group name can not be updated', async () => { + let towerWrapper; + await act(async () => { + towerWrapper = mountWithContexts( + , + { + context: { router: { history } }, + } + ); + }); + expect( + towerWrapper.find('input#instance-group-name').prop('disabled') + ).toBeTruthy(); + expect( + towerWrapper.find('input#instance-group-name').prop('value') + ).toEqual('tower'); + }); + + test('handleSubmit should call the api and redirect to details page', async () => { + await act(async () => { + wrapper.find('InstanceGroupForm').invoke('onSubmit')( + updatedInstanceGroup + ); + }); + expect(InstanceGroupsAPI.update).toHaveBeenCalledWith( + 42, + updatedInstanceGroup + ); + }); + + test('should navigate to instance group details when cancel is clicked', async () => { + await act(async () => { + wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); + }); + expect(history.location.pathname).toEqual('/instance_groups/42/details'); + }); + + test('should navigate to instance group details after successful submission', async () => { + await act(async () => { + wrapper.find('InstanceGroupForm').invoke('onSubmit')( + updatedInstanceGroup + ); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(0); + expect(history.location.pathname).toEqual('/instance_groups/42/details'); + }); + + test('failed form submission should show an error message', async () => { + const error = { + response: { + data: { detail: 'An error occurred' }, + }, + }; + InstanceGroupsAPI.update.mockImplementationOnce(() => + Promise.reject(error) + ); + await act(async () => { + wrapper.find('InstanceGroupForm').invoke('onSubmit')( + updatedInstanceGroup + ); + }); + wrapper.update(); + expect(wrapper.find('FormSubmitError').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx b/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx index a2477d2f53..2f55092394 100644 --- a/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/shared/InstanceGroupForm.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { func, shape } from 'prop-types'; -import { Formik } from 'formik'; +import { Formik, useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Form } from '@patternfly/react-core'; @@ -11,21 +11,24 @@ import { required, minMaxValue } from '../../../util/validators'; import { FormColumnLayout } from '../../../components/FormLayout'; function InstanceGroupFormFields({ i18n }) { + const [instanceGroupNameField, ,] = useField('name'); return ( <> Date: Mon, 20 Jul 2020 16:27:38 -0700 Subject: [PATCH 010/188] start notification template list --- .../NotificationTemplate.jsx | 5 + .../NotificationTemplateAdd.jsx | 5 + .../NotificationTemplateList.jsx | 170 ++++++++++++++++++ .../NotificationTemplateListItem.jsx | 45 +++++ .../NotificationTemplateList/index.js | 4 + .../NotificationTemplates.jsx | 62 ++++--- 6 files changed, 270 insertions(+), 21 deletions(-) create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx new file mode 100644 index 0000000000..d271962a40 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function NotificationTemplate() { + return
; +} diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx new file mode 100644 index 0000000000..bbf39b61a9 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function NotificationTemplateAdd() { + return
; +} diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx new file mode 100644 index 0000000000..50d0f3f400 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx @@ -0,0 +1,170 @@ +import React, { useCallback, useEffect } from 'react'; +import { useLocation, useRouteMatch } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Card, PageSection } from '@patternfly/react-core'; +import { NotificationTemplatesAPI } from '../../../api'; +import PaginatedDataList, { + ToolbarAddButton, + ToolbarDeleteButton, +} from '../../../components/PaginatedDataList'; +import AlertModal from '../../../components/AlertModal'; +import ErrorDetail from '../../../components/ErrorDetail'; +import DataListToolbar from '../../../components/DataListToolbar'; +import NotificationTemplateListItem from './NotificationTemplateListItem'; +import useRequest, { useDeleteItems } from '../../../util/useRequest'; +import useSelected from '../../../util/useSelected'; +import { getQSConfig, parseQueryString } from '../../../util/qs'; + +const QS_CONFIG = getQSConfig('notification-templates', { + page: 1, + page_size: 20, + order_by: 'name', +}); + +function NotificationTemplatesList({ i18n }) { + const location = useLocation(); + const match = useRouteMatch(); + + const addUrl = `${match.url}/add`; + + const { + result: { templates, count, actions }, + error: contentError, + isLoading: isTemplatesLoading, + request: fetchTemplates, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const responses = await Promise.all([ + NotificationTemplatesAPI.read(params), + NotificationTemplatesAPI.readOptions(), + ]); + return { + templates: responses[0].data.results, + count: responses[0].data.count, + actions: responses[1].data.actions, + }; + }, [location]), + { + templates: [], + count: 0, + actions: {}, + } + ); + + useEffect(() => { + fetchTemplates(); + }, [fetchTemplates]); + + const { selected, isAllSelected, handleSelect, setSelected } = useSelected( + templates + ); + + const { + isLoading: isDeleteLoading, + deleteItems: deleteTemplates, + deletionError, + clearDeletionError, + } = useDeleteItems( + useCallback(async () => { + return Promise.all( + selected.map(({ id }) => NotificationTemplatesAPI.destroy(id)) + ); + }, [selected]), + { + qsConfig: QS_CONFIG, + allItemsSelected: isAllSelected, + fetchItems: fetchTemplates, + } + ); + + const handleDelete = async () => { + await deleteTemplates(); + setSelected([]); + }; + + const canAdd = actions && actions.POST; + + return ( + <> + + + ( + setSelected([...templates])} + qsConfig={QS_CONFIG} + additionalControls={[ + ...(canAdd + ? [] + : []), + , + ]} + /> + )} + renderItem={template => ( + row.id === template.id)} + onSelect={() => handleSelect(template)} + /> + )} + emptyStateControls={ + canAdd ? : null + } + /> + + + + {i18n._(t`Failed to delete one or more organizations.`)} + + + + ); +} + +export default withI18n()(NotificationTemplatesList); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx new file mode 100644 index 0000000000..4bf773b4f8 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Link } from 'react-router-dom'; +import { + Badge as PFBadge, + Button, + DataListAction as _DataListAction, + DataListCheck, + DataListItem, + DataListItemCells, + DataListItemRow, + Tooltip, +} from '@patternfly/react-core'; +import DataListCell from '../../../components/DataListCell'; + +export default function NotificationTemplatesListItem({ + template, + detailUrl, + isSelected, + onSelect, +}) { + const labelId = `check-action-${template.id}`; + return ( + + + + + + {template.name} + + , + ]} + /> + + + ); +} diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js new file mode 100644 index 0000000000..06c347d889 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js @@ -0,0 +1,4 @@ +import NotificationTemplatesList from './NotificationTemplateList'; + +export default NotificationTemplatesList; +export { default as NotificationTemplatesListItem } from './NotificationTemplateListItem'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx index 857201bc6b..6d828175a0 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.jsx @@ -1,28 +1,48 @@ -import React, { Component, Fragment } from 'react'; +import React, { useState } from 'react'; +import { Route, Switch, useRouteMatch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { - PageSection, - PageSectionVariants, - Title, -} from '@patternfly/react-core'; +import NotificationTemplateList from './NotificationTemplateList'; +import NotificationTemplateAdd from './NotificationTemplateAdd'; +import NotificationTemplate from './NotificationTemplate'; +import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs'; -class NotificationTemplates extends Component { - render() { - const { i18n } = this.props; - const { light } = PageSectionVariants; +function NotificationTemplates({ i18n }) { + const match = useRouteMatch(); + const [breadcrumbConfig, setBreadcrumbConfig] = useState({ + '/notification_templates': i18n._(t`Notification Templates`), + '/notification_templates/add': i18n._(t`Create New Notification Template`), + }); - return ( - - - - {i18n._(t`Notification Templates`)} - - - - - ); - } + const updateBreadcrumbConfig = notification => { + const { id } = notification; + setBreadcrumbConfig({ + '/notification_templates': i18n._(t`Notification Templates`), + '/notification_templates/add': i18n._( + t`Create New Notification Template` + ), + [`/notification_templates/${id}`]: notification.name, + [`/notification_templates/${id}/edit`]: i18n._(t`Edit Details`), + [`/notification_templates/${id}/details`]: i18n._(t`Details`), + }); + }; + + return ( + <> + + + + + + + + + + + + + + ); } export default withI18n()(NotificationTemplates); From 182dce3dc34bf807def911a611b1e1cce55c6f70 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Tue, 21 Jul 2020 16:39:58 -0700 Subject: [PATCH 011/188] flushing out notification template detail --- .../NotificationTemplate.jsx | 59 ++++++++++++++++++- .../NotificationTemplateDetail.jsx | 48 +++++++++++++++ .../NotificationTemplateDetail/index.js | 1 + .../NotificationTemplateList.jsx | 12 ++-- .../NotificationTemplateListItem.jsx | 50 +++++++++++++++- .../OrganizationList/OrganizationListItem.jsx | 16 ++--- .../TemplateList/TemplateListItem.jsx | 8 +-- 7 files changed, 165 insertions(+), 29 deletions(-) create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx index d271962a40..5182ba645a 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplate.jsx @@ -1,5 +1,58 @@ -import React from 'react'; +import React, { useEffect, useCallback } from 'react'; +import { t } from '@lingui/macro'; +import { withI18n } from '@lingui/react'; +import { Card, PageSection } from '@patternfly/react-core'; +import { Link, useParams } from 'react-router-dom'; +import useRequest from '../../util/useRequest'; +import ContentError from '../../components/ContentError'; +import { NotificationTemplatesAPI } from '../../api'; +import NotificationTemplateDetail from './NotificationTemplateDetail'; -export default function NotificationTemplate() { - return
; +function NotificationTemplate({ i18n, setBreadcrumb }) { + const { id: templateId } = useParams(); + const { + result: template, + isLoading, + error, + request: fetchTemplate, + } = useRequest( + useCallback(async () => { + const { data } = await NotificationTemplatesAPI.readDetail(templateId); + return data; + }, [templateId]), + null + ); + + useEffect(() => { + fetchTemplate(); + }, [fetchTemplate]); + + if (error) { + return ( + + + + {error.response.status === 404 && ( + + {i18n._(t`Notification Template not found.`)}{' '} + + {i18n._(t`View all Notification Templates.`)} + + + )} + + + + ); + } + + return ( + + + + + + ); } + +export default withI18n()(NotificationTemplate); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx new file mode 100644 index 0000000000..5c8a592a1c --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -0,0 +1,48 @@ +import React, { Fragment, useState, useEffect, useCallback } from 'react'; +import { Link, useHistory, useParams } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { + Button, + Chip, + TextList, + TextListItem, + TextListItemVariants, + TextListVariants, + Label, +} from '@patternfly/react-core'; +import { t } from '@lingui/macro'; + +import AlertModal from '../../../components/AlertModal'; +import { CardBody, CardActionsRow } from '../../../components/Card'; +import ChipGroup from '../../../components/ChipGroup'; +import ContentError from '../../../components/ContentError'; +import ContentLoading from '../../../components/ContentLoading'; +import CredentialChip from '../../../components/CredentialChip'; +import { + Detail, + DetailList, + DeletedDetail, + UserDateDetail, +} from '../../../components/DetailList'; +import DeleteButton from '../../../components/DeleteButton'; +import ErrorDetail from '../../../components/ErrorDetail'; +import LaunchButton from '../../../components/LaunchButton'; +import { VariablesDetail } from '../../../components/CodeMirrorInput'; +import { JobTemplatesAPI } from '../../../api'; +import useRequest, { useDismissableError } from '../../../util/useRequest'; + +function NotificationTemplateDetail({ i18n, template }) { + return ( + + + + + + ); +} + +export default withI18n()(NotificationTemplateDetail); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js new file mode 100644 index 0000000000..431403014d --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/index.js @@ -0,0 +1 @@ +export default from './NotificationTemplateDetail'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx index 50d0f3f400..3dac16f6a2 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.jsx @@ -105,12 +105,8 @@ function NotificationTemplatesList({ i18n }) { isDefault: true, }, { - name: i18n._(t`Created By (Username)`), - key: 'created_by__username', - }, - { - name: i18n._(t`Modified By (Username)`), - key: 'modified_by__username', + name: i18n._(t`Type`), + key: 'notification_type', }, ]} toolbarSortColumns={[ @@ -118,6 +114,10 @@ function NotificationTemplatesList({ i18n }) { name: i18n._(t`Name`), key: 'name', }, + { + name: i18n._(t`Type`), + key: 'notification_type', + }, ]} renderToolbar={props => ( {}; + const labelId = `template-name-${template.id}`; + return ( @@ -37,9 +48,42 @@ export default function NotificationTemplatesListItem({ {template.name} , + + {template.notification_type} + , ]} /> + + {template.summary_fields.user_capabilities.edit ? ( + + + + ) : ( +
+ )} + + + + ); } + +export default withI18n()(NotificationTemplateListItem); diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx index 51f78c173c..37d01a9e0a 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx @@ -62,12 +62,10 @@ function OrganizationListItem({ /> - - - {organization.name} - - + + + {organization.name} + , @@ -85,11 +83,7 @@ function OrganizationListItem({ , ]} /> - + {organization.summary_fields.user_capabilities.edit ? ( + )} + {summary_fields.user_capabilities && + summary_fields.user_capabilities.delete && ( + + {i18n._(t`Delete`)} + + )} + + {error && ( + + {i18n._(t`Failed to delete notification.`)} + + + )} ); } diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx new file mode 100644 index 0000000000..b089b6b89f --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; +import { CardBody } from '../../../components/Card'; +import { OrganizationsAPI } from '../../../api'; +import { Config } from '../../../contexts/Config'; + +import NotificationTemplateForm from '../shared/NotificationTemplateForm'; + +function NotificationTemplateEdit({ template }) { + const detailsUrl = `/notification_templates/${template.id}/details`; + const history = useHistory(); + const [formError, setFormError] = useState(null); + + const handleSubmit = async ( + values, + groupsToAssociate, + groupsToDisassociate + ) => { + try { + await OrganizationsAPI.update(template.id, values); + await Promise.all( + groupsToAssociate.map(id => + OrganizationsAPI.associateInstanceGroup(template.id, id) + ) + ); + await Promise.all( + groupsToDisassociate.map(id => + OrganizationsAPI.disassociateInstanceGroup(template.id, id) + ) + ); + history.push(detailsUrl); + } catch (error) { + setFormError(error); + } + }; + + const handleCancel = () => { + history.push(detailsUrl); + }; + + return ( + + + {({ me }) => ( + + )} + + + ); +} + +NotificationTemplateEdit.propTypes = { + template: PropTypes.shape().isRequired, +}; + +NotificationTemplateEdit.contextTypes = { + custom_virtualenvs: PropTypes.arrayOf(PropTypes.string), +}; + +export { NotificationTemplateEdit as _NotificationTemplateEdit }; +export default NotificationTemplateEdit; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/index.js new file mode 100644 index 0000000000..be9b40a69c --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/index.js @@ -0,0 +1,3 @@ +import NotificationTemplateEdit from './NotificationTemplateEdit'; + +export default NotificationTemplateEdit; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx index bf2ea92fb8..f5bf5f8be4 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -14,6 +14,7 @@ import { } from '@patternfly/react-core'; import { PencilAltIcon, BellIcon } from '@patternfly/react-icons'; import DataListCell from '../../../components/DataListCell'; +import { NOTIFICATION_TYPES } from '../constants'; const DataListAction = styled(_DataListAction)` align-items: center; @@ -49,7 +50,8 @@ function NotificationTemplateListItem({ , - {template.notification_type} + {NOTIFICATION_TYPES[template.notification_type] || + template.notification_type} , ]} /> diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js index 06c347d889..335e76dd6c 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/index.js @@ -1,4 +1,4 @@ -import NotificationTemplatesList from './NotificationTemplateList'; +import NotificationTemplateList from './NotificationTemplateList'; -export default NotificationTemplatesList; -export { default as NotificationTemplatesListItem } from './NotificationTemplateListItem'; +export default NotificationTemplateList; +export { default as NotificationTemplateListItem } from './NotificationTemplateListItem'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/constants.js b/awx/ui_next/src/screens/NotificationTemplate/constants.js new file mode 100644 index 0000000000..5937e48743 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/constants.js @@ -0,0 +1,12 @@ +/* eslint-disable-next-line import/prefer-default-export */ +export const NOTIFICATION_TYPES = { + email: 'Email', + grafana: 'Grafana', + irc: 'IRC', + mattermost: 'Mattermost', + pagerduty: 'Pagerduty', + rocketchat: 'Rocket.Chat', + slack: 'Slack', + twilio: 'Twilio', + webhook: 'Webhook', +}; diff --git a/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx new file mode 100644 index 0000000000..c08caaa3e5 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/shared/NotificationTemplateForm.jsx @@ -0,0 +1,3 @@ +export default function NotificationTemplateForm() { + // +} From 1405f6ca51624081af40707b61947108e7865981 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 6 Aug 2020 11:37:23 -0700 Subject: [PATCH 014/188] add notification status indicator --- .../components/StatusLabel/StatusLabel.jsx | 67 +++++++++++++++++++ .../src/components/StatusLabel/index.js | 1 + .../NotificationTemplateListItem.jsx | 28 +++++--- 3 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 awx/ui_next/src/components/StatusLabel/StatusLabel.jsx create mode 100644 awx/ui_next/src/components/StatusLabel/index.js diff --git a/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx new file mode 100644 index 0000000000..95ba558cea --- /dev/null +++ b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx @@ -0,0 +1,67 @@ +import 'styled-components/macro'; +import React from 'react'; +import { oneOf } from 'prop-types'; +import { Label } from '@patternfly/react-core'; +import { + CheckCircleIcon, + ExclamationCircleIcon, + SyncAltIcon, + ExclamationTriangleIcon, + ClockIcon, +} from '@patternfly/react-icons'; +import styled, { keyframes } from 'styled-components'; + +const Spin = keyframes` + from { + transform: rotate(0); + } + to { + transform: rotate(1turn); + } +`; + +const RunningIcon = styled(SyncAltIcon)` + animation: ${Spin} 1.75s linear infinite; +`; + +const colors = { + success: 'green', + failed: 'red', + error: 'red', + running: 'blue', + pending: 'blue', + waiting: 'grey', + canceled: 'orange', +}; +const icons = { + success: CheckCircleIcon, + failed: ExclamationCircleIcon, + error: ExclamationCircleIcon, + running: RunningIcon, + pending: ClockIcon, + waiting: ClockIcon, + canceled: ExclamationTriangleIcon, +}; + +export default function StatusLabel({ status }) { + const label = status.charAt(0).toUpperCase() + status.slice(1); + const color = colors[status] || 'grey'; + const Icon = icons[status]; + + return ( + + ); +} + +StatusLabel.propTypes = { + status: oneOf([ + 'success', + 'failed', + 'error', + 'running', + 'pending', + 'canceled', + ]).isRequired, +}; diff --git a/awx/ui_next/src/components/StatusLabel/index.js b/awx/ui_next/src/components/StatusLabel/index.js new file mode 100644 index 0000000000..b9dfc8cd99 --- /dev/null +++ b/awx/ui_next/src/components/StatusLabel/index.js @@ -0,0 +1 @@ +export { default } from './StatusLabel'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx index f5bf5f8be4..102e4b9777 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -1,3 +1,4 @@ +import 'styled-components/macro'; import React from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -14,6 +15,7 @@ import { } from '@patternfly/react-core'; import { PencilAltIcon, BellIcon } from '@patternfly/react-icons'; import DataListCell from '../../../components/DataListCell'; +import StatusLabel from '../../../components/StatusLabel'; import { NOTIFICATION_TYPES } from '../constants'; const DataListAction = styled(_DataListAction)` @@ -33,6 +35,8 @@ function NotificationTemplateListItem({ const sendTestNotification = () => {}; const labelId = `template-name-${template.id}`; + const lastNotification = template.summary_fields?.recent_notifications[0]; + return ( @@ -49,13 +53,28 @@ function NotificationTemplateListItem({ {template.name} , + + {lastNotification && ( + + )} + , + {i18n._(t`Type`)} {NOTIFICATION_TYPES[template.notification_type] || template.notification_type} , ]} /> + + + {template.summary_fields.user_capabilities.edit ? ( )} - - - From 8bb1c985c05340c7566a6b7ba9ae196abe3277f2 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Fri, 7 Aug 2020 11:28:01 -0700 Subject: [PATCH 015/188] send test notifications --- .../src/api/models/NotificationTemplates.js | 4 +++ .../components/StatusLabel/StatusLabel.jsx | 1 + .../NotificationTemplateListItem.jsx | 32 +++++++++++++++---- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/awx/ui_next/src/api/models/NotificationTemplates.js b/awx/ui_next/src/api/models/NotificationTemplates.js index 7736921ad2..69cd5f4022 100644 --- a/awx/ui_next/src/api/models/NotificationTemplates.js +++ b/awx/ui_next/src/api/models/NotificationTemplates.js @@ -5,6 +5,10 @@ class NotificationTemplates extends Base { super(http); this.baseUrl = '/api/v2/notification_templates/'; } + + test(id) { + return this.http.post(`${this.baseUrl}${id}/test/`); + } } export default NotificationTemplates; diff --git a/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx index 95ba558cea..0f2be56fdc 100644 --- a/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx +++ b/awx/ui_next/src/components/StatusLabel/StatusLabel.jsx @@ -62,6 +62,7 @@ StatusLabel.propTypes = { 'error', 'running', 'pending', + 'waiting', 'canceled', ]).isRequired, }; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx index 102e4b9777..ed26638ed6 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -1,5 +1,5 @@ import 'styled-components/macro'; -import React from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; @@ -14,8 +14,10 @@ import { Tooltip, } from '@patternfly/react-core'; import { PencilAltIcon, BellIcon } from '@patternfly/react-icons'; +import { NotificationTemplatesAPI } from '../../../api'; import DataListCell from '../../../components/DataListCell'; import StatusLabel from '../../../components/StatusLabel'; +import useRequest from '../../../util/useRequest'; import { NOTIFICATION_TYPES } from '../constants'; const DataListAction = styled(_DataListAction)` @@ -32,10 +34,27 @@ function NotificationTemplateListItem({ onSelect, i18n, }) { - const sendTestNotification = () => {}; - const labelId = `template-name-${template.id}`; + const latestStatus = template.summary_fields?.recent_notifications[0]?.status; + const [status, setStatus] = useState(latestStatus); - const lastNotification = template.summary_fields?.recent_notifications[0]; + useEffect(() => { + setStatus(latestStatus); + }, [latestStatus]); + + const { request: sendTestNotification, isLoading, error } = useRequest( + useCallback(() => { + NotificationTemplatesAPI.test(template.id); + setStatus('pending'); + }, [template.id]) + ); + + useEffect(() => { + if (error) { + setStatus('error'); + } + }, [error]); + + const labelId = `template-name-${template.id}`; return ( @@ -54,9 +73,7 @@ function NotificationTemplateListItem({ , - {lastNotification && ( - - )} + {status && } , {i18n._(t`Type`)} @@ -71,6 +88,7 @@ function NotificationTemplateListItem({ aria-label={i18n._(t`Test Notification`)} variant="plain" onClick={sendTestNotification} + disabled={isLoading} > From 4c555815b35fefb2bb7d914e08395deca2cb50b8 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 10 Aug 2020 16:23:54 -0700 Subject: [PATCH 016/188] add notification list tests --- .../StatusLabel/StatusLabel.test.jsx | 61 ++++++ .../NotificationTemplateDetail.jsx | 20 +- .../NotificationTemplateList.test.jsx | 202 ++++++++++++++++++ .../NotificationTemplateListItem.jsx | 11 +- .../NotificationTemplateListItem.test.jsx | 64 ++++++ .../NotificationTemplates.test.jsx | 6 - 6 files changed, 336 insertions(+), 28 deletions(-) create mode 100644 awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx create mode 100644 awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx diff --git a/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx b/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx new file mode 100644 index 0000000000..58fb6c1a28 --- /dev/null +++ b/awx/ui_next/src/components/StatusLabel/StatusLabel.test.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import StatusLabel from './StatusLabel'; + +describe('StatusLabel', () => { + test('should render success', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('CheckCircleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('green'); + expect(wrapper.text()).toEqual('Success'); + }); + + test('should render failed', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('red'); + expect(wrapper.text()).toEqual('Failed'); + }); + + test('should render error', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('red'); + expect(wrapper.text()).toEqual('Error'); + }); + + test('should render running', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('SyncAltIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('blue'); + expect(wrapper.text()).toEqual('Running'); + }); + + test('should render pending', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ClockIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('blue'); + expect(wrapper.text()).toEqual('Pending'); + }); + + test('should render waiting', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ClockIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('grey'); + expect(wrapper.text()).toEqual('Waiting'); + }); + + test('should render canceled', () => { + const wrapper = mount(); + expect(wrapper).toHaveLength(1); + expect(wrapper.find('ExclamationTriangleIcon')).toHaveLength(1); + expect(wrapper.find('Label').prop('color')).toEqual('orange'); + expect(wrapper.text()).toEqual('Canceled'); + }); +}); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx index 18f45a4c21..d7f37f9fab 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -1,33 +1,17 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { Link, useHistory } from 'react-router-dom'; import { withI18n } from '@lingui/react'; -import { - Button, - Chip, - TextList, - TextListItem, - TextListItemVariants, - TextListVariants, - Label, -} from '@patternfly/react-core'; +import { Button } from '@patternfly/react-core'; import { t } from '@lingui/macro'; - import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; -import ChipGroup from '../../../components/ChipGroup'; -import ContentError from '../../../components/ContentError'; -import ContentLoading from '../../../components/ContentLoading'; -import CredentialChip from '../../../components/CredentialChip'; import { Detail, DetailList, DeletedDetail, - UserDateDetail, } from '../../../components/DetailList'; import DeleteButton from '../../../components/DeleteButton'; import ErrorDetail from '../../../components/ErrorDetail'; -import LaunchButton from '../../../components/LaunchButton'; -import { VariablesDetail } from '../../../components/CodeMirrorInput'; import { NotificationTemplatesAPI } from '../../../api'; import useRequest, { useDismissableError } from '../../../util/useRequest'; import { NOTIFICATION_TYPES } from '../constants'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx new file mode 100644 index 0000000000..d39bffe087 --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateList.test.jsx @@ -0,0 +1,202 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { OrganizationsAPI } from '../../../api'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import NotificationTemplateList from './NotificationTemplateList'; + +jest.mock('../../../api'); + +const mockTemplates = { + data: { + count: 3, + results: [ + { + name: 'Boston', + id: 1, + url: '/notification_templates/1', + type: 'slack', + summary_fields: { + recent_notifications: [ + { + status: 'success', + }, + ], + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + name: 'Minneapolis', + id: 2, + url: '/notification_templates/2', + summary_fields: { + recent_notifications: [], + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + { + name: 'Philidelphia', + id: 3, + url: '/notification_templates/3', + summary_fields: { + recent_notifications: [ + { + status: 'failed', + }, + { + status: 'success', + }, + ], + user_capabilities: { + delete: true, + edit: true, + }, + }, + }, + ], + }, +}; + +describe('', () => { + let wrapper; + beforeEach(() => { + OrganizationsAPI.read.mockResolvedValue(mockTemplates); + OrganizationsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + }, + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should load notifications', async () => { + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1); + expect(wrapper.find('NotificationTemplateListItem').length).toBe(3); + }); + + test('should select item', async () => { + const itemCheckboxInput = 'input#select-template-1'; + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(false); + await act(async () => { + wrapper + .find(itemCheckboxInput) + .closest('DataListCheck') + .props() + .onChange(); + }); + wrapper.update(); + expect(wrapper.find(itemCheckboxInput).prop('checked')).toEqual(true); + }); + + test('should delete notifications', async () => { + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1); + await act(async () => { + wrapper + .find('Checkbox#select-all') + .props() + .onChange(true); + }); + wrapper.update(); + await act(async () => { + wrapper.find('button[aria-label="Delete"]').simulate('click'); + wrapper.update(); + }); + const deleteButton = global.document.querySelector( + 'body div[role="dialog"] button[aria-label="confirm delete"]' + ); + expect(deleteButton).not.toEqual(null); + await act(async () => { + deleteButton.click(); + }); + expect(OrganizationsAPI.destroy).toHaveBeenCalledTimes(3); + expect(OrganizationsAPI.read).toHaveBeenCalledTimes(2); + }); + + test('should show error dialog shown for failed deletion', async () => { + const itemCheckboxInput = 'input#select-template-1'; + OrganizationsAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/organizations/1', + }, + data: 'An error occurred', + }, + }) + ); + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + await act(async () => { + wrapper + .find(itemCheckboxInput) + .closest('DataListCheck') + .props() + .onChange(); + }); + wrapper.update(); + await act(async () => { + wrapper.find('button[aria-label="Delete"]').simulate('click'); + wrapper.update(); + }); + const deleteButton = global.document.querySelector( + 'body div[role="dialog"] button[aria-label="confirm delete"]' + ); + expect(deleteButton).not.toEqual(null); + await act(async () => { + deleteButton.click(); + }); + wrapper.update(); + + const modal = wrapper.find('Modal'); + expect(modal.prop('isOpen')).toEqual(true); + expect(modal.prop('title')).toEqual('Error!'); + }); + + test('should show add button', async () => { + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(wrapper.find('ToolbarAddButton').length).toBe(1); + }); + + test('should hide add button (rbac)', async () => { + OrganizationsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + }, + }, + }); + await act(async () => { + wrapper = mountWithContexts(); + }); + wrapper.update(); + expect(wrapper.find('ToolbarAddButton').length).toBe(0); + }); +}); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx index ed26638ed6..0087e7f9a8 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.jsx @@ -34,7 +34,10 @@ function NotificationTemplateListItem({ onSelect, i18n, }) { - const latestStatus = template.summary_fields?.recent_notifications[0]?.status; + const recentNotifications = template.summary_fields?.recent_notifications; + const latestStatus = recentNotifications + ? recentNotifications[0]?.status + : null; const [status, setStatus] = useState(latestStatus); useEffect(() => { @@ -44,7 +47,7 @@ function NotificationTemplateListItem({ const { request: sendTestNotification, isLoading, error } = useRequest( useCallback(() => { NotificationTemplatesAPI.test(template.id); - setStatus('pending'); + setStatus('running'); }, [template.id]) ); @@ -72,11 +75,11 @@ function NotificationTemplateListItem({ {template.name} , - + {status && } , - {i18n._(t`Type`)} + {i18n._(t`Type:`)}{' '} {NOTIFICATION_TYPES[template.notification_type] || template.notification_type} , diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx new file mode 100644 index 0000000000..5a4566779e --- /dev/null +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateList/NotificationTemplateListItem.test.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { NotificationTemplatesAPI } from '../../../api'; +import NotificationTemplateListItem from './NotificationTemplateListItem'; + +jest.mock('../../../api/models/NotificationTemplates'); + +const template = { + id: 3, + notification_type: 'slack', + name: 'Test Notification', + summary_fields: { + user_capabilities: { + edit: true, + }, + recent_notifications: [ + { + status: 'success', + }, + ], + }, +}; + +describe('', () => { + test('should render template row', () => { + const wrapper = mountWithContexts( + + ); + + const cells = wrapper.find('DataListCell'); + expect(cells).toHaveLength(3); + expect(cells.at(0).text()).toEqual('Test Notification'); + expect(cells.at(1).text()).toEqual('Success'); + expect(cells.at(2).text()).toEqual('Type: Slack'); + }); + + test('should send test notification', async () => { + NotificationTemplatesAPI.test.mockResolvedValue({}); + + const wrapper = mountWithContexts( + + ); + await act(async () => { + wrapper + .find('Button') + .at(0) + .invoke('onClick')(); + }); + expect(NotificationTemplatesAPI.test).toHaveBeenCalledTimes(1); + expect( + wrapper + .find('DataListCell') + .at(1) + .text() + ).toEqual('Running'); + }); +}); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx index 93babc8e06..9333850cf9 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplates.test.jsx @@ -1,18 +1,14 @@ import React from 'react'; - import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; - import NotificationTemplates from './NotificationTemplates'; describe('', () => { let pageWrapper; let pageSections; - let title; beforeEach(() => { pageWrapper = mountWithContexts(); pageSections = pageWrapper.find('PageSection'); - title = pageWrapper.find('Title'); }); afterEach(() => { @@ -22,8 +18,6 @@ describe('', () => { test('initially renders without crashing', () => { expect(pageWrapper.length).toBe(1); expect(pageSections.length).toBe(2); - expect(title.length).toBe(1); - expect(title.props().size).toBe('2xl'); expect(pageSections.first().props().variant).toBe('light'); }); }); From 65d4c347c9152ee11efa3a5457017b914f3c1506 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Mon, 10 Aug 2020 16:48:29 -0700 Subject: [PATCH 017/188] add ObjectDetails for HTTP Headers display --- .../components/DetailList/ObjectDetail.jsx | 51 +++++++++++++++++++ .../src/components/DetailList/index.js | 1 + .../NotificationTemplateDetail.jsx | 6 ++- 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 awx/ui_next/src/components/DetailList/ObjectDetail.jsx diff --git a/awx/ui_next/src/components/DetailList/ObjectDetail.jsx b/awx/ui_next/src/components/DetailList/ObjectDetail.jsx new file mode 100644 index 0000000000..bf008866a8 --- /dev/null +++ b/awx/ui_next/src/components/DetailList/ObjectDetail.jsx @@ -0,0 +1,51 @@ +import 'styled-components/macro'; +import React from 'react'; +import { shape, node, number } from 'prop-types'; +import { TextListItemVariants } from '@patternfly/react-core'; +import { DetailName, DetailValue } from './Detail'; +import CodeMirrorInput from '../CodeMirrorInput'; + +function ObjectDetail({ value, label, rows, fullHeight }) { + return ( + <> + +
+ + {label} + +
+
+ + + + + ); +} +ObjectDetail.propTypes = { + value: shape.isRequired, + label: node.isRequired, + rows: number, +}; +ObjectDetail.defaultProps = { + rows: null, +}; + +export default ObjectDetail; diff --git a/awx/ui_next/src/components/DetailList/index.js b/awx/ui_next/src/components/DetailList/index.js index 6a12824bad..f16ed0e292 100644 --- a/awx/ui_next/src/components/DetailList/index.js +++ b/awx/ui_next/src/components/DetailList/index.js @@ -3,3 +3,4 @@ export { default as Detail, DetailName, DetailValue } from './Detail'; export { default as DeletedDetail } from './DeletedDetail'; export { default as UserDateDetail } from './UserDateDetail'; export { default as DetailBadge } from './DetailBadge'; +export { default as ObjectDetail } from './ObjectDetail'; diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx index d7f37f9fab..24c199836f 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -9,6 +9,7 @@ import { Detail, DetailList, DeletedDetail, + ObjectDetail, } from '../../../components/DetailList'; import DeleteButton from '../../../components/DeleteButton'; import ErrorDetail from '../../../components/ErrorDetail'; @@ -310,11 +311,12 @@ function NotificationTemplateDetail({ i18n, template }) { value={configuration.http_method} dataCy="nt-detail-webhook-http-method" /> - {/* */} + /> )} From 5fb1b1ceea9512c75a5e70e7e9027b533574ec57 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 11 Aug 2020 12:52:52 -0400 Subject: [PATCH 018/188] changelog updates for recent additions --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63473b431e..f482fe3a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,11 @@ This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/`. ## 14.1.0 (TBD) +- Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 +- Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847 - Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 +- Upgraded git-python to fix a bug that caused workflows to sometimes fail - https://github.com/ansible/awx/issues/6119 +- Fixed a bug in the AWX CLI that prevented Workflow nodes from importing properly - https://github.com/ansible/awx/issues/7793 ## 14.0.0 (Aug 6, 2020) - As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to: From d27d4e4f28d01da23a4d508d2b9cdbf5fa1a5b52 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Tue, 11 Aug 2020 10:36:39 -0700 Subject: [PATCH 019/188] workaround import/dependency bug in tests --- awx/ui_next/src/components/DetailList/index.js | 6 +++++- .../NotificationTemplateDetail.jsx | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/components/DetailList/index.js b/awx/ui_next/src/components/DetailList/index.js index f16ed0e292..8bebb27ce4 100644 --- a/awx/ui_next/src/components/DetailList/index.js +++ b/awx/ui_next/src/components/DetailList/index.js @@ -3,4 +3,8 @@ export { default as Detail, DetailName, DetailValue } from './Detail'; export { default as DeletedDetail } from './DeletedDetail'; export { default as UserDateDetail } from './UserDateDetail'; export { default as DetailBadge } from './DetailBadge'; -export { default as ObjectDetail } from './ObjectDetail'; +/* + NOTE: ObjectDetail cannot be imported here, as it causes circular + dependencies in testing environment. Import it directly from + DetailList/ObjectDetail +*/ diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx index 24c199836f..951ba5bd8b 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateDetail/NotificationTemplateDetail.jsx @@ -9,8 +9,8 @@ import { Detail, DetailList, DeletedDetail, - ObjectDetail, } from '../../../components/DetailList'; +import ObjectDetail from '../../../components/DetailList/ObjectDetail'; import DeleteButton from '../../../components/DeleteButton'; import ErrorDetail from '../../../components/ErrorDetail'; import { NotificationTemplatesAPI } from '../../../api'; From 8a4d45ddb6efda72efe461a74c90c1527415d8c7 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Mon, 3 Aug 2020 17:46:28 -0400 Subject: [PATCH 020/188] Add smart inventory host list view --- .../src/components/HostToggle/HostToggle.jsx | 8 +- .../components/HostToggle/HostToggle.test.jsx | 14 +- .../screens/Host/HostList/HostListItem.jsx | 6 +- .../src/screens/Inventory/Inventories.jsx | 9 +- .../InventoryHostDetail.test.jsx | 13 +- .../src/screens/Inventory/SmartInventory.jsx | 2 +- .../SmartInventoryHostList.jsx | 120 ++++++++++++++++ .../SmartInventoryHostList.test.jsx | 136 ++++++++++++++++++ .../SmartInventoryHostListItem.jsx | 84 +++++++++++ .../SmartInventoryHostListItem.test.jsx | 52 +++++++ .../SmartInventoryHosts.jsx | 20 ++- .../SmartInventoryHosts.test.jsx | 28 ++++ 12 files changed, 466 insertions(+), 26 deletions(-) create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx create mode 100644 awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx diff --git a/awx/ui_next/src/components/HostToggle/HostToggle.jsx b/awx/ui_next/src/components/HostToggle/HostToggle.jsx index d08a15a70c..43452ee3f0 100644 --- a/awx/ui_next/src/components/HostToggle/HostToggle.jsx +++ b/awx/ui_next/src/components/HostToggle/HostToggle.jsx @@ -8,7 +8,7 @@ import ErrorDetail from '../ErrorDetail'; import useRequest from '../../util/useRequest'; import { HostsAPI } from '../../api'; -function HostToggle({ host, onToggle, className, i18n }) { +function HostToggle({ host, isDisabled = false, onToggle, className, i18n }) { const [isEnabled, setIsEnabled] = useState(host.enabled); const [showError, setShowError] = useState(false); @@ -54,7 +54,11 @@ function HostToggle({ host, onToggle, className, i18n }) { label={i18n._(t`On`)} labelOff={i18n._(t`Off`)} isChecked={isEnabled} - isDisabled={isLoading || !host.summary_fields.user_capabilities.edit} + isDisabled={ + isLoading || + isDisabled || + !host.summary_fields.user_capabilities.edit + } onChange={toggleHost} aria-label={i18n._(t`Toggle host`)} /> diff --git a/awx/ui_next/src/components/HostToggle/HostToggle.test.jsx b/awx/ui_next/src/components/HostToggle/HostToggle.test.jsx index 63dd971285..7391036bf2 100644 --- a/awx/ui_next/src/components/HostToggle/HostToggle.test.jsx +++ b/awx/ui_next/src/components/HostToggle/HostToggle.test.jsx @@ -19,7 +19,7 @@ const mockHost = { }, user_capabilities: { delete: true, - update: true, + edit: true, }, recent_jobs: [], }, @@ -68,6 +68,18 @@ describe('', () => { expect(onToggle).toHaveBeenCalledWith(true); }); + test('should be enabled', async () => { + const wrapper = mountWithContexts(); + expect(wrapper.find('Switch').prop('isDisabled')).toEqual(false); + }); + + test('should be disabled', async () => { + const wrapper = mountWithContexts( + + ); + expect(wrapper.find('Switch').prop('isDisabled')).toEqual(true); + }); + test('should show error modal', async () => { HostsAPI.update.mockImplementation(() => { throw new Error('nope'); diff --git a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx index e2751f6f06..377fb453ab 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostListItem.jsx @@ -54,11 +54,7 @@ function HostListItem({ i18n, host, isSelected, onSelect, detailUrl }) { {i18n._(t`Inventory`)} {host.summary_fields.inventory.name} diff --git a/awx/ui_next/src/screens/Inventory/Inventories.jsx b/awx/ui_next/src/screens/Inventory/Inventories.jsx index 0ad93adbe9..30a46b64c3 100644 --- a/awx/ui_next/src/screens/Inventory/Inventories.jsx +++ b/awx/ui_next/src/screens/Inventory/Inventories.jsx @@ -105,14 +105,7 @@ function Inventories({ i18n }) { - - {({ me }) => ( - - )} - + diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.test.jsx index ae7d60b1f7..a6a7c7ef73 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostDetail/InventoryHostDetail.test.jsx @@ -77,11 +77,18 @@ describe('', () => { describe('User has read-only permissions', () => { beforeAll(() => { - const readOnlyHost = { ...mockHost }; + const readOnlyHost = { + ...mockHost, + summary_fields: { + ...mockHost.summary_fields, + user_capabilities: { + ...mockHost.summary_fields.user_capabilities, + }, + }, + }; readOnlyHost.summary_fields.user_capabilities.edit = false; readOnlyHost.summary_fields.recent_jobs = []; - - wrapper = mountWithContexts(); + wrapper = mountWithContexts(); }); afterAll(() => { diff --git a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx index acb08661b6..18291a2959 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventory.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventory.jsx @@ -47,7 +47,7 @@ function SmartInventory({ i18n, setBreadcrumb }) { useEffect(() => { fetchInventory(); - }, [fetchInventory, location.pathname]); + }, [fetchInventory]); useEffect(() => { if (inventory) { diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx new file mode 100644 index 0000000000..aa6290669c --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.jsx @@ -0,0 +1,120 @@ +import React, { useEffect, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Button } from '@patternfly/react-core'; +import DataListToolbar from '../../../components/DataListToolbar'; +import PaginatedDataList from '../../../components/PaginatedDataList'; +import SmartInventoryHostListItem from './SmartInventoryHostListItem'; +import useRequest from '../../../util/useRequest'; +import useSelected from '../../../util/useSelected'; +import { getQSConfig, parseQueryString } from '../../../util/qs'; +import { InventoriesAPI } from '../../../api'; +import { Inventory } from '../../../types'; + +const QS_CONFIG = getQSConfig('host', { + page: 1, + page_size: 20, + order_by: 'name', +}); + +function SmartInventoryHostList({ i18n, inventory }) { + const location = useLocation(); + + const { + result: { hosts, count }, + error: contentError, + isLoading, + request: fetchHosts, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const { data } = await InventoriesAPI.readHosts(inventory.id, params); + return { + hosts: data.results, + count: data.count, + }; + }, [location.search, inventory.id]), + { + hosts: [], + count: 0, + } + ); + + const { selected, isAllSelected, handleSelect, setSelected } = useSelected( + hosts + ); + + useEffect(() => { + fetchHosts(); + }, [fetchHosts]); + + return ( + ( + setSelected(isSelected ? [...hosts] : [])} + qsConfig={QS_CONFIG} + additionalControls={ + inventory?.summary_fields?.user_capabilities?.adhoc + ? [ + , + ] + : [] + } + /> + )} + renderItem={host => ( + row.id === host.id)} + onSelect={() => handleSelect(host)} + /> + )} + /> + ); +} + +SmartInventoryHostList.propTypes = { + inventory: Inventory.isRequired, +}; + +export default withI18n()(SmartInventoryHostList); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx new file mode 100644 index 0000000000..ae3f00d66f --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostList.test.jsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { InventoriesAPI } from '../../../api'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; +import SmartInventoryHostList from './SmartInventoryHostList'; +import mockInventory from '../shared/data.inventory.json'; +import mockHosts from '../shared/data.hosts.json'; + +jest.mock('../../../api'); + +describe('', () => { + describe('User has adhoc permissions', () => { + let wrapper; + const clonedInventory = { + ...mockInventory, + summary_fields: { + ...mockInventory.summary_fields, + user_capabilities: { + ...mockInventory.summary_fields.user_capabilities, + }, + }, + }; + + beforeAll(async () => { + InventoriesAPI.readHosts.mockResolvedValue({ + data: mockHosts, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + afterAll(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('initially renders successfully', () => { + expect(wrapper.find('SmartInventoryHostList').length).toBe(1); + }); + + test('should fetch hosts from api and render them in the list', () => { + expect(InventoriesAPI.readHosts).toHaveBeenCalled(); + expect(wrapper.find('SmartInventoryHostListItem').length).toBe(3); + }); + + test('should disable run commands button when no hosts are selected', () => { + wrapper.find('DataListCheck').forEach(el => { + expect(el.props().checked).toBe(false); + }); + const runCommandsButton = wrapper.find( + 'button[aria-label="Run commands"]' + ); + expect(runCommandsButton.length).toBe(1); + expect(runCommandsButton.prop('disabled')).toEqual(true); + }); + + test('should enable run commands button when at least one host is selected', () => { + act(() => { + wrapper.find('DataListCheck[id="select-host-2"]').invoke('onChange')( + true + ); + }); + wrapper.update(); + const runCommandsButton = wrapper.find( + 'button[aria-label="Run commands"]' + ); + expect(runCommandsButton.prop('disabled')).toEqual(false); + }); + + test('should select and deselect all items', async () => { + act(() => { + wrapper.find('DataListToolbar').invoke('onSelectAll')(true); + }); + wrapper.update(); + wrapper.find('DataListCheck').forEach(el => { + expect(el.props().checked).toEqual(true); + }); + act(() => { + wrapper.find('DataListToolbar').invoke('onSelectAll')(false); + }); + wrapper.update(); + wrapper.find('DataListCheck').forEach(el => { + expect(el.props().checked).toEqual(false); + }); + }); + + test('should show content error when api throws an error', async () => { + InventoriesAPI.readHosts.mockImplementation(() => + Promise.reject(new Error()) + ); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentError', el => el.length === 1); + }); + }); + + describe('User does not have adhoc permissions', () => { + let wrapper; + const clonedInventory = { + ...mockInventory, + summary_fields: { + user_capabilities: { + adhoc: false, + }, + }, + }; + + test('should hide run commands button', async () => { + InventoriesAPI.readHosts.mockResolvedValue({ + data: { results: [], count: 0 }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + const runCommandsButton = wrapper.find( + 'button[aria-label="Run commands"]' + ); + expect(runCommandsButton.length).toBe(0); + jest.clearAllMocks(); + wrapper.unmount(); + }); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx new file mode 100644 index 0000000000..960218206b --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.jsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { string, bool, func } from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import 'styled-components/macro'; + +import { + DataListAction, + DataListCheck, + DataListItem, + DataListItemCells, + DataListItemRow, +} from '@patternfly/react-core'; +import DataListCell from '../../../components/DataListCell'; +import HostToggle from '../../../components/HostToggle'; +import Sparkline from '../../../components/Sparkline'; +import { Host } from '../../../types'; + +function SmartInventoryHostListItem({ + i18n, + detailUrl, + host, + isSelected, + onSelect, +}) { + const recentPlaybookJobs = host.summary_fields.recent_jobs.map(job => ({ + ...job, + type: 'job', + })); + + const labelId = `check-action-${host.id}`; + + return ( + + + + + + {host.name} + +
, + + + , + + <> + {i18n._(t`Inventory`)} + + {host.summary_fields.inventory.name} + + + , + ]} + /> + + + + +
+ ); +} + +SmartInventoryHostListItem.propTypes = { + detailUrl: string.isRequired, + host: Host.isRequired, + isSelected: bool.isRequired, + onSelect: func.isRequired, +}; + +export default withI18n()(SmartInventoryHostListItem); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx new file mode 100644 index 0000000000..a2462a831a --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHostListItem.test.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import SmartInventoryHostListItem from './SmartInventoryHostListItem'; + +const mockHost = { + id: 2, + name: 'Host Two', + url: '/api/v2/hosts/2', + inventory: 1, + summary_fields: { + inventory: { + id: 1, + name: 'Inv 1', + }, + user_capabilities: { + edit: true, + }, + recent_jobs: [], + }, +}; + +describe('', () => { + let wrapper; + + beforeEach(() => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + test('should render expected row cells', () => { + const cells = wrapper.find('DataListCell'); + expect(cells).toHaveLength(3); + expect(cells.at(0).text()).toEqual('Host Two'); + expect(cells.at(1).find('Sparkline').length).toEqual(1); + expect(cells.at(2).text()).toContain('Inv 1'); + }); + + test('should display disabled host toggle', () => { + expect(wrapper.find('HostToggle').length).toBe(1); + expect(wrapper.find('HostToggle Switch').prop('isDisabled')).toEqual(true); + }); +}); diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx index 608e664e95..0aa24cdc59 100644 --- a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.jsx @@ -1,10 +1,18 @@ -import React, { Component } from 'react'; -import { CardBody } from '../../../components/Card'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import SmartInventoryHostList from './SmartInventoryHostList'; +import { Inventory } from '../../../types'; -class SmartInventoryHosts extends Component { - render() { - return Coming soon :); - } +function SmartInventoryHosts({ inventory }) { + return ( + + + + ); } +SmartInventoryHosts.propTypes = { + inventory: Inventory.isRequired, +}; + export default SmartInventoryHosts; diff --git a/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx new file mode 100644 index 0000000000..8fed1ef7f7 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/SmartInventoryHosts/SmartInventoryHosts.test.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { createMemoryHistory } from 'history'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import SmartInventoryHosts from './SmartInventoryHosts'; + +jest.mock('../../../api'); + +describe('', () => { + test('should render smart inventory host list', () => { + const history = createMemoryHistory({ + initialEntries: ['/inventories/smart_inventory/1/hosts'], + }); + const match = { + path: '/inventories/smart_inventory/:id/hosts', + url: '/inventories/smart_inventory/1/hosts', + isExact: true, + }; + const wrapper = mountWithContexts( + , + { + context: { router: { history, route: { match } } }, + } + ); + expect(wrapper.find('SmartInventoryHostList').length).toBe(1); + jest.clearAllMocks(); + wrapper.unmount(); + }); +}); From aace8f5032d63daf35f80e4f65611f8bbe8c8f61 Mon Sep 17 00:00:00 2001 From: nixocio Date: Tue, 11 Aug 2020 16:44:03 -0400 Subject: [PATCH 021/188] Remove undefined prop from SelectedList call Remove undefined prop from `SelectedList` call. --- awx/ui_next/src/components/OptionsList/OptionsList.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/ui_next/src/components/OptionsList/OptionsList.jsx b/awx/ui_next/src/components/OptionsList/OptionsList.jsx index a3b4b39922..b2242d7202 100644 --- a/awx/ui_next/src/components/OptionsList/OptionsList.jsx +++ b/awx/ui_next/src/components/OptionsList/OptionsList.jsx @@ -49,7 +49,6 @@ function OptionsList({ deselectItem(item)} isReadOnly={readOnly} renderItemChip={renderItemChip} From 8f04026404da9d35060cfa9ecd1aa82113713b68 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 11 Aug 2020 14:20:38 -0400 Subject: [PATCH 022/188] kebabify additional controls when advanced search is displayed --- .../AddDropDownButton/AddDropDownButton.jsx | 43 +++- .../DataListToolbar/DataListToolbar.jsx | 189 ++++++++++-------- .../PaginatedDataList/ToolbarAddButton.jsx | 18 +- .../PaginatedDataList/ToolbarDeleteButton.jsx | 106 +++++----- awx/ui_next/src/components/Search/Search.jsx | 4 +- .../src/components/Search/Search.test.jsx | 42 +++- awx/ui_next/src/contexts/Kebabified.jsx | 8 + 7 files changed, 264 insertions(+), 146 deletions(-) create mode 100644 awx/ui_next/src/contexts/Kebabified.jsx diff --git a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx index 78655e44d9..3bf1963bce 100644 --- a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx +++ b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx @@ -1,25 +1,46 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, Fragment } from 'react'; import { Link } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; import PropTypes from 'prop-types'; -import { Dropdown, DropdownPosition } from '@patternfly/react-core'; +import { + Dropdown, + DropdownPosition, + DropdownItem, +} from '@patternfly/react-core'; import { ToolbarAddButton } from '../PaginatedDataList'; +import { toTitleCase } from '../../util/strings'; +import { useKebabified } from '../../contexts/Kebabified'; -function AddDropDownButton({ dropdownItems }) { +function AddDropDownButton({ dropdownItems, i18n }) { + const { isKebabified } = useKebabified(); const [isOpen, setIsOpen] = useState(false); const element = useRef(null); - const toggle = e => { - if (!element || !element.current.contains(e.target)) { - setIsOpen(false); - } - }; - useEffect(() => { + const toggle = e => { + if (!isKebabified && (!element || !element.current.contains(e.target))) { + setIsOpen(false); + } + }; + document.addEventListener('click', toggle, false); return () => { document.removeEventListener('click', toggle); }; - }, []); + }, [isKebabified]); + + if (isKebabified) { + return ( + + {dropdownItems.map(item => ( + + {toTitleCase(`${i18n._(t`Add`)} ${item.label}`)} + + ))} + + ); + } return (
@@ -52,4 +73,4 @@ AddDropDownButton.propTypes = { }; export { AddDropDownButton as _AddDropDownButton }; -export default AddDropDownButton; +export default withI18n()(AddDropDownButton); diff --git a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx index 9e02017fdf..1ddfb57df6 100644 --- a/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx +++ b/awx/ui_next/src/components/DataListToolbar/DataListToolbar.jsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import PropTypes from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -9,103 +9,128 @@ import { ToolbarGroup, ToolbarItem, ToolbarToggleGroup, + Dropdown, + KebabToggle, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons'; import ExpandCollapse from '../ExpandCollapse'; import Search from '../Search'; import Sort from '../Sort'; - import { SearchColumns, SortColumns, QSConfig } from '../../types'; +import { KebabifiedProvider } from '../../contexts/Kebabified'; -class DataListToolbar extends React.Component { - render() { - const { - itemCount, - clearAllFilters, - searchColumns, - searchableKeys, - relatedSearchableKeys, - sortColumns, - showSelectAll, - isAllSelected, - isCompact, - onSort, - onSearch, - onReplaceSearch, - onRemove, - onCompact, - onExpand, - onSelectAll, - additionalControls, - i18n, - qsConfig, - pagination, - } = this.props; +function DataListToolbar({ + itemCount, + clearAllFilters, + searchColumns, + searchableKeys, + relatedSearchableKeys, + sortColumns, + showSelectAll, + isAllSelected, + isCompact, + onSort, + onSearch, + onReplaceSearch, + onRemove, + onCompact, + onExpand, + onSelectAll, + additionalControls, + i18n, + qsConfig, + pagination, +}) { + const showExpandCollapse = onCompact && onExpand; + const [kebabIsOpen, setKebabIsOpen] = useState(false); + const [advancedSearchShown, setAdvancedSearchShown] = useState(false); - const showExpandCollapse = onCompact && onExpand; - return ( - - - {showSelectAll && ( - - - - - - )} - } breakpoint="lg"> + const onShowAdvancedSearch = shown => { + setAdvancedSearchShown(shown); + setKebabIsOpen(false); + }; + + return ( + + + {showSelectAll && ( + - - - - - - {showExpandCollapse && ( - - - - - - - - )} + + )} + } breakpoint="lg"> + + + + + + + + {showExpandCollapse && ( + + + + + + + + )} + {advancedSearchShown && ( + + } + isOpen={kebabIsOpen} + isPlain + dropdownItems={additionalControls.map(control => { + return ( + + {control} + + ); + })} + /> + + )} + {!advancedSearchShown && ( {additionalControls.map(control => ( {control} ))} - {pagination && itemCount > 0 && ( - {pagination} - )} - - - ); - } + )} + {!advancedSearchShown && pagination && itemCount > 0 && ( + {pagination} + )} + + + ); } DataListToolbar.propTypes = { diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx index 19ae9a68c9..589a09a64e 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx @@ -1,16 +1,32 @@ import React from 'react'; import { string, func } from 'prop-types'; import { Link } from 'react-router-dom'; -import { Button, Tooltip } from '@patternfly/react-core'; +import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import { useKebabified } from '../../contexts/Kebabified'; function ToolbarAddButton({ linkTo, onClick, i18n, isDisabled }) { + const { isKebabified } = useKebabified(); + if (!linkTo && !onClick) { throw new Error( 'ToolbarAddButton requires either `linkTo` or `onClick` prop' ); } + if (isKebabified) { + return ( + + {i18n._(t`Add`)} + + ); + } if (linkTo) { return ( diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx index 2d5625a95c..7be476dfc1 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarDeleteButton.jsx @@ -8,10 +8,11 @@ import { shape, checkPropTypes, } from 'prop-types'; -import { Button, Tooltip } from '@patternfly/react-core'; +import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import AlertModal from '../AlertModal'; +import { Kebabified } from '../../contexts/Kebabified'; const requireNameOrUsername = props => { const { name, username } = props; @@ -138,54 +139,69 @@ class ToolbarDeleteButton extends React.Component { // we can delete the extra
around the below. // See: https://github.com/patternfly/patternfly-react/issues/1894 return ( - - -
- -
-
- {isModalOpen && ( - + {({ isKebabified }) => ( + + {isKebabified ? ( + {i18n._(t`Delete`)} - , - +
+
+ )} + {isModalOpen && ( + + {i18n._(t`Delete`)} + , + , + ]} > - {i18n._(t`Cancel`)} - , - ]} - > -
{i18n._(t`This action will delete the following:`)}
- {itemsToDelete.map(item => ( - - {item.name || item.username} -
-
- ))} -
+
{i18n._(t`This action will delete the following:`)}
+ {itemsToDelete.map(item => ( + + {item.name || item.username} +
+
+ ))} + + )} + )} - + ); } } diff --git a/awx/ui_next/src/components/Search/Search.jsx b/awx/ui_next/src/components/Search/Search.jsx index 8049a326e7..e92f5c2d16 100644 --- a/awx/ui_next/src/components/Search/Search.jsx +++ b/awx/ui_next/src/components/Search/Search.jsx @@ -40,6 +40,7 @@ function Search({ location, searchableKeys, relatedSearchableKeys, + onShowAdvancedSearch, }) { const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false); const [searchKey, setSearchKey] = useState( @@ -62,7 +63,7 @@ function Search({ const { key: actualSearchKey } = columns.find( ({ name }) => name === target.innerText ); - + onShowAdvancedSearch(actualSearchKey === 'advanced'); setIsFilterDropdownOpen(false); setSearchKey(actualSearchKey); }; @@ -301,6 +302,7 @@ Search.propTypes = { columns: SearchColumns.isRequired, onSearch: PropTypes.func, onRemove: PropTypes.func, + onShowAdvancedSearch: PropTypes.func.isRequired, }; Search.defaultProps = { diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx index a34b6bcc09..e896fcdb37 100644 --- a/awx/ui_next/src/components/Search/Search.test.jsx +++ b/awx/ui_next/src/components/Search/Search.test.jsx @@ -36,7 +36,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -64,7 +69,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -95,7 +105,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -119,7 +134,12 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + ); @@ -150,7 +170,11 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + , { context: { router: { history } } } @@ -197,6 +221,7 @@ describe('', () => { qsConfig={qsConfigNew} columns={columns} onRemove={onRemove} + onShowAdvancedSearch={jest.fn} /> , @@ -243,6 +268,7 @@ describe('', () => { qsConfig={qsConfigNew} columns={columns} onRemove={onRemove} + onShowAdvancedSearch={jest.fn} /> , @@ -277,7 +303,11 @@ describe('', () => { collapseListedFiltersBreakpoint="lg" > - + , { context: { router: { history } } } diff --git a/awx/ui_next/src/contexts/Kebabified.jsx b/awx/ui_next/src/contexts/Kebabified.jsx new file mode 100644 index 0000000000..34007631b0 --- /dev/null +++ b/awx/ui_next/src/contexts/Kebabified.jsx @@ -0,0 +1,8 @@ +import React, { useContext } from 'react'; + +// eslint-disable-next-line import/prefer-default-export +export const KebabifiedContext = React.createContext({}); + +export const KebabifiedProvider = KebabifiedContext.Provider; +export const Kebabified = KebabifiedContext.Consumer; +export const useKebabified = () => useContext(KebabifiedContext); From a352de3da7834cf67ea75eccad30e3c86b89bfc5 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 11 Aug 2020 14:43:59 -0400 Subject: [PATCH 023/188] change name of hook to be useKebabifiedMenu --- .../src/components/AddDropDownButton/AddDropDownButton.jsx | 4 ++-- .../src/components/PaginatedDataList/ToolbarAddButton.jsx | 4 ++-- awx/ui_next/src/contexts/Kebabified.jsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx index 3bf1963bce..ca4c4a40b6 100644 --- a/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx +++ b/awx/ui_next/src/components/AddDropDownButton/AddDropDownButton.jsx @@ -10,10 +10,10 @@ import { } from '@patternfly/react-core'; import { ToolbarAddButton } from '../PaginatedDataList'; import { toTitleCase } from '../../util/strings'; -import { useKebabified } from '../../contexts/Kebabified'; +import { useKebabifiedMenu } from '../../contexts/Kebabified'; function AddDropDownButton({ dropdownItems, i18n }) { - const { isKebabified } = useKebabified(); + const { isKebabified } = useKebabifiedMenu(); const [isOpen, setIsOpen] = useState(false); const element = useRef(null); diff --git a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx index 589a09a64e..4c5c295976 100644 --- a/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx +++ b/awx/ui_next/src/components/PaginatedDataList/ToolbarAddButton.jsx @@ -4,10 +4,10 @@ import { Link } from 'react-router-dom'; import { Button, DropdownItem, Tooltip } from '@patternfly/react-core'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { useKebabified } from '../../contexts/Kebabified'; +import { useKebabifiedMenu } from '../../contexts/Kebabified'; function ToolbarAddButton({ linkTo, onClick, i18n, isDisabled }) { - const { isKebabified } = useKebabified(); + const { isKebabified } = useKebabifiedMenu(); if (!linkTo && !onClick) { throw new Error( diff --git a/awx/ui_next/src/contexts/Kebabified.jsx b/awx/ui_next/src/contexts/Kebabified.jsx index 34007631b0..c50431c73f 100644 --- a/awx/ui_next/src/contexts/Kebabified.jsx +++ b/awx/ui_next/src/contexts/Kebabified.jsx @@ -5,4 +5,4 @@ export const KebabifiedContext = React.createContext({}); export const KebabifiedProvider = KebabifiedContext.Provider; export const Kebabified = KebabifiedContext.Consumer; -export const useKebabified = () => useContext(KebabifiedContext); +export const useKebabifiedMenu = () => useContext(KebabifiedContext); From 1323626d5e7eebe5c712cbc8dad1c947b4eb656a Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 11 Aug 2020 16:10:05 -0400 Subject: [PATCH 024/188] add onShowAdvancedSearch callback test --- .../src/components/Search/Search.test.jsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx index e896fcdb37..32c2548a35 100644 --- a/awx/ui_next/src/components/Search/Search.test.jsx +++ b/awx/ui_next/src/components/Search/Search.test.jsx @@ -93,6 +93,52 @@ describe('', () => { expect(onSearch).toBeCalledWith('description__icontains', 'test-321'); }); + test('changing key select to and from advanced causes onShowAdvancedSearch callback to be invoked', () => { + const searchButton = 'button[aria-label="Search submit button"]'; + const searchTextInput = 'input[aria-label="Search text input"]'; + const columns = [ + { name: 'Name', key: 'name__icontains', isDefault: true }, + { name: 'Description', key: 'description__icontains' }, + { name: 'Advanced', key: 'advanced' }, + ]; + const onSearch = jest.fn(); + const onShowAdvancedSearch = jest.fn(); + const wrapper = mountWithContexts( + {}} + collapseListedFiltersBreakpoint="lg" + > + + + + + ); + + act(() => { + wrapper + .find('Select[aria-label="Simple key select"]') + .invoke('onSelect')({ target: { innerText: 'Advanced' } }); + }); + wrapper.update(); + expect(onShowAdvancedSearch).toHaveBeenCalledTimes(1); + expect(onShowAdvancedSearch).toBeCalledWith(true); + jest.clearAllMocks(); + act(() => { + wrapper + .find('Select[aria-label="Simple key select"]') + .invoke('onSelect')({ target: { innerText: 'Description' } }); + }); + wrapper.update(); + expect(onShowAdvancedSearch).toHaveBeenCalledTimes(1); + expect(onShowAdvancedSearch).toBeCalledWith(false); + }); + test('attempt to search with empty string', () => { const searchButton = 'button[aria-label="Search submit button"]'; const searchTextInput = 'input[aria-label="Search text input"]'; From 54d13b63974b5296ca6ed76de8ea3779dc2be311 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 12 Aug 2020 09:23:14 -0400 Subject: [PATCH 025/188] remove unnecessary selectors from kebabification test --- awx/ui_next/src/components/Search/Search.test.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx/ui_next/src/components/Search/Search.test.jsx b/awx/ui_next/src/components/Search/Search.test.jsx index 32c2548a35..6c1badfa56 100644 --- a/awx/ui_next/src/components/Search/Search.test.jsx +++ b/awx/ui_next/src/components/Search/Search.test.jsx @@ -94,8 +94,6 @@ describe('', () => { }); test('changing key select to and from advanced causes onShowAdvancedSearch callback to be invoked', () => { - const searchButton = 'button[aria-label="Search submit button"]'; - const searchTextInput = 'input[aria-label="Search text input"]'; const columns = [ { name: 'Name', key: 'name__icontains', isDefault: true }, { name: 'Description', key: 'description__icontains' }, From dfad5117fac4959741c83dd91dec5ac935fa9554 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Wed, 12 Aug 2020 09:47:10 -0400 Subject: [PATCH 026/188] Add changelog preparing for awx.awx 14.1.0 bug fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f482fe3a05..b316f49d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This is a list of high-level changes for each release of AWX. A full list of com - Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 - Upgraded git-python to fix a bug that caused workflows to sometimes fail - https://github.com/ansible/awx/issues/6119 - Fixed a bug in the AWX CLI that prevented Workflow nodes from importing properly - https://github.com/ansible/awx/issues/7793 +- Fixed a bug in the awx.awx collection release process that templated the wrong version - https://github.com/ansible/awx/issues/7870 ## 14.0.0 (Aug 6, 2020) - As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to: From aa8d3d5ae42c33b8ce7a69248cf6cf20326dc9f0 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 12 Aug 2020 09:49:24 -0400 Subject: [PATCH 027/188] Update websockets.md Add more details about backplane websocket functionality. --- docs/websockets.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/websockets.md b/docs/websockets.md index c9daad83eb..79144ecead 100644 --- a/docs/websockets.md +++ b/docs/websockets.md @@ -16,7 +16,9 @@ Previously, AWX leveraged RabbitMQ to deliver Ansible events that emanated from #### Broadcast Backplane Token -AWX node(s) connect to every other node via the Websocket backplane. Authentication is accomplished via a shared secret that is exchanged via the http header `secret`. The shared secret payload consists of a a `secret`, containing the shared secret, and a `nonce` which is used to mitigate replay attack windows. +AWX node(s) connect to every other node via the Websocket backplane. The backplane websockets initiate from the `wsbroadcast` process and connect to other nodes via the same nginx process that serves webpage websocket connections and marshalls incoming web/API requests. If you have configured AWX to run with an ssl terminated connection in front of nginx then you likely will have nginx configured to handle http traffic and thus the websocket connection will flow unencrypted over http. If you have nginx configured with ssl enabled, then the websocket traffic will flow encrypted. + +Authentication is accomplished via a shared secret that is generated and set at playbook install time. The shared secret is used to derive a payload that is exchanged via the http(s) header `secret`. The shared secret payload consists of a a `secret`, containing the shared secret, and a `nonce` which is used to mitigate replay attack windows. Note that the nonce timestamp is considered valid if it is within `300` second threshold. This is to allow for machine clock skews. ``` @@ -28,6 +30,8 @@ Note that the nonce timestamp is considered valid if it is within `300` second t The payload is encrypted using `HMAC-SHA256` with `settings.BROADCAST_WEBSOCKET_SECRET` as the key. The final payload that is sent, including the http header, is of the form: `secret: nonce_plaintext:HMAC_SHA256({"secret": settings.BROADCAST_WEBSOCKET_SECRET, "nonce": nonce_plaintext})`. +Upon receiving the payload, AWX decrypted the `secret` header using the known shared secret and ensures the `secret` value of the decrypted payload matches the known shared secret, `settings.BROADCAST_WEBSOCKET_SECRET`. If it does not match, the connection is closed. If it does match, the `nonce` is compared to the current time. If the nonce is off by more than `300` seconds, the connection is closed. If both tests pass, the connection is accepted. + ## Protocol You can connect to the AWX channels implementation using any standard websocket library by pointing it to `/websocket`. You must From 5dd2cb10b4fdff6ac1ce833a772073c74edde98e Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 12 Aug 2020 09:52:10 -0400 Subject: [PATCH 028/188] Update websockets.md --- docs/websockets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/websockets.md b/docs/websockets.md index 79144ecead..12b81248b4 100644 --- a/docs/websockets.md +++ b/docs/websockets.md @@ -30,7 +30,7 @@ Note that the nonce timestamp is considered valid if it is within `300` second t The payload is encrypted using `HMAC-SHA256` with `settings.BROADCAST_WEBSOCKET_SECRET` as the key. The final payload that is sent, including the http header, is of the form: `secret: nonce_plaintext:HMAC_SHA256({"secret": settings.BROADCAST_WEBSOCKET_SECRET, "nonce": nonce_plaintext})`. -Upon receiving the payload, AWX decrypted the `secret` header using the known shared secret and ensures the `secret` value of the decrypted payload matches the known shared secret, `settings.BROADCAST_WEBSOCKET_SECRET`. If it does not match, the connection is closed. If it does match, the `nonce` is compared to the current time. If the nonce is off by more than `300` seconds, the connection is closed. If both tests pass, the connection is accepted. +Upon receiving the payload, AWX decrypts the `secret` header using the known shared secret and ensures the `secret` value of the decrypted payload matches the known shared secret, `settings.BROADCAST_WEBSOCKET_SECRET`. If it does not match, the connection is closed. If it does match, the `nonce` is compared to the current time. If the nonce is off by more than `300` seconds, the connection is closed. If both tests pass, the connection is accepted. ## Protocol From 214cb76e1e8f1216c1a0d74697fde574212f3db4 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Tue, 11 Aug 2020 14:22:04 -0400 Subject: [PATCH 029/188] Add custom host toggle tooltip for smart inventory hosts --- .../src/components/HostToggle/HostToggle.jsx | 22 +++++++++++-------- .../SmartInventoryHostListItem.jsx | 16 ++++++++++++-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/awx/ui_next/src/components/HostToggle/HostToggle.jsx b/awx/ui_next/src/components/HostToggle/HostToggle.jsx index 43452ee3f0..638ccb2347 100644 --- a/awx/ui_next/src/components/HostToggle/HostToggle.jsx +++ b/awx/ui_next/src/components/HostToggle/HostToggle.jsx @@ -8,7 +8,18 @@ import ErrorDetail from '../ErrorDetail'; import useRequest from '../../util/useRequest'; import { HostsAPI } from '../../api'; -function HostToggle({ host, isDisabled = false, onToggle, className, i18n }) { +function HostToggle({ + i18n, + className, + host, + isDisabled = false, + onToggle, + tooltip = i18n._( + t`Indicates if a host is available and should be included in running + jobs. For hosts that are part of an external inventory, this may be + reset by the inventory sync process.` + ), +}) { const [isEnabled, setIsEnabled] = useState(host.enabled); const [showError, setShowError] = useState(false); @@ -39,14 +50,7 @@ function HostToggle({ host, isDisabled = false, onToggle, className, i18n }) { return ( - + - + + Smart inventory hosts are read-only. +
+ Toggle indicates if a host is available and should be included + in running jobs. For hosts that are part of an external + inventory, this may be reset by the inventory sync process. + + } + /> From dd68b6ed73451468b401f8e15ff422fcaae695e7 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Wed, 12 Aug 2020 12:21:10 -0400 Subject: [PATCH 030/188] update existing relatedSearchKey requests to new convention and fix UJT searchKeys --- .../src/components/JobList/JobList.jsx | 17 ++++----- .../LaunchPrompt/steps/CredentialsStep.jsx | 15 +++----- .../LaunchPrompt/steps/InventoryStep.jsx | 17 ++++----- .../components/Lookup/ApplicationLookup.jsx | 21 +++++----- .../components/Lookup/CredentialLookup.jsx | 17 ++++----- .../Lookup/InstanceGroupsLookup.jsx | 20 +++++----- .../src/components/Lookup/InventoryLookup.jsx | 15 +++----- .../Lookup/MultiCredentialsLookup.jsx | 22 ++++++----- .../src/components/Lookup/ProjectLookup.jsx | 17 ++++----- .../Schedule/ScheduleList/ScheduleList.jsx | 20 ++++++---- .../ApplicationTokenList.jsx | 15 +++----- .../ApplicationsList/ApplicationsList.jsx | 20 ++++++---- .../CredentialsStep.jsx | 15 +++----- .../Host/HostGroups/HostGroupsList.jsx | 20 ++++++---- .../src/screens/Host/HostList/HostList.jsx | 14 +++---- .../InventoryGroupHostList.jsx | 20 ++++++---- .../InventoryHostGroupsList.jsx | 20 ++++++---- .../Inventory/InventoryList/InventoryList.jsx | 20 ++++++---- .../OrganizationList/OrganizationList.jsx | 20 ++++++---- .../Project/ProjectList/ProjectList.jsx | 20 ++++++---- .../src/screens/Team/TeamList/TeamList.jsx | 20 ++++++---- .../screens/Team/TeamRoles/TeamRolesList.jsx | 23 ++++++----- .../Template/TemplateList/TemplateList.jsx | 38 +++++++++---------- .../User/UserAccess/UserAccessList.jsx | 21 ++++++---- .../screens/User/UserTeams/UserTeamList.jsx | 17 ++++----- .../User/UserTokenList/UserTokenList.jsx | 15 ++++---- 26 files changed, 267 insertions(+), 232 deletions(-) diff --git a/awx/ui_next/src/components/JobList/JobList.jsx b/awx/ui_next/src/components/JobList/JobList.jsx index b1108b8cb9..502b4378c7 100644 --- a/awx/ui_next/src/components/JobList/JobList.jsx +++ b/awx/ui_next/src/components/JobList/JobList.jsx @@ -38,7 +38,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { const [selected, setSelected] = useState([]); const location = useLocation(); const { - result: { results, count, actions, relatedSearchFields }, + result: { results, count, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, request: fetchJobs, @@ -53,10 +53,12 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { return { results: response.data.results, count: response.data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location] // eslint-disable-line react-hooks/exhaustive-deps @@ -64,8 +66,8 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { { results: [], count: 0, - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); useEffect(() => { @@ -138,11 +140,6 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { } }; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( <> diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx index 357b9472db..1b736c6ad0 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/CredentialsStep.jsx @@ -52,7 +52,7 @@ function CredentialsStep({ i18n }) { }, [fetchTypes]); const { - result: { credentials, count, actions, relatedSearchFields }, + result: { credentials, count, relatedSearchableKeys, searchableKeys }, error: credentialsError, isLoading: isCredentialsLoading, request: fetchCredentials, @@ -72,13 +72,15 @@ function CredentialsStep({ i18n }) { return { credentials: data.results, count: data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [selectedType, history.location.search]), - { credentials: [], count: 0, actions: {}, relatedSearchFields: [] } + { credentials: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { @@ -104,11 +106,6 @@ function CredentialsStep({ i18n }) { /> ); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( <> {types && types.length > 0 && ( diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx index 0c23c0c2b2..e7026c66b7 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx @@ -27,7 +27,7 @@ function InventoryStep({ i18n }) { const { isLoading, error, - result: { inventories, count, actions, relatedSearchFields }, + result: { inventories, count, relatedSearchableKeys, searchableKeys }, request: fetchInventories, } = useRequest( useCallback(async () => { @@ -39,17 +39,19 @@ function InventoryStep({ i18n }) { return { inventories: data.results, count: data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location]), { count: 0, inventories: [], - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -57,11 +59,6 @@ function InventoryStep({ i18n }) { fetchInventories(); }, [fetchInventories]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - if (isLoading) { return ; } diff --git a/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx b/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx index 8056114046..01fd154c47 100644 --- a/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx @@ -22,7 +22,7 @@ function ApplicationLookup({ i18n, onChange, value, label }) { const location = useLocation(); const { error, - result: { applications, itemCount, actions, relatedSearchFields }, + result: { applications, itemCount, relatedSearchableKeys, searchableKeys }, request: fetchApplications, } = useRequest( useCallback(async () => { @@ -40,23 +40,24 @@ function ApplicationLookup({ i18n, onChange, value, label }) { return { applications: results, itemCount: count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), - { applications: [], itemCount: 0, actions: {}, relatedSearchFields: [] } + { + applications: [], + itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], + } ); useEffect(() => { fetchApplications(); }, [fetchApplications]); - - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [ credentialTypeId, @@ -78,8 +80,8 @@ function CredentialLookup({ { count: 0, credentials: [], - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -87,11 +89,6 @@ function CredentialLookup({ fetchCredentials(); }, [fetchCredentials]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - // TODO: replace credential type search with REST-based grabbing of cred types return ( diff --git a/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx b/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx index 21fe8cfa8f..49c257cb6a 100644 --- a/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx @@ -30,7 +30,7 @@ function InstanceGroupsLookup(props) { } = props; const { - result: { instanceGroups, count, actions, relatedSearchFields }, + result: { instanceGroups, count, relatedSearchableKeys, searchableKeys }, request: fetchInstanceGroups, error, isLoading, @@ -44,24 +44,26 @@ function InstanceGroupsLookup(props) { return { instanceGroups: data.results, count: data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location]), - { instanceGroups: [], count: 0, actions: {}, relatedSearchFields: [] } + { + instanceGroups: [], + count: 0, + relatedSearchableKeys: [], + searchableKeys: [], + } ); useEffect(() => { fetchInstanceGroups(); }, [fetchInstanceGroups]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location]), - { inventories: [], count: 0, actions: {}, relatedSearchFields: [] } + { inventories: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { fetchInventories(); }, [fetchInventories]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( <> val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [selectedType, history.location]), { credentials: [], credentialsCount: 0, - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -104,11 +111,6 @@ function MultiCredentialsLookup(props) { const isVault = selectedType?.kind === 'vault'; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location.search, autocomplete]), { count: 0, projects: [], - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -67,11 +69,6 @@ function ProjectLookup({ fetchProjects(); }, [fetchProjects]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( val.slice(0, -8)), + searchableKeys: Object.keys( + scheduleActions.data.actions?.GET || {} + ).filter(key => scheduleActions.data.actions?.GET[key].filterable), }; }, [location, loadSchedules, loadScheduleOptions]), { schedules: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -106,10 +116,6 @@ function ScheduleList({ actions && Object.prototype.hasOwnProperty.call(actions, 'POST') && !hideAddButton; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx b/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx index 7d123385ef..2ad56eaa7f 100644 --- a/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx +++ b/awx/ui_next/src/screens/Application/ApplicationTokens/ApplicationTokenList.jsx @@ -26,7 +26,7 @@ function ApplicationTokenList({ i18n }) { const { error, isLoading, - result: { tokens, itemCount, actions, relatedSearchFields }, + result: { tokens, itemCount, relatedSearchableKeys, searchableKeys }, request: fetchTokens, } = useRequest( useCallback(async () => { @@ -52,13 +52,15 @@ function ApplicationTokenList({ i18n }) { return { tokens: modifiedResults, itemCount: count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [id, location.search]), - { tokens: [], itemCount: 0, actions: {}, relatedSearchFields: [] } + { tokens: [], itemCount: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { @@ -91,11 +93,6 @@ function ApplicationTokenList({ i18n }) { setSelected([]); }; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( <> { const params = parseQueryString(QS_CONFIG, location.search); @@ -46,16 +52,20 @@ function ApplicationsList({ i18n }) { applications: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { applications: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -89,10 +99,6 @@ function ApplicationsList({ i18n }) { }; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx index 40a305e72f..bc91a98894 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginPrompt/CredentialsStep.jsx @@ -25,7 +25,7 @@ function CredentialsStep({ i18n }) { const history = useHistory(); const { - result: { credentials, count, actions, relatedSearchFields }, + result: { credentials, count, relatedSearchableKeys, searchableKeys }, error: credentialsError, isLoading: isCredentialsLoading, request: fetchCredentials, @@ -39,24 +39,21 @@ function CredentialsStep({ i18n }) { return { credentials: data.results, count: data.count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location.search]), - { credentials: [], count: 0, actions: {}, relatedSearchFields: [] } + { credentials: [], count: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { fetchCredentials(); }, [fetchCredentials]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - if (credentialsError) { return ; } diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx index bfcde651a4..6233df73bf 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx @@ -33,7 +33,13 @@ function HostGroupsList({ i18n, host }) { const invId = host.summary_fields.inventory.id; const { - result: { groups, itemCount, actions, relatedSearchFields }, + result: { + groups, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchGroups, @@ -55,16 +61,20 @@ function HostGroupsList({ i18n, host }) { groups: results, itemCount: count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [hostId, search]), { groups: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -128,10 +138,6 @@ function HostGroupsList({ i18n, host }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Host/HostList/HostList.jsx b/awx/ui_next/src/screens/Host/HostList/HostList.jsx index 3e6a2589c6..a11ec3f4d1 100644 --- a/awx/ui_next/src/screens/Host/HostList/HostList.jsx +++ b/awx/ui_next/src/screens/Host/HostList/HostList.jsx @@ -29,7 +29,7 @@ function HostList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { hosts, count, actions, relatedSearchFields }, + result: { hosts, count, actions, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, request: fetchHosts, @@ -44,16 +44,20 @@ function HostList({ i18n }) { hosts: results[0].data.results, count: results[0].data.count, actions: results[1].data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( results[1]?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys(results[1].data.actions?.GET || {}).filter( + key => results[1].data.actions?.GET[key].filterable + ), }; }, [location]), { hosts: [], count: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -97,10 +101,6 @@ function HostList({ i18n }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx index 17fc055997..8584c01207 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx @@ -32,7 +32,13 @@ function InventoryGroupHostList({ i18n }) { const history = useHistory(); const { - result: { hosts, hostCount, actions, relatedSearchFields }, + result: { + hosts, + hostCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchHosts, @@ -48,16 +54,20 @@ function InventoryGroupHostList({ i18n }) { hosts: response.data.results, hostCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [groupId, inventoryId, location.search]), { hosts: [], hostCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -127,10 +137,6 @@ function InventoryGroupHostList({ i18n }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); const addFormUrl = `/inventories/inventory/${inventoryId}/groups/${groupId}/nested_hosts/add`; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx index 4b74189602..2f8d8fa1ea 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx @@ -31,7 +31,13 @@ function InventoryHostGroupsList({ i18n }) { const { search } = useLocation(); const { - result: { groups, itemCount, actions, relatedSearchFields }, + result: { + groups, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchGroups, @@ -53,16 +59,20 @@ function InventoryHostGroupsList({ i18n }) { groups: results, itemCount: count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [hostId, search]), // eslint-disable-line react-hooks/exhaustive-deps { groups: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -126,10 +136,6 @@ function InventoryHostGroupsList({ i18n }) { const canAdd = actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); return ( <> diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx index 94f22e70a3..e5ff1f6dd4 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx @@ -29,7 +29,13 @@ function InventoryList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { results, itemCount, actions, relatedSearchFields }, + result: { + results, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchInventories, @@ -44,16 +50,20 @@ function InventoryList({ i18n }) { results: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { results: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -97,10 +107,6 @@ function InventoryList({ i18n }) { const hasContentLoading = isDeleteLoading || isLoading; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...inventories] : []); diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx index 152004ae5c..7b3c0eeda2 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationList.jsx @@ -31,7 +31,13 @@ function OrganizationsList({ i18n }) { const addUrl = `${match.url}/add`; const { - result: { organizations, organizationCount, actions, relatedSearchFields }, + result: { + organizations, + organizationCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading: isOrgsLoading, request: fetchOrganizations, @@ -46,16 +52,20 @@ function OrganizationsList({ i18n }) { organizations: orgs.data.results, organizationCount: orgs.data.count, actions: orgActions.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( orgActions?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys(orgActions.data.actions?.GET || {}).filter( + key => orgActions.data.actions?.GET[key].filterable + ), }; }, [location]), { organizations: [], organizationCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -90,10 +100,6 @@ function OrganizationsList({ i18n }) { const hasContentLoading = isDeleteLoading || isOrgsLoading; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...organizations] : []); diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx index 13b3a98c2c..7afbe124b3 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx @@ -30,7 +30,13 @@ function ProjectList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { results, itemCount, actions, relatedSearchFields }, + result: { + results, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchProjects, @@ -45,16 +51,20 @@ function ProjectList({ i18n }) { results: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { results: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -89,10 +99,6 @@ function ProjectList({ i18n }) { const hasContentLoading = isDeleteLoading || isLoading; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...projects] : []); diff --git a/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx b/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx index f660064266..07de516ca4 100644 --- a/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx +++ b/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx @@ -29,7 +29,13 @@ function TeamList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { teams, itemCount, actions, relatedSearchFields }, + result: { + teams, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchTeams, @@ -44,16 +50,20 @@ function TeamList({ i18n }) { teams: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { teams: [], itemCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -85,10 +95,6 @@ function TeamList({ i18n }) { const hasContentLoading = isDeleteLoading || isLoading; const canAdd = actions && actions.POST; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); const handleSelectAll = isSelected => { setSelected(isSelected ? [...teams] : []); diff --git a/awx/ui_next/src/screens/Team/TeamRoles/TeamRolesList.jsx b/awx/ui_next/src/screens/Team/TeamRoles/TeamRolesList.jsx index 05ffce9fe1..31db276045 100644 --- a/awx/ui_next/src/screens/Team/TeamRoles/TeamRolesList.jsx +++ b/awx/ui_next/src/screens/Team/TeamRoles/TeamRolesList.jsx @@ -37,7 +37,13 @@ function TeamRolesList({ i18n, me, team }) { isLoading, request: fetchRoles, contentError, - result: { roleCount, roles, isAdminOfOrg, actions, relatedSearchFields }, + result: { + roleCount, + roles, + isAdminOfOrg, + relatedSearchableKeys, + searchableKeys, + }, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, search); @@ -58,18 +64,20 @@ function TeamRolesList({ i18n, me, team }) { roleCount: count, roles: results, isAdminOfOrg: orgAdminCount > 0, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [me.id, team.id, team.organization, search]), { roles: [], roleCount: 0, isAdminOfOrg: false, - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -98,11 +106,6 @@ function TeamRolesList({ i18n, me, team }) { ); const canAdd = team?.summary_fields?.user_capabilities?.edit || isAdminOfOrg; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - const detailUrl = role => { const { resource_id, resource_type } = role.summary_fields; diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx index 31c04263ad..455bf4ce6c 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx @@ -36,7 +36,14 @@ function TemplateList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { results, count, jtActions, wfjtActions, relatedSearchFields }, + result: { + results, + count, + jtActions, + wfjtActions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchTemplates, @@ -47,20 +54,19 @@ function TemplateList({ i18n }) { UnifiedJobTemplatesAPI.read(params), JobTemplatesAPI.readOptions(), WorkflowJobTemplatesAPI.readOptions(), + UnifiedJobTemplatesAPI.readOptions(), ]); return { results: responses[0].data.results, count: responses[0].data.count, jtActions: responses[1].data.actions, wfjtActions: responses[2].data.actions, - relatedSearchFields: [ - ...(responses[1]?.data?.related_search_fields || []).map(val => - val.slice(0, -8) - ), - ...(responses[2]?.data?.related_search_fields || []).map(val => - val.slice(0, -8) - ), - ], + relatedSearchableKeys: ( + responses[3]?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + responses[3].data.actions?.GET || {} + ).filter(key => responses[3].data.actions?.GET[key].filterable), }; }, [location]), { @@ -68,7 +74,8 @@ function TemplateList({ i18n }) { count: 0, jtActions: {}, wfjtActions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -128,17 +135,6 @@ function TemplateList({ i18n }) { const canAddWFJT = wfjtActions && Object.prototype.hasOwnProperty.call(wfjtActions, 'POST'); // spreading Set() returns only unique keys - const relatedSearchableKeys = [...new Set(relatedSearchFields)] || []; - const searchableKeys = [ - ...new Set([ - ...Object.keys(jtActions?.GET || {}).filter( - key => jtActions.GET[key].filterable - ), - ...Object.keys(wfjtActions?.GET || {}).filter( - key => wfjtActions.GET[key].filterable - ), - ]), - ]; const addButtonOptions = []; if (canAddJT) { diff --git a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx index 4d9ff7a671..5e528913c1 100644 --- a/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx +++ b/awx/ui_next/src/screens/User/UserAccess/UserAccessList.jsx @@ -38,7 +38,13 @@ function UserAccessList({ i18n, user }) { isLoading, request: fetchRoles, error, - result: { roleCount, roles, actions, relatedSearchFields }, + result: { + roleCount, + roles, + actions, + relatedSearchableKeys, + searchableKeys, + }, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, search); @@ -55,16 +61,20 @@ function UserAccessList({ i18n, user }) { roleCount: count, roles: results, actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [user.id, search]), { roles: [], roleCount: 0, actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -92,11 +102,6 @@ function UserAccessList({ i18n, user }) { user?.summary_fields?.user_capabilities?.edit || (actions && Object.prototype.hasOwnProperty.call(actions, 'POST')); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - const saveRoles = () => { setIsWizardOpen(false); fetchRoles(); diff --git a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx index 25d245cffd..868cbb9e55 100644 --- a/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx +++ b/awx/ui_next/src/screens/User/UserTeams/UserTeamList.jsx @@ -20,7 +20,7 @@ function UserTeamList({ i18n }) { const { id: userId } = useParams(); const { - result: { teams, count, actions, relatedSearchFields }, + result: { teams, count, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, request: fetchOrgs, @@ -39,17 +39,19 @@ function UserTeamList({ i18n }) { return { teams: results, count: teamCount, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [userId, location.search]), { teams: [], count: 0, - actions: {}, - relatedSearchFields: [], + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -57,11 +59,6 @@ function UserTeamList({ i18n }) { fetchOrgs(); }, [fetchOrgs]); - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); - return ( { const params = parseQueryString(QS_CONFIG, location.search); @@ -53,13 +53,15 @@ function UserTokenList({ i18n }) { return { tokens: modifiedResults, itemCount: count, - actions: actionsResponse.data.actions, - relatedSearchFields: ( + relatedSearchableKeys: ( actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [id, location.search]), - { tokens: [], itemCount: 0, actions: {}, relatedSearchFields: [] } + { tokens: [], itemCount: 0, relatedSearchableKeys: [], searchableKeys: [] } ); useEffect(() => { @@ -93,10 +95,7 @@ function UserTokenList({ i18n }) { }; const canAdd = true; - const relatedSearchableKeys = relatedSearchFields || []; - const searchableKeys = Object.keys(actions?.GET || {}).filter( - key => actions.GET[key].filterable - ); + return ( <> Date: Mon, 3 Aug 2020 13:06:26 -0400 Subject: [PATCH 031/188] Adds User Token Details page --- awx/ui_next/src/screens/User/User.jsx | 10 +- .../src/screens/User/UserToken/UserToken.jsx | 117 +++++++++++++++++ .../screens/User/UserToken/UserToken.test.jsx | 101 +++++++++++++++ .../src/screens/User/UserToken/index.js | 1 + .../User/UserTokenDetail/UserTokenDetail.jsx | 81 ++++++++++++ .../UserTokenDetail/UserTokenDetail.test.jsx | 120 ++++++++++++++++++ .../src/screens/User/UserTokenDetail/index.js | 1 + .../User/UserTokenList/UserTokenListItem.jsx | 6 +- .../screens/User/UserTokens/UserTokens.jsx | 13 +- awx/ui_next/src/screens/User/Users.jsx | 6 +- 10 files changed, 447 insertions(+), 9 deletions(-) create mode 100644 awx/ui_next/src/screens/User/UserToken/UserToken.jsx create mode 100644 awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx create mode 100644 awx/ui_next/src/screens/User/UserToken/index.js create mode 100644 awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx create mode 100644 awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx create mode 100644 awx/ui_next/src/screens/User/UserTokenDetail/index.js diff --git a/awx/ui_next/src/screens/User/User.jsx b/awx/ui_next/src/screens/User/User.jsx index 95dcb487b4..d071cedd8f 100644 --- a/awx/ui_next/src/screens/User/User.jsx +++ b/awx/ui_next/src/screens/User/User.jsx @@ -80,7 +80,9 @@ function User({ i18n, setBreadcrumb, me }) { } let showCardHeader = true; - if (['edit', 'add'].some(name => location.pathname.includes(name))) { + if ( + ['edit', 'add', 'tokens'].some(name => location.pathname.includes(name)) + ) { showCardHeader = false; } @@ -131,7 +133,11 @@ function User({ i18n, setBreadcrumb, me }) { )} - + diff --git a/awx/ui_next/src/screens/User/UserToken/UserToken.jsx b/awx/ui_next/src/screens/User/UserToken/UserToken.jsx new file mode 100644 index 0000000000..af6f0f0b16 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserToken/UserToken.jsx @@ -0,0 +1,117 @@ +import React, { useEffect, useCallback } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + Link, + Redirect, + Route, + Switch, + useLocation, + useParams, +} from 'react-router-dom'; +import { CaretLeftIcon } from '@patternfly/react-icons'; +import { Card, PageSection } from '@patternfly/react-core'; +import RoutedTabs from '../../../components/RoutedTabs'; +import ContentError from '../../../components/ContentError'; +import { TokensAPI } from '../../../api'; +import useRequest from '../../../util/useRequest'; +import UserTokenDetail from '../UserTokenDetail'; + +function UserToken({ i18n, setBreadcrumb, user }) { + const location = useLocation(); + const { id, tokenId } = useParams(); + const { + isLoading, + error, + request: fetchToken, + result: { token, actions }, + } = useRequest( + useCallback(async () => { + const [response, actionsResponse] = await Promise.all([ + TokensAPI.readDetail(tokenId), + TokensAPI.readOptions(), + ]); + setBreadcrumb(user, response.data); + return { + token: response.data, + actions: actionsResponse.data.actions.POST, + }; + }, [setBreadcrumb, user, tokenId]), + { token: null, actions: null } + ); + useEffect(() => { + fetchToken(); + }, [fetchToken]); + + const tabsArray = [ + { + name: ( + <> + + {i18n._(t`Back to Tokens`)} + + ), + link: `/users/${id}/tokens`, + id: 99, + }, + { + name: i18n._(t`Details`), + link: `/users/${id}/tokens/${tokenId}/details`, + id: 0, + }, + ]; + + let showCardHeader = true; + + if (location.pathname.endsWith('edit')) { + showCardHeader = false; + } + + if (!isLoading && error) { + return ( + + + + {error.response.status === 404 && ( + + {i18n._(t`Token not found.`)}{' '} + + {i18n._(t`View all tokens.`)} + + + )} + + + + ); + } + + return ( + <> + {showCardHeader && } + + + {token && ( + + + + )} + + {!isLoading && ( + + {id && ( + {i18n._(t`View Tokens`)} + )} + + )} + + + + ); +} + +export default withI18n()(UserToken); diff --git a/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx b/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx new file mode 100644 index 0000000000..8e71f1b085 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserToken/UserToken.test.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { TokensAPI } from '../../../api'; +import UserToken from './UserToken'; + +jest.mock('../../../api/models/Tokens'); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + tokenId: 2, + }), +})); +describe('', () => { + let wrapper; + const user = { + id: 1, + type: 'user', + url: '/api/v2/users/1/', + summary_fields: { + user_capabilities: { + edit: true, + delete: false, + }, + }, + created: '2020-06-19T12:55:13.138692Z', + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + email: 'a@g.com', + }; + test('should call api for token details and actions', async () => { + TokensAPI.readDetail.mockResolvedValue({ + id: 2, + type: 'o_auth2_access_token', + url: '/api/v2/tokens/2/', + + summary_fields: { + user: { + id: 1, + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + }, + application: { + id: 3, + name: 'hg', + }, + }, + created: '2020-06-23T19:56:38.422053Z', + modified: '2020-06-23T19:56:38.441353Z', + description: 'cdfsg', + scope: 'read', + }); + TokensAPI.readOptions.mockResolvedValue({ + data: { actions: { POST: true } }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('UserToken').length).toBe(1); + }); + test('should call api for token details and actions', async () => { + TokensAPI.readDetail.mockResolvedValue({ + id: 2, + type: 'o_auth2_access_token', + url: '/api/v2/tokens/2/', + + summary_fields: { + user: { + id: 1, + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + }, + application: { + id: 3, + name: 'hg', + }, + }, + created: '2020-06-23T19:56:38.422053Z', + modified: '2020-06-23T19:56:38.441353Z', + description: 'cdfsg', + scope: 'read', + }); + TokensAPI.readOptions.mockResolvedValue({ + data: { actions: { POST: true } }, + }); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(TokensAPI.readDetail).toBeCalledWith(2); + expect(TokensAPI.readOptions).toBeCalled(); + }); +}); diff --git a/awx/ui_next/src/screens/User/UserToken/index.js b/awx/ui_next/src/screens/User/UserToken/index.js new file mode 100644 index 0000000000..f899410e7d --- /dev/null +++ b/awx/ui_next/src/screens/User/UserToken/index.js @@ -0,0 +1 @@ +export { default } from './UserToken'; diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx new file mode 100644 index 0000000000..37eeb63416 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx @@ -0,0 +1,81 @@ +import React, { useCallback } from 'react'; +import { Link, useHistory, useParams } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Button } from '@patternfly/react-core'; + +import AlertModal from '../../../components/AlertModal'; +import { CardBody, CardActionsRow } from '../../../components/Card'; +import DeleteButton from '../../../components/DeleteButton'; +import { DetailList, Detail } from '../../../components/DetailList'; +import ErrorDetail from '../../../components/ErrorDetail'; +import { formatDateString } from '../../../util/dates'; +import { TokensAPI } from '../../../api'; +import useRequest, { useDismissableError } from '../../../util/useRequest'; + +function UserTokenDetail({ token, canEditOrDelete, i18n }) { + const { scope, description, created, modified, summary_fields } = token; + const history = useHistory(); + const { id, tokenId } = useParams(); + const { request: deleteTeam, isLoading, error: deleteError } = useRequest( + useCallback(async () => { + await TokensAPI.destroy(tokenId); + history.push(`/users/${id}/tokens`); + }, [tokenId, id, history]) + ); + + const { error, dismissError } = useDismissableError(deleteError); + + return ( + + + + + + + + + + {canEditOrDelete && ( + <> + + + {i18n._(t`Delete`)} + + + )} + + {error && ( + + {i18n._(t`Failed to user token.`)} + + + )} + + ); +} + +export default withI18n()(UserTokenDetail); diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx new file mode 100644 index 0000000000..6b0d382e0c --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { TokensAPI } from '../../../api'; +import UserTokenDetail from './UserTokenDetail'; + +jest.mock('../../../api/models/Tokens'); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + tokenId: 2, + }), +})); +describe('', () => { + let wrapper; + const token = { + id: 2, + type: 'o_auth2_access_token', + url: '/api/v2/tokens/2/', + + summary_fields: { + user: { + id: 1, + username: 'admin', + first_name: 'Alex', + last_name: 'Corey', + }, + application: { + id: 3, + name: 'hg', + }, + }, + created: '2020-06-23T19:56:38.422053Z', + modified: '2020-06-23T19:56:38.441353Z', + description: 'cdfsg', + scope: 'read', + }; + test('should call api for token details and actions', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('UserTokenDetail').length).toBe(1); + }); + test('should call api for token details and actions', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + + expect(wrapper.find('Detail[label="Application"]').prop('value')).toBe( + 'hg' + ); + expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe( + 'cdfsg' + ); + expect(wrapper.find('Detail[label="Scope"]').prop('value')).toBe('read'); + expect(wrapper.find('Detail[label="Created"]').prop('value')).toBe( + '6/23/2020, 7:56:38 PM' + ); + expect(wrapper.find('Detail[label="Last Modified"]').prop('value')).toBe( + '6/23/2020, 7:56:38 PM' + ); + expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(1); + expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(1); + }); + test('should not render edit or delete buttons', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(0); + expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(0); + }); + test('should delete token properly', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await act(async () => + wrapper.find('Button[aria-label="Delete"]').prop('onClick')() + ); + wrapper.update(); + await act(async () => wrapper.find('DeleteButton').prop('onConfirm')()); + expect(TokensAPI.destroy).toBeCalledWith(2); + }); + test('should throw deletion error', async () => { + TokensAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/tokens', + }, + data: 'An error occurred', + status: 400, + }, + }) + ); + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + await act(async () => + wrapper.find('Button[aria-label="Delete"]').prop('onClick')() + ); + wrapper.update(); + await act(async () => wrapper.find('DeleteButton').prop('onConfirm')()); + expect(TokensAPI.destroy).toBeCalledWith(2); + wrapper.update(); + expect(wrapper.find('ErrorDetail').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/index.js b/awx/ui_next/src/screens/User/UserTokenDetail/index.js new file mode 100644 index 0000000000..a6a9011996 --- /dev/null +++ b/awx/ui_next/src/screens/User/UserTokenDetail/index.js @@ -0,0 +1 @@ +export { default } from './UserTokenDetail'; diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx index 4b1198c5a9..7fabcb878c 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Link, useParams } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { @@ -22,6 +23,7 @@ const NameLabel = styled.b` `; function UserTokenListItem({ i18n, token, isSelected, onSelect }) { + const { id } = useParams(); const labelId = `check-action-${token.id}`; return ( @@ -41,7 +43,9 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) { {token.summary_fields?.application?.name ? ( {i18n._(t`Application`)} - {token.summary_fields.application.name} + + {token.summary_fields.application.name} + ) : ( i18n._(t`Personal access token`) diff --git a/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx b/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx index dc072a6546..c73519d7f9 100644 --- a/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx +++ b/awx/ui_next/src/screens/User/UserTokens/UserTokens.jsx @@ -3,17 +3,20 @@ import { withI18n } from '@lingui/react'; import { Switch, Route, useParams } from 'react-router-dom'; import UserTokenAdd from '../UserTokenAdd'; import UserTokenList from '../UserTokenList'; +import UserToken from '../UserToken'; -function UserTokens() { - const { id: userId } = useParams(); - +function UserTokens({ setBreadcrumb, user }) { + const { id } = useParams(); return ( - + + + + - + ); diff --git a/awx/ui_next/src/screens/User/Users.jsx b/awx/ui_next/src/screens/User/Users.jsx index 6f21f8be10..e9fe2d4ef2 100644 --- a/awx/ui_next/src/screens/User/Users.jsx +++ b/awx/ui_next/src/screens/User/Users.jsx @@ -18,7 +18,7 @@ function Users({ i18n }) { const match = useRouteMatch(); const addUserBreadcrumb = useCallback( - user => { + (user, token) => { if (!user) { return; } @@ -34,6 +34,10 @@ function Users({ i18n }) { [`/users/${user.id}/organizations`]: i18n._(t`Organizations`), [`/users/${user.id}/tokens`]: i18n._(t`Tokens`), [`/users/${user.id}/tokens/add`]: i18n._(t`Create user token`), + [`/users/${user.id}/tokens/${token && token.id}`]: `Application Name`, + [`/users/${user.id}/tokens/${token && token.id}/details`]: i18n._( + t`Details` + ), }); }, [i18n] From 15fda43a1007ef9aff006fe3f00fedc2d9c7a52f Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Tue, 4 Aug 2020 14:29:37 -0400 Subject: [PATCH 032/188] Utilizes UserDateDetail, Capitalizes Scope value, fixes spelling errors --- .../components/Lookup/ApplicationLookup.jsx | 2 +- .../User/UserTokenDetail/UserTokenDetail.jsx | 28 ++++++++++++------- .../UserTokenDetail/UserTokenDetail.test.jsx | 12 ++++---- .../User/UserTokenList/UserTokenListItem.jsx | 8 ++++-- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx b/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx index 01fd154c47..ca5871c2cc 100644 --- a/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ApplicationLookup.jsx @@ -44,7 +44,7 @@ function ApplicationLookup({ i18n, onChange, value, label }) { actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), searchableKeys: Object.keys( - actionsResponse.data.actions?.GET || {} + actionsResponse?.data?.actions?.GET || {} ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx index 37eeb63416..4e6891767d 100644 --- a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx +++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.jsx @@ -7,23 +7,26 @@ import { Button } from '@patternfly/react-core'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; import DeleteButton from '../../../components/DeleteButton'; -import { DetailList, Detail } from '../../../components/DetailList'; +import { + DetailList, + Detail, + UserDateDetail, +} from '../../../components/DetailList'; import ErrorDetail from '../../../components/ErrorDetail'; -import { formatDateString } from '../../../util/dates'; import { TokensAPI } from '../../../api'; import useRequest, { useDismissableError } from '../../../util/useRequest'; +import { toTitleCase } from '../../../util/strings'; function UserTokenDetail({ token, canEditOrDelete, i18n }) { const { scope, description, created, modified, summary_fields } = token; const history = useHistory(); const { id, tokenId } = useParams(); - const { request: deleteTeam, isLoading, error: deleteError } = useRequest( + const { request: deleteToken, isLoading, error: deleteError } = useRequest( useCallback(async () => { await TokensAPI.destroy(tokenId); history.push(`/users/${id}/tokens`); }, [tokenId, id, history]) ); - const { error, dismissError } = useDismissableError(deleteError); return ( @@ -32,14 +35,19 @@ function UserTokenDetail({ token, canEditOrDelete, i18n }) { - - - + + @@ -55,7 +63,7 @@ function UserTokenDetail({ token, canEditOrDelete, i18n }) { {i18n._(t`Delete`)} diff --git a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx index 6b0d382e0c..a3462f758f 100644 --- a/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx +++ b/awx/ui_next/src/screens/User/UserTokenDetail/UserTokenDetail.test.jsx @@ -58,13 +58,13 @@ describe('', () => { expect(wrapper.find('Detail[label="Description"]').prop('value')).toBe( 'cdfsg' ); - expect(wrapper.find('Detail[label="Scope"]').prop('value')).toBe('read'); - expect(wrapper.find('Detail[label="Created"]').prop('value')).toBe( - '6/23/2020, 7:56:38 PM' - ); - expect(wrapper.find('Detail[label="Last Modified"]').prop('value')).toBe( - '6/23/2020, 7:56:38 PM' + expect(wrapper.find('Detail[label="Scope"]').prop('value')).toBe('Read'); + expect(wrapper.find('UserDateDetail[label="Created"]').prop('date')).toBe( + '2020-06-23T19:56:38.422053Z' ); + expect( + wrapper.find('UserDateDetail[label="Last Modified"]').prop('date') + ).toBe('2020-06-23T19:56:38.441353Z'); expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(1); expect(wrapper.find('Button[aria-label="Delete"]').length).toBe(1); }); diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx index 7fabcb878c..52eb44a7f1 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenListItem.jsx @@ -11,7 +11,7 @@ import { import styled from 'styled-components'; import { toTitleCase } from '../../../util/strings'; -import { formatDateStringUTC } from '../../../util/dates'; +import { formatDateString } from '../../../util/dates'; import DataListCell from '../../../components/DataListCell'; const Label = styled.b` @@ -48,7 +48,9 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) { ) : ( - i18n._(t`Personal access token`) + + {i18n._(t`Personal access token`)} + )} , @@ -57,7 +59,7 @@ function UserTokenListItem({ i18n, token, isSelected, onSelect }) { , - {formatDateStringUTC(token.expires)} + {formatDateString(token.expires)} , ]} /> From 98b7f3b61890d8ee5715f757464343dda6fe8e19 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 6 Aug 2020 13:28:59 -0400 Subject: [PATCH 033/188] Support workflow prompting on launch --- .../src/components/LaunchPrompt/steps/InventoryStep.jsx | 2 -- .../components/LaunchPrompt/steps/useInventoryStep.jsx | 6 +++++- .../screens/Template/TemplateList/TemplateListItem.jsx | 9 +++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx index e7026c66b7..696809f547 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/InventoryStep.jsx @@ -9,7 +9,6 @@ import useRequest from '../../../util/useRequest'; import OptionsList from '../../OptionsList'; import ContentLoading from '../../ContentLoading'; import ContentError from '../../ContentError'; -import { required } from '../../../util/validators'; const QS_CONFIG = getQSConfig('inventory', { page: 1, @@ -20,7 +19,6 @@ const QS_CONFIG = getQSConfig('inventory', { function InventoryStep({ i18n }) { const [field, , helpers] = useField({ name: 'inventory', - validate: required(null, i18n), }); const history = useHistory(); diff --git a/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx b/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx index aa8acbd6f6..ebc875d506 100644 --- a/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx +++ b/awx/ui_next/src/components/LaunchPrompt/steps/useInventoryStep.jsx @@ -9,7 +9,11 @@ export default function useInventoryStep(config, resource, visitedSteps, i18n) { const [stepErrors, setStepErrors] = useState({}); const validate = values => { - if (!config.ask_inventory_on_launch) { + if ( + !config.ask_inventory_on_launch || + (['workflow_job', 'workflow_job_template'].includes(resource.type) && + !resource.inventory) + ) { return {}; } const errors = {}; diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx index 716c83e608..16166ec108 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx @@ -44,9 +44,7 @@ function TemplateListItem({ fetchTemplates, }) { const [isDisabled, setIsDisabled] = useState(false); - const labelId = `check-action-${template.id}`; - const canLaunch = template.summary_fields.user_capabilities.start; const copyTemplate = useCallback(async () => { if (template.type === 'job_template') { @@ -105,8 +103,11 @@ function TemplateListItem({ , ]} /> - - {canLaunch && template.type === 'job_template' && ( + + {template.summary_fields.user_capabilities.start && ( {({ handleLaunch }) => ( From 1e9a71a2e4cd6d3d9ca3ef209ad77c381f47d582 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 13 Aug 2020 11:55:57 -0400 Subject: [PATCH 034/188] Run prettier --- .../src/screens/Template/TemplateList/TemplateListItem.jsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx index 16166ec108..eb0be40346 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateListItem.jsx @@ -103,10 +103,7 @@ function TemplateListItem({ , ]} /> - + {template.summary_fields.user_capabilities.start && ( From 54e87378d5127678fd4f38febbce395fc696e6e4 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Mon, 17 Aug 2020 20:43:02 -0400 Subject: [PATCH 035/188] Pin pytest-xdist --- requirements/requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 6fd0b2ed2f..3dbcc2f97c 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -12,7 +12,7 @@ pytest-django pytest-pythonpath pytest-mock==1.11.1 pytest-timeout -pytest-xdist +pytest-xdist==1.34.0 # 2.0.0 broke zuul for some reason tox # for awxkit logutils jupyter From 33e2c059eda92453bc15b3465392ddd1bac25a68 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Mon, 17 Aug 2020 14:35:40 -0400 Subject: [PATCH 036/188] make event stdout encoding more resilient to UTF-16 surrogate pairs see: https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates --- CHANGELOG.md | 1 + awx/api/renderers.py | 18 ++++++++++++++++++ awx/settings/defaults.py | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b316f49d55..54573a057f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This is a list of high-level changes for each release of AWX. A full list of com - Upgraded git-python to fix a bug that caused workflows to sometimes fail - https://github.com/ansible/awx/issues/6119 - Fixed a bug in the AWX CLI that prevented Workflow nodes from importing properly - https://github.com/ansible/awx/issues/7793 - Fixed a bug in the awx.awx collection release process that templated the wrong version - https://github.com/ansible/awx/issues/7870 +- Fixed a bug that caused errors rendering stdout that contained UTF-16 surrogate pairs - https://github.com/ansible/awx/pull/7918 ## 14.0.0 (Aug 6, 2020) - As part of our commitment to inclusivity in open source, we recently took some time to audit AWX's source code and user interface and replace certain terminology with more inclusive language. Strictly speaking, this isn't a bug or a feature, but we think it's important and worth calling attention to: diff --git a/awx/api/renderers.py b/awx/api/renderers.py index bd4136a76a..92d59e2c7b 100644 --- a/awx/api/renderers.py +++ b/awx/api/renderers.py @@ -7,6 +7,24 @@ from prometheus_client.parser import text_string_to_metric_families # Django REST Framework from rest_framework import renderers from rest_framework.request import override_method +from rest_framework.utils import encoders + + +class SurrogateEncoder(encoders.JSONEncoder): + + def encode(self, obj): + ret = super(SurrogateEncoder, self).encode(obj) + try: + ret.encode() + except UnicodeEncodeError as e: + if 'surrogates not allowed' in e.reason: + ret = ret.encode('utf-8', 'replace').decode() + return ret + + +class DefaultJSONRenderer(renderers.JSONRenderer): + + encoder_class = SurrogateEncoder class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index fe7c8c0ba3..355d247f62 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -310,7 +310,7 @@ REST_FRAMEWORK = { 'awx.api.parsers.JSONParser', ), 'DEFAULT_RENDERER_CLASSES': ( - 'rest_framework.renderers.JSONRenderer', + 'awx.api.renderers.DefaultJSONRenderer', 'awx.api.renderers.BrowsableAPIRenderer', ), 'DEFAULT_METADATA_CLASS': 'awx.api.metadata.Metadata', From 806a4686001afe24c700e0b0c6d7693ba3d809ae Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 19 Aug 2020 10:26:53 -0400 Subject: [PATCH 037/188] Use organization api to create users This ensures that the user will be related to the chosen organization when it is created. --- awx/ui_next/src/api/models/Organizations.js | 4 ++++ awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx | 5 +++-- .../src/screens/User/UserAdd/UserAdd.test.jsx | 12 ++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index 3cbe64c284..267c9aba1e 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -15,6 +15,10 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { readTeams(id, params) { return this.http.get(`${this.baseUrl}${id}/teams/`, { params }); } + + createUser(id, data) { + return this.http.post(`${this.baseUrl}${id}/users/`, data); + } } export default Organizations; diff --git a/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx b/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx index d54da84592..78d18340be 100644 --- a/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx +++ b/awx/ui_next/src/screens/User/UserAdd/UserAdd.jsx @@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom'; import { Card, PageSection } from '@patternfly/react-core'; import { CardBody } from '../../../components/Card'; import UserForm from '../shared/UserForm'; -import { UsersAPI } from '../../../api'; +import { OrganizationsAPI } from '../../../api'; function UserAdd() { const [formSubmitError, setFormSubmitError] = useState(null); @@ -11,10 +11,11 @@ function UserAdd() { const handleSubmit = async values => { setFormSubmitError(null); + const { organization, ...userValues } = values; try { const { data: { id }, - } = await UsersAPI.create(values); + } = await OrganizationsAPI.createUser(organization, userValues); history.push(`/users/${id}/details`); } catch (error) { setFormSubmitError(error); diff --git a/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx b/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx index ec18de0974..55327cfcbd 100644 --- a/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx +++ b/awx/ui_next/src/screens/User/UserAdd/UserAdd.test.jsx @@ -6,7 +6,7 @@ import { waitForElement, } from '../../../../testUtils/enzymeHelpers'; import UserAdd from './UserAdd'; -import { UsersAPI } from '../../../api'; +import { OrganizationsAPI } from '../../../api'; jest.mock('../../../api'); let wrapper; @@ -16,7 +16,7 @@ describe('', () => { await act(async () => { wrapper = mountWithContexts(); }); - UsersAPI.create.mockResolvedValueOnce({ data: {} }); + OrganizationsAPI.createUser.mockResolvedValueOnce({ data: {} }); const updatedUserData = { username: 'sysadmin', email: 'sysadmin@ansible.com', @@ -30,7 +30,11 @@ describe('', () => { await act(async () => { wrapper.find('UserForm').prop('handleSubmit')(updatedUserData); }); - expect(UsersAPI.create).toHaveBeenCalledWith(updatedUserData); + + const { organization, ...userData } = updatedUserData; + expect(OrganizationsAPI.createUser.mock.calls).toEqual([ + [organization, userData], + ]); }); test('should navigate to users list when cancel is clicked', async () => { @@ -58,7 +62,7 @@ describe('', () => { is_superuser: true, is_system_auditor: false, }; - UsersAPI.create.mockResolvedValueOnce({ + OrganizationsAPI.createUser.mockResolvedValueOnce({ data: { id: 5, ...userData, From dc9f2441dfde3bf1657e4cc8ad43fd06e2db1bab Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 19 Aug 2020 10:52:00 -0400 Subject: [PATCH 038/188] Embolden user organization name --- .../screens/User/UserOrganizations/UserOrganizationListItem.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx b/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx index bc01af942d..d31c1c7670 100644 --- a/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx +++ b/awx/ui_next/src/screens/User/UserOrganizations/UserOrganizationListItem.jsx @@ -19,7 +19,7 @@ export default function UserOrganizationListItem({ organization }) { to={`/organizations/${organization.id}/details`} id={labelId} > - {organization.name} + {organization.name} , From 612bb81976ba5d590656e534d270d65c697129c2 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 19 Aug 2020 11:02:28 -0400 Subject: [PATCH 039/188] add a deprecation warning for mercurial project syncs see: https://github.com/ansible/awx/issues/7932 --- CHANGELOG.md | 1 + awx/playbooks/action_plugins/hg_deprecation.py | 15 +++++++++++++++ awx/playbooks/project_update_hg_tasks.yml | 3 +++ 3 files changed, 19 insertions(+) create mode 100644 awx/playbooks/action_plugins/hg_deprecation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 54573a057f..15723a0106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/`. ## 14.1.0 (TBD) +- Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 - Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 - Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847 - Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 diff --git a/awx/playbooks/action_plugins/hg_deprecation.py b/awx/playbooks/action_plugins/hg_deprecation.py new file mode 100644 index 0000000000..d4593b2360 --- /dev/null +++ b/awx/playbooks/action_plugins/hg_deprecation.py @@ -0,0 +1,15 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.action import ActionBase + + +class ActionModule(ActionBase): + + def run(self, tmp=None, task_vars=None): + self._supports_check_mode = False + result = super(ActionModule, self).run(tmp, task_vars) + result['changed'] = result['failed'] = False + result['msg'] = '' + self._display.deprecated("Mercurial support is deprecated") + return result diff --git a/awx/playbooks/project_update_hg_tasks.yml b/awx/playbooks/project_update_hg_tasks.yml index 251013698f..3553d60984 100644 --- a/awx/playbooks/project_update_hg_tasks.yml +++ b/awx/playbooks/project_update_hg_tasks.yml @@ -1,4 +1,7 @@ --- +- name: Mercurial support is deprecated. + hg_deprecation: + - name: update project using hg hg: dest: "{{project_path|quote}}" From 8a2bf8c1fc993f0d712d281f1cc2bed7867736e5 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Wed, 19 Aug 2020 12:18:04 -0400 Subject: [PATCH 040/188] adds fix to allow look up to fetch data --- awx/ui_next/src/components/Lookup/CredentialLookup.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx index da62a053ce..22f7dc54a6 100644 --- a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx +++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx @@ -68,8 +68,8 @@ function CredentialLookup({ actionsResponse?.data?.related_search_fields || [] ).map(val => val.slice(0, -8)), searchableKeys: Object.keys( - actionsResponse.data.actions?.GET || {} - ).filter(key => actionsResponse.data.actions?.GET[key].filterable), + actionsResponse.data?.actions?.GET || {} + ).filter(key => actionsResponse.data?.actions?.GET[key].filterable), }; }, [ credentialTypeId, From 815d691622c612153b62f6c23d35b2d682f62a0f Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 19 Aug 2020 12:21:32 -0400 Subject: [PATCH 041/188] clean up old authtoken support just use Bearer tokens - those are the only type of tokens we support --- awxkit/awxkit/api/client.py | 7 +++---- awxkit/awxkit/awx/utils.py | 2 +- awxkit/awxkit/cli/client.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/awxkit/awxkit/api/client.py b/awxkit/awxkit/api/client.py index 7844a7fa11..77c71a569b 100644 --- a/awxkit/awxkit/api/client.py +++ b/awxkit/awxkit/api/client.py @@ -15,12 +15,11 @@ class ConnectionException(exc.Common): class Token_Auth(requests.auth.AuthBase): - def __init__(self, token, auth_type='Token'): + def __init__(self, token): self.token = token - self.auth_type = auth_type def __call__(self, request): - request.headers['Authorization'] = '{0.auth_type} {0.token}'.format(self) + request.headers['Authorization'] = 'Bearer {0.token}'.format(self) return request @@ -57,7 +56,7 @@ class Connection(object): else: self.session.auth = (username, password) elif token: - self.session.auth = Token_Auth(token, auth_type=kwargs.get('auth_type', 'Token')) + self.session.auth = Token_Auth(token) else: self.session.auth = None diff --git a/awxkit/awxkit/awx/utils.py b/awxkit/awxkit/awx/utils.py index 2238297b67..d25e555ad6 100644 --- a/awxkit/awxkit/awx/utils.py +++ b/awxkit/awxkit/awx/utils.py @@ -90,7 +90,7 @@ def as_user(v, username, password=None): if session_id: del connection.session.cookies['sessionid'] if access_token: - kwargs = dict(token=access_token, auth_type='Bearer') + kwargs = dict(token=access_token) else: kwargs = connection.get_session_requirements() else: diff --git a/awxkit/awxkit/cli/client.py b/awxkit/awxkit/cli/client.py index 3feded89dc..f14d6df135 100755 --- a/awxkit/awxkit/cli/client.py +++ b/awxkit/awxkit/cli/client.py @@ -88,7 +88,7 @@ class CLI(object): token = self.get_config('token') if token: self.root.connection.login( - None, None, token=token, auth_type='Bearer' + None, None, token=token, ) else: config.use_sessions = True From b14515b28792253df17efbe398131ce7003ee02b Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 19 Aug 2020 13:18:44 -0400 Subject: [PATCH 042/188] fix a bug that prevents the explicit removal of instances from groups --- awx/main/management/commands/remove_from_queue.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/main/management/commands/remove_from_queue.py b/awx/main/management/commands/remove_from_queue.py index df7530992c..b249749219 100644 --- a/awx/main/management/commands/remove_from_queue.py +++ b/awx/main/management/commands/remove_from_queue.py @@ -32,4 +32,7 @@ class Command(BaseCommand): sys.exit(1) i = i.first() ig.instances.remove(i) + if i.hostname in ig.policy_instance_list: + ig.policy_instance_list.remove(i.hostname) + ig.save() print("Instance removed from instance group") From 7bff11379c55e0d027772a95b7fb7a64a877df4c Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Wed, 19 Aug 2020 13:32:48 -0400 Subject: [PATCH 043/188] Update awx/ui_next/src/components/Lookup/CredentialLookup.jsx Co-authored-by: Jake McDermott --- awx/ui_next/src/components/Lookup/CredentialLookup.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx index 22f7dc54a6..c67e0087c6 100644 --- a/awx/ui_next/src/components/Lookup/CredentialLookup.jsx +++ b/awx/ui_next/src/components/Lookup/CredentialLookup.jsx @@ -69,7 +69,7 @@ function CredentialLookup({ ).map(val => val.slice(0, -8)), searchableKeys: Object.keys( actionsResponse.data?.actions?.GET || {} - ).filter(key => actionsResponse.data?.actions?.GET[key].filterable), + ).filter(key => actionsResponse.data?.actions?.GET[key]?.filterable), }; }, [ credentialTypeId, From c0cb1dee91bae4a45580841eb3682cfad93b0104 Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 19 Aug 2020 13:55:42 -0400 Subject: [PATCH 044/188] Adds workflow detail tab to workflow results --- awx/ui_next/src/screens/Job/Job.jsx | 4 +--- .../src/screens/Job/JobDetail/JobDetail.jsx | 15 ++++++++++++++- .../src/screens/Job/JobDetail/JobDetail.test.jsx | 2 +- .../screens/Job/WorkflowDetail/WorkflowDetail.jsx | 7 ------- .../src/screens/Job/WorkflowDetail/index.js | 1 - 5 files changed, 16 insertions(+), 13 deletions(-) delete mode 100644 awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx delete mode 100644 awx/ui_next/src/screens/Job/WorkflowDetail/index.js diff --git a/awx/ui_next/src/screens/Job/Job.jsx b/awx/ui_next/src/screens/Job/Job.jsx index bc95cfaf39..9530c4eeb3 100644 --- a/awx/ui_next/src/screens/Job/Job.jsx +++ b/awx/ui_next/src/screens/Job/Job.jsx @@ -7,10 +7,8 @@ import { Card, PageSection } from '@patternfly/react-core'; import { JobsAPI } from '../../api'; import ContentError from '../../components/ContentError'; import RoutedTabs from '../../components/RoutedTabs'; - import JobDetail from './JobDetail'; import JobOutput from './JobOutput'; -import WorkflowDetail from './WorkflowDetail'; import { WorkflowOutput } from './WorkflowOutput'; import { JOB_TYPE_URL_SEGMENTS } from '../../constants'; @@ -129,7 +127,7 @@ class Job extends Component { {job && job.type === 'workflow_job' && [ - + , diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx index dc7a3e0e6b..cd7b2c4dd3 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx @@ -84,6 +84,7 @@ function JobDetail({ job, i18n }) { instance_group: instanceGroup, inventory, job_template: jobTemplate, + workflow_job_template: workflowJobTemplate, labels, project, } = job.summary_fields; @@ -143,7 +144,7 @@ function JobDetail({ job, i18n }) { /> {jobTemplate && ( {jobTemplate.name} @@ -151,6 +152,18 @@ function JobDetail({ job, i18n }) { } /> )} + {workflowJobTemplate && ( + + {workflowJobTemplate.name} + + } + /> + )} ', () => { assertDetail('Status', 'Successful'); assertDetail('Started', '8/8/2019, 7:24:18 PM'); assertDetail('Finished', '8/8/2019, 7:24:50 PM'); - assertDetail('Template', mockJobData.summary_fields.job_template.name); + assertDetail('Job Template', mockJobData.summary_fields.job_template.name); assertDetail('Job Type', 'Run'); assertDetail('Launched By', mockJobData.summary_fields.created_by.username); assertDetail('Inventory', mockJobData.summary_fields.inventory.name); diff --git a/awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx b/awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx deleted file mode 100644 index 26d0384ab3..0000000000 --- a/awx/ui_next/src/screens/Job/WorkflowDetail/WorkflowDetail.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -function WorkflowDetail() { - return
Workflow Detail!
; -} - -export default WorkflowDetail; diff --git a/awx/ui_next/src/screens/Job/WorkflowDetail/index.js b/awx/ui_next/src/screens/Job/WorkflowDetail/index.js deleted file mode 100644 index 3ced22dd95..0000000000 --- a/awx/ui_next/src/screens/Job/WorkflowDetail/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './WorkflowDetail'; From 40f67414741b44a8c76c104825362aded869898b Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 3 Aug 2020 12:09:00 -0400 Subject: [PATCH 045/188] Adding import/export awx kit features Changed library structure Origional TowerModule becomes TowerLegacyModule TowerModule from tower_api becomes TowerAPIModule A real base TowerModule is created in tower_module.py A new TowerAWXKitModule is created in tower_awxkit TowerAWXKitModule and TowerAPIModule are child classes of TowerModule --- awx_collection/plugins/inventory/tower.py | 6 +- awx_collection/plugins/lookup/tower_api.py | 6 +- .../plugins/module_utils/tower_api.py | 242 +----------------- .../plugins/module_utils/tower_awxkit.py | 50 ++++ .../{ansible_tower.py => tower_legacy.py} | 4 +- .../plugins/module_utils/tower_module.py | 240 +++++++++++++++++ .../plugins/modules/tower_credential.py | 4 +- .../modules/tower_credential_input_source.py | 4 +- .../plugins/modules/tower_credential_type.py | 4 +- .../plugins/modules/tower_export.py | 155 +++++++++++ awx_collection/plugins/modules/tower_group.py | 4 +- awx_collection/plugins/modules/tower_host.py | 4 +- .../plugins/modules/tower_import.py | 95 +++++++ .../plugins/modules/tower_inventory.py | 4 +- .../plugins/modules/tower_inventory_source.py | 4 +- .../plugins/modules/tower_job_cancel.py | 4 +- .../plugins/modules/tower_job_launch.py | 4 +- .../plugins/modules/tower_job_list.py | 4 +- .../plugins/modules/tower_job_template.py | 4 +- .../plugins/modules/tower_job_wait.py | 4 +- awx_collection/plugins/modules/tower_label.py | 4 +- .../plugins/modules/tower_license.py | 4 +- awx_collection/plugins/modules/tower_meta.py | 4 +- .../plugins/modules/tower_notification.py | 4 +- .../plugins/modules/tower_organization.py | 4 +- .../plugins/modules/tower_project.py | 4 +- .../plugins/modules/tower_receive.py | 4 +- awx_collection/plugins/modules/tower_role.py | 4 +- .../plugins/modules/tower_schedule.py | 4 +- awx_collection/plugins/modules/tower_send.py | 4 +- .../plugins/modules/tower_settings.py | 4 +- awx_collection/plugins/modules/tower_team.py | 4 +- awx_collection/plugins/modules/tower_token.py | 4 +- awx_collection/plugins/modules/tower_user.py | 4 +- .../modules/tower_workflow_job_template.py | 4 +- .../tower_workflow_job_template_node.py | 4 +- .../plugins/modules/tower_workflow_launch.py | 4 +- .../modules/tower_workflow_template.py | 6 +- awx_collection/test/awx/conftest.py | 25 +- awx_collection/test/awx/test_module_utils.py | 18 +- .../roles/generate/templates/tower_module.j2 | 4 +- 41 files changed, 652 insertions(+), 315 deletions(-) create mode 100644 awx_collection/plugins/module_utils/tower_awxkit.py rename awx_collection/plugins/module_utils/{ansible_tower.py => tower_legacy.py} (97%) create mode 100644 awx_collection/plugins/module_utils/tower_module.py create mode 100644 awx_collection/plugins/modules/tower_export.py create mode 100644 awx_collection/plugins/modules/tower_import.py diff --git a/awx_collection/plugins/inventory/tower.py b/awx_collection/plugins/inventory/tower.py index 872e2a3328..7dc4aaa1a3 100644 --- a/awx_collection/plugins/inventory/tower.py +++ b/awx_collection/plugins/inventory/tower.py @@ -72,7 +72,7 @@ from ansible.errors import AnsibleParserError, AnsibleOptionsError from ansible.plugins.inventory import BaseInventoryPlugin from ansible.config.manager import ensure_type -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def handle_error(**kwargs): @@ -104,12 +104,12 @@ class InventoryModule(BaseInventoryPlugin): # Defer processing of params to logic shared with the modules module_params = {} - for plugin_param, module_param in TowerModule.short_params.items(): + for plugin_param, module_param in TowerAPIModule.short_params.items(): opt_val = self.get_option(plugin_param) if opt_val is not None: module_params[module_param] = opt_val - module = TowerModule( + module = TowerAPIModule( argument_spec={}, direct_params=module_params, error_callback=handle_error, warn_callback=self.warn_callback ) diff --git a/awx_collection/plugins/lookup/tower_api.py b/awx_collection/plugins/lookup/tower_api.py index 9829507125..76b32be60a 100644 --- a/awx_collection/plugins/lookup/tower_api.py +++ b/awx_collection/plugins/lookup/tower_api.py @@ -115,7 +115,7 @@ from ansible.plugins.lookup import LookupBase from ansible.errors import AnsibleError from ansible.module_utils._text import to_native from ansible.utils.display import Display -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule class LookupModule(LookupBase): @@ -133,13 +133,13 @@ class LookupModule(LookupBase): # Defer processing of params to logic shared with the modules module_params = {} - for plugin_param, module_param in TowerModule.short_params.items(): + for plugin_param, module_param in TowerAPIModule.short_params.items(): opt_val = self.get_option(plugin_param) if opt_val is not None: module_params[module_param] = opt_val # Create our module - module = TowerModule( + module = TowerAPIModule( argument_spec={}, direct_params=module_params, error_callback=self.handle_error, warn_callback=self.warn_callback ) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index d0120ec003..089273bff1 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -1,37 +1,17 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils.basic import AnsibleModule, env_fallback +from . tower_module import TowerModule from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError -from ansible.module_utils.six import PY2, string_types -from ansible.module_utils.six.moves import StringIO -from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode +from ansible.module_utils.six import PY2 +from ansible.module_utils.six.moves.urllib.parse import urlencode from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.http_cookiejar import CookieJar -from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError -from socket import gethostbyname import re from json import loads, dumps -from os.path import isfile, expanduser, split, join, exists, isdir -from os import access, R_OK, getcwd -from distutils.util import strtobool -try: - import yaml - HAS_YAML = True -except ImportError: - HAS_YAML = False - - -class ConfigFileException(Exception): - pass - - -class ItemNotDefined(Exception): - pass - - -class TowerModule(AnsibleModule): +class TowerAPIModule(TowerModule): + # TODO: Move the collection version check into tower_module.py # This gets set by the make process so whatever is in here is irrelevant _COLLECTION_VERSION = "0.0.1-devel" _COLLECTION_TYPE = "awx" @@ -41,197 +21,15 @@ class TowerModule(AnsibleModule): 'awx': 'AWX', 'tower': 'Red Hat Ansible Tower', } - url = None - AUTH_ARGSPEC = dict( - tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), - tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), - tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), - validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), - tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), - tower_config_file=dict(type='path', required=False, default=None), - ) - short_params = { - 'host': 'tower_host', - 'username': 'tower_username', - 'password': 'tower_password', - 'verify_ssl': 'validate_certs', - 'oauth_token': 'tower_oauthtoken', - } - host = '127.0.0.1' - username = None - password = None - verify_ssl = True - oauth_token = None - oauth_token_id = None session = None cookie_jar = CookieJar() - authenticated = False - config_name = 'tower_cli.cfg' - ENCRYPTED_STRING = "$encrypted$" - version_checked = False - error_callback = None - warn_callback = None def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): - full_argspec = {} - full_argspec.update(TowerModule.AUTH_ARGSPEC) - full_argspec.update(argument_spec) kwargs['supports_check_mode'] = True - self.error_callback = error_callback - self.warn_callback = warn_callback - - self.json_output = {'changed': False} - - if direct_params is not None: - self.params = direct_params - else: - super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs) - - self.load_config_files() - - # Parameters specified on command line will override settings in any config - for short_param, long_param in self.short_params.items(): - direct_value = self.params.get(long_param) - if direct_value is not None: - setattr(self, short_param, direct_value) - - # Perform magic depending on whether tower_oauthtoken is a string or a dict - if self.params.get('tower_oauthtoken'): - token_param = self.params.get('tower_oauthtoken') - if type(token_param) is dict: - if 'token' in token_param: - self.oauth_token = self.params.get('tower_oauthtoken')['token'] - else: - self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") - elif isinstance(token_param, string_types): - self.oauth_token = self.params.get('tower_oauthtoken') - else: - error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) - self.fail_json(msg=error_msg) - - # Perform some basic validation - if not re.match('^https{0,1}://', self.host): - self.host = "https://{0}".format(self.host) - - # Try to parse the hostname as a url - try: - self.url = urlparse(self.host) - except Exception as e: - self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e)) - - # Try to resolve the hostname - hostname = self.url.netloc.split(':')[0] - try: - gethostbyname(hostname) - except Exception as e: - self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e)) - + super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, error_callback=error_callback, warn_callback=warn_callback, **kwargs) self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl) - def load_config_files(self): - # Load configs like TowerCLI would have from least import to most - config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))] - local_dir = getcwd() - config_files.append(join(local_dir, self.config_name)) - while split(local_dir)[1]: - local_dir = split(local_dir)[0] - config_files.insert(2, join(local_dir, ".{0}".format(self.config_name))) - - # If we have a specified tower config, load it - if self.params.get('tower_config_file'): - duplicated_params = [ - fn for fn in self.AUTH_ARGSPEC - if fn != 'tower_config_file' and self.params.get(fn) is not None - ] - if duplicated_params: - self.warn(( - 'The parameter(s) {0} were provided at the same time as tower_config_file. ' - 'Precedence may be unstable, we suggest either using config file or params.' - ).format(', '.join(duplicated_params))) - try: - # TODO: warn if there are conflicts with other params - self.load_config(self.params.get('tower_config_file')) - except ConfigFileException as cfe: - # Since we were told specifically to load this we want it to fail if we have an error - self.fail_json(msg=cfe) - else: - for config_file in config_files: - if exists(config_file) and not isdir(config_file): - # Only throw a formatting error if the file exists and is not a directory - try: - self.load_config(config_file) - except ConfigFileException: - self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file)) - - def load_config(self, config_path): - # Validate the config file is an actual file - if not isfile(config_path): - raise ConfigFileException('The specified config file does not exist') - - if not access(config_path, R_OK): - raise ConfigFileException("The specified config file cannot be read") - - # Read in the file contents: - with open(config_path, 'r') as f: - config_string = f.read() - - # First try to yaml load the content (which will also load json) - try: - try_config_parsing = True - if HAS_YAML: - try: - config_data = yaml.load(config_string, Loader=yaml.SafeLoader) - # If this is an actual ini file, yaml will return the whole thing as a string instead of a dict - if type(config_data) is not dict: - raise AssertionError("The yaml config file is not properly formatted as a dict.") - try_config_parsing = False - - except(AttributeError, yaml.YAMLError, AssertionError): - try_config_parsing = True - - if try_config_parsing: - # TowerCLI used to support a config file with a missing [general] section by prepending it if missing - if '[general]' not in config_string: - config_string = '[general]\n{0}'.format(config_string) - - config = ConfigParser() - - try: - placeholder_file = StringIO(config_string) - # py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3 - # This "if" removes the deprecation warning - if hasattr(config, 'read_file'): - config.read_file(placeholder_file) - else: - config.readfp(placeholder_file) - - # If we made it here then we have values from reading the ini file, so let's pull them out into a dict - config_data = {} - for honorred_setting in self.short_params: - try: - config_data[honorred_setting] = config.get('general', honorred_setting) - except NoOptionError: - pass - - except Exception as e: - raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e)) - - except Exception as e: - raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) - - # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here - for honorred_setting in self.short_params: - if honorred_setting in config_data: - # Veriffy SSL must be a boolean - if honorred_setting == 'verify_ssl': - if type(config_data[honorred_setting]) is str: - setattr(self, honorred_setting, strtobool(config_data[honorred_setting])) - else: - setattr(self, honorred_setting, bool(config_data[honorred_setting])) - else: - setattr(self, honorred_setting, config_data[honorred_setting]) - @staticmethod def param_to_endpoint(name): exceptions = { @@ -650,13 +448,13 @@ class TowerModule(AnsibleModule): """ if isinstance(obj, dict): for val in obj.values(): - if TowerModule.has_encrypted_values(val): + if TowerAPIModule.has_encrypted_values(val): return True elif isinstance(obj, list): for val in obj: - if TowerModule.has_encrypted_values(val): + if TowerAPIModule.has_encrypted_values(val): return True - elif obj == TowerModule.ENCRYPTED_STRING: + elif obj == TowerAPIModule.ENCRYPTED_STRING: return True return False @@ -678,10 +476,9 @@ class TowerModule(AnsibleModule): # This will exit from the module on its own # If the method successfully updates an item and on_update param is defined, # the on_update parameter will be called as a method pasing in this object and the json from the response - # This will return one of three things: + # This will return one of two things: # 1. None if the existing_item does not need to be updated # 2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module. - # 3. An ItemNotDefined exception, if the existing_item does not exist # Note: common error codes from the Tower API can cause the module to fail response = None if existing_item: @@ -777,25 +574,6 @@ class TowerModule(AnsibleModule): # Sanity check: Did the server send back some kind of internal error? self.warn('Failed to release tower token {0}: {1}'.format(self.oauth_token_id, e)) - def fail_json(self, **kwargs): - # Try to log out if we are authenticated - self.logout() - if self.error_callback: - self.error_callback(**kwargs) - else: - super(TowerModule, self).fail_json(**kwargs) - - def exit_json(self, **kwargs): - # Try to log out if we are authenticated - self.logout() - super(TowerModule, self).exit_json(**kwargs) - - def warn(self, warning): - if self.warn_callback is not None: - self.warn_callback(warning) - else: - super(TowerModule, self).warn(warning) - def is_job_done(self, job_status): if job_status in ['new', 'pending', 'waiting', 'running']: return False diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py new file mode 100644 index 0000000000..eedf3a7387 --- /dev/null +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from . tower_module import TowerModule +from ansible.module_utils.basic import missing_required_lib + +try: + from awxkit.api.client import Connection + from awxkit.api.pages.api import ApiV2 + from awxkit.api import get_registered_page + HAS_AWX_KIT = True +except ImportError: + HAS_AWX_KIT = False + + +class TowerAWXKitModule(TowerModule): + connection = None + apiV2Ref = None + + def __init__(self, argument_spec, **kwargs): + kwargs['supports_check_mode'] = False + + super(TowerAWXKitModule, self).__init__(argument_spec=argument_spec, **kwargs) + + # Die if we don't have AWX_KIT installed + if not HAS_AWX_KIT: + self.exit_module(msg=missing_required_lib('awxkit')) + + # Establish our conneciton object + self.connection = Connection(self.host, verify=self.verify_ssl) + + def authenticate(self): + try: + self.connection.login(username=self.username, password=self.password, token=self.oauth_token) + # If we have neither of these, then we can try un-authenticated access + self.authenticated = True + except Exception: + self.exit_module("Failed to authenticate") + + def get_api_v2_object(self): + if not self.apiV2Ref: + if not self.authenticated: + self.authenticate() + v2_index = get_registered_page('/api/v2/')(self.connection).get() + self.api_ref = ApiV2(connection=self.connection, **{'json': v2_index}) + return self.api_ref + + def logout(self): + if self.authenticated: + self.connection.logout() diff --git a/awx_collection/plugins/module_utils/ansible_tower.py b/awx_collection/plugins/module_utils/tower_legacy.py similarity index 97% rename from awx_collection/plugins/module_utils/ansible_tower.py rename to awx_collection/plugins/module_utils/tower_legacy.py index 17d6a38680..3c8408610d 100644 --- a/awx_collection/plugins/module_utils/ansible_tower.py +++ b/awx_collection/plugins/module_utils/tower_legacy.py @@ -91,7 +91,7 @@ def tower_check_mode(module): module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo)) -class TowerModule(AnsibleModule): +class TowerLegacyModule(AnsibleModule): def __init__(self, argument_spec, **kwargs): args = dict( tower_host=dict(), @@ -110,7 +110,7 @@ class TowerModule(AnsibleModule): ('tower_config_file', 'validate_certs'), )) - super(TowerModule, self).__init__(argument_spec=args, **kwargs) + super(TowerLegacyModule, self).__init__(argument_spec=args, **kwargs) if not HAS_TOWER_CLI: self.fail_json(msg=missing_required_lib('ansible-tower-cli'), diff --git a/awx_collection/plugins/module_utils/tower_module.py b/awx_collection/plugins/module_utils/tower_module.py new file mode 100644 index 0000000000..5d5b4d4239 --- /dev/null +++ b/awx_collection/plugins/module_utils/tower_module.py @@ -0,0 +1,240 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.six import PY2, string_types +from ansible.module_utils.six.moves import StringIO +from ansible.module_utils.six.moves.urllib.parse import urlparse +from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError +from socket import gethostbyname +import re +from os.path import isfile, expanduser, split, join, exists, isdir +from os import access, R_OK, getcwd +from distutils.util import strtobool + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +class ConfigFileException(Exception): + pass + + +class ItemNotDefined(Exception): + pass + + +class TowerModule(AnsibleModule): + url = None + AUTH_ARGSPEC = dict( + tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), + tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), + tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), + validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), + tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), + tower_config_file=dict(type='path', required=False, default=None), + ) + short_params = { + 'host': 'tower_host', + 'username': 'tower_username', + 'password': 'tower_password', + 'verify_ssl': 'validate_certs', + 'oauth_token': 'tower_oauthtoken', + } + host = '127.0.0.1' + username = None + password = None + verify_ssl = True + oauth_token = None + oauth_token_id = None + authenticated = False + config_name = 'tower_cli.cfg' + ENCRYPTED_STRING = "$encrypted$" + version_checked = False + error_callback = None + warn_callback = None + + def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs): + full_argspec = {} + full_argspec.update(TowerModule.AUTH_ARGSPEC) + full_argspec.update(argument_spec) + kwargs['supports_check_mode'] = True + + self.error_callback = error_callback + self.warn_callback = warn_callback + + self.json_output = {'changed': False} + + if direct_params is not None: + self.params = direct_params + else: + super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs) + + self.load_config_files() + + # Parameters specified on command line will override settings in any config + for short_param, long_param in self.short_params.items(): + direct_value = self.params.get(long_param) + if direct_value is not None: + setattr(self, short_param, direct_value) + + # Perform magic depending on whether tower_oauthtoken is a string or a dict + if self.params.get('tower_oauthtoken'): + token_param = self.params.get('tower_oauthtoken') + if type(token_param) is dict: + if 'token' in token_param: + self.oauth_token = self.params.get('tower_oauthtoken')['token'] + else: + self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") + elif isinstance(token_param, string_types): + self.oauth_token = self.params.get('tower_oauthtoken') + else: + error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) + self.fail_json(msg=error_msg) + + # Perform some basic validation + if not re.match('^https{0,1}://', self.host): + self.host = "https://{0}".format(self.host) + + # Try to parse the hostname as a url + try: + self.url = urlparse(self.host) + except Exception as e: + self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e)) + + # Try to resolve the hostname + hostname = self.url.netloc.split(':')[0] + try: + gethostbyname(hostname) + except Exception as e: + self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e)) + + def load_config_files(self): + # Load configs like TowerCLI would have from least import to most + config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))] + local_dir = getcwd() + config_files.append(join(local_dir, self.config_name)) + while split(local_dir)[1]: + local_dir = split(local_dir)[0] + config_files.insert(2, join(local_dir, ".{0}".format(self.config_name))) + + # If we have a specified tower config, load it + if self.params.get('tower_config_file'): + duplicated_params = [ + fn for fn in self.AUTH_ARGSPEC + if fn != 'tower_config_file' and self.params.get(fn) is not None + ] + if duplicated_params: + self.warn(( + 'The parameter(s) {0} were provided at the same time as tower_config_file. ' + 'Precedence may be unstable, we suggest either using config file or params.' + ).format(', '.join(duplicated_params))) + try: + # TODO: warn if there are conflicts with other params + self.load_config(self.params.get('tower_config_file')) + except ConfigFileException as cfe: + # Since we were told specifically to load this we want it to fail if we have an error + self.fail_json(msg=cfe) + else: + for config_file in config_files: + if exists(config_file) and not isdir(config_file): + # Only throw a formatting error if the file exists and is not a directory + try: + self.load_config(config_file) + except ConfigFileException: + self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file)) + + def load_config(self, config_path): + # Validate the config file is an actual file + if not isfile(config_path): + raise ConfigFileException('The specified config file does not exist') + + if not access(config_path, R_OK): + raise ConfigFileException("The specified config file cannot be read") + + # Read in the file contents: + with open(config_path, 'r') as f: + config_string = f.read() + + # First try to yaml load the content (which will also load json) + try: + try_config_parsing = True + if HAS_YAML: + try: + config_data = yaml.load(config_string, Loader=yaml.SafeLoader) + # If this is an actual ini file, yaml will return the whole thing as a string instead of a dict + if type(config_data) is not dict: + raise AssertionError("The yaml config file is not properly formatted as a dict.") + try_config_parsing = False + + except(AttributeError, yaml.YAMLError, AssertionError): + try_config_parsing = True + + if try_config_parsing: + # TowerCLI used to support a config file with a missing [general] section by prepending it if missing + if '[general]' not in config_string: + config_string = '[general]\n{0}'.format(config_string) + + config = ConfigParser() + + try: + placeholder_file = StringIO(config_string) + # py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3 + # This "if" removes the deprecation warning + if hasattr(config, 'read_file'): + config.read_file(placeholder_file) + else: + config.readfp(placeholder_file) + + # If we made it here then we have values from reading the ini file, so let's pull them out into a dict + config_data = {} + for honorred_setting in self.short_params: + try: + config_data[honorred_setting] = config.get('general', honorred_setting) + except NoOptionError: + pass + + except Exception as e: + raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e)) + + except Exception as e: + raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) + + # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here + for honorred_setting in self.short_params: + if honorred_setting in config_data: + # Veriffy SSL must be a boolean + if honorred_setting == 'verify_ssl': + if type(config_data[honorred_setting]) is str: + setattr(self, honorred_setting, strtobool(config_data[honorred_setting])) + else: + setattr(self, honorred_setting, bool(config_data[honorred_setting])) + else: + setattr(self, honorred_setting, config_data[honorred_setting]) + + + def logout(self): + # This method is intended to be overridden + pass + + def fail_json(self, **kwargs): + # Try to log out if we are authenticated + self.logout() + if self.error_callback: + self.error_callback(**kwargs) + else: + super(TowerModule, self).fail_json(**kwargs) + + def exit_json(self, **kwargs): + # Try to log out if we are authenticated + self.logout() + super(TowerModule, self).exit_json(**kwargs) + + def warn(self, warning): + if self.warn_callback is not None: + self.warn_callback(warning) + else: + super(TowerModule, self).warn(warning) diff --git a/awx_collection/plugins/modules/tower_credential.py b/awx_collection/plugins/modules/tower_credential.py index 97a801aa8f..2d4cbd3331 100644 --- a/awx_collection/plugins/modules/tower_credential.py +++ b/awx_collection/plugins/modules/tower_credential.py @@ -269,7 +269,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule KIND_CHOICES = { 'ssh': 'Machine', @@ -336,7 +336,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']]) + module = TowerAPIModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']]) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_credential_input_source.py b/awx_collection/plugins/modules/tower_credential_input_source.py index bc2cb85579..cdc55cb1f0 100644 --- a/awx_collection/plugins/modules/tower_credential_input_source.py +++ b/awx_collection/plugins/modules/tower_credential_input_source.py @@ -70,7 +70,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -85,7 +85,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters description = module.params.get('description') diff --git a/awx_collection/plugins/modules/tower_credential_type.py b/awx_collection/plugins/modules/tower_credential_type.py index 561ae78f5a..53f0cc45c9 100644 --- a/awx_collection/plugins/modules/tower_credential_type.py +++ b/awx_collection/plugins/modules/tower_credential_type.py @@ -81,7 +81,7 @@ EXAMPLES = ''' RETURN = ''' # ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule KIND_CHOICES = { 'ssh': 'Machine', @@ -105,7 +105,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py new file mode 100644 index 0000000000..84c08fe454 --- /dev/null +++ b/awx_collection/plugins/modules/tower_export.py @@ -0,0 +1,155 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_export +author: "John Westcott IV (@john-westcott-iv)" +version_added: "3.7" +short_description: export resources from Ansible Tower. +description: + - Export assets from Ansible Tower. +options: + all: + description: + - Export all assets + type: bool + default: 'False' + organizations: + description: + - organization name to export + default: '' + type: str + user: + description: + - user name to export + default: '' + type: str + team: + description: + - team name to export + default: '' + type: str + credential_type: + description: + - credential type name to export + default: '' + type: str + credential: + description: + - credential name to export + default: '' + type: str + notification_template: + description: + - notification template name to export + default: '' + type: str + inventory_script: + description: + - inventory script name to export + default: '' + type: str + inventory: + description: + - inventory name to export + default: '' + type: str + project: + description: + - project name to export + default: '' + type: str + job_template: + description: + - job template name to export + default: '' + type: str + workflow: + description: + - workflow name to export + default: '' + type: str +requirements: + - "awxkit >= 9.3.0" +notes: + - Specifying a name of "all" for any asset type will export all items of that asset type. +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Export all tower assets + tower_export: + all: True +- name: Export all inventories + tower_export: + inventory: 'all' +- name: Export a job template named "My Template" and all Credentials + tower_export: + job_template: "My Template" + credential: 'all' +''' + +from os import environ + +from ..module_utils.tower_awxkit import TowerAWXKitModule + +try: + from awxkit.api.pages.api import EXPORTABLE_RESOURCES + HAS_EXPORTABLE_RESOURCES=True +except ImportError: + HAS_EXPORTABLE_RESOURCES=False + + +def main(): + argument_spec = dict( + all=dict(type='bool', default=False), + ) + + # We are not going to raise an error here because the __init__ method of TowerAWXKitModule will do that for us + if HAS_EXPORTABLE_RESOURCES: + for resource in EXPORTABLE_RESOURCES: + argument_spec[resource] = dict() + + module = TowerAWXKitModule(argument_spec=argument_spec) + + if not HAS_EXPORTABLE_RESOURCES: + module.fail_json(msg="Your version of awxkit does not have import/export") + + # The export process will never change a Tower system + module.json_output['changed'] = False + + # The exporter code currently works like the following: + # Empty list == all assets of that type + # string = just one asset of that type (by name) + # None = skip asset type + # Here we are going to setup a dict of values to export + export_args = {} + for resource in EXPORTABLE_RESOURCES: + if module.params.get('all') or module.params.get(resource) == 'all': + # If we are exporting everything or we got the keyword "all" we pass in an empty list for this asset type + export_args[resource] = [] + else: + # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items + export_args[resource] = module.params.get(resource) + + # Run the export process + module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_group.py b/awx_collection/plugins/modules/tower_group.py index a64826eb88..9e2eaf4e7e 100644 --- a/awx_collection/plugins/modules/tower_group.py +++ b/awx_collection/plugins/modules/tower_group.py @@ -76,7 +76,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -94,7 +94,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_host.py b/awx_collection/plugins/modules/tower_host.py index cb4712a27c..f6bfe55404 100644 --- a/awx_collection/plugins/modules/tower_host.py +++ b/awx_collection/plugins/modules/tower_host.py @@ -72,7 +72,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -89,7 +89,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py new file mode 100644 index 0000000000..0bb5dec75a --- /dev/null +++ b/awx_collection/plugins/modules/tower_import.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_import +author: "John Westcott (@john-westcott-iv)" +version_added: "3.7" +short_description: import resources into Ansible Tower. +description: + - Import assets into Ansible Tower. See + U(https://www.ansible.com/tower) for an overview. +options: + assets: + description: + - The assets to import. + - This can be the output of tower_export or loaded from a file + required: True + type: dict +requirements: + - "awxkit >= 9.3.0" +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Import all tower assets + tower_import: + assets: "{{ export_output.assets }}" +''' + +from ..module_utils.tower_awxkit import TowerAWXKitModule + +# These two lines are not needed if awxkit changes to do progamatic notifications on issues +from ansible.module_utils.six.moves import StringIO +import logging + +# In this module we don't use EXPORTABLE_RESOURCES, we just want to validate that our installed awxkit has import/export +try: + from awxkit.api.pages.api import EXPORTABLE_RESOURCES + HAS_EXPORTABLE_RESOURCES=True +except ImportError: + HAS_EXPORTABLE_RESOURCES=False + +def main(): + argument_spec = dict( + assets=dict(type='dict', required=True) + ) + + module = TowerAWXKitModule(argument_spec=argument_spec, supports_check_mode=False) + + assets = module.params.get('assets') + + if not HAS_EXPORTABLE_RESOURCES: + module.fail_json(msg="Your version of awxkit does not appear to have import/export") + + # Currently the import process does not return anything on error + # It simply just logs to pythons logger + # Setup a log gobbler to get error messages from import_assets + logger = logging.getLogger('awxkit.api.pages.api') + logger.setLevel(logging.WARNING) + log_capture_string = StringIO() + ch = logging.StreamHandler(log_capture_string) + ch.setLevel(logging.WARNING) + logger.addHandler(ch) + log_contents = '' + + # Run the import process + try: + module.json_output['changed'] = module.get_api_v2_object().import_assets(assets) + except Exception as e: + module.fail_json(msg="Failed to import assets {0}".format(e)) + finally: + # Finally consume the logs incase there were any errors and die if there were + log_contents = log_capture_string.getvalue() + log_capture_string.close() + if log_contents != '': + module.fail_json(msg=log_contents) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_inventory.py b/awx_collection/plugins/modules/tower_inventory.py index d0ced63048..7f03645e01 100644 --- a/awx_collection/plugins/modules/tower_inventory.py +++ b/awx_collection/plugins/modules/tower_inventory.py @@ -71,7 +71,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -88,7 +88,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index afa6c229e2..5b0e2961df 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -144,7 +144,7 @@ EXAMPLES = ''' private: false ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule from json import dumps @@ -184,7 +184,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_job_cancel.py b/awx_collection/plugins/modules/tower_job_cancel.py index 5e82834f6c..7404d452a4 100644 --- a/awx_collection/plugins/modules/tower_job_cancel.py +++ b/awx_collection/plugins/modules/tower_job_cancel.py @@ -50,7 +50,7 @@ id: ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -61,7 +61,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters job_id = module.params.get('job_id') diff --git a/awx_collection/plugins/modules/tower_job_launch.py b/awx_collection/plugins/modules/tower_job_launch.py index f3447bf24c..25a1c52fdf 100644 --- a/awx_collection/plugins/modules/tower_job_launch.py +++ b/awx_collection/plugins/modules/tower_job_launch.py @@ -124,7 +124,7 @@ status: sample: pending ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -146,7 +146,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) optional_args = {} # Extract our parameters diff --git a/awx_collection/plugins/modules/tower_job_list.py b/awx_collection/plugins/modules/tower_job_list.py index 2ecfd9d98a..642a48b03b 100644 --- a/awx_collection/plugins/modules/tower_job_list.py +++ b/awx_collection/plugins/modules/tower_job_list.py @@ -80,7 +80,7 @@ results: ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -93,7 +93,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, mutually_exclusive=[ ('page', 'all_pages'), diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index 1f1d776f28..1f12d5aadd 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -317,7 +317,7 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -388,7 +388,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py index 77a6977c5c..5e5801b25f 100644 --- a/awx_collection/plugins/modules/tower_job_wait.py +++ b/awx_collection/plugins/modules/tower_job_wait.py @@ -92,7 +92,7 @@ status: ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import time @@ -120,7 +120,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters job_id = module.params.get('job_id') diff --git a/awx_collection/plugins/modules/tower_label.py b/awx_collection/plugins/modules/tower_label.py index d0820d93a8..6a3a8288a2 100644 --- a/awx_collection/plugins/modules/tower_label.py +++ b/awx_collection/plugins/modules/tower_label.py @@ -54,7 +54,7 @@ EXAMPLES = ''' organization: My Organization ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -67,7 +67,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_license.py b/awx_collection/plugins/modules/tower_license.py index a1d9840d50..25d337cc24 100644 --- a/awx_collection/plugins/modules/tower_license.py +++ b/awx_collection/plugins/modules/tower_license.py @@ -43,12 +43,12 @@ EXAMPLES = ''' eula_accepted: True ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): - module = TowerModule( + module = TowerAPIModule( argument_spec=dict( data=dict(type='dict', required=True), eula_accepted=dict(type='bool', required=True), diff --git a/awx_collection/plugins/modules/tower_meta.py b/awx_collection/plugins/modules/tower_meta.py index 6d5c801ade..9455bdf0f4 100644 --- a/awx_collection/plugins/modules/tower_meta.py +++ b/awx_collection/plugins/modules/tower_meta.py @@ -62,11 +62,11 @@ EXAMPLES = ''' ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): - module = TowerModule(argument_spec={}) + module = TowerAPIModule(argument_spec={}) namespace = { 'awx': 'awx', 'tower': 'ansible' diff --git a/awx_collection/plugins/modules/tower_notification.py b/awx_collection/plugins/modules/tower_notification.py index bfe672a50e..12dd28ef31 100644 --- a/awx_collection/plugins/modules/tower_notification.py +++ b/awx_collection/plugins/modules/tower_notification.py @@ -300,7 +300,7 @@ EXAMPLES = ''' RETURN = ''' # ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule OLD_INPUT_NAMES = ( 'username', 'sender', 'recipients', 'use_tls', @@ -355,7 +355,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index fbbbf2885c..1637828149 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -88,7 +88,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -106,7 +106,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 12f9e2809c..36a4f8666a 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -157,7 +157,7 @@ EXAMPLES = ''' import time -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def wait_for_project_update(module, last_request): @@ -205,7 +205,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_receive.py b/awx_collection/plugins/modules/tower_receive.py index b673e9b81d..bd08682503 100644 --- a/awx_collection/plugins/modules/tower_receive.py +++ b/awx_collection/plugins/modules/tower_receive.py @@ -134,7 +134,7 @@ assets: sample: [ {}, {} ] ''' -from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI +from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI try: from tower_cli.cli.transfer.receive import Receiver @@ -163,7 +163,7 @@ def main(): workflow=dict(type='list', default=[], elements='str'), ) - module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) + module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False) module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="awx.awx:14.0.0") diff --git a/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py index 4cba215b0a..d0d010a0a7 100644 --- a/awx_collection/plugins/modules/tower_role.py +++ b/awx_collection/plugins/modules/tower_role.py @@ -89,7 +89,7 @@ EXAMPLES = ''' state: present ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -109,7 +109,7 @@ def main(): state=dict(choices=['present', 'absent'], default='present'), ) - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) role_type = module.params.pop('role') role_field = role_type + '_role' diff --git a/awx_collection/plugins/modules/tower_schedule.py b/awx_collection/plugins/modules/tower_schedule.py index 24f8468e4a..4922aaa688 100644 --- a/awx_collection/plugins/modules/tower_schedule.py +++ b/awx_collection/plugins/modules/tower_schedule.py @@ -136,7 +136,7 @@ EXAMPLES = ''' register: result ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -161,7 +161,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters rrule = module.params.get('rrule') diff --git a/awx_collection/plugins/modules/tower_send.py b/awx_collection/plugins/modules/tower_send.py index 7ac60ece59..772b2b67ec 100644 --- a/awx_collection/plugins/modules/tower_send.py +++ b/awx_collection/plugins/modules/tower_send.py @@ -81,7 +81,7 @@ import os import sys from ansible.module_utils.six.moves import StringIO -from ..module_utils.ansible_tower import TowerModule, tower_auth_config, HAS_TOWER_CLI +from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI from tempfile import mkstemp @@ -103,7 +103,7 @@ def main(): password_management=dict(default='default', choices=['default', 'random']), ) - module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) + module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False) module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="awx.awx:14.0.0") diff --git a/awx_collection/plugins/modules/tower_settings.py b/awx_collection/plugins/modules/tower_settings.py index 9db41d9975..c2e8ed1ae5 100644 --- a/awx_collection/plugins/modules/tower_settings.py +++ b/awx_collection/plugins/modules/tower_settings.py @@ -70,7 +70,7 @@ EXAMPLES = ''' last_name: "surname" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule try: import yaml @@ -111,7 +111,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, required_one_of=[['name', 'settings']], mutually_exclusive=[['name', 'settings']], diff --git a/awx_collection/plugins/modules/tower_team.py b/awx_collection/plugins/modules/tower_team.py index e1506b2425..8ed56e48dc 100644 --- a/awx_collection/plugins/modules/tower_team.py +++ b/awx_collection/plugins/modules/tower_team.py @@ -60,7 +60,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -74,7 +74,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_token.py b/awx_collection/plugins/modules/tower_token.py index 165590520d..ee6fd5c200 100644 --- a/awx_collection/plugins/modules/tower_token.py +++ b/awx_collection/plugins/modules/tower_token.py @@ -117,7 +117,7 @@ tower_token: returned: on successful create ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def return_token(module, last_response): @@ -143,7 +143,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule( + module = TowerAPIModule( argument_spec=argument_spec, mutually_exclusive=[ ('existing_token', 'existing_token_id'), diff --git a/awx_collection/plugins/modules/tower_user.py b/awx_collection/plugins/modules/tower_user.py index 7d049de016..15c41cb081 100644 --- a/awx_collection/plugins/modules/tower_user.py +++ b/awx_collection/plugins/modules/tower_user.py @@ -102,7 +102,7 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -119,7 +119,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters username = module.params.get('username') diff --git a/awx_collection/plugins/modules/tower_workflow_job_template.py b/awx_collection/plugins/modules/tower_workflow_job_template.py index c3ad692af4..8fb350b919 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template.py @@ -137,7 +137,7 @@ EXAMPLES = ''' organization: Default ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json @@ -176,7 +176,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters name = module.params.get('name') diff --git a/awx_collection/plugins/modules/tower_workflow_job_template_node.py b/awx_collection/plugins/modules/tower_workflow_job_template_node.py index 16902e9421..7ef9e14619 100644 --- a/awx_collection/plugins/modules/tower_workflow_job_template_node.py +++ b/awx_collection/plugins/modules/tower_workflow_job_template_node.py @@ -157,7 +157,7 @@ EXAMPLES = ''' - my-first-node ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -185,7 +185,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters identifier = module.params.get('identifier') diff --git a/awx_collection/plugins/modules/tower_workflow_launch.py b/awx_collection/plugins/modules/tower_workflow_launch.py index 8ef73d82fc..249feeed35 100644 --- a/awx_collection/plugins/modules/tower_workflow_launch.py +++ b/awx_collection/plugins/modules/tower_workflow_launch.py @@ -91,7 +91,7 @@ EXAMPLES = ''' wait: False ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule import json import time @@ -111,7 +111,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) optional_args = {} # Extract our parameters diff --git a/awx_collection/plugins/modules/tower_workflow_template.py b/awx_collection/plugins/modules/tower_workflow_template.py index 9a652a4373..a8557b2ad2 100644 --- a/awx_collection/plugins/modules/tower_workflow_template.py +++ b/awx_collection/plugins/modules/tower_workflow_template.py @@ -108,8 +108,8 @@ EXAMPLES = ''' RETURN = ''' # ''' -from ..module_utils.ansible_tower import ( - TowerModule, +from ..module_utils.tower_legacy import ( + TowerLegacyModule, tower_auth_config, tower_check_mode ) @@ -140,7 +140,7 @@ def main(): state=dict(choices=['present', 'absent'], default='present'), ) - module = TowerModule( + module = TowerLegacyModule( argument_spec=argument_spec, supports_check_mode=False ) diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py index 025b8d033d..53270d0e7f 100644 --- a/awx_collection/test/awx/conftest.py +++ b/awx_collection/test/awx/conftest.py @@ -10,7 +10,7 @@ from contextlib import redirect_stdout, suppress from unittest import mock import logging -from requests.models import Response +from requests.models import Response, PreparedRequest import pytest @@ -23,6 +23,11 @@ try: except ImportError: HAS_TOWER_CLI = False +try: + import awxkit + HAS_AWX_KIT = True +except ImportError: + HAS_AWX_KIT = False logger = logging.getLogger('awx.main.tests') @@ -90,7 +95,8 @@ def run_module(request, collection_import): if 'params' in kwargs and method == 'GET': # query params for GET are handled a bit differently by # tower-cli and python requests as opposed to REST framework APIRequestFactory - kwargs_copy.setdefault('data', {}) + if not kwargs_copy.get('data'): + kwargs_copy['data'] = {} if isinstance(kwargs['params'], dict): kwargs_copy['data'].update(kwargs['params']) elif isinstance(kwargs['params'], list): @@ -117,6 +123,8 @@ def run_module(request, collection_import): request_user.username, resp.status_code ) + resp.request = PreparedRequest() + resp.request.prepare(method=method, url=url) return resp def new_open(self, method, url, **kwargs): @@ -142,11 +150,22 @@ def run_module(request, collection_import): def mock_load_params(self): self.params = module_params - with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params): + if getattr(resource_module, 'TowerAWXKitModule', None): + resource_class = resource_module.TowerAWXKitModule + elif getattr(resource_module, 'TowerAPIModule', None): + resource_class = resource_module.TowerAPIModule + elif getattr(resource_module, 'TowerLegacyModule', None): + resource_class = resource_module.TowerLegacyModule + else: + raise("The module has neither a TowerLegacyModule, TowerAWXKitModule or a TowerAPIModule") + + with mock.patch.object(resource_class, '_load_params', new=mock_load_params): # Call the test utility (like a mock server) instead of issuing HTTP requests with mock.patch('ansible.module_utils.urls.Request.open', new=new_open): if HAS_TOWER_CLI: tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request) + elif HAS_AWX_KIT: + tower_cli_mgr = mock.patch('awxkit.api.client.requests.Session.request', new=new_request) else: tower_cli_mgr = suppress() with tower_cli_mgr: diff --git a/awx_collection/test/awx/test_module_utils.py b/awx_collection/test/awx/test_module_utils.py index 0f443890e8..e93b6ee939 100644 --- a/awx_collection/test/awx/test_module_utils.py +++ b/awx_collection/test/awx/test_module_utils.py @@ -30,12 +30,12 @@ def mock_ping_response(self, method, url, **kwargs): def test_version_warning(collection_import, silence_warning): - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule cli_data = {'ANSIBLE_MODULE_ARGS': {}} testargs = ['module_file2.py', json.dumps(cli_data)] with mock.patch.object(sys, 'argv', testargs): with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): - my_module = TowerModule(argument_spec=dict()) + my_module = TowerAPIModule(argument_spec=dict()) my_module._COLLECTION_VERSION = "1.0.0" my_module._COLLECTION_TYPE = "not-junk" my_module.collection_to_version['not-junk'] = 'not-junk' @@ -46,12 +46,12 @@ def test_version_warning(collection_import, silence_warning): def test_type_warning(collection_import, silence_warning): - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule cli_data = {'ANSIBLE_MODULE_ARGS': {}} testargs = ['module_file2.py', json.dumps(cli_data)] with mock.patch.object(sys, 'argv', testargs): with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): - my_module = TowerModule(argument_spec={}) + my_module = TowerAPIModule(argument_spec={}) my_module._COLLECTION_VERSION = "1.2.3" my_module._COLLECTION_TYPE = "junk" my_module.collection_to_version['junk'] = 'junk' @@ -63,7 +63,7 @@ def test_type_warning(collection_import, silence_warning): def test_duplicate_config(collection_import, silence_warning): # imports done here because of PATH issues unique to this test suite - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule data = { 'name': 'zigzoom', 'zig': 'zoom', @@ -71,12 +71,12 @@ def test_duplicate_config(collection_import, silence_warning): 'tower_config_file': 'my_config' } - with mock.patch.object(TowerModule, 'load_config') as mock_load: + with mock.patch.object(TowerAPIModule, 'load_config') as mock_load: argument_spec = dict( name=dict(required=True), zig=dict(type='str'), ) - TowerModule(argument_spec=argument_spec, direct_params=data) + TowerAPIModule(argument_spec=argument_spec, direct_params=data) assert mock_load.mock_calls[-1] == mock.call('my_config') silence_warning.assert_called_once_with( @@ -92,8 +92,8 @@ def test_no_templated_values(collection_import): Those replacements should happen at build time, so they should not be checked into source. """ - TowerModule = collection_import('plugins.module_utils.tower_api').TowerModule - assert TowerModule._COLLECTION_VERSION == "0.0.1-devel", ( + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule + assert TowerAPIModule._COLLECTION_VERSION == "0.0.1-devel", ( 'The collection version is templated when the collection is built ' 'and the code should retain the placeholder of "0.0.1-devel".' ) diff --git a/awx_collection/tools/roles/generate/templates/tower_module.j2 b/awx_collection/tools/roles/generate/templates/tower_module.j2 index a9834db28d..3606cff547 100644 --- a/awx_collection/tools/roles/generate/templates/tower_module.j2 +++ b/awx_collection/tools/roles/generate/templates/tower_module.j2 @@ -96,7 +96,7 @@ EXAMPLES = ''' {% endif %} ''' -from ..module_utils.tower_api import TowerModule +from ..module_utils.tower_api import TowerAPIModule def main(): @@ -142,7 +142,7 @@ def main(): ) # Create a module for ourselves - module = TowerModule(argument_spec=argument_spec) + module = TowerAPIModule(argument_spec=argument_spec) # Extract our parameters {% for option in item['json']['actions']['POST'] %} From 08e5dd87e6c0771162fb0ab246c53117b94fa29e Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 3 Aug 2020 14:14:40 -0400 Subject: [PATCH 046/188] Adding integration tests and example in import --- .../plugins/modules/tower_import.py | 4 + .../targets/tower_export/tasks/main.yml | 77 +++++++++++++ .../targets/tower_import/tasks/main.yml | 108 ++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 awx_collection/tests/integration/targets/tower_export/tasks/main.yml create mode 100644 awx_collection/tests/integration/targets/tower_import/tasks/main.yml diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index 0bb5dec75a..eeea7b35a3 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -38,6 +38,10 @@ EXAMPLES = ''' - name: Import all tower assets tower_import: assets: "{{ export_output.assets }}" + +- name: Import orgs from a json file + tower_import: + assets: "{{ lookup('file', 'org.json') | from_json() }}" ''' from ..module_utils.tower_awxkit import TowerAWXKitModule diff --git a/awx_collection/tests/integration/targets/tower_export/tasks/main.yml b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml new file mode 100644 index 0000000000..ce33f50019 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + org_name1: "AWX-Collection-tests-tower_export-organization-{{ test_id }}" + org_name2: "AWX-Collection-tests-tower_export-organization2-{{ test_id }}" + inventory_name1: "AWX-Collection-tests-tower_export-inv1-{{ test_id }}" + +- block: + - name: Create some organizations + tower_organization: + name: "{{ item }}" + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" + + - name: Create an inventory + tower_inventory: + name: "{{ inventory_name1 }}" + organization: "{{ org_name1 }}" + + - name: Export all tower assets + tower_export: + all: True + register: all_assets + + - assert: + that: + - all_assets is not changed + - all_assets is successful + - all_assets['assets']['organizations'] | length() >= 2 + + - name: Export all inventories + tower_export: + inventory: 'all' + register: inventory_export + + - assert: + that: + - inventory_export is successful + - inventory_export is not changed + - inventory_export['assets']['inventory'] | length() >= 1 + - "'organizations' not in inventory_export['assets']" + + # This mimics the example in the module + - name: Export an all and a specific + tower_export: + inventory: 'all' + organizations: "{{ org_name1 }}" + register: mixed_export + + - assert: + that: + - mixed_export is successful + - mixed_export is not changed + - mixed_export['assets']['inventory'] | length() >= 1 + - mixed_export['assets']['organizations'] | length() == 1 + - "'workflow_job_templates' not in mixed_export['assets']" + + always: + - name: Remove our inventory + tower_inventory: + name: "{{ inventory_name1 }}" + organization: "{{ org_name1 }}" + state: absent + + - name: Remove test organizations + tower_organization: + name: "{{ item }}" + state: absent + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml new file mode 100644 index 0000000000..09a91c85b0 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -0,0 +1,108 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + org_name1: "AWX-Collection-tests-tower_import-organization-{{ test_id }}" + org_name2: "AWX-Collection-tests-tower_import-organization2-{{ test_id }}" + +- block: + - name: "Import something" + tower_import: + assets: + organizations: + - name: "{{ org_name1 }}" + description: "" + max_hosts: 0 + custom_virtualenv: null + related: + notification_templates: [] + notification_templates_started: [] + notification_templates_success: [] + notification_templates_error: [] + notification_templates_approvals: [] + natural_key: + name: "Default" + type: "organization" + register: import_output + + - assert: + that: + - import_output is changed + + - name: "Import something again (awxkit is not idempotent, this tests a filure)" + tower_import: + assets: + organizations: + - name: "{{ org_name1 }}" + description: "" + max_hosts: 0 + custom_virtualenv: null + related: + notification_templates: [] + notification_templates_started: [] + notification_templates_success: [] + notification_templates_error: [] + notification_templates_approvals: [] + natural_key: + name: "Default" + type: "organization" + register: import_output + ignore_errors: True + + - assert: + that: + - import_output is failed + - "'Organization with this Name already exists' in import_output.msg" + + - name: "Write out a json file" + copy: + content: | + { + "organizations": [ + { + "name": "{{ org_name2 }}", + "description": "", + "max_hosts": 0, + "custom_virtualenv": null, + "related": { + "notification_templates": [], + "notification_templates_started": [], + "notification_templates_success": [], + "notification_templates_error": [], + "notification_templates_approvals": [] + }, + "natural_key": { + "name": "Default", + "type": "organization" + } + } + ] + } + dest: ./org.json + + - name: "Load assets from a file" + tower_import: + assets: "{{ lookup('file', 'org.json') | from_json() }}" + register: import_output + + - assert: + that: + - import_output is changed + + always: + - name: Remove organizations + tower_organization: + name: "{{ item }}" + state: absent + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" + + - name: Delete org.json + file: + path: ./org.json + state: absent From 748bdbd2dd7e31cf858dcd8093c715bee196b3d7 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 11:21:18 -0400 Subject: [PATCH 047/188] Fix python3 Zuul error with awxkit --- awx_collection/test/awx/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py index 53270d0e7f..5db00d6325 100644 --- a/awx_collection/test/awx/conftest.py +++ b/awx_collection/test/awx/conftest.py @@ -24,7 +24,10 @@ except ImportError: HAS_TOWER_CLI = False try: - import awxkit + # Because awxkit will be a directory at the root of this makefile and we are using python3, import awxkit will work even if its not installed. + # However, awxkit will not contain api whih causes a stack failure down on line 170 when we try to mock it. + # So here we are importing awxkit.api to prevent that. Then you only get an error on tests for awxkit functionality. + import awxkit.api HAS_AWX_KIT = True except ImportError: HAS_AWX_KIT = False From 8a0cd747e11bc72aa2c7a2bd508e95dde078b00e Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 11:33:29 -0400 Subject: [PATCH 048/188] Fixing truthy linting issues --- .../tests/integration/targets/tower_export/tasks/main.yml | 2 +- .../tests/integration/targets/tower_import/tasks/main.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awx_collection/tests/integration/targets/tower_export/tasks/main.yml b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml index ce33f50019..7ffbc15820 100644 --- a/awx_collection/tests/integration/targets/tower_export/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_export/tasks/main.yml @@ -25,7 +25,7 @@ - name: Export all tower assets tower_export: - all: True + all: true register: all_assets - assert: diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml index 09a91c85b0..ea18bb584f 100644 --- a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -33,7 +33,7 @@ that: - import_output is changed - - name: "Import something again (awxkit is not idempotent, this tests a filure)" + - name: "Import something again (awxkit is not idempotent, this tests a failure)" tower_import: assets: organizations: @@ -51,7 +51,7 @@ name: "Default" type: "organization" register: import_output - ignore_errors: True + ignore_errors: true - assert: that: From f2b9bdd5529256c8154c04408b1bcd0af622286c Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 13:06:13 -0400 Subject: [PATCH 049/188] Removed default: '' and updated [] to '' per specification --- .../plugins/modules/tower_export.py | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index 84c08fe454..6dabf17f39 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -30,57 +30,46 @@ options: organizations: description: - organization name to export - default: '' type: str user: description: - user name to export - default: '' type: str team: description: - team name to export - default: '' type: str credential_type: description: - credential type name to export - default: '' type: str credential: description: - credential name to export - default: '' type: str notification_template: description: - notification template name to export - default: '' type: str inventory_script: description: - inventory script name to export - default: '' type: str inventory: description: - inventory name to export - default: '' type: str project: description: - project name to export - default: '' type: str job_template: description: - job template name to export - default: '' type: str workflow: description: - workflow name to export - default: '' type: str requirements: - "awxkit >= 9.3.0" @@ -132,15 +121,15 @@ def main(): module.json_output['changed'] = False # The exporter code currently works like the following: - # Empty list == all assets of that type - # string = just one asset of that type (by name) - # None = skip asset type + # Empty string == all assets of that type + # Non-Empty string = just one asset of that type (by name or ID) + # Asset type not present or None = skip asset type (unless everything is None, then export all) # Here we are going to setup a dict of values to export export_args = {} for resource in EXPORTABLE_RESOURCES: if module.params.get('all') or module.params.get(resource) == 'all': - # If we are exporting everything or we got the keyword "all" we pass in an empty list for this asset type - export_args[resource] = [] + # If we are exporting everything or we got the keyword "all" we pass in an empty string for this asset type + export_args[resource] = '' else: # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items export_args[resource] = module.params.get(resource) From 9bf19daa5e544487ddc715c9e7791325836ad4b2 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Tue, 4 Aug 2020 13:25:05 -0400 Subject: [PATCH 050/188] Another linting issue --- .../tests/integration/targets/tower_import/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml index ea18bb584f..9835ff89a5 100644 --- a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -52,7 +52,7 @@ type: "organization" register: import_output ignore_errors: true - + - assert: that: - import_output is failed From 5107f164a23dcb88836aba646a8b50308cc154fa Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 10:32:46 -0400 Subject: [PATCH 051/188] Expanding examples --- awx_collection/plugins/modules/tower_import.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index eeea7b35a3..0dddeb64a0 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -35,11 +35,16 @@ extends_documentation_fragment: awx.awx.auth ''' EXAMPLES = ''' -- name: Import all tower assets +- name: Export all assets + tower_export: + all: True + registeR: export_output + +- name: Import all tower assets from our export tower_import: assets: "{{ export_output.assets }}" -- name: Import orgs from a json file +- name: Load data from a json file created by a command like awx export --organization Default tower_import: assets: "{{ lookup('file', 'org.json') | from_json() }}" ''' From 3fe61cfa4fa7ed38cb4bd3275d81ee110c2b4241 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 14:48:01 -0400 Subject: [PATCH 052/188] Fixing linting issues --- awx_collection/plugins/modules/tower_export.py | 4 ++-- awx_collection/plugins/modules/tower_import.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index 6dabf17f39..ad8da8f3ce 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -97,9 +97,9 @@ from ..module_utils.tower_awxkit import TowerAWXKitModule try: from awxkit.api.pages.api import EXPORTABLE_RESOURCES - HAS_EXPORTABLE_RESOURCES=True + HAS_EXPORTABLE_RESOURCES = True except ImportError: - HAS_EXPORTABLE_RESOURCES=False + HAS_EXPORTABLE_RESOURCES = False def main(): diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index 0dddeb64a0..37de035123 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -58,9 +58,9 @@ import logging # In this module we don't use EXPORTABLE_RESOURCES, we just want to validate that our installed awxkit has import/export try: from awxkit.api.pages.api import EXPORTABLE_RESOURCES - HAS_EXPORTABLE_RESOURCES=True + HAS_EXPORTABLE_RESOURCES = True except ImportError: - HAS_EXPORTABLE_RESOURCES=False + HAS_EXPORTABLE_RESOURCES = False def main(): argument_spec = dict( From 8688740e933e5875d6e9298636f406db9caf2f42 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 14:57:30 -0400 Subject: [PATCH 053/188] Fixing ansible pep8 issues --- awx_collection/plugins/module_utils/tower_api.py | 4 +++- awx_collection/plugins/module_utils/tower_module.py | 1 - awx_collection/plugins/modules/tower_import.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 089273bff1..6b5ed87531 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -10,6 +10,7 @@ from ansible.module_utils.six.moves.http_cookiejar import CookieJar import re from json import loads, dumps + class TowerAPIModule(TowerModule): # TODO: Move the collection version check into tower_module.py # This gets set by the make process so whatever is in here is irrelevant @@ -27,7 +28,8 @@ class TowerAPIModule(TowerModule): def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): kwargs['supports_check_mode'] = True - super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, error_callback=error_callback, warn_callback=warn_callback, **kwargs) + super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, + error_callback=error_callback, warn_callback=warn_callback, **kwargs) self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl) @staticmethod diff --git a/awx_collection/plugins/module_utils/tower_module.py b/awx_collection/plugins/module_utils/tower_module.py index 5d5b4d4239..553a35248c 100644 --- a/awx_collection/plugins/module_utils/tower_module.py +++ b/awx_collection/plugins/module_utils/tower_module.py @@ -215,7 +215,6 @@ class TowerModule(AnsibleModule): else: setattr(self, honorred_setting, config_data[honorred_setting]) - def logout(self): # This method is intended to be overridden pass diff --git a/awx_collection/plugins/modules/tower_import.py b/awx_collection/plugins/modules/tower_import.py index 37de035123..a39a98a5e3 100644 --- a/awx_collection/plugins/modules/tower_import.py +++ b/awx_collection/plugins/modules/tower_import.py @@ -62,6 +62,7 @@ try: except ImportError: HAS_EXPORTABLE_RESOURCES = False + def main(): argument_spec = dict( assets=dict(type='dict', required=True) From c2e0c0655ba2b2ec63bcabfd03e026e1991827fa Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 15:04:09 -0400 Subject: [PATCH 054/188] Fixing validate-module errors --- .../plugins/modules/tower_export.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index ad8da8f3ce..9f8f479b71 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -31,43 +31,43 @@ options: description: - organization name to export type: str - user: + users: description: - user name to export type: str - team: + teams: description: - team name to export type: str - credential_type: + credential_types: description: - credential type name to export type: str - credential: + credentials: description: - credential name to export type: str - notification_template: + notification_templates: description: - notification template name to export type: str - inventory_script: + inventory_sources: description: - - inventory script name to export + - inventory soruce to export type: str inventory: description: - inventory name to export type: str - project: + projects: description: - project name to export type: str - job_template: + job_templates: description: - job template name to export type: str - workflow: + workflow_job_templates: description: - workflow name to export type: str @@ -110,7 +110,7 @@ def main(): # We are not going to raise an error here because the __init__ method of TowerAWXKitModule will do that for us if HAS_EXPORTABLE_RESOURCES: for resource in EXPORTABLE_RESOURCES: - argument_spec[resource] = dict() + argument_spec[resource] = dict(type='str') module = TowerAWXKitModule(argument_spec=argument_spec) From 01e08ba0e14eaf3444396610ac96cf57e3592257 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Thu, 6 Aug 2020 15:07:23 -0400 Subject: [PATCH 055/188] Fixing exit_module -> exit_json --- awx_collection/plugins/module_utils/tower_awxkit.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index eedf3a7387..24a83b3c0d 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -24,7 +24,7 @@ class TowerAWXKitModule(TowerModule): # Die if we don't have AWX_KIT installed if not HAS_AWX_KIT: - self.exit_module(msg=missing_required_lib('awxkit')) + self.exit_json(msg=missing_required_lib('awxkit')) # Establish our conneciton object self.connection = Connection(self.host, verify=self.verify_ssl) @@ -35,7 +35,7 @@ class TowerAWXKitModule(TowerModule): # If we have neither of these, then we can try un-authenticated access self.authenticated = True except Exception: - self.exit_module("Failed to authenticate") + self.exit_json("Failed to authenticate") def get_api_v2_object(self): if not self.apiV2Ref: @@ -48,3 +48,5 @@ class TowerAWXKitModule(TowerModule): def logout(self): if self.authenticated: self.connection.logout() + + From 76f08744f6a89bd97ad0105969b43f8e88915695 Mon Sep 17 00:00:00 2001 From: beeankha Date: Tue, 18 Aug 2020 15:02:17 -0400 Subject: [PATCH 056/188] Fix linter whitespace error --- awx_collection/plugins/module_utils/tower_awxkit.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index 24a83b3c0d..ddd2d190c7 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -48,5 +48,3 @@ class TowerAWXKitModule(TowerModule): def logout(self): if self.authenticated: self.connection.logout() - - From a2eab45d61fd7f097c11f2e9200cef0a46f07dd2 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 12:12:30 -0400 Subject: [PATCH 057/188] Trying to gobble up logs incase there are errors --- .../plugins/modules/tower_export.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index 9f8f479b71..e7a788559c 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -92,7 +92,8 @@ EXAMPLES = ''' ''' from os import environ - +import logging +from ansible.module_utils.six.moves import StringIO from ..module_utils.tower_awxkit import TowerAWXKitModule try: @@ -134,11 +135,31 @@ def main(): # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items export_args[resource] = module.params.get(resource) - # Run the export process - module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) + # Currently the import process does not return anything on error + # It simply just logs to pythons logger + # Setup a log gobbler to get error messages from import_assets + log_capture_string = StringIO() + ch = logging.StreamHandler(log_capture_string) + for logger_name in ['awxkit.api.pages.api', 'awxkit.api.pages.page']: + logger = logging.getLogger(logger_name) + logger.setLevel(logging.WARNING) + ch.setLevel(logging.WARNING) - module.exit_json(**module.json_output) + logger.addHandler(ch) + log_contents = '' + # Run the import process + try: + module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) + module.exit_json(**module.json_output) + except Exception as e: + module.fail_json(msg="Failed to export assets {0}".format(e)) + finally: + # Finally consume the logs incase there were any errors and die if there were + log_contents = log_capture_string.getvalue() + log_capture_string.close() + if log_contents != '': + module.fail_json(msg=log_contents) if __name__ == '__main__': main() From 3abd77c4c02a29f68d2148a317bb0c8cf6c99a23 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 12:18:07 -0400 Subject: [PATCH 058/188] Fixing oauth token login and making module respect token over username/password --- awx_collection/plugins/module_utils/tower_awxkit.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index ddd2d190c7..506b64fb20 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -31,9 +31,12 @@ class TowerAWXKitModule(TowerModule): def authenticate(self): try: - self.connection.login(username=self.username, password=self.password, token=self.oauth_token) - # If we have neither of these, then we can try un-authenticated access - self.authenticated = True + if self.oauth_token: + self.connection.login(None, None, token=self.oauth_token, auth_type='Bearer') + self.authenticated = True + elif self.username: + self.connection.login(username=self.username, password=self.password) + self.authenticated = True except Exception: self.exit_json("Failed to authenticate") From 2c8c1ff595b89c63c55c160f218cd186d94f4255 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 14:14:49 -0400 Subject: [PATCH 059/188] Fixing sanity error --- awx_collection/plugins/modules/tower_export.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx_collection/plugins/modules/tower_export.py b/awx_collection/plugins/modules/tower_export.py index e7a788559c..bd951d1744 100644 --- a/awx_collection/plugins/modules/tower_export.py +++ b/awx_collection/plugins/modules/tower_export.py @@ -161,5 +161,6 @@ def main(): if log_contents != '': module.fail_json(msg=log_contents) + if __name__ == '__main__': main() From b93319e3591ae925dc95110581afc0713e3105cf Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 14:27:02 -0400 Subject: [PATCH 060/188] Updating to remove auth_type since its not longer required --- awx_collection/plugins/module_utils/tower_awxkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/module_utils/tower_awxkit.py b/awx_collection/plugins/module_utils/tower_awxkit.py index 506b64fb20..fc4e232f1b 100644 --- a/awx_collection/plugins/module_utils/tower_awxkit.py +++ b/awx_collection/plugins/module_utils/tower_awxkit.py @@ -32,7 +32,7 @@ class TowerAWXKitModule(TowerModule): def authenticate(self): try: if self.oauth_token: - self.connection.login(None, None, token=self.oauth_token, auth_type='Bearer') + self.connection.login(None, None, token=self.oauth_token) self.authenticated = True elif self.username: self.connection.login(username=self.username, password=self.password) From a5afe0214a50f96bc5b074aded25a40a433a9001 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Wed, 19 Aug 2020 14:29:42 -0400 Subject: [PATCH 061/188] Trying to make AWXKIT tests not run on python2 --- awx_collection/tests/integration/targets/tower_export/aliases | 1 + awx_collection/tests/integration/targets/tower_import/aliases | 1 + 2 files changed, 2 insertions(+) create mode 100644 awx_collection/tests/integration/targets/tower_export/aliases create mode 100644 awx_collection/tests/integration/targets/tower_import/aliases diff --git a/awx_collection/tests/integration/targets/tower_export/aliases b/awx_collection/tests/integration/targets/tower_export/aliases new file mode 100644 index 0000000000..527d07c3cb --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_export/aliases @@ -0,0 +1 @@ +skip/python2 diff --git a/awx_collection/tests/integration/targets/tower_import/aliases b/awx_collection/tests/integration/targets/tower_import/aliases new file mode 100644 index 0000000000..527d07c3cb --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_import/aliases @@ -0,0 +1 @@ +skip/python2 From efa12b12ec434ffad9259e9f9c423ea6f13cb872 Mon Sep 17 00:00:00 2001 From: nixocio Date: Wed, 19 Aug 2020 17:17:32 -0400 Subject: [PATCH 062/188] Use a patternfly CSS variable instead of red Use a patternfly CSS variable instead of red. See: https://pf4.patternfly.org/documentation/overview/global-css-variables --- .../InstanceGroupDetails/InstanceGroupDetails.jsx | 8 ++++++-- .../InstanceGroupList/InstanceGroupListItem.jsx | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx index 8df5e5b863..c6d3313ffe 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx @@ -3,7 +3,7 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link, useHistory } from 'react-router-dom'; import { Button } from '@patternfly/react-core'; -import 'styled-components/macro'; +import styled from 'styled-components'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; @@ -17,6 +17,10 @@ import { import useRequest, { useDismissableError } from '../../../util/useRequest'; import { InstanceGroupsAPI } from '../../../api'; +const Unavailable = styled.span` + color: var(--pf-global--danger-color--200); +`; + function InstanceGroupDetails({ instanceGroup, i18n }) { const { id, name } = instanceGroup; @@ -78,7 +82,7 @@ function InstanceGroupDetails({ instanceGroup, i18n }) { ) : ( {i18n._(t`Unavailable`)}} + value={{i18n._(t`Unavailable`)}} dataCy="instance-group-used-capacity" /> )} diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx index 93e334d367..915aa44d44 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx @@ -46,6 +46,10 @@ const DataListAction = styled(_DataListAction)` grid-template-columns: 40px; `; +const Unavailable = styled.span` + color: var(--pf-global--danger-color--200); +`; + function InstanceGroupListItem({ instanceGroup, detailUrl, @@ -78,7 +82,7 @@ function InstanceGroupListItem({ /> ); } - return {i18n._(t`Unavailable`)}; + return {i18n._(t`Unavailable`)}; } return null; } From 821cfba88a8db85c0fab420585f1df62f89c06ef Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 20 Aug 2020 10:02:24 -0400 Subject: [PATCH 063/188] changelog for arm64 builds --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15723a0106..79d1ff8cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ This is a list of high-level changes for each release of AWX. A full list of commits can be found at `https://github.com/ansible/awx/releases/tag/`. ## 14.1.0 (TBD) +- AWX images can now be built on ARM64 - https://github.com/ansible/awx/pull/7607 - Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 - Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 - Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847 From 0266ed383659c703efa03b2f3d87b5ff964e6992 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 20 Aug 2020 10:43:34 -0400 Subject: [PATCH 064/188] more changelog updates --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d1ff8cc3..60f79d4f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This is a list of high-level changes for each release of AWX. A full list of com ## 14.1.0 (TBD) - AWX images can now be built on ARM64 - https://github.com/ansible/awx/pull/7607 - Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 +- Added resource import/export support to the official AWX collection - https://github.com/ansible/awx/issues/7329 - Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 - Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847 - Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 From 70295c3e7505ebd2834daacc5ac335186e609bfe Mon Sep 17 00:00:00 2001 From: nixocio Date: Thu, 20 Aug 2020 11:36:41 -0400 Subject: [PATCH 065/188] Add list of jobs for instance groups Add list of jobs for instance groups. See: https://github.com/ansible/awx/issues/7930 --- awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx index a42048f503..4597bbd61c 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx @@ -17,10 +17,10 @@ import { InstanceGroupsAPI } from '../../api'; import RoutedTabs from '../../components/RoutedTabs'; import ContentError from '../../components/ContentError'; import ContentLoading from '../../components/ContentLoading'; +import JobList from '../../components/JobList'; import InstanceGroupDetails from './InstanceGroupDetails'; import InstanceGroupEdit from './InstanceGroupEdit'; -import Jobs from './Jobs'; import Instances from './Instances'; function InstanceGroup({ i18n, setBreadcrumb }) { @@ -126,7 +126,10 @@ function InstanceGroup({ i18n, setBreadcrumb }) {
- + )} From 2cfa4eb60a190565770f14d7749381aa8a5d8520 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Fri, 6 Dec 2019 13:18:02 -0500 Subject: [PATCH 066/188] Add archive option to SCM_TYPE_CHOICES for Remote Archives Signed-off-by: Philip Douglass --- awx/main/models/projects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/models/projects.py b/awx/main/models/projects.py index 5d8cfd5290..bc52d4269c 100644 --- a/awx/main/models/projects.py +++ b/awx/main/models/projects.py @@ -55,6 +55,7 @@ class ProjectOptions(models.Model): ('hg', _('Mercurial')), ('svn', _('Subversion')), ('insights', _('Red Hat Insights')), + ('archive', _('Remote Archive')), ] class Meta: From 47cabc42290f5df77eb280bf4ef74e4d8c8f1fed Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 13:32:25 -0400 Subject: [PATCH 067/188] Add archive SCM url handling to update_scm_url() Signed-off-by: Philip Douglass --- awx/main/utils/common.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index 75fcd306ef..f34cf4e4d8 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -257,7 +257,7 @@ def update_scm_url(scm_type, url, username=True, password=True, # git: https://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS # hg: http://www.selenic.com/mercurial/hg.1.html#url-paths # svn: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.advanced.reposurls - if scm_type not in ('git', 'hg', 'svn', 'insights'): + if scm_type not in ('git', 'hg', 'svn', 'insights', 'archive'): raise ValueError(_('Unsupported SCM type "%s"') % str(scm_type)) if not url.strip(): return '' @@ -303,7 +303,8 @@ def update_scm_url(scm_type, url, username=True, password=True, 'git': ('ssh', 'git', 'git+ssh', 'http', 'https', 'ftp', 'ftps', 'file'), 'hg': ('http', 'https', 'ssh', 'file'), 'svn': ('http', 'https', 'svn', 'svn+ssh', 'file'), - 'insights': ('http', 'https') + 'insights': ('http', 'https'), + 'archive': ('http', 'https'), } if parts.scheme not in scm_type_schemes.get(scm_type, ()): raise ValueError(_('Unsupported %s URL') % scm_type) @@ -339,7 +340,7 @@ def update_scm_url(scm_type, url, username=True, password=True, #raise ValueError('Password not supported for SSH with Mercurial.') netloc_password = '' - if netloc_username and parts.scheme != 'file' and scm_type != "insights": + if netloc_username and parts.scheme != 'file' and scm_type not in ("insights", "archive"): netloc = u':'.join([urllib.parse.quote(x,safe='') for x in (netloc_username, netloc_password) if x]) else: netloc = u'' From 997351eee3d831f394310aa5712bc090cf840846 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 13:43:47 -0400 Subject: [PATCH 068/188] Add archive project data to Dashboard view Signed-off-by: Philip Douglass --- awx/api/views/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index f6378f5282..c5b22d105a 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -242,6 +242,8 @@ class DashboardView(APIView): svn_failed_projects = svn_projects.filter(last_job_failed=True) hg_projects = user_projects.filter(scm_type='hg') hg_failed_projects = hg_projects.filter(last_job_failed=True) + archive_projects = user_projects.filter(scm_type='archive') + archive_failed_projects = archive_projects.filter(last_job_failed=True) data['scm_types'] = {} data['scm_types']['git'] = {'url': reverse('api:project_list', request=request) + "?scm_type=git", 'label': 'Git', @@ -258,6 +260,11 @@ class DashboardView(APIView): 'failures_url': reverse('api:project_list', request=request) + "?scm_type=hg&last_job_failed=True", 'total': hg_projects.count(), 'failed': hg_failed_projects.count()} + data['scm_types']['archive'] = {'url': reverse('api:project_list', request=request) + "?scm_type=archive", + 'label': 'Remote Archive', + 'failures_url': reverse('api:project_list', request=request) + "?scm_type=archive&last_job_failed=True", + 'total': archive_projects.count(), + 'failed': archive_failed_projects.count()} user_list = get_user_queryset(request.user, models.User) team_list = get_user_queryset(request.user, models.Team) From d224aa09f0bd908944e9be778867f018149b3974 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 13:49:27 -0400 Subject: [PATCH 069/188] Add archive to TestProjectUpdateCredentials test parametrize Signed-off-by: Philip Douglass --- awx/main/tests/unit/test_tasks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 71bcd8d03c..7dcea33ccd 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -1792,16 +1792,19 @@ class TestProjectUpdateCredentials(TestJobExecution): dict(scm_type='git'), dict(scm_type='hg'), dict(scm_type='svn'), + dict(scm_type='archive'), ], 'test_ssh_key_auth': [ dict(scm_type='git'), dict(scm_type='hg'), dict(scm_type='svn'), + dict(scm_type='archive'), ], 'test_awx_task_env': [ dict(scm_type='git'), dict(scm_type='hg'), dict(scm_type='svn'), + dict(scm_type='archive'), ] } From 2f3f6e60d1f3f084748f1c13580e6cc138eb8649 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 13:52:08 -0400 Subject: [PATCH 070/188] Add archive to scm_type expected test results Signed-off-by: Philip Douglass --- .../tests/functional/analytics/test_projects_by_scm_type.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/main/tests/functional/analytics/test_projects_by_scm_type.py b/awx/main/tests/functional/analytics/test_projects_by_scm_type.py index 29ffbd6283..1590b5d3bb 100644 --- a/awx/main/tests/functional/analytics/test_projects_by_scm_type.py +++ b/awx/main/tests/functional/analytics/test_projects_by_scm_type.py @@ -12,7 +12,8 @@ def test_empty(): 'git': 0, 'svn': 0, 'hg': 0, - 'insights': 0 + 'insights': 0, + 'archive': 0, } @@ -24,7 +25,8 @@ def test_multiple(scm_type): 'git': 0, 'svn': 0, 'hg': 0, - 'insights': 0 + 'insights': 0, + 'archive': 0, } for i in range(random.randint(0, 10)): Project(scm_type=scm_type).save() From 8157ab2fa94923fd149a8ab45aa1c8f6e2d9fc4f Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 16:29:57 -0400 Subject: [PATCH 071/188] Hide scm_branch field when scm_type is archive Signed-off-by: Philip Douglass --- awx/ui/client/src/projects/projects.form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/projects/projects.form.js b/awx/ui/client/src/projects/projects.form.js index 13ef01d2f0..dac0118c03 100644 --- a/awx/ui/client/src/projects/projects.form.js +++ b/awx/ui/client/src/projects/projects.form.js @@ -124,7 +124,7 @@ export default ['i18n', 'NotificationsList', 'TemplateList', scm_branch: { labelBind: "scmBranchLabel", type: 'text', - ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights'", + ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights' && scm_type.value !== 'archive'", ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)', awPopOver: '

' + i18n._("Branch to checkout. In addition to branches, you can input tags, commit hashes, and arbitrary refs. Some commit hashes and refs may not be availble unless you also provide a custom refspec.") + '

', dataTitle: i18n._('SCM Branch'), From 8402cf97de0888f11976b9b7c17e43574d1fcc76 Mon Sep 17 00:00:00 2001 From: nixocio Date: Sun, 16 Aug 2020 14:57:00 -0400 Subject: [PATCH 072/188] Add type column to users list Add type column to users list. Also, update `UserListItem` to be a functional component. See: https://github.com/ansible/awx/issues/5684 --- .../screens/User/UserList/UserListItem.jsx | 147 ++++++++++-------- .../User/UserList/UserListItem.test.jsx | 8 +- 2 files changed, 89 insertions(+), 66 deletions(-) diff --git a/awx/ui_next/src/screens/User/UserList/UserListItem.jsx b/awx/ui_next/src/screens/User/UserList/UserListItem.jsx index 19b8b5b476..b772a47566 100644 --- a/awx/ui_next/src/screens/User/UserList/UserListItem.jsx +++ b/awx/ui_next/src/screens/User/UserList/UserListItem.jsx @@ -19,72 +19,89 @@ import DataListCell from '../../../components/DataListCell'; import { User } from '../../../types'; -class UserListItem extends React.Component { - static propTypes = { - user: User.isRequired, - detailUrl: string.isRequired, - isSelected: bool.isRequired, - onSelect: func.isRequired, - }; +function UserListItem({ user, isSelected, onSelect, detailUrl, i18n }) { + const labelId = `check-action-${user.id}`; - render() { - const { user, isSelected, onSelect, detailUrl, i18n } = this.props; - const labelId = `check-action-${user.id}`; - return ( - - - - - - {user.username} - -
, - - {user.first_name && ( - - {i18n._(t`First Name`)} - {user.first_name} - - )} - , - - {user.last_name && ( - - {i18n._(t`Last Name`)} - {user.last_name} - - )} - , - ]} - /> - - {user.summary_fields.user_capabilities.edit && ( - - - - )} - - -
- ); + let user_type; + if (user.is_superuser) { + user_type = i18n._(t`System Administrator`); + } else if (user.is_system_auditor) { + user_type = i18n._(t`System Auditor`); + } else { + user_type = i18n._(t`Normal User`); } + + return ( + + + + + + {user.username} + + , + + {user.first_name && ( + + {i18n._(t`First Name`)} + {user.first_name} + + )} + , + + {user.last_name && ( + + {i18n._(t`Last Name`)} + {user.last_name} + + )} + , + + {user_type} + , + ]} + /> + + {user.summary_fields.user_capabilities.edit && ( + + + + )} + + + + ); } + +UserListItem.prototype = { + user: User.isRequired, + detailUrl: string.isRequired, + isSelected: bool.isRequired, + onSelect: func.isRequired, +}; + export default withI18n()(UserListItem); diff --git a/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx b/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx index acdc7eecc0..e11b6da00c 100644 --- a/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx +++ b/awx/ui_next/src/screens/User/UserList/UserListItem.test.jsx @@ -28,12 +28,18 @@ describe('UserListItem with full permissions', () => { ); }); - test('initially renders succesfully', () => { + test('initially renders successfully', () => { expect(wrapper.length).toBe(1); }); test('edit button shown to users with edit capabilities', () => { expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy(); }); + + test('should display user type', () => { + expect( + wrapper.find('DataListCell[aria-label="user type"]').prop('children') + ).toEqual('System Administrator'); + }); }); describe('UserListItem without full permissions', () => { From aee2a81b271d8b85475cb6990bf01e7e65afd6db Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 13 Aug 2020 15:30:00 -0400 Subject: [PATCH 073/188] update newly useRequested lists to get advanced searchableKeys --- awx/ui_next/src/api/models/Credentials.js | 5 +++ awx/ui_next/src/api/models/Inventories.js | 5 +++ awx/ui_next/src/api/models/JobTemplates.js | 5 +++ awx/ui_next/src/api/models/Organizations.js | 8 +++++ awx/ui_next/src/api/models/Projects.js | 5 +++ awx/ui_next/src/api/models/Teams.js | 4 +++ .../src/api/models/WorkflowJobTemplates.js | 4 +++ .../components/Lookup/OrganizationLookup.jsx | 21 ++++++++--- .../NotificationList/NotificationList.jsx | 28 ++++++++++----- .../ResourceAccessList/ResourceAccessList.jsx | 17 +++++++-- .../ResourceAccessList.test.jsx | 9 +++++ .../InventoryGroups/InventoryGroupsList.jsx | 18 +++++++++- .../OrganizationTeamList.jsx | 21 ++++++++--- .../OrganizationTeamList.test.jsx | 9 +++++ .../ProjectJobTemplatesList.jsx | 18 +++++++++- .../Modals/NodeModals/NodeModal.test.jsx | 36 +++++++++++++++++++ .../NodeTypeStep/InventorySourcesList.jsx | 21 ++++++++--- .../InventorySourcesList.test.jsx | 9 +++++ .../NodeTypeStep/JobTemplatesList.jsx | 25 +++++++++---- .../NodeTypeStep/JobTemplatesList.test.jsx | 18 ++++++++++ .../NodeTypeStep/NodeTypeStep.test.jsx | 36 +++++++++++++++++++ .../NodeModals/NodeTypeStep/ProjectsList.jsx | 21 ++++++++--- .../NodeTypeStep/ProjectsList.test.jsx | 9 +++++ .../NodeTypeStep/WorkflowJobTemplatesList.jsx | 30 ++++++++++++---- .../WorkflowJobTemplatesList.test.jsx | 9 +++++ .../src/screens/User/UserList/UserList.jsx | 18 +++++++++- 26 files changed, 367 insertions(+), 42 deletions(-) diff --git a/awx/ui_next/src/api/models/Credentials.js b/awx/ui_next/src/api/models/Credentials.js index ec7f97812d..95e954fc0c 100644 --- a/awx/ui_next/src/api/models/Credentials.js +++ b/awx/ui_next/src/api/models/Credentials.js @@ -6,6 +6,7 @@ class Credentials extends Base { this.baseUrl = '/api/v2/credentials/'; this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readInputSources = this.readInputSources.bind(this); } @@ -15,6 +16,10 @@ class Credentials extends Base { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readInputSources(id, params) { return this.http.get(`${this.baseUrl}${id}/input_sources/`, { params, diff --git a/awx/ui_next/src/api/models/Inventories.js b/awx/ui_next/src/api/models/Inventories.js index ab828e32d6..077a534d27 100644 --- a/awx/ui_next/src/api/models/Inventories.js +++ b/awx/ui_next/src/api/models/Inventories.js @@ -7,6 +7,7 @@ class Inventories extends InstanceGroupsMixin(Base) { this.baseUrl = '/api/v2/inventories/'; this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readHosts = this.readHosts.bind(this); this.readHostDetail = this.readHostDetail.bind(this); this.readGroups = this.readGroups.bind(this); @@ -20,6 +21,10 @@ class Inventories extends InstanceGroupsMixin(Base) { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + createHost(id, data) { return this.http.post(`${this.baseUrl}${id}/hosts/`, data); } diff --git a/awx/ui_next/src/api/models/JobTemplates.js b/awx/ui_next/src/api/models/JobTemplates.js index 0e2eba8079..4f631cec2a 100644 --- a/awx/ui_next/src/api/models/JobTemplates.js +++ b/awx/ui_next/src/api/models/JobTemplates.js @@ -16,6 +16,7 @@ class JobTemplates extends SchedulesMixin( this.disassociateLabel = this.disassociateLabel.bind(this); this.readCredentials = this.readCredentials.bind(this); this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readWebhookKey = this.readWebhookKey.bind(this); } @@ -66,6 +67,10 @@ class JobTemplates extends SchedulesMixin( }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readScheduleList(id, params) { return this.http.get(`${this.baseUrl}${id}/schedules/`, { params, diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index 267c9aba1e..e6f12a26a3 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -12,10 +12,18 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { return this.http.get(`${this.baseUrl}${id}/access_list/`, { params }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readTeams(id, params) { return this.http.get(`${this.baseUrl}${id}/teams/`, { params }); } + readTeamsOptions(id) { + return this.http.options(`${this.baseUrl}${id}/teams/`); + } + createUser(id, data) { return this.http.post(`${this.baseUrl}${id}/users/`, data); } diff --git a/awx/ui_next/src/api/models/Projects.js b/awx/ui_next/src/api/models/Projects.js index 269ef18f8a..38879a2bc2 100644 --- a/awx/ui_next/src/api/models/Projects.js +++ b/awx/ui_next/src/api/models/Projects.js @@ -11,6 +11,7 @@ class Projects extends SchedulesMixin( this.baseUrl = '/api/v2/projects/'; this.readAccessList = this.readAccessList.bind(this); + this.readAccessOptions = this.readAccessOptions.bind(this); this.readInventories = this.readInventories.bind(this); this.readPlaybooks = this.readPlaybooks.bind(this); this.readSync = this.readSync.bind(this); @@ -21,6 +22,10 @@ class Projects extends SchedulesMixin( return this.http.get(`${this.baseUrl}${id}/access_list/`, { params }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readInventories(id) { return this.http.get(`${this.baseUrl}${id}/inventories/`); } diff --git a/awx/ui_next/src/api/models/Teams.js b/awx/ui_next/src/api/models/Teams.js index 1a205993d4..180c59032c 100644 --- a/awx/ui_next/src/api/models/Teams.js +++ b/awx/ui_next/src/api/models/Teams.js @@ -35,6 +35,10 @@ class Teams extends Base { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readUsersAccessOptions(teamId) { return this.http.options(`${this.baseUrl}${teamId}/users/`); } diff --git a/awx/ui_next/src/api/models/WorkflowJobTemplates.js b/awx/ui_next/src/api/models/WorkflowJobTemplates.js index 3074608796..6326ebecae 100644 --- a/awx/ui_next/src/api/models/WorkflowJobTemplates.js +++ b/awx/ui_next/src/api/models/WorkflowJobTemplates.js @@ -54,6 +54,10 @@ class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) { }); } + readAccessOptions(id) { + return this.http.options(`${this.baseUrl}${id}/access_list/`); + } + readSurvey(id) { return this.http.get(`${this.baseUrl}${id}/survey_spec/`); } diff --git a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx index ec60c553cd..dfc4e1391f 100644 --- a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx @@ -29,21 +29,32 @@ function OrganizationLookup({ history, }) { const { - result: { itemCount, organizations }, + result: { itemCount, organizations, relatedSearchableKeys, searchableKeys }, error: contentError, request: fetchOrganizations, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, history.location.search); - const { data } = await OrganizationsAPI.read(params); + const [response, actionsResponse] = await Promise.all([ + OrganizationsAPI.read(params), + OrganizationsAPI.readOptions(), + ]); return { - organizations: data.results, - itemCount: data.count, + organizations: response.data.results, + itemCount: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [history.location.search]), { organizations: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -98,6 +109,8 @@ function OrganizationLookup({ key: 'name', }, ]} + searchableKeys={searchableKeys} + relatedSearchableKeys={relatedSearchableKeys} readOnly={!canDelete} selectItem={item => dispatch({ type: 'SELECT_ITEM', item })} deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })} diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.jsx index b1bca91d37..882a304654 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationList.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationList.jsx @@ -23,7 +23,7 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { const [toggleError, setToggleError] = useState(null); const { - result: fetchNotificationsResult, + result: fetchNotificationsResults, result: { notifications, itemCount, @@ -31,6 +31,8 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { successTemplateIds, errorTemplateIds, typeLabels, + relatedSearchableKeys, + searchableKeys, }, error: contentError, isLoading, @@ -43,15 +45,13 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { { data: { results: notificationsResults, count: notificationsCount }, }, - { - data: { actions }, - }, + actionsResponse, ] = await Promise.all([ NotificationTemplatesAPI.read(params), NotificationTemplatesAPI.readOptions(), ]); - const labels = actions.GET.notification_type.choices.reduce( + const labels = actionsResponse.data.actions.GET.notification_type.choices.reduce( (map, notifType) => ({ ...map, [notifType[0]]: notifType[1] }), {} ); @@ -78,6 +78,12 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { successTemplateIds: successTemplates.results.map(su => su.id), errorTemplateIds: errorTemplates.results.map(e => e.id), typeLabels: labels, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [apiModel, id, location]), { @@ -87,6 +93,8 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { successTemplateIds: [], errorTemplateIds: [], typeLabels: {}, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -108,8 +116,8 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { status ); setValue({ - ...fetchNotificationsResult, - [`${status}TemplateIds`]: fetchNotificationsResult[ + ...fetchNotificationsResults, + [`${status}TemplateIds`]: fetchNotificationsResults[ `${status}TemplateIds` ].filter(i => i !== notificationId), }); @@ -120,8 +128,8 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { status ); setValue({ - ...fetchNotificationsResult, - [`${status}TemplateIds`]: fetchNotificationsResult[ + ...fetchNotificationsResults, + [`${status}TemplateIds`]: fetchNotificationsResults[ `${status}TemplateIds` ].concat(notificationId), }); @@ -179,6 +187,8 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={notification => ( { const params = parseQueryString(QS_CONFIG, location.search); - const response = await apiModel.readAccessList(resource.id, params); + const [response, actionsResponse] = await Promise.all([ + apiModel.readAccessList(resource.id, params), + apiModel.readAccessOptions(resource.id), + ]); return { accessRecords: response.data.results, itemCount: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [apiModel, location, resource.id]), { accessRecords: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -106,6 +117,8 @@ function ResourceAccessList({ i18n, apiModel, resource }) { key: 'last_name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( ', () => { beforeEach(async () => { OrganizationsAPI.readAccessList.mockResolvedValue({ data }); + OrganizationsAPI.readAccessOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); TeamsAPI.disassociateRole.mockResolvedValue({}); UsersAPI.disassociateRole.mockResolvedValue({}); await act(async () => { diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx index 3f3dcbdb1c..3ba0ec1fff 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroupsList.jsx @@ -47,7 +47,13 @@ function InventoryGroupsList({ i18n }) { const { id: inventoryId } = useParams(); const { - result: { groups, groupCount, actions }, + result: { + groups, + groupCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchGroups, @@ -62,12 +68,20 @@ function InventoryGroupsList({ i18n }) { groups: response.data.results, groupCount: response.data.count, actions: actionsResponse.data.actions, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [inventoryId, location]), { groups: [], groupCount: 0, actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -168,6 +182,8 @@ function InventoryGroupsList({ i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={item => ( { const params = parseQueryString(QS_CONFIG, location.search); - const results = await OrganizationsAPI.readTeams(id, params); + const [response, actionsResponse] = await Promise.all([ + OrganizationsAPI.readTeams(id, params), + OrganizationsAPI.readTeamsOptions(id), + ]); return { - teams: results.data.results, - count: results.data.count, + teams: response.data.results, + count: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [id, location]), { teams: [], count: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -71,6 +82,8 @@ function OrganizationTeamList({ id, i18n }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={item => ( ', () => { beforeEach(() => { OrganizationsAPI.readTeams.mockResolvedValue(listData); + OrganizationsAPI.readTeamsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); }); afterEach(() => { diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx index d804a6ea83..f22c393866 100644 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesList.jsx @@ -27,7 +27,13 @@ function ProjectJobTemplatesList({ i18n }) { const location = useLocation(); const { - result: { jobTemplates, itemCount, actions }, + result: { + jobTemplates, + itemCount, + actions, + relatedSearchableKeys, + searchableKeys, + }, error: contentError, isLoading, request: fetchTemplates, @@ -43,12 +49,20 @@ function ProjectJobTemplatesList({ i18n }) { jobTemplates: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location, projectId]), { jobTemplates: [], itemCount: 0, actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -142,6 +156,8 @@ function ProjectJobTemplatesList({ i18n }) { key: 'type', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( { ], }, }); + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); ProjectsAPI.read.mockResolvedValue({ data: { count: 1, @@ -53,6 +62,15 @@ describe('NodeModal', () => { ], }, }); + ProjectsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); InventorySourcesAPI.read.mockResolvedValue({ data: { count: 1, @@ -66,6 +84,15 @@ describe('NodeModal', () => { ], }, }); + InventorySourcesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); WorkflowJobTemplatesAPI.read.mockResolvedValue({ data: { count: 1, @@ -79,6 +106,15 @@ describe('NodeModal', () => { ], }, }); + WorkflowJobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); }); afterAll(() => { jest.clearAllMocks(); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.jsx index 2ccdbef448..47e400f716 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.jsx @@ -20,22 +20,33 @@ function InventorySourcesList({ i18n, nodeResource, onUpdateNodeResource }) { const location = useLocation(); const { - result: { inventorySources, count }, + result: { inventorySources, count, relatedSearchableKeys, searchableKeys }, error, isLoading, request: fetchInventorySources, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const results = await InventorySourcesAPI.read(params); + const [response, actionsResponse] = await Promise.all([ + InventorySourcesAPI.read(params), + InventorySourcesAPI.readOptions(), + ]); return { - inventorySources: results.data.results, - count: results.data.count, + inventorySources: response.data.results, + count: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { inventorySources: [], count: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -94,6 +105,8 @@ function InventorySourcesList({ i18n, nodeResource, onUpdateNodeResource }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} /> ); } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx index 8725beff57..ad6fd57211 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/InventorySourcesList.test.jsx @@ -38,6 +38,15 @@ describe('InventorySourcesList', () => { ], }, }); + InventorySourcesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( { const params = parseQueryString(QS_CONFIG, location.search); - const results = await JobTemplatesAPI.read(params, { - role_level: 'execute_role', - }); + const [response, actionsResponse] = await Promise.all([ + JobTemplatesAPI.read(params, { + role_level: 'execute_role', + }), + JobTemplatesAPI.readOptions(), + ]); return { - jobTemplates: results.data.results, - count: results.data.count, + jobTemplates: response.data.results, + count: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { jobTemplates: [], count: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -92,6 +103,8 @@ function JobTemplatesList({ i18n, nodeResource, onUpdateNodeResource }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} /> ); } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx index 580e96d465..3b9fdec0e9 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/JobTemplatesList.test.jsx @@ -38,6 +38,15 @@ describe('JobTemplatesList', () => { ], }, }); + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( { }); test('Error shown when read() request errors', async () => { JobTemplatesAPI.read.mockRejectedValue(new Error()); + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( { ], }, }); + JobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); ProjectsAPI.read.mockResolvedValue({ data: { count: 1, @@ -48,6 +57,15 @@ describe('NodeTypeStep', () => { ], }, }); + ProjectsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); InventorySourcesAPI.read.mockResolvedValue({ data: { count: 1, @@ -61,6 +79,15 @@ describe('NodeTypeStep', () => { ], }, }); + InventorySourcesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); WorkflowJobTemplatesAPI.read.mockResolvedValue({ data: { count: 1, @@ -74,6 +101,15 @@ describe('NodeTypeStep', () => { ], }, }); + WorkflowJobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); }); afterAll(() => { jest.clearAllMocks(); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx index e7dfa098c9..3a9b747e35 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx @@ -20,22 +20,33 @@ function ProjectsList({ i18n, nodeResource, onUpdateNodeResource }) { const location = useLocation(); const { - result: { projects, count }, + result: { projects, count, relatedSearchableKeys, searchableKeys }, error, isLoading, request: fetchProjects, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, location.search); - const results = await ProjectsAPI.read(params); + const [response, actionsResponse] = await Promise.all([ + ProjectsAPI.read(params), + ProjectsAPI.readOptions(), + ]); return { - projects: results.data.results, - count: results.data.count, + projects: response.data.results, + count: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { projects: [], count: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -101,6 +112,8 @@ function ProjectsList({ i18n, nodeResource, onUpdateNodeResource }) { key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} /> ); } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx index eff4ccf517..765a329d3d 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.test.jsx @@ -38,6 +38,15 @@ describe('ProjectsList', () => { ], }, }); + ProjectsAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( { const params = parseQueryString(QS_CONFIG, location.search); - const results = await WorkflowJobTemplatesAPI.read(params, { - role_level: 'execute_role', - }); + const [response, actionsResponse] = await Promise.all([ + WorkflowJobTemplatesAPI.read(params, { + role_level: 'execute_role', + }), + WorkflowJobTemplatesAPI.readOptions(), + ]); return { - workflowJobTemplates: results.data.results, - count: results.data.count, + workflowJobTemplates: response.data.results, + count: response.data.count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { workflowJobTemplates: [], count: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -100,6 +116,8 @@ function WorkflowJobTemplatesList({ key: 'name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} /> ); } diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx index f3bf00a1d9..121c62cce6 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/WorkflowJobTemplatesList.test.jsx @@ -38,6 +38,15 @@ describe('WorkflowJobTemplatesList', () => { ], }, }); + WorkflowJobTemplatesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper = mountWithContexts( val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; }, [location]), { users: [], itemCount: 0, actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -124,6 +138,8 @@ function UserList({ i18n }) { key: 'last_name', }, ]} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderToolbar={props => ( Date: Thu, 20 Aug 2020 12:53:26 -0400 Subject: [PATCH 074/188] add searchable keys support for AssociateModal and SelectResourceStep lists --- .../components/AddRole/AddResourceRole.jsx | 6 ++++ .../components/AddRole/SelectResourceStep.jsx | 29 +++++++++++++++---- .../AddRole/SelectResourceStep.test.jsx | 21 ++++++++++++++ .../AssociateModal/AssociateModal.jsx | 24 +++++++++++---- .../AssociateModal/AssociateModal.test.jsx | 10 +++++++ .../UserAndTeamAccessAdd.jsx | 1 + .../UserAndTeamAccessAdd.test.jsx | 13 +++++++++ .../getResourceAccessConfig.js | 6 ++++ .../Host/HostGroups/HostGroupsList.jsx | 6 ++++ .../Host/HostGroups/HostGroupsList.test.jsx | 9 ++++++ .../InventoryGroupHostList.jsx | 6 ++++ .../InventoryHostGroupsList.jsx | 6 ++++ .../InventoryHostGroupsList.test.jsx | 9 ++++++ 13 files changed, 135 insertions(+), 11 deletions(-) diff --git a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx index 2da3b02e9d..95cb910295 100644 --- a/awx/ui_next/src/components/AddRole/AddResourceRole.jsx +++ b/awx/ui_next/src/components/AddRole/AddResourceRole.jsx @@ -11,8 +11,12 @@ import { TeamsAPI, UsersAPI } from '../../api'; const readUsers = async queryParams => UsersAPI.read(Object.assign(queryParams, { is_superuser: false })); +const readUsersOptions = async () => UsersAPI.readOptions(); + const readTeams = async queryParams => TeamsAPI.read(queryParams); +const readTeamsOptions = async () => TeamsAPI.readOptions(); + class AddResourceRole extends React.Component { constructor(props) { super(props); @@ -259,6 +263,7 @@ class AddResourceRole extends React.Component { displayKey="username" onRowClick={this.handleResourceCheckboxClick} fetchItems={readUsers} + fetchOptions={readUsersOptions} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={selectedResourceRows} sortedColumnKey="username" @@ -270,6 +275,7 @@ class AddResourceRole extends React.Component { sortColumns={teamSortColumns} onRowClick={this.handleResourceCheckboxClick} fetchItems={readTeams} + fetchOptions={readTeamsOptions} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={selectedResourceRows} /> diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx index 461327587e..f9a73d24ce 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.jsx @@ -29,6 +29,7 @@ function SelectResourceStep({ selectedLabel, selectedResourceRows, fetchItems, + fetchOptions, i18n, }) { const location = useLocation(); @@ -37,7 +38,7 @@ function SelectResourceStep({ isLoading, error, request: readResourceList, - result: { resources, itemCount }, + result: { resources, itemCount, relatedSearchableKeys, searchableKeys }, } = useRequest( useCallback(async () => { const queryParams = parseQueryString( @@ -45,14 +46,28 @@ function SelectResourceStep({ location.search ); - const { - data: { count, results }, - } = await fetchItems(queryParams); - return { resources: results, itemCount: count }; - }, [location, fetchItems, sortColumns]), + const [ + { + data: { count, results }, + }, + actionsResponse, + ] = await Promise.all([fetchItems(queryParams), fetchOptions()]); + return { + resources: results, + itemCount: count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), + }; + }, [location, fetchItems, fetchOptions, sortColumns]), { resources: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -84,6 +99,8 @@ function SelectResourceStep({ onRowClick={onRowClick} toolbarSearchColumns={searchColumns} toolbarSortColumns={sortColumns} + toolbarSearchableKeys={searchableKeys} + toolbarRelatedSearchableKeys={relatedSearchableKeys} renderItem={item => ( i.id === item.id)} diff --git a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx index c4c83f9d3b..4e307b7595 100644 --- a/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx +++ b/awx/ui_next/src/components/AddRole/SelectResourceStep.test.jsx @@ -35,6 +35,7 @@ describe('', () => { displayKey="username" onRowClick={() => {}} fetchItems={() => {}} + fetchOptions={() => {}} /> ); }); @@ -49,6 +50,15 @@ describe('', () => { ], }, }); + const options = jest.fn().mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); let wrapper; await act(async () => { wrapper = mountWithContexts( @@ -58,6 +68,7 @@ describe('', () => { displayKey="username" onRowClick={() => {}} fetchItems={handleSearch} + fetchOptions={options} /> ); }); @@ -78,6 +89,15 @@ describe('', () => { { id: 2, username: 'bar', url: 'item/2' }, ], }; + const options = jest.fn().mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); let wrapper; await act(async () => { wrapper = mountWithContexts( @@ -87,6 +107,7 @@ describe('', () => { displayKey="username" onRowClick={handleRowClick} fetchItems={() => ({ data })} + fetchOptions={options} selectedResourceRows={[]} /> ); diff --git a/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx b/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx index 339b5ed744..f7a1b2da67 100644 --- a/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx +++ b/awx/ui_next/src/components/AssociateModal/AssociateModal.jsx @@ -21,6 +21,7 @@ function AssociateModal({ onClose, onAssociate, fetchRequest, + optionsRequest, isModalOpen = false, }) { const history = useHistory(); @@ -28,24 +29,35 @@ function AssociateModal({ const { request: fetchItems, - result: { items, itemCount }, + result: { items, itemCount, relatedSearchableKeys, searchableKeys }, error: contentError, isLoading, } = useRequest( useCallback(async () => { const params = parseQueryString(QS_CONFIG, history.location.search); - const { - data: { count, results }, - } = await fetchRequest(params); + const [ + { + data: { count, results }, + }, + actionsResponse, + ] = await Promise.all([fetchRequest(params), optionsRequest()]); return { items: results, itemCount: count, + relatedSearchableKeys: ( + actionsResponse?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + actionsResponse.data.actions?.GET || {} + ).filter(key => actionsResponse.data.actions?.GET[key].filterable), }; - }, [fetchRequest, history.location.search]), + }, [fetchRequest, optionsRequest, history.location.search]), { items: [], itemCount: 0, + relatedSearchableKeys: [], + searchableKeys: [], } ); @@ -132,6 +144,8 @@ function AssociateModal({ key: 'name', }, ]} + searchableKeys={searchableKeys} + relatedSearchableKeys={relatedSearchableKeys} />
diff --git a/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx b/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx index 2b2280b38b..4b58e900e3 100644 --- a/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx +++ b/awx/ui_next/src/components/AssociateModal/AssociateModal.test.jsx @@ -15,6 +15,15 @@ describe('', () => { const onClose = jest.fn(); const onAssociate = jest.fn().mockResolvedValue(); const fetchRequest = jest.fn().mockReturnValue({ data: { ...mockHosts } }); + const optionsRequest = jest.fn().mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); beforeEach(async () => { await act(async () => { @@ -23,6 +32,7 @@ describe('', () => { onClose={onClose} onAssociate={onAssociate} fetchRequest={fetchRequest} + optionsRequest={optionsRequest} isModalOpen /> ); diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx index ceac1a32f5..ad83ce61e9 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.jsx @@ -96,6 +96,7 @@ function UserAndTeamAccessAdd({ displayKey="name" onRowClick={handleResourceSelect} fetchItems={selectedResourceType.fetchItems} + fetchOptions={selectedResourceType.fetchOptions} selectedLabel={i18n._(t`Selected`)} selectedResourceRows={resourcesSelected} sortedColumnKey="username" diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx index a46d5d87a3..7ad19c9057 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/UserAndTeamAccessAdd.test.jsx @@ -43,6 +43,15 @@ describe('', () => { count: 1, }, }; + const options = { + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }; let wrapper; beforeEach(async () => { await act(async () => { @@ -111,11 +120,13 @@ describe('', () => { test('should call api to associate role', async () => { JobTemplatesAPI.read.mockResolvedValue(resources); + JobTemplatesAPI.readOptions.mockResolvedValue(options); UsersAPI.associateRole.mockResolvedValue({}); await act(async () => wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({ fetchItems: JobTemplatesAPI.read, + fetchOptions: JobTemplatesAPI.readOptions, label: 'Job template', selectedResource: 'jobTemplate', searchColumns: [ @@ -169,6 +180,7 @@ describe('', () => { test('should throw error', async () => { JobTemplatesAPI.read.mockResolvedValue(resources); + JobTemplatesAPI.readOptions.mockResolvedValue(options); UsersAPI.associateRole.mockRejectedValue( new Error({ response: { @@ -192,6 +204,7 @@ describe('', () => { await act(async () => wrapper.find('SelectableCard[label="Job templates"]').prop('onClick')({ fetchItems: JobTemplatesAPI.read, + fetchOptions: JobTemplatesAPI.readOptions, label: 'Job template', selectedResource: 'jobTemplate', searchColumns: [ diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js index cd922c23aa..8df258973b 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js @@ -39,6 +39,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => JobTemplatesAPI.read(queryParams), + fetchOptions: () => JobTemplatesAPI.readOptions(), }, { selectedResource: 'workflowJobTemplate', @@ -69,6 +70,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => WorkflowJobTemplatesAPI.read(queryParams), + fetchOptions: () => WorkflowJobTemplatesAPI.readOptions(), }, { selectedResource: 'credential', @@ -110,6 +112,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => CredentialsAPI.read(queryParams), + fetchOptions: () => CredentialsAPI.readOptions(), }, { selectedResource: 'inventory', @@ -136,6 +139,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => InventoriesAPI.read(queryParams), + fetchOptions: () => InventoriesAPI.readOptions(), }, { selectedResource: 'project', @@ -177,6 +181,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => ProjectsAPI.read(queryParams), + fetchOptions: () => ProjectsAPI.readOptions(), }, { selectedResource: 'organization', @@ -203,6 +208,7 @@ export default function getResourceAccessConfig(i18n) { }, ], fetchItems: queryParams => OrganizationsAPI.read(queryParams), + fetchOptions: () => OrganizationsAPI.readOptions(), }, ]; } diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx index 6233df73bf..f902b5e0a6 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.jsx @@ -118,6 +118,11 @@ function HostGroupsList({ i18n, host }) { [invId, hostId] ); + const fetchGroupsOptions = useCallback( + () => InventoriesAPI.readGroupsOptions(invId), + [invId] + ); + const { request: handleAssociate, error: associateError } = useRequest( useCallback( async groupsToAssociate => { @@ -224,6 +229,7 @@ function HostGroupsList({ i18n, host }) { setIsModalOpen(false)} diff --git a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx index 1b2984ed81..4720de35ed 100644 --- a/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx +++ b/awx/ui_next/src/screens/Host/HostGroups/HostGroupsList.test.jsx @@ -207,6 +207,15 @@ describe('', () => { results: [{ id: 123, name: 'foo', url: '/api/v2/groups/123/' }], }, }); + InventoriesAPI.readGroupsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper.find('ToolbarAddButton').simulate('click'); }); diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx index 8584c01207..84203a700e 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx @@ -111,6 +111,11 @@ function InventoryGroupHostList({ i18n }) { [groupId, inventoryId] ); + const fetchHostsOptions = useCallback( + () => InventoriesAPI.readHostsOptions(inventoryId), + [inventoryId] + ); + const { request: handleAssociate, error: associateErr } = useRequest( useCallback( async hostsToAssociate => { @@ -227,6 +232,7 @@ function InventoryGroupHostList({ i18n }) { setIsModalOpen(false)} diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx index 2f8d8fa1ea..609de04f5b 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.jsx @@ -116,6 +116,11 @@ function InventoryHostGroupsList({ i18n }) { [invId, hostId] ); + const fetchGroupsOptions = useCallback( + () => InventoriesAPI.readGroupsOptions(invId), + [invId] + ); + const { request: handleAssociate, error: associateError } = useRequest( useCallback( async groupsToAssociate => { @@ -221,6 +226,7 @@ function InventoryHostGroupsList({ i18n }) { setIsModalOpen(false)} diff --git a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx index 1b72f73bd1..493e9dc65a 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryHostGroups/InventoryHostGroupsList.test.jsx @@ -199,6 +199,15 @@ describe('', () => { results: [{ id: 123, name: 'foo', url: '/api/v2/groups/123/' }], }, }); + InventoriesAPI.readGroupsOptions.mockResolvedValue({ + data: { + actions: { + GET: {}, + POST: {}, + }, + related_search_fields: [], + }, + }); await act(async () => { wrapper.find('ToolbarAddButton').simulate('click'); }); From 681b765b9afe62418c8860a47bce920eb7e46a60 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 3 Aug 2020 15:20:17 -0400 Subject: [PATCH 075/188] Adds support for toggling approval notifications on orgs and wfjts --- .../src/api/mixins/Notifications.mixin.js | 14 ++++ awx/ui_next/src/api/models/Organizations.js | 21 ++++++ .../src/api/models/WorkflowJobTemplates.js | 21 ++++++ .../NotificationList/NotificationList.jsx | 37 ++++++++++- .../NotificationList/NotificationListItem.jsx | 50 ++++++++++---- .../NotificationListItem.test.jsx | 65 +++++++++++++++++-- .../NotificationListItem.test.jsx.snap | 10 ++- .../CredentialEdit/CredentialEdit.test.jsx | 5 +- .../src/screens/Organization/Organization.jsx | 1 + .../screens/Template/WorkflowJobTemplate.jsx | 1 + 10 files changed, 200 insertions(+), 25 deletions(-) diff --git a/awx/ui_next/src/api/mixins/Notifications.mixin.js b/awx/ui_next/src/api/mixins/Notifications.mixin.js index 0198f0054f..87a7002ec5 100644 --- a/awx/ui_next/src/api/mixins/Notifications.mixin.js +++ b/awx/ui_next/src/api/mixins/Notifications.mixin.js @@ -87,6 +87,13 @@ const NotificationsMixin = parent => notificationId, notificationType ) { + if (notificationType === 'approvals') { + return this.associateNotificationTemplatesApprovals( + resourceId, + notificationId + ); + } + if (notificationType === 'started') { return this.associateNotificationTemplatesStarted( resourceId, @@ -126,6 +133,13 @@ const NotificationsMixin = parent => notificationId, notificationType ) { + if (notificationType === 'approvals') { + return this.disassociateNotificationTemplatesApprovals( + resourceId, + notificationId + ); + } + if (notificationType === 'started') { return this.disassociateNotificationTemplatesStarted( resourceId, diff --git a/awx/ui_next/src/api/models/Organizations.js b/awx/ui_next/src/api/models/Organizations.js index 267c9aba1e..a76b1e9ab1 100644 --- a/awx/ui_next/src/api/models/Organizations.js +++ b/awx/ui_next/src/api/models/Organizations.js @@ -19,6 +19,27 @@ class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) { createUser(id, data) { return this.http.post(`${this.baseUrl}${id}/users/`, data); } + + readNotificationTemplatesApprovals(id, params) { + return this.http.get( + `${this.baseUrl}${id}/notification_templates_approvals/`, + { params } + ); + } + + associateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId } + ); + } + + disassociateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId, disassociate: true } + ); + } } export default Organizations; diff --git a/awx/ui_next/src/api/models/WorkflowJobTemplates.js b/awx/ui_next/src/api/models/WorkflowJobTemplates.js index 3074608796..91739d1082 100644 --- a/awx/ui_next/src/api/models/WorkflowJobTemplates.js +++ b/awx/ui_next/src/api/models/WorkflowJobTemplates.js @@ -65,6 +65,27 @@ class WorkflowJobTemplates extends SchedulesMixin(NotificationsMixin(Base)) { destroySurvey(id) { return this.http.delete(`${this.baseUrl}${id}/survey_spec/`); } + + readNotificationTemplatesApprovals(id, params) { + return this.http.get( + `${this.baseUrl}${id}/notification_templates_approvals/`, + { params } + ); + } + + associateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId } + ); + } + + disassociateNotificationTemplatesApprovals(resourceId, notificationId) { + return this.http.post( + `${this.baseUrl}${resourceId}/notification_templates_approvals/`, + { id: notificationId, disassociate: true } + ); + } } export default WorkflowJobTemplates; diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.jsx index b1bca91d37..f274e4cca3 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationList.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationList.jsx @@ -17,7 +17,13 @@ const QS_CONFIG = getQSConfig('notification', { order_by: 'name', }); -function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { +function NotificationList({ + apiModel, + canToggleNotifications, + id, + i18n, + showApprovalsToggle, +}) { const location = useLocation(); const [isToggleLoading, setIsToggleLoading] = useState(false); const [toggleError, setToggleError] = useState(null); @@ -27,6 +33,7 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { result: { notifications, itemCount, + approvalsTemplateIds, startedTemplateIds, successTemplateIds, errorTemplateIds, @@ -71,7 +78,7 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { apiModel.readNotificationTemplatesError(id, idMatchParams), ]); - return { + const rtnObj = { notifications: notificationsResults, itemCount: notificationsCount, startedTemplateIds: startedTemplates.results.map(st => st.id), @@ -79,10 +86,27 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { errorTemplateIds: errorTemplates.results.map(e => e.id), typeLabels: labels, }; - }, [apiModel, id, location]), + + if (showApprovalsToggle) { + const { + data: approvalsTemplates, + } = await apiModel.readNotificationTemplatesApprovals( + id, + idMatchParams + ); + rtnObj.approvalsTemplateIds = approvalsTemplates.results.map( + st => st.id + ); + } else { + rtnObj.approvalsTemplateIds = []; + } + + return rtnObj; + }, [apiModel, id, location, showApprovalsToggle]), { notifications: [], itemCount: 0, + approvalsTemplateIds: [], startedTemplateIds: [], successTemplateIds: [], errorTemplateIds: [], @@ -186,10 +210,12 @@ function NotificationList({ apiModel, canToggleNotifications, id, i18n }) { detailUrl={`/notifications/${notification.id}`} canToggleNotifications={canToggleNotifications && !isToggleLoading} toggleNotification={handleNotificationToggle} + approvalsTurnedOn={approvalsTemplateIds.includes(notification.id)} errorTurnedOn={errorTemplateIds.includes(notification.id)} startedTurnedOn={startedTemplateIds.includes(notification.id)} successTurnedOn={successTemplateIds.includes(notification.id)} typeLabels={typeLabels} + showApprovalsToggle={showApprovalsToggle} /> )} /> @@ -212,6 +238,11 @@ NotificationList.propTypes = { apiModel: shape({}).isRequired, id: number.isRequired, canToggleNotifications: bool.isRequired, + showApprovalsToggle: bool, +}; + +NotificationList.defaultProps = { + showApprovalsToggle: false, }; export default withI18n()(NotificationList); diff --git a/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx b/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx index 6857114479..cbf048bc7c 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationListItem.jsx @@ -17,25 +17,25 @@ const DataListAction = styled(_DataListAction)` align-items: center; display: grid; grid-gap: 16px; - grid-template-columns: repeat(3, max-content); + grid-template-columns: ${props => `repeat(${props.columns}, max-content)`}; `; const Label = styled.b` margin-right: 20px; `; -function NotificationListItem(props) { - const { - canToggleNotifications, - notification, - detailUrl, - startedTurnedOn, - successTurnedOn, - errorTurnedOn, - toggleNotification, - i18n, - typeLabels, - } = props; - +function NotificationListItem({ + canToggleNotifications, + notification, + detailUrl, + approvalsTurnedOn, + startedTurnedOn, + successTurnedOn, + errorTurnedOn, + toggleNotification, + i18n, + typeLabels, + showApprovalsToggle, +}) { return ( + {showApprovalsToggle && ( + + toggleNotification( + notification.id, + approvalsTurnedOn, + 'approvals' + ) + } + aria-label={i18n._(t`Toggle notification approvals`)} + /> + )} ', () => { /> ); expect(wrapper.find('NotificationListItem')).toMatchSnapshot(); + expect(wrapper.find('Switch').length).toBe(3); + }); + + test('shows approvals toggle when configured', () => { + wrapper = mountWithContexts( + + ); + expect(wrapper.find('Switch').length).toBe(4); }); test('displays correct label in correct column', () => { @@ -58,7 +73,46 @@ describe('', () => { expect(typeCell.text()).toContain('Slack'); }); - test('handles start click when toggle is on', () => { + test('handles approvals click when toggle is on', () => { + wrapper = mountWithContexts( + + ); + wrapper + .find('Switch[aria-label="Toggle notification approvals"]') + .first() + .find('input') + .simulate('change'); + expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'approvals'); + }); + + test('handles approvals click when toggle is off', () => { + wrapper = mountWithContexts( + + ); + wrapper + .find('Switch[aria-label="Toggle notification approvals"]') + .find('input') + .simulate('change'); + expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'approvals'); + }); + + test('handles started click when toggle is on', () => { wrapper = mountWithContexts( ', () => { /> ); wrapper - .find('Switch') - .first() + .find('Switch[aria-label="Toggle notification start"]') .find('input') .simulate('change'); expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'started'); }); - test('handles start click when toggle is off', () => { + test('handles started click when toggle is off', () => { wrapper = mountWithContexts( ', () => { expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'started'); }); - test('handles error click when toggle is on', () => { + test('handles success click when toggle is on', () => { wrapper = mountWithContexts( ', () => { expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'success'); }); - test('handles error click when toggle is off', () => { + test('handles success click when toggle is off', () => { wrapper = mountWithContexts( initially renders succesfully and displays correct label 1`] = ` initially renders succe "notification_type": "slack", } } + showApprovalsToggle={false} startedTurnedOn={false} successTurnedOn={false} toggleNotification={[MockFunction]} @@ -215,6 +217,7 @@ exports[` initially renders succe initially renders succe initially renders succe align-items: center; display: grid; grid-gap: 16px; - grid-template-columns: repeat(3, max-content); + grid-template-columns: ", + [Function], + "; ", ], }, @@ -257,11 +263,13 @@ exports[` initially renders succe aria-label="actions" aria-labelledby="items-list-item-9000" className="sc-bwzfXH llKtln" + columns={3} id="items-list-item-9000" rowid="items-list-item-9000" >
', () => { test('handleCancel returns the user to credential detail', async () => { await waitForElement(wrapper, 'isLoading', el => el.length === 0); - wrapper.find('Button[aria-label="Cancel"]').simulate('click'); + await act(async () => { + wrapper.find('Button[aria-label="Cancel"]').simulate('click'); + }); + wrapper.update(); expect(history.location.pathname).toEqual('/credentials/3/details'); }); diff --git a/awx/ui_next/src/screens/Organization/Organization.jsx b/awx/ui_next/src/screens/Organization/Organization.jsx index 6e8cf87f1d..d16113d8dc 100644 --- a/awx/ui_next/src/screens/Organization/Organization.jsx +++ b/awx/ui_next/src/screens/Organization/Organization.jsx @@ -200,6 +200,7 @@ class Organization extends Component { id={Number(match.params.id)} canToggleNotifications={canToggleNotifications} apiModel={OrganizationsAPI} + showApprovalsToggle /> )} diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx index 8f19ebb6d3..51abaab73a 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplate.jsx @@ -233,6 +233,7 @@ class WorkflowJobTemplate extends Component { id={Number(match.params.id)} canToggleNotifications={canToggleNotifications} apiModel={WorkflowJobTemplatesAPI} + showApprovalsToggle /> )} From b011e34faeb65d88ef49b1b7400562a48a6f92bc Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 16:34:49 -0400 Subject: [PATCH 076/188] Show fields for archive when selected as scm_type Signed-off-by: Philip Douglass --- .../client/src/projects/add/projects-add.controller.js | 10 +++++++++- .../src/projects/edit/projects-edit.controller.js | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/projects/add/projects-add.controller.js b/awx/ui/client/src/projects/add/projects-add.controller.js index c34a6ecdf2..952cb07974 100644 --- a/awx/ui/client/src/projects/add/projects-add.controller.js +++ b/awx/ui/client/src/projects/add/projects-add.controller.js @@ -23,7 +23,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', $scope.canEditOrg = true; const virtualEnvs = ConfigData.custom_virtualenvs || []; $scope.custom_virtualenvs_options = virtualEnvs; - + const [ProjectModel] = resolvedModels; $scope.canAdd = ProjectModel.options('actions.POST'); @@ -170,6 +170,14 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm', $scope.lookupType = 'scm_credential'; $scope.scmBranchLabel = i18n._('SCM Branch/Tag/Revision'); break; + case 'archive': + $scope.credentialLabel = "SCM " + i18n._("Credential"); + $scope.urlPopover = '

' + i18n._('Example URLs for Remote Archive SCM include:') + '

' + + '
  • https://github.com/username/project/archive/v0.0.1.tar.gz
  • ' + + '
  • http://github.com/username/project/archive/v0.0.2.zip
'; + $scope.credRequired = false; + $scope.lookupType = 'scm_credential'; + break; case 'insights': $scope.pathRequired = false; $scope.scmRequired = false; diff --git a/awx/ui/client/src/projects/edit/projects-edit.controller.js b/awx/ui/client/src/projects/edit/projects-edit.controller.js index 27f6d3b376..8215bbab47 100644 --- a/awx/ui/client/src/projects/edit/projects-edit.controller.js +++ b/awx/ui/client/src/projects/edit/projects-edit.controller.js @@ -291,6 +291,14 @@ export default ['$scope', '$rootScope', '$stateParams', 'ProjectsForm', 'Rest', $scope.lookupType = 'scm_credential'; $scope.scmBranchLabel = i18n._('SCM Branch/Tag/Revision'); break; + case 'archive': + $scope.credentialLabel = "SCM " + i18n._("Credential"); + $scope.urlPopover = '

' + i18n._('Example URLs for Remote Archive SCM include:') + '

' + + '
  • https://github.com/username/project/archive/v0.0.1.tar.gz
  • ' + + '
  • http://github.com/username/project/archive/v0.0.2.zip
'; + $scope.credRequired = false; + $scope.lookupType = 'scm_credential'; + break; case 'insights': $scope.pathRequired = false; $scope.scmRequired = false; From 6720cd9bda2bd11da2e2661cf777483f767225d0 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 30 Jul 2019 22:34:14 -0400 Subject: [PATCH 077/188] Add block to download and unpack a remote archive Signed-off-by: Philip Douglass --- awx/playbooks/project_update.yml | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index e572496497..22478f9f0c 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -101,6 +101,65 @@ tags: - update_insights + - block: + - name: Ensure the project directory is present + file: + dest: "{{ project_path|quote }}" + state: directory + + - name: Get archive from url + get_url: + url: "{{ scm_url|quote }}" + dest: "{{ project_path|quote }}" + url_username: "{{ scm_username|default(omit) }}" + url_password: "{{ scm_password|default(omit) }}" + force_basic_auth: yes + force: "{{ scm_clean }}" + register: get_url_result + + - name: Unpack archive + unarchive: + src: "{{ get_url_result.dest }}" + dest: "{{ project_path|quote }}" + keep_newer: "{{ not scm_clean }}" + list_files: yes + register: unarchived + + - set_fact: + archive_root_dirs: "{{ archive_root_dirs |default([]) |union(item.split('/')[0:1]) }}" + loop: "{{ unarchived.files }}" + + - block: + - name: Delete unarchived single root directory + file: + dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" + state: absent + + - name: Link single root directory to project directory + file: + src: "{{ project_path|quote }}" + dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" + state: link + + - name: Unpack archive + unarchive: + src: "{{ get_url_result.dest }}" + dest: "{{ project_path|quote }}" + keep_newer: "{{ not scm_clean }}" + + - name: Delete link + file: + dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" + state: absent + when: archive_root_dirs |length == 1 + + - name: Set scm_version to archive sha1 checksum + set_fact: + scm_version: "{{ get_url_result.checksum_src }}" + + tags: + - update_archive + - name: Repository Version debug: msg: "Repository Version {{ scm_version }}" @@ -109,6 +168,7 @@ - update_hg - update_svn - update_insights + - update_archive - hosts: localhost gather_facts: false From f72b777b079dbe060b2f2b8cceb1b3336974106c Mon Sep 17 00:00:00 2001 From: Philip DOUGLASS Date: Sun, 16 Aug 2020 14:47:23 -0400 Subject: [PATCH 078/188] Add plugin to cleanly manage possibly versioned project archives Signed-off-by: Philip Douglass --- .../action_plugins/project_archive.py | 82 ++++++++++++++++++ awx/playbooks/library/project_archive.py | 40 +++++++++ awx/playbooks/project_update.yml | 85 ++++++++----------- 3 files changed, 157 insertions(+), 50 deletions(-) create mode 100644 awx/playbooks/action_plugins/project_archive.py create mode 100644 awx/playbooks/library/project_archive.py diff --git a/awx/playbooks/action_plugins/project_archive.py b/awx/playbooks/action_plugins/project_archive.py new file mode 100644 index 0000000000..753650d12b --- /dev/null +++ b/awx/playbooks/action_plugins/project_archive.py @@ -0,0 +1,82 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import zipfile +import tarfile +import os + +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display + +display = Display() + + +class ActionModule(ActionBase): + def run(self, tmp=None, task_vars=None): + self._supports_check_mode = False + + result = super(ActionModule, self).run(tmp, task_vars) + + src = self._task.args.get("src") + proj_path = self._task.args.get("project_path") + force = self._task.args.get("force", False) + + try: + archive = zipfile.ZipFile(src) + get_filenames = archive.namelist + get_members = archive.infolist + except zipfile.BadZipFile: + archive = tarfile.open(src) + get_filenames = archive.getnames + get_members = archive.getmembers + except tarfile.ReadError: + result["failed"] = True + result["msg"] = "{0} is not a valid archive".format(src) + return result + + # Most well formed archives contain a single root directory, typically named + # project-name-1.0.0. The project contents should be inside that directory. + start_index = 0 + root_contents = set( + [filename.split(os.path.sep)[0] for filename in get_filenames()] + ) + if len(root_contents) == 1: + start_index = len(list(root_contents)[0]) + 1 + + for member in get_members(): + try: + filename = member.filename + except AttributeError: + filename = member.name + + # Skip the archive base directory + if not filename[start_index:]: + continue + + dest = os.path.join(proj_path, filename[start_index:]) + + if not force and os.path.exists(dest): + continue + + try: + is_dir = member.is_dir() + except AttributeError: + is_dir = member.isdir() + + if is_dir: + os.makedirs(dest, exist_ok=True) + else: + try: + member_f = archive.open(member) + except TypeError: + member_f = tarfile.ExFileObject(archive, member) + + with open(dest, "wb") as f: + f.write(member_f.read()) + member_f.close() + + archive.close() + + result["changed"] = True + return result diff --git a/awx/playbooks/library/project_archive.py b/awx/playbooks/library/project_archive.py new file mode 100644 index 0000000000..4a046e354d --- /dev/null +++ b/awx/playbooks/library/project_archive.py @@ -0,0 +1,40 @@ +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "status": ["stableinterface"], + "supported_by": "community", +} + + +DOCUMENTATION = """ +--- +module: project_archive +short_description: unpack a project archive +description: + - Unpacks an archive that contains a project, in order to support handling versioned + artifacts from (for example) GitHub Releases or Artifactory builds. + - Handles projects in the archive root, or in a single base directory of the archive. +version_added: "2.9" +options: + src: + description: + - The source archive of the project artifact + required: true + project_path: + description: + - Directory to write the project archive contents + required: true + force: + description: + - Files in the project_path will be overwritten by matching files in the archive + default: False + +author: + - "Philip Douglass" @philipsd6 +""" + +EXAMPLES = """ +- project_archive: + src: "{{ project_path }}/.archive/project.tar.gz" + project_path: "{{ project_path }}" + force: "{{ scm_clean }}" +""" diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index 22478f9f0c..169273d628 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -102,61 +102,46 @@ - update_insights - block: - - name: Ensure the project directory is present - file: - dest: "{{ project_path|quote }}" - state: directory + - name: Ensure the project archive directory is present + file: + dest: "{{ project_path|quote }}/.archive" + state: directory - - name: Get archive from url - get_url: - url: "{{ scm_url|quote }}" - dest: "{{ project_path|quote }}" - url_username: "{{ scm_username|default(omit) }}" - url_password: "{{ scm_password|default(omit) }}" - force_basic_auth: yes - force: "{{ scm_clean }}" - register: get_url_result + - name: Get archive from url + get_url: + url: "{{ scm_url|quote }}" + dest: "{{ project_path|quote }}/.archive/" + url_username: "{{ scm_username|default(omit) }}" + url_password: "{{ scm_password|default(omit) }}" + force_basic_auth: true + register: get_archive - - name: Unpack archive - unarchive: - src: "{{ get_url_result.dest }}" - dest: "{{ project_path|quote }}" - keep_newer: "{{ not scm_clean }}" - list_files: yes - register: unarchived + - name: Unpack archive + project_archive: + src: "{{ get_archive.dest }}" + project_path: "{{ project_path|quote }}" + force: "{{ scm_clean }}" + when: get_archive.changed or scm_clean + register: unarchived - - set_fact: - archive_root_dirs: "{{ archive_root_dirs |default([]) |union(item.split('/')[0:1]) }}" - loop: "{{ unarchived.files }}" + - name: Find previous archives + find: + paths: "{{ project_path|quote }}/.archive/" + excludes: + - "{{ get_archive.dest|basename }}" + when: unarchived.changed + register: previous_archive - - block: - - name: Delete unarchived single root directory - file: - dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" - state: absent - - - name: Link single root directory to project directory - file: - src: "{{ project_path|quote }}" - dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" - state: link - - - name: Unpack archive - unarchive: - src: "{{ get_url_result.dest }}" - dest: "{{ project_path|quote }}" - keep_newer: "{{ not scm_clean }}" - - - name: Delete link - file: - dest: "{{ project_path|quote }}/{{ archive_root_dirs[0] }}" - state: absent - when: archive_root_dirs |length == 1 - - - name: Set scm_version to archive sha1 checksum - set_fact: - scm_version: "{{ get_url_result.checksum_src }}" + - name: Remove previous archives + file: + path: "{{ item.path }}" + state: absent + loop: "{{ previous_archive.files }}" + when: previous_archive.files|default([]) + - name: Set scm_version to archive sha1 checksum + set_fact: + scm_version: "{{ get_archive.checksum_src }}" tags: - update_archive From 70cbccd2ef281b954f1c971adbd938ed98282fe8 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Tue, 18 Aug 2020 18:42:11 -0400 Subject: [PATCH 079/188] Add migration for new Remote Archive SCM Type --- .../0118_add_remote_archive_scm_type.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 awx/main/migrations/0118_add_remote_archive_scm_type.py diff --git a/awx/main/migrations/0118_add_remote_archive_scm_type.py b/awx/main/migrations/0118_add_remote_archive_scm_type.py new file mode 100644 index 0000000000..246ca4c823 --- /dev/null +++ b/awx/main/migrations/0118_add_remote_archive_scm_type.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.11 on 2020-08-18 22:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0117_v400_remove_cloudforms_inventory'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='scm_type', + field=models.CharField(blank=True, choices=[('', 'Manual'), ('git', 'Git'), ('hg', 'Mercurial'), ('svn', 'Subversion'), ('insights', 'Red Hat Insights'), ('archive', 'Remote Archive')], default='', help_text='Specifies the source control system used to store the project.', max_length=8, verbose_name='SCM Type'), + ), + migrations.AlterField( + model_name='projectupdate', + name='scm_type', + field=models.CharField(blank=True, choices=[('', 'Manual'), ('git', 'Git'), ('hg', 'Mercurial'), ('svn', 'Subversion'), ('insights', 'Red Hat Insights'), ('archive', 'Remote Archive')], default='', help_text='Specifies the source control system used to store the project.', max_length=8, verbose_name='SCM Type'), + ), + ] From d2595003329dbc79faaddb7e331471ec27cb3a3c Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Wed, 19 Aug 2020 18:55:13 -0400 Subject: [PATCH 080/188] Implement Remote Archive SCM Type for ui_next Signed-off-by: Philip Douglass --- .../src/components/Lookup/ProjectLookup.jsx | 1 + .../getResourceAccessConfig.js | 2 + .../Project/ProjectAdd/ProjectAdd.test.jsx | 1 + .../Project/ProjectEdit/ProjectEdit.test.jsx | 1 + .../Project/ProjectList/ProjectList.jsx | 1 + .../Project/ProjectList/ProjectList.test.jsx | 22 ++++++++++- .../screens/Project/shared/ProjectForm.jsx | 8 ++++ .../Project/shared/ProjectForm.test.jsx | 1 + .../shared/ProjectSubForms/ArchiveSubForm.jsx | 38 +++++++++++++++++++ .../Project/shared/ProjectSubForms/index.js | 1 + .../NodeModals/NodeTypeStep/ProjectsList.jsx | 1 + awx/ui_next/src/types.js | 2 +- 12 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx diff --git a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx index dcc16669b8..5c8ec16dee 100644 --- a/awx/ui_next/src/components/Lookup/ProjectLookup.jsx +++ b/awx/ui_next/src/components/Lookup/ProjectLookup.jsx @@ -105,6 +105,7 @@ function ProjectLookup({ [`git`, i18n._(t`Git`)], [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], + [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], ], }, diff --git a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js index cd922c23aa..fd19a58299 100644 --- a/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js +++ b/awx/ui_next/src/components/UserAndTeamAccessAdd/getResourceAccessConfig.js @@ -87,6 +87,7 @@ export default function getResourceAccessConfig(i18n) { [`git`, i18n._(t`Git`)], [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], + [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], ], }, @@ -154,6 +155,7 @@ export default function getResourceAccessConfig(i18n) { [`git`, i18n._(t`Git`)], [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], + [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], ], }, diff --git a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx index a47bb424bf..cb2c16a23f 100644 --- a/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx @@ -37,6 +37,7 @@ describe('', () => { ['git', 'Git'], ['hg', 'Mercurial'], ['svn', 'Subversion'], + ['archive', 'Remote Archive'], ['insights', 'Red Hat Insights'], ], }, diff --git a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx index 5b75396d65..5171e0f532 100644 --- a/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectEdit/ProjectEdit.test.jsx @@ -49,6 +49,7 @@ describe('', () => { ['git', 'Git'], ['hg', 'Mercurial'], ['svn', 'Subversion'], + ['archive', 'Remote Archive'], ['insights', 'Red Hat Insights'], ], }, diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx index 7afbe124b3..b7b8661c1b 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.jsx @@ -138,6 +138,7 @@ function ProjectList({ i18n }) { [`git`, i18n._(t`Git`)], [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], + [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], ], }, diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx index b50e569f18..5a6945d892 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx @@ -61,6 +61,24 @@ const mockProjects = [ }, }, }, + { + id: 4, + name: 'Project 4', + url: '/api/v2/projects/4', + type: 'project', + scm_type: 'archive', + scm_revision: 'odsd9ajf8aagjisooajfij34ikdj3fs994s4daiaos7', + summary_fields: { + last_job: { + id: 9004, + status: 'successful', + }, + user_capabilities: { + delete: false, + update: false, + }, + }, + }, ]; describe('', () => { @@ -94,7 +112,7 @@ describe('', () => { }); wrapper.update(); - expect(wrapper.find('ProjectListItem')).toHaveLength(3); + expect(wrapper.find('ProjectListItem')).toHaveLength(4); }); test('should select project when checked', async () => { @@ -133,7 +151,7 @@ describe('', () => { wrapper.update(); const items = wrapper.find('ProjectListItem'); - expect(items).toHaveLength(3); + expect(items).toHaveLength(4); items.forEach(item => { expect(item.prop('isSelected')).toEqual(true); }); diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx index 56c740d73d..5a9f7b1ea7 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx @@ -25,6 +25,7 @@ import { GitSubForm, HgSubForm, SvnSubForm, + ArchiveSubForm, InsightsSubForm, ManualSubForm, } from './ProjectSubForms'; @@ -240,6 +241,13 @@ function ProjectFormFields({ scmUpdateOnLaunch={formik.values.scm_update_on_launch} /> ), + archive: ( + + ), insights: ( ', () => { ['git', 'Git'], ['hg', 'Mercurial'], ['svn', 'Subversion'], + ['archive', 'Remote Archive'], ['insights', 'Red Hat Insights'], ], }, diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx new file mode 100644 index 0000000000..ba65b0b6ff --- /dev/null +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/ArchiveSubForm.jsx @@ -0,0 +1,38 @@ +import 'styled-components/macro'; +import React from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { + UrlFormField, + ScmCredentialFormField, + ScmTypeOptions, +} from './SharedFields'; + +const ArchiveSubForm = ({ + i18n, + credential, + onCredentialSelection, + scmUpdateOnLaunch, +}) => ( + <> + + {i18n._(t`Example URLs for Remote Archive Source Control include:`)} +
    +
  • https://github.com/username/project/archive/v0.0.1.tar.gz
  • +
  • https://github.com/username/project/archive/v0.0.2.zip
  • +
+ + } + /> + + + +); + +export default withI18n()(ArchiveSubForm); diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js index 022673187f..8cf3e5594b 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js @@ -3,3 +3,4 @@ export { default as HgSubForm } from './HgSubForm'; export { default as InsightsSubForm } from './InsightsSubForm'; export { default as ManualSubForm } from './ManualSubForm'; export { default as SvnSubForm } from './SvnSubForm'; +export { default as ArchiveSubForm } from './ArchiveSubForm'; diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx index e7dfa098c9..4e73c5b6e0 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Modals/NodeModals/NodeTypeStep/ProjectsList.jsx @@ -79,6 +79,7 @@ function ProjectsList({ i18n, nodeResource, onUpdateNodeResource }) { [`git`, i18n._(t`Git`)], [`hg`, i18n._(t`Mercurial`)], [`svn`, i18n._(t`Subversion`)], + [`archive`, i18n._(t`Remote Archive`)], [`insights`, i18n._(t`Red Hat Insights`)], ], }, diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index 7a66ae3c68..5ee55ef5d9 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -148,7 +148,7 @@ export const Project = shape({ created: string, name: string.isRequired, description: string, - scm_type: oneOf(['', 'git', 'hg', 'svn', 'insights']), + scm_type: oneOf(['', 'git', 'hg', 'svn', 'archive', 'insights']), scm_url: string, scm_branch: string, scm_refspec: string, From 6ed65a9c81f01696be0722534802fddc6819d707 Mon Sep 17 00:00:00 2001 From: nixocio Date: Fri, 7 Aug 2020 13:35:10 -0400 Subject: [PATCH 081/188] Add label to show isolated group Add label to show isolated group. See: https://tower-mockups.testing.ansible.com/patternfly/instance-groups/instance-groups/ --- .../InstanceGroupDetails.jsx | 20 +++++++++++++++-- .../InstanceGroupDetails.test.jsx | 22 ++++++++++++++++++- .../InstanceGroupListItem.jsx | 16 ++++++++++---- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx index c6d3313ffe..0d6964559a 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx @@ -2,8 +2,8 @@ import React, { useCallback } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Link, useHistory } from 'react-router-dom'; -import { Button } from '@patternfly/react-core'; import styled from 'styled-components'; +import { Button, Label } from '@patternfly/react-core'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; @@ -46,12 +46,28 @@ function InstanceGroupDetails({ instanceGroup, i18n }) { ); }; + const verifyIsIsolated = item => { + if (item.is_isolated) { + return ( + <> + {item.name} + + + + + ); + } + return <>{item.name}; + }; + return ( ', () => { }); wrapper.update(); - expectDetailToMatch(wrapper, 'Name', instanceGroups[0].name); + + expect(wrapper.find('Detail[label="Name"]').text()).toEqual( + expect.stringContaining(instanceGroups[0].name) + ); + expect(wrapper.find('Detail[label="Name"]')).toHaveLength(1); expectDetailToMatch(wrapper, 'Type', `Instance group`); const dates = wrapper.find('UserDateDetail'); expect(dates).toHaveLength(2); @@ -144,4 +150,18 @@ describe('', () => { expect(wrapper.find('Button[aria-label="Edit"]').length).toBe(0); }); + + test('should display isolated label', async () => { + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + wrapper.update(); + expect( + wrapper.find('Label[aria-label="isolated instance"]').prop('children') + ).toEqual('Isolated'); + }); }); diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx index 915aa44d44..f531f8b182 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx @@ -6,15 +6,16 @@ import { Link } from 'react-router-dom'; import 'styled-components/macro'; import { Badge as PFBadge, - Progress, - ProgressMeasureLocation, - ProgressSize, Button, DataListAction as _DataListAction, DataListCheck, DataListItem, - DataListItemRow, DataListItemCells, + DataListItemRow, + Label, + Progress, + ProgressMeasureLocation, + ProgressSize, Tooltip, } from '@patternfly/react-core'; import { PencilAltIcon } from '@patternfly/react-icons'; @@ -112,6 +113,13 @@ function InstanceGroupListItem({ {instanceGroup.name} + {instanceGroup.is_isolated ? ( + + + + ) : null} , Date: Thu, 20 Aug 2020 14:24:05 -0400 Subject: [PATCH 082/188] Only disable single notification row when toggling, not all rows --- .../NotificationList/NotificationList.jsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.jsx index f274e4cca3..b11e27f9e5 100644 --- a/awx/ui_next/src/components/NotificationList/NotificationList.jsx +++ b/awx/ui_next/src/components/NotificationList/NotificationList.jsx @@ -25,7 +25,7 @@ function NotificationList({ showApprovalsToggle, }) { const location = useLocation(); - const [isToggleLoading, setIsToggleLoading] = useState(false); + const [loadingToggleIds, setLoadingToggleIds] = useState([]); const [toggleError, setToggleError] = useState(null); const { @@ -123,7 +123,7 @@ function NotificationList({ isCurrentlyOn, status ) => { - setIsToggleLoading(true); + setLoadingToggleIds(loadingToggleIds.concat([notificationId])); try { if (isCurrentlyOn) { await apiModel.disassociateNotificationTemplate( @@ -153,7 +153,9 @@ function NotificationList({ } catch (err) { setToggleError(err); } finally { - setIsToggleLoading(false); + setLoadingToggleIds( + loadingToggleIds.filter(item => item !== notificationId) + ); } }; @@ -208,7 +210,10 @@ function NotificationList({ key={notification.id} notification={notification} detailUrl={`/notifications/${notification.id}`} - canToggleNotifications={canToggleNotifications && !isToggleLoading} + canToggleNotifications={ + canToggleNotifications && + !loadingToggleIds.includes(notification.id) + } toggleNotification={handleNotificationToggle} approvalsTurnedOn={approvalsTemplateIds.includes(notification.id)} errorTurnedOn={errorTemplateIds.includes(notification.id)} @@ -223,7 +228,7 @@ function NotificationList({ setToggleError(null)} > {i18n._(t`Failed to toggle notification.`)} From 222a65c8753ce6b2552d6c0e4449545c6f2adf1c Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 17 Aug 2020 16:49:15 -0400 Subject: [PATCH 083/188] Adds extra variables to schedule details. Updates parameters by which we display prompt fields on schedule details. Extend VariableDetails component to be able to handle values that come in raw JSON form. --- .../CodeMirrorInput/VariablesDetail.jsx | 28 +++-- .../CodeMirrorInput/VariablesField.jsx | 6 +- .../CodeMirrorInput/VariablesInput.jsx | 6 +- .../src/components/Schedule/Schedule.test.jsx | 20 ++- .../ScheduleDetail/ScheduleDetail.jsx | 115 ++++++++++++++---- .../ScheduleDetail/ScheduleDetail.test.jsx | 46 ++++++- awx/ui_next/src/util/yaml.js | 8 +- 7 files changed, 186 insertions(+), 43 deletions(-) diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx index 7fbcd63cfa..3d02eb43bd 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesDetail.jsx @@ -1,10 +1,15 @@ import 'styled-components/macro'; import React, { useState, useEffect } from 'react'; -import { string, node, number } from 'prop-types'; +import { node, number, oneOfType, shape, string } from 'prop-types'; import { Split, SplitItem, TextListItemVariants } from '@patternfly/react-core'; import { DetailName, DetailValue } from '../DetailList'; import MultiButtonToggle from '../MultiButtonToggle'; -import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml'; +import { + yamlToJson, + jsonToYaml, + isJsonObject, + isJsonString, +} from '../../util/yaml'; import CodeMirrorInput from './CodeMirrorInput'; import { JSON_MODE, YAML_MODE } from './constants'; @@ -15,7 +20,7 @@ function getValueAsMode(value, mode) { } return '---'; } - const modeMatches = isJson(value) === (mode === JSON_MODE); + const modeMatches = isJsonString(value) === (mode === JSON_MODE); if (modeMatches) { return value; } @@ -23,12 +28,21 @@ function getValueAsMode(value, mode) { } function VariablesDetail({ value, label, rows, fullHeight }) { - const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE); - const [currentValue, setCurrentValue] = useState(value || '---'); + const [mode, setMode] = useState( + isJsonObject(value) || isJsonString(value) ? JSON_MODE : YAML_MODE + ); + const [currentValue, setCurrentValue] = useState( + isJsonObject(value) ? JSON.stringify(value, null, 2) : value || '---' + ); const [error, setError] = useState(null); useEffect(() => { - setCurrentValue(getValueAsMode(value, mode)); + setCurrentValue( + getValueAsMode( + isJsonObject(value) ? JSON.stringify(value, null, 2) : value, + mode + ) + ); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [value]); @@ -95,7 +109,7 @@ function VariablesDetail({ value, label, rows, fullHeight }) { ); } VariablesDetail.propTypes = { - value: string.isRequired, + value: oneOfType([shape({}), string]).isRequired, label: node.isRequired, rows: number, }; diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx index f39d413fac..4d63fcc663 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx @@ -7,7 +7,7 @@ import styled from 'styled-components'; import { Split, SplitItem } from '@patternfly/react-core'; import { CheckboxField, FieldTooltip } from '../FormField'; import MultiButtonToggle from '../MultiButtonToggle'; -import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml'; +import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml'; import CodeMirrorInput from './CodeMirrorInput'; import { JSON_MODE, YAML_MODE } from './constants'; @@ -30,7 +30,9 @@ function VariablesField({ tooltip, }) { const [field, meta, helpers] = useField(name); - const [mode, setMode] = useState(isJson(field.value) ? JSON_MODE : YAML_MODE); + const [mode, setMode] = useState( + isJsonString(field.value) ? JSON_MODE : YAML_MODE + ); return (
diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx index 5f3886e20b..a43962bd76 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesInput.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { string, func, bool, number } from 'prop-types'; import { Split, SplitItem } from '@patternfly/react-core'; import styled from 'styled-components'; -import { yamlToJson, jsonToYaml, isJson } from '../../util/yaml'; +import { yamlToJson, jsonToYaml, isJsonString } from '../../util/yaml'; import MultiButtonToggle from '../MultiButtonToggle'; import CodeMirrorInput from './CodeMirrorInput'; import { JSON_MODE, YAML_MODE } from './constants'; @@ -18,11 +18,11 @@ const SplitItemRight = styled(SplitItem)` function VariablesInput(props) { const { id, label, readOnly, rows, error, onError, className } = props; /* eslint-disable react/destructuring-assignment */ - const defaultValue = isJson(props.value) + const defaultValue = isJsonString(props.value) ? formatJson(props.value) : props.value; const [value, setValue] = useState(defaultValue); - const [mode, setMode] = useState(isJson(value) ? JSON_MODE : YAML_MODE); + const [mode, setMode] = useState(isJsonString(value) ? JSON_MODE : YAML_MODE); const isControlled = !!props.onChange; /* eslint-enable react/destructuring-assignment */ diff --git a/awx/ui_next/src/components/Schedule/Schedule.test.jsx b/awx/ui_next/src/components/Schedule/Schedule.test.jsx index f0f58c0710..e3c394cc95 100644 --- a/awx/ui_next/src/components/Schedule/Schedule.test.jsx +++ b/awx/ui_next/src/components/Schedule/Schedule.test.jsx @@ -6,10 +6,12 @@ import { mountWithContexts, waitForElement, } from '../../../testUtils/enzymeHelpers'; -import { SchedulesAPI } from '../../api'; +import { JobTemplatesAPI, SchedulesAPI } from '../../api'; import Schedule from './Schedule'; +jest.mock('../../api/models/JobTemplates'); jest.mock('../../api/models/Schedules'); +jest.mock('../../api/models/WorkflowJobTemplates'); SchedulesAPI.readDetail.mockResolvedValue({ data: { @@ -62,6 +64,22 @@ SchedulesAPI.readCredentials.mockResolvedValue({ }, }); +JobTemplatesAPI.readLaunch.mockResolvedValue({ + data: { + ask_credential_on_launch: false, + ask_diff_mode_on_launch: false, + ask_inventory_on_launch: false, + ask_job_type_on_launch: false, + ask_limit_on_launch: false, + ask_scm_branch_on_launch: false, + ask_skip_tags_on_launch: false, + ask_tags_on_launch: false, + ask_variables_on_launch: false, + ask_verbosity_on_launch: false, + survey_enabled: false, + }, +}); + describe('', () => { let wrapper; let history; diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx index 64b8863fe6..4c63669590 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.jsx @@ -17,10 +17,15 @@ import ScheduleOccurrences from '../ScheduleOccurrences'; import ScheduleToggle from '../ScheduleToggle'; import { formatDateString } from '../../../util/dates'; import useRequest, { useDismissableError } from '../../../util/useRequest'; -import { SchedulesAPI } from '../../../api'; +import { + JobTemplatesAPI, + SchedulesAPI, + WorkflowJobTemplatesAPI, +} from '../../../api'; import DeleteButton from '../../DeleteButton'; import ErrorDetail from '../../ErrorDetail'; import ChipGroup from '../../ChipGroup'; +import { VariablesDetail } from '../../CodeMirrorInput'; const PromptTitle = styled(Title)` --pf-c-title--m-md--FontWeight: 700; @@ -35,9 +40,9 @@ function ScheduleDetail({ schedule, i18n }) { diff_mode, dtend, dtstart, + extra_data, job_tags, job_type, - inventory, limit, modified, name, @@ -67,20 +72,47 @@ function ScheduleDetail({ schedule, i18n }) { const { error, dismissError } = useDismissableError(deleteError); const { - result: [credentials, preview], + result: [credentials, preview, launchData], isLoading, error: readContentError, request: fetchCredentialsAndPreview, } = useRequest( useCallback(async () => { - const [{ data }, { data: schedulePreview }] = await Promise.all([ + const promises = [ SchedulesAPI.readCredentials(id), SchedulesAPI.createPreview({ rrule, }), - ]); - return [data.results, schedulePreview]; - }, [id, rrule]), + ]; + + if ( + schedule?.summary_fields?.unified_job_template?.unified_job_type === + 'job' + ) { + promises.push( + JobTemplatesAPI.readLaunch( + schedule.summary_fields.unified_job_template.id + ) + ); + } else if ( + schedule?.summary_fields?.unified_job_template?.unified_job_type === + 'workflow_job' + ) { + promises.push( + WorkflowJobTemplatesAPI.readLaunch( + schedule.summary_fields.unified_job_template.id + ) + ); + } else { + promises.push(Promise.resolve()); + } + + const [{ data }, { data: schedulePreview }, launch] = await Promise.all( + promises + ); + + return [data.results, schedulePreview, launch?.data]; + }, [id, schedule, rrule]), [] ); @@ -93,15 +125,33 @@ function ScheduleDetail({ schedule, i18n }) { rule.options.freq === RRule.MINUTELY && dtstart === dtend ? i18n._(t`None (Run Once)`) : rule.toText().replace(/^\w/, c => c.toUpperCase()); + + const { + ask_credential_on_launch, + ask_diff_mode_on_launch, + ask_inventory_on_launch, + ask_job_type_on_launch, + ask_limit_on_launch, + ask_scm_branch_on_launch, + ask_skip_tags_on_launch, + ask_tags_on_launch, + ask_variables_on_launch, + ask_verbosity_on_launch, + survey_enabled, + } = launchData || {}; + const showPromptedFields = - (credentials && credentials.length > 0) || - job_type || - (inventory && summary_fields.inventory) || - scm_branch || - limit || - typeof diff_mode === 'boolean' || - (job_tags && job_tags.length > 0) || - (skip_tags && skip_tags.length > 0); + ask_credential_on_launch || + ask_diff_mode_on_launch || + ask_inventory_on_launch || + ask_job_type_on_launch || + ask_limit_on_launch || + ask_scm_branch_on_launch || + ask_skip_tags_on_launch || + ask_tags_on_launch || + ask_variables_on_launch || + ask_verbosity_on_launch || + survey_enabled; if (isLoading) { return ; @@ -144,8 +194,10 @@ function ScheduleDetail({ schedule, i18n }) { {i18n._(t`Prompted Fields`)} - - {inventory && summary_fields.inventory && ( + {ask_job_type_on_launch && ( + + )} + {ask_inventory_on_launch && ( )} - - - {typeof diff_mode === 'boolean' && ( + {ask_scm_branch_on_launch && ( + + )} + {ask_limit_on_launch && ( + + )} + {ask_diff_mode_on_launch && typeof diff_mode === 'boolean' && ( )} - {credentials && credentials.length > 0 && ( + {ask_credential_on_launch && ( )} - {job_tags && job_tags.length > 0 && ( + {ask_tags_on_launch && job_tags && job_tags.length > 0 && ( )} - {skip_tags && skip_tags.length > 0 && ( + {ask_skip_tags_on_launch && skip_tags && skip_tags.length > 0 && ( )} + {(ask_variables_on_launch || survey_enabled) && ( + + )} )} diff --git a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx index fe9175c6de..da325174d5 100644 --- a/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx +++ b/awx/ui_next/src/components/Schedule/ScheduleDetail/ScheduleDetail.test.jsx @@ -2,14 +2,48 @@ import React from 'react'; import { Route } from 'react-router-dom'; import { createMemoryHistory } from 'history'; import { act } from 'react-dom/test-utils'; -import { SchedulesAPI } from '../../../api'; +import { SchedulesAPI, JobTemplatesAPI } from '../../../api'; import { mountWithContexts, waitForElement, } from '../../../../testUtils/enzymeHelpers'; import ScheduleDetail from './ScheduleDetail'; +jest.mock('../../../api/models/JobTemplates'); jest.mock('../../../api/models/Schedules'); +jest.mock('../../../api/models/WorkflowJobTemplates'); + +const allPrompts = { + data: { + ask_credential_on_launch: true, + ask_diff_mode_on_launch: true, + ask_inventory_on_launch: true, + ask_job_type_on_launch: true, + ask_limit_on_launch: true, + ask_scm_branch_on_launch: true, + ask_skip_tags_on_launch: true, + ask_tags_on_launch: true, + ask_variables_on_launch: true, + ask_verbosity_on_launch: true, + survey_enabled: true, + }, +}; + +const noPrompts = { + data: { + ask_credential_on_launch: false, + ask_diff_mode_on_launch: false, + ask_inventory_on_launch: false, + ask_job_type_on_launch: false, + ask_limit_on_launch: false, + ask_scm_branch_on_launch: false, + ask_skip_tags_on_launch: false, + ask_tags_on_launch: false, + ask_variables_on_launch: false, + ask_verbosity_on_launch: false, + survey_enabled: false, + }, +}; const schedule = { url: '/api/v2/schedules/1', @@ -53,6 +87,7 @@ const schedule = { dtstart: '2020-03-16T04:00:00Z', dtend: '2020-07-06T04:00:00Z', next_run: '2020-03-16T04:00:00Z', + extra_data: {}, }; SchedulesAPI.createPreview.mockResolvedValue({ @@ -79,6 +114,7 @@ describe('', () => { results: [], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( ', () => { expect(wrapper.find('Detail[label="Credentials"]').length).toBe(0); expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(0); expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(0); + expect(wrapper.find('VariablesDetail').length).toBe(0); }); test('details should render with the proper values with prompts', async () => { SchedulesAPI.readCredentials.mockResolvedValue({ @@ -151,6 +188,7 @@ describe('', () => { ], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(allPrompts); const scheduleWithPrompts = { ...schedule, job_type: 'run', @@ -161,6 +199,7 @@ describe('', () => { limit: 'localhost', diff_mode: true, verbosity: 1, + extra_data: { foo: 'fii' }, }; await act(async () => { wrapper = mountWithContexts( @@ -182,7 +221,6 @@ describe('', () => { ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - // await waitForElement(wrapper, 'Title', el => el.length > 0); expect( wrapper .find('Detail[label="Name"]') @@ -231,6 +269,7 @@ describe('', () => { expect(wrapper.find('Detail[label="Credentials"]').length).toBe(1); expect(wrapper.find('Detail[label="Job Tags"]').length).toBe(1); expect(wrapper.find('Detail[label="Skip Tags"]').length).toBe(1); + expect(wrapper.find('VariablesDetail').length).toBe(1); }); test('error shown when error encountered fetching credentials', async () => { SchedulesAPI.readCredentials.mockRejectedValueOnce( @@ -245,6 +284,7 @@ describe('', () => { }, }) ); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( ', () => { results: [], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( ', () => { results: [], }, }); + JobTemplatesAPI.readLaunch.mockResolvedValueOnce(noPrompts); await act(async () => { wrapper = mountWithContexts( Date: Thu, 20 Aug 2020 21:42:07 -0400 Subject: [PATCH 084/188] Switch back to built-in kubectl connection plugin There's a bug in the upstream community.kubernetes plugin. We can open up a follow-up PR once that has been patched. --- awx/main/isolated/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/isolated/manager.py b/awx/main/isolated/manager.py index 551867986f..623bb5700e 100644 --- a/awx/main/isolated/manager.py +++ b/awx/main/isolated/manager.py @@ -58,7 +58,7 @@ class IsolatedManager(object): os.chmod(temp.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) for host in hosts: inventory['all']['hosts'][host] = { - "ansible_connection": "community.kubernetes.kubectl", + "ansible_connection": "kubectl", "ansible_kubectl_config": path, } else: From 80fe98b8d6fbe076729e1bc8b55b7b5de3c61774 Mon Sep 17 00:00:00 2001 From: mabashian Date: Fri, 21 Aug 2020 09:20:29 -0400 Subject: [PATCH 085/188] Fix npm audit warnings --- awx/ui_next/package-lock.json | 2875 +++++++++++++++------------------ awx/ui_next/package.json | 2 +- 2 files changed, 1267 insertions(+), 1610 deletions(-) diff --git a/awx/ui_next/package-lock.json b/awx/ui_next/package-lock.json index 6e5590c6db..04fc14aa73 100644 --- a/awx/ui_next/package-lock.json +++ b/awx/ui_next/package-lock.json @@ -13,9 +13,9 @@ } }, "@babel/compat-data": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.10.3.tgz", - "integrity": "sha512-BDIfJ9uNZuI0LajPfoYV28lX8kyCPMHY6uY4WH1lJdcicmAfxCK5ASzaeV0D/wsUaRH/cLk+amuxtC37sZ8TUg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz", + "integrity": "sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ==", "dev": true, "requires": { "browserslist": "^4.12.0", @@ -105,127 +105,127 @@ } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.3.tgz", - "integrity": "sha512-lo4XXRnBlU6eRM92FkiZxpo1xFLmv3VsPFk61zJKMm7XYJfwqXHsYJTY6agoc4a3L8QPw1HqWehO18coZgbT6A==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", + "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", "dev": true, "requires": { - "@babel/helper-explode-assignable-expression": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-explode-assignable-expression": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-builder-react-jsx": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.3.tgz", - "integrity": "sha512-vkxmuFvmovtqTZknyMGj9+uQAZzz5Z9mrbnkJnPkaYGfKTaSsYcjQdXP0lgrWLVh8wU6bCjOmXOpx+kqUi+S5Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz", + "integrity": "sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/types": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-builder-react-jsx-experimental": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.1.tgz", - "integrity": "sha512-irQJ8kpQUV3JasXPSFQ+LCCtJSc5ceZrPFVj6TElR6XCHssi3jV8ch3odIrNtjJFRZZVbrOEfJMI79TPU/h1pQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz", + "integrity": "sha512-Buewnx6M4ttG+NLkKyt7baQn7ScC/Td+e99G914fRU8fGIUivDDgVIQeDHFa5e4CRSJQt58WpNHhsAZgtzVhsg==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-module-imports": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-module-imports": "^7.10.4", + "@babel/types": "^7.10.5" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-compilation-targets": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.2.tgz", - "integrity": "sha512-hYgOhF4To2UTB4LTaZepN/4Pl9LD4gfbJx8A34mqoluT8TLbof1mhUlYuNWTEebONa8+UlCC4X0TEXu7AOUyGA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz", + "integrity": "sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.10.1", + "@babel/compat-data": "^7.10.4", "browserslist": "^4.12.0", "invariant": "^2.2.4", "levenary": "^1.1.1", @@ -241,366 +241,255 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.3.tgz", - "integrity": "sha512-iRT9VwqtdFmv7UheJWthGc/h2s7MqoweBF9RUj77NFZsg9VfISvBTum3k6coAhJ8RWv2tj3yUjA03HxPd0vfpQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz", + "integrity": "sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-member-expression-to-functions": "^7.10.3", - "@babel/helper-optimise-call-expression": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/helper-replace-supers": "^7.10.1", - "@babel/helper-split-export-declaration": "^7.10.1" + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.10.5", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.1.tgz", - "integrity": "sha512-Rx4rHS0pVuJn5pJOqaqcZR4XSgeF9G/pO/79t+4r7380tXFJdzImFnxMU19f83wjSrmKHq6myrM10pFHTGzkUA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz", + "integrity": "sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-regex": "^7.10.1", + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-regex": "^7.10.4", "regexpu-core": "^4.7.0" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-define-map": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.3.tgz", - "integrity": "sha512-bxRzDi4Sin/k0drWCczppOhov1sBSdBvXJObM1NLHQzjhXhwRtn7aRWGvLJWCYbuu2qUk3EKs6Ci9C9ps8XokQ==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.3", - "@babel/types": "^7.10.3", - "lodash": "^4.17.13" + "@babel/helper-function-name": "^7.10.4", + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-explode-assignable-expression": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.3.tgz", - "integrity": "sha512-0nKcR64XrOC3lsl+uhD15cwxPvaB6QKUDlD84OT9C3myRbhJqTMYir69/RWItUvHpharv0eJ/wk7fl34ONSwZw==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.11.4.tgz", + "integrity": "sha512-ux9hm3zR4WV1Y3xXxXkdG/0gxF9nvI0YVmKVhvK9AfMoaQkemL3sJpXw+Xbz65azo8qJiEz2XVDUpK3KYhH3ZQ==", "dev": true, "requires": { - "@babel/traverse": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" }, "dependencies": { - "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.3" - } - }, - "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", - "dev": true, - "requires": { - "@babel/types": "^7.10.1" - } - }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, - "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", - "dev": true - }, - "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - } - }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true } } }, @@ -623,56 +512,56 @@ } }, "@babel/helper-hoist-variables": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.3.tgz", - "integrity": "sha512-9JyafKoBt5h20Yv1+BXQMdcXXavozI1vt401KBiRc2qzUepbVnd7ogVNymY1xkQN9fekGwfxtotH2Yf5xsGzgg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", + "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-member-expression-to-functions": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.3.tgz", - "integrity": "sha512-q7+37c4EPLSjNb2NmWOjNwj0+BOyYlssuQ58kHEWk1Z78K5i8vTUsteq78HMieRPQSl/NtpQyJfdjt3qZ5V2vw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", + "integrity": "sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.11.0" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } @@ -687,409 +576,334 @@ } }, "@babel/helper-module-transforms": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz", - "integrity": "sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz", + "integrity": "sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.10.1", - "@babel/helper-replace-supers": "^7.10.1", - "@babel/helper-simple-access": "^7.10.1", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/template": "^7.10.1", - "@babel/types": "^7.10.1", - "lodash": "^4.17.13" + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/template": "^7.10.4", + "@babel/types": "^7.11.0", + "lodash": "^4.17.19" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-optimise-call-expression": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.3.tgz", - "integrity": "sha512-kT2R3VBH/cnSz+yChKpaKRJQJWxdGoc6SjioRId2wkeV3bK0wLLioFpJROrX0U4xr/NmxSSAWT/9Ih5snwIIzg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/helper-plugin-utils": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.3.tgz", - "integrity": "sha512-j/+j8NAWUTxOtx4LKHybpSClxHoq6I91DQ/mKgAXn5oNUPIUiGppjPIX3TDtJWPrdfP9Kfl7e4fgVMiQR9VE/g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true }, "@babel/helper-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.1.tgz", - "integrity": "sha512-7isHr19RsIJWWLLFn21ubFt223PjQyg1HY7CZEMRr820HttHPpVvrsIN3bUOo44DEfFV4kBXO7Abbn9KTUZV7g==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", + "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", "dev": true, "requires": { - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/helper-remap-async-to-generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.3.tgz", - "integrity": "sha512-sLB7666ARbJUGDO60ZormmhQOyqMX/shKBXZ7fy937s+3ID8gSrneMvKSSb+8xIM5V7Vn6uNVtOY1vIm26XLtA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.11.4.tgz", + "integrity": "sha512-tR5vJ/vBa9wFy3m5LLv2faapJLnDFxNWff2SAYkSE4rLUdbp7CdObYFgI7wK4T/Mj4UzpjPwzR8Pzmr5m7MHGA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-wrap-function": "^7.10.1", - "@babel/template": "^7.10.3", - "@babel/traverse": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-wrap-function": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" - } - }, - "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" - } - }, - "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", - "dev": true, - "requires": { - "@babel/types": "^7.10.3" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", - "dev": true, - "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" - } - }, - "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true } } }, "@babel/helper-replace-supers": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz", - "integrity": "sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.10.1", - "@babel/helper-optimise-call-expression": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", + "integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "dev": true, "requires": { - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", + "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.0", + "@babel/types": "^7.11.0", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -1111,66 +925,94 @@ } }, "@babel/helper-simple-access": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz", - "integrity": "sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", "dev": true, "requires": { - "@babel/template": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz", + "integrity": "sha512-0XIdiQln4Elglgjbwo9wuJpL/K7AGCY26kmEt0+pRP0TAj4jjyNq1MjoRvikrTVqKcx4Gysxt4cXvVFXP/JO2Q==", + "dev": true, + "requires": { + "@babel/types": "^7.11.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } @@ -1190,126 +1032,125 @@ "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" }, "@babel/helper-wrap-function": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.1.tgz", - "integrity": "sha512-C0MzRGteVDn+H32/ZgbAv5r56f2o1fZSA/rj/TYo8JEJNHg+9BdSmKBUND0shxWRztWhjlT2cvHYuynpPsVJwQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", + "integrity": "sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.1", - "@babel/template": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/helper-function-name": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", + "integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "dev": true, "requires": { - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", + "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.0", + "@babel/types": "^7.11.0", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -1331,125 +1172,124 @@ } }, "@babel/helpers": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.1.tgz", - "integrity": "sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", "dev": true, "requires": { - "@babel/template": "^7.10.1", - "@babel/traverse": "^7.10.1", - "@babel/types": "^7.10.1" + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/generator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.3.tgz", - "integrity": "sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.4.tgz", + "integrity": "sha512-Rn26vueFx0eOoz7iifCN2UHT6rGtnkSGWSoDRIy8jZN3B91PzeSULbswfLoOWuTuAcNwpG/mxy+uCTDnZ9Mp1g==", "dev": true, "requires": { - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "jsesc": "^2.5.1", - "lodash": "^4.17.13", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/traverse": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.3.tgz", - "integrity": "sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz", + "integrity": "sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/generator": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-split-export-declaration": "^7.10.1", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3", + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.11.0", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.11.0", + "@babel/types": "^7.11.0", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.13" + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -1486,24 +1326,24 @@ "integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==" }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.3.tgz", - "integrity": "sha512-WUUWM7YTOudF4jZBAJIW9D7aViYC/Fn0Pln4RIHlQALyno3sXSjqmTA4Zy1TKC2D49RCR8Y/Pn4OIUtEypK3CA==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz", + "integrity": "sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/helper-remap-async-to-generator": "^7.10.3", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.10.4", "@babel/plugin-syntax-async-generators": "^7.8.0" } }, "@babel/plugin-proposal-class-properties": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz", - "integrity": "sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", + "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-proposal-decorators": { @@ -1518,94 +1358,115 @@ } }, "@babel/plugin-proposal-dynamic-import": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz", - "integrity": "sha512-Cpc2yUVHTEGPlmiQzXj026kqwjEQAD9I4ZC16uzdbgWgitg/UHKHLffKNCQZ5+y8jpIZPJcKcwsr2HwPh+w3XA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz", + "integrity": "sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0" } }, - "@babel/plugin-proposal-json-strings": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.1.tgz", - "integrity": "sha512-m8r5BmV+ZLpWPtMY2mOKN7wre6HIO4gfIiV+eOmsnZABNenrt/kzYBwrh+KOfgumSWpnlGs5F70J8afYMSJMBg==", + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz", + "integrity": "sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz", + "integrity": "sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.0" } }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.1.tgz", - "integrity": "sha512-56cI/uHYgL2C8HVuHOuvVowihhX0sxb3nnfVRzUeVHTWmRHTZrKuAh/OBIMggGU/S1g/1D2CRCXqP+3u7vX7iA==", + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz", + "integrity": "sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz", + "integrity": "sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" } }, "@babel/plugin-proposal-numeric-separator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.1.tgz", - "integrity": "sha512-jjfym4N9HtCiNfyyLAVD8WqPYeHUrw4ihxuAynWj6zzp2gf9Ey2f7ImhFm6ikB3CLf5Z/zmcJDri6B4+9j9RsA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz", + "integrity": "sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-numeric-separator": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.3.tgz", - "integrity": "sha512-ZZh5leCIlH9lni5bU/wB/UcjtcVLgR8gc+FAgW2OOY+m9h1II3ItTO1/cewNUcsIDZSYcSaz/rYVls+Fb0ExVQ==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz", + "integrity": "sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-transform-parameters": "^7.10.1" + "@babel/plugin-transform-parameters": "^7.10.4" } }, "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.1.tgz", - "integrity": "sha512-VqExgeE62YBqI3ogkGoOJp1R6u12DFZjqwJhqtKc2o5m1YTUuUWnos7bZQFBhwkxIFpWYJ7uB75U7VAPPiKETA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz", + "integrity": "sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.3.tgz", - "integrity": "sha512-yyG3n9dJ1vZ6v5sfmIlMMZ8azQoqx/5/nZTSWX1td6L1H1bsjzA8TInDChpafCZiJkeOFzp/PtrfigAQXxI1Ng==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz", + "integrity": "sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0" } }, "@babel/plugin-proposal-private-methods": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.1.tgz", - "integrity": "sha512-RZecFFJjDiQ2z6maFprLgrdnm0OzoC23Mx89xf1CcEsxmHuzuXOdniEuI+S3v7vjQG4F5sa6YtUp+19sZuSxHg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz", + "integrity": "sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.1.tgz", - "integrity": "sha512-JjfngYRvwmPwmnbRZyNiPFI8zxCZb8euzbCG/LxyKdeTb59tVciKo9GK9bi6JYKInk1H11Dq9j/zRqIH4KigfQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz", + "integrity": "sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-async-generators": { @@ -1618,21 +1479,21 @@ } }, "@babel/plugin-syntax-class-properties": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.1.tgz", - "integrity": "sha512-Gf2Yx/iRs1JREDtVZ56OrjjgFHCaldpTnuy9BHla10qyVT3YkIIGEtoDWhyop0ksu1GvNjHIoYRBqm3zoR1jyQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", + "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-decorators": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.1.tgz", - "integrity": "sha512-a9OAbQhKOwSle1Vr0NJu/ISg1sPfdEkfRKWpgPuzhnWWzForou2gIeUIIwjAMHRekhhpJ7eulZlYs0H14Cbi+g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.4.tgz", + "integrity": "sha512-2NaoC6fAk2VMdhY1eerkfHV+lVYC1u8b+jmRJISqANCJlTxYy19HGdIkkQtix2UtkcPuPu+IlDgrVseZnU03bw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-dynamic-import": { @@ -1644,13 +1505,22 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, - "@babel/plugin-syntax-flow": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.10.1.tgz", - "integrity": "sha512-b3pWVncLBYoPP60UOTc7NMlbtsHQ6ITim78KQejNHK6WJ2mzV5kCcg4mIWpasAfJEgwVTibwo2e+FU7UEIKQUg==", + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-flow": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.10.4.tgz", + "integrity": "sha512-yxQsX1dJixF4qEEdzVbst3SZQ58Nrooz8NV9Z9GL4byTE25BvJgl5lf0RECUf0fh28rZBb/RYTWn/eeKwCMrZQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-json-strings": { @@ -1663,12 +1533,21 @@ } }, "@babel/plugin-syntax-jsx": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.1.tgz", - "integrity": "sha512-+OxyOArpVFXQeXKLO9o+r2I4dIoVoy6+Uu0vKELrlweDM3QJADZj+Z+5ERansZqIZBcLj42vHnDI8Rz9BnRIuQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz", + "integrity": "sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-nullish-coalescing-operator": { @@ -1681,12 +1560,12 @@ } }, "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.1.tgz", - "integrity": "sha512-uTd0OsHrpe3tH5gRPTxG8Voh99/WCU78vIm5NMRYPAqC8lR4vajt6KkCAknCHrx24vkPdd/05yfdGSB4EIY2mg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-object-rest-spread": { @@ -1717,245 +1596,244 @@ } }, "@babel/plugin-syntax-top-level-await": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.1.tgz", - "integrity": "sha512-hgA5RYkmZm8FTFT3yu2N9Bx7yVVOKYT6yEdXXo6j2JTm0wNxgqaGeQVaSHRjhfnQbX91DtjFB6McRFSlcJH3xQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz", + "integrity": "sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-typescript": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.1.tgz", - "integrity": "sha512-X/d8glkrAtra7CaQGMiGs/OGa6XgUzqPcBXCIGFCpCqnfGlT0Wfbzo/B89xHhnInTaItPK8LALblVXcUOEh95Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz", + "integrity": "sha512-oSAEz1YkBCAKr5Yiq8/BNtvSAPwkp/IyUnwZogd8p+F0RuYQQrLeRUzIQhueQTTBy/F+a40uS7OFKxnkRvmvFQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.1.tgz", - "integrity": "sha512-6AZHgFJKP3DJX0eCNJj01RpytUa3SOGawIxweHkNX2L6PYikOZmoh5B0d7hIHaIgveMjX990IAa/xK7jRTN8OA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz", + "integrity": "sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.1.tgz", - "integrity": "sha512-XCgYjJ8TY2slj6SReBUyamJn3k2JLUIiiR5b6t1mNCMSvv7yx+jJpaewakikp0uWFQSF7ChPPoe3dHmXLpISkg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz", + "integrity": "sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-remap-async-to-generator": "^7.10.1" + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.10.4" }, "dependencies": { "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.1.tgz", - "integrity": "sha512-B7K15Xp8lv0sOJrdVAoukKlxP9N59HS48V1J3U/JGj+Ad+MHq+am6xJVs85AgXrQn4LV8vaYFOB+pr/yIuzW8Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz", + "integrity": "sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.1.tgz", - "integrity": "sha512-8bpWG6TtF5akdhIm/uWTyjHqENpy13Fx8chg7pFH875aNLwX8JxIxqm08gmAT+Whe6AOmaTeLPe7dpLbXt+xUw==", + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz", + "integrity": "sha512-00dYeDE0EVEHuuM+26+0w/SCL0BH2Qy7LwHuI4Hi4MH5gkC8/AqMN5uWFJIsoXZrAphiMm1iXzBw6L2T+eA0ew==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "lodash": "^4.17.13" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-classes": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.3.tgz", - "integrity": "sha512-irEX0ChJLaZVC7FvvRoSIxJlmk0IczFLcwaRXUArBKYHCHbOhe57aG8q3uw/fJsoSXvZhjRX960hyeAGlVBXZw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz", + "integrity": "sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-define-map": "^7.10.3", - "@babel/helper-function-name": "^7.10.3", - "@babel/helper-optimise-call-expression": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/helper-replace-supers": "^7.10.1", - "@babel/helper-split-export-declaration": "^7.10.1", + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-define-map": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", "globals": "^11.1.0" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz", - "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", + "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.11.0" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-computed-properties": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.3.tgz", - "integrity": "sha512-GWzhaBOsdbjVFav96drOz7FzrcEW6AP5nax0gLIpstiFaI3LOb2tAg06TimaWU6YKOfUACK3FVrxPJ4GSc5TgA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz", + "integrity": "sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-destructuring": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.1.tgz", - "integrity": "sha512-V/nUc4yGWG71OhaTH705pU8ZSdM6c1KmmLP8ys59oOYbT7RpMYAR3MsVOt6OHL0WzG7BlTU076va9fjJyYzJMA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz", + "integrity": "sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.1.tgz", - "integrity": "sha512-19VIMsD1dp02RvduFUmfzj8uknaO3uiHHF0s3E1OHnVsNj8oge8EQ5RzHRbJjGSetRnkEuBYO7TG1M5kKjGLOA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz", + "integrity": "sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.1.tgz", - "integrity": "sha512-wIEpkX4QvX8Mo9W6XF3EdGttrIPZWozHfEaDTU0WJD/TDnXMvdDh30mzUl/9qWhnf7naicYartcEfUghTCSNpA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz", + "integrity": "sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.1.tgz", - "integrity": "sha512-lr/przdAbpEA2BUzRvjXdEDLrArGRRPwbaF9rvayuHRvdQ7lUTTkZnhZrJ4LE2jvgMRFF4f0YuPQ20vhiPYxtA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz", + "integrity": "sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw==", "dev": true, "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-flow-strip-types": { @@ -1969,353 +1847,353 @@ } }, "@babel/plugin-transform-for-of": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.1.tgz", - "integrity": "sha512-US8KCuxfQcn0LwSCMWMma8M2R5mAjJGsmoCBVwlMygvmDUMkTCykc84IqN1M7t+agSfOmLYTInLCHJM+RUoz+w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz", + "integrity": "sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-function-name": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.1.tgz", - "integrity": "sha512-//bsKsKFBJfGd65qSNNh1exBy5Y9gD9ZN+DvrJ8f7HXr4avE5POW6zB7Rj6VnqHV33+0vXWUwJT0wSHubiAQkw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz", + "integrity": "sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/code-frame": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", - "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.3" + "@babel/highlight": "^7.10.4" } }, "@babel/helper-function-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz", - "integrity": "sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.3", - "@babel/template": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/highlight": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", - "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.3.tgz", - "integrity": "sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA==", + "version": "7.11.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.4.tgz", + "integrity": "sha512-MggwidiH+E9j5Sh8pbrX5sJvMcsqS5o+7iB42M9/k0CD63MjYbdP4nhSh7uB5wnv2/RVzTZFTxzF/kIa5mrCqA==", "dev": true }, "@babel/template": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.3.tgz", - "integrity": "sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.3", - "@babel/parser": "^7.10.3", - "@babel/types": "^7.10.3" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" } }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-literals": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.1.tgz", - "integrity": "sha512-qi0+5qgevz1NHLZroObRm5A+8JJtibb7vdcPQF1KQE12+Y/xxl8coJ+TpPW9iRq+Mhw/NKLjm+5SHtAHCC7lAw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz", + "integrity": "sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.1.tgz", - "integrity": "sha512-UmaWhDokOFT2GcgU6MkHC11i0NQcL63iqeufXWfRy6pUOGYeCGEKhvfFO6Vz70UfYJYHwveg62GS83Rvpxn+NA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz", + "integrity": "sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.1.tgz", - "integrity": "sha512-31+hnWSFRI4/ACFr1qkboBbrTxoBIzj7qA69qlq8HY8p7+YCzkCT6/TvQ1a4B0z27VeWtAeJd6pr5G04dc1iHw==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz", + "integrity": "sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.1.tgz", - "integrity": "sha512-AQG4fc3KOah0vdITwt7Gi6hD9BtQP/8bhem7OjbaMoRNCH5Djx42O2vYMfau7QnAzQCa+RJnhJBmFFMGpQEzrg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz", + "integrity": "sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-simple-access": "^7.10.1", + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.3.tgz", - "integrity": "sha512-GWXWQMmE1GH4ALc7YXW56BTh/AlzvDWhUNn9ArFF0+Cz5G8esYlVbXfdyHa1xaD1j+GnBoCeoQNlwtZTVdiG/A==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz", + "integrity": "sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw==", "dev": true, "requires": { - "@babel/helper-hoist-variables": "^7.10.3", - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3", + "@babel/helper-hoist-variables": "^7.10.4", + "@babel/helper-module-transforms": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-umd": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.1.tgz", - "integrity": "sha512-EIuiRNMd6GB6ulcYlETnYYfgv4AxqrswghmBRQbWLHZxN4s7mupxzglnHqk9ZiUpDI4eRWewedJJNj67PWOXKA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz", + "integrity": "sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.3.tgz", - "integrity": "sha512-I3EH+RMFyVi8Iy/LekQm948Z4Lz4yKT7rK+vuCAeRm0kTa6Z5W7xuhRxDNJv0FPya/her6AUgrDITb70YHtTvA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz", + "integrity": "sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.10.4" } }, "@babel/plugin-transform-new-target": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.1.tgz", - "integrity": "sha512-MBlzPc1nJvbmO9rPr1fQwXOM2iGut+JC92ku6PbiJMMK7SnQc1rytgpopveE3Evn47gzvGYeCdgfCDbZo0ecUw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz", + "integrity": "sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-object-super": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.1.tgz", - "integrity": "sha512-WnnStUDN5GL+wGQrJylrnnVlFhFmeArINIR9gjhSeYyvroGhBrSAXYg/RHsnfzmsa+onJrTJrEClPzgNmmQ4Gw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz", + "integrity": "sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-replace-supers": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4" } }, "@babel/plugin-transform-parameters": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.1.tgz", - "integrity": "sha512-tJ1T0n6g4dXMsL45YsSzzSDZCxiHXAQp/qHrucOq5gEHncTA3xDxnd5+sZcoQp+N1ZbieAaB8r/VUCG0gqseOg==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz", + "integrity": "sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-get-function-arity": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz", - "integrity": "sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-property-literals": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.1.tgz", - "integrity": "sha512-Kr6+mgag8auNrgEpbfIWzdXYOvqDHZOF0+Bx2xh4H2EDNwcbRb9lY6nkZg8oSjsX+DH9Ebxm9hOqtKW+gRDeNA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz", + "integrity": "sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-react-constant-elements": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.1.tgz", - "integrity": "sha512-V4os6bkWt/jbrzfyVcZn2ZpuHZkvj3vyBU0U/dtS8SZuMS7Rfx5oknTrtfyXJ2/QZk8gX7Yls5Z921ItNpE30Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.4.tgz", + "integrity": "sha512-cYmQBW1pXrqBte1raMkAulXmi7rjg3VI6ZLg9QIic8Hq7BtYXaWuZSxsr2siOMI6SWwpxjWfnwhTUrd7JlAV7g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-react-display-name": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.3.tgz", - "integrity": "sha512-dOV44bnSW5KZ6kYF6xSHBth7TFiHHZReYXH/JH3XnFNV+soEL1F5d8JT7AJ3ZBncd19Qul7SN4YpBnyWOnQ8KA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.4.tgz", + "integrity": "sha512-Zd4X54Mu9SBfPGnEcaGcOrVAYOtjT2on8QZkLKEq1S/tHexG39d9XXGZv19VfRrDjPJzFmPfTAqOQS1pfFOujw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-react-jsx": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.3.tgz", - "integrity": "sha512-Y21E3rZmWICRJnvbGVmDLDZ8HfNDIwjGF3DXYHx1le0v0mIHCs0Gv5SavyW5Z/jgAHLaAoJPiwt+Dr7/zZKcOQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz", + "integrity": "sha512-L+MfRhWjX0eI7Js093MM6MacKU4M6dnCRa/QPDwYMxjljzSCzzlzKzj9Pk4P3OtrPcxr2N3znR419nr3Xw+65A==", "dev": true, "requires": { - "@babel/helper-builder-react-jsx": "^7.10.3", - "@babel/helper-builder-react-jsx-experimental": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-builder-react-jsx": "^7.10.4", + "@babel/helper-builder-react-jsx-experimental": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-jsx-development": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.1.tgz", - "integrity": "sha512-XwDy/FFoCfw9wGFtdn5Z+dHh6HXKHkC6DwKNWpN74VWinUagZfDcEJc3Y8Dn5B3WMVnAllX8Kviaw7MtC5Epwg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.4.tgz", + "integrity": "sha512-RM3ZAd1sU1iQ7rI2dhrZRZGv0aqzNQMbkIUCS1txYpi9wHQ2ZHNjo5TwX+UD6pvFW4AbWqLVYvKy5qJSAyRGjQ==", "dev": true, "requires": { - "@babel/helper-builder-react-jsx-experimental": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-builder-react-jsx-experimental": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-jsx-self": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.1.tgz", - "integrity": "sha512-4p+RBw9d1qV4S749J42ZooeQaBomFPrSxa9JONLHJ1TxCBo3TzJ79vtmG2S2erUT8PDDrPdw4ZbXGr2/1+dILA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz", + "integrity": "sha512-yOvxY2pDiVJi0axdTWHSMi5T0DILN+H+SaeJeACHKjQLezEzhLx9nEF9xgpBLPtkZsks9cnb5P9iBEi21En3gg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-jsx-source": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.1.tgz", - "integrity": "sha512-neAbaKkoiL+LXYbGDvh6PjPG+YeA67OsZlE78u50xbWh2L1/C81uHiNP5d1fw+uqUIoiNdCC8ZB+G4Zh3hShJA==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz", + "integrity": "sha512-wTeqHVkN1lfPLubRiZH3o73f4rfon42HpgxUSs86Nc+8QIcm/B9s8NNVXu/gwGcOyd7yDib9ikxoDLxJP0UiDA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-syntax-jsx": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.10.4" } }, "@babel/plugin-transform-react-pure-annotations": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.3.tgz", - "integrity": "sha512-n/fWYGqvTl7OLZs/QcWaKMFdADPvC3V6jYuEOpPyvz97onsW9TXn196fHnHW1ZgkO20/rxLOgKnEtN1q9jkgqA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz", + "integrity": "sha512-+njZkqcOuS8RaPakrnR9KvxjoG1ASJWpoIv/doyWngId88JoFlPlISenGXjrVacZUIALGUr6eodRs1vmPnF23A==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-regenerator": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.3.tgz", - "integrity": "sha512-H5kNeW0u8mbk0qa1jVIVTeJJL6/TJ81ltD4oyPx0P499DhMJrTmmIFCmJ3QloGpQG8K9symccB7S7SJpCKLwtw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz", + "integrity": "sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw==", "dev": true, "requires": { "regenerator-transform": "^0.14.2" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.1.tgz", - "integrity": "sha512-qN1OMoE2nuqSPmpTqEM7OvJ1FkMEV+BjVeZZm9V9mq/x1JLKQ4pcv8riZJMNN3u2AUGl0ouOMjRr2siecvHqUQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz", + "integrity": "sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-runtime": { @@ -2339,108 +2217,109 @@ } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz", - "integrity": "sha512-AR0E/lZMfLstScFwztApGeyTHJ5u3JUKMjneqRItWeEqDdHWZwAOKycvQNCasCK/3r5YXsuNG25funcJDu7Y2g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz", + "integrity": "sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-spread": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.1.tgz", - "integrity": "sha512-8wTPym6edIrClW8FI2IoaePB91ETOtg36dOkj3bYcNe7aDMN2FXEoUa+WrmPc4xa1u2PQK46fUX2aCb+zo9rfw==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz", + "integrity": "sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.11.0" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.1.tgz", - "integrity": "sha512-j17ojftKjrL7ufX8ajKvwRilwqTok4q+BjkknmQw9VNHnItTyMP5anPFzxFJdCQs7clLcWpCV3ma+6qZWLnGMA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz", + "integrity": "sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/helper-regex": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-regex": "^7.10.4" } }, "@babel/plugin-transform-template-literals": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.3.tgz", - "integrity": "sha512-yaBn9OpxQra/bk0/CaA4wr41O0/Whkg6nqjqApcinxM7pro51ojhX6fv1pimAnVjVfDy14K0ULoRL70CA9jWWA==", + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz", + "integrity": "sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-annotate-as-pure": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz", - "integrity": "sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", + "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", "dev": true, "requires": { - "@babel/types": "^7.10.1" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } } } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.1.tgz", - "integrity": "sha512-qX8KZcmbvA23zDi+lk9s6hC1FM7jgLHYIjuLgULgc8QtYnmB3tAVIYkNoKRQ75qWBeyzcoMoK8ZQmogGtC/w0g==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz", + "integrity": "sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-typescript": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.3.tgz", - "integrity": "sha512-qU9Lu7oQyh3PGMQncNjQm8RWkzw6LqsWZQlZPQMgrGt6s3YiBIaQ+3CQV/FA/icGS5XlSWZGwo/l8ErTyelS0Q==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.11.0.tgz", + "integrity": "sha512-edJsNzTtvb3MaXQwj8403B7mZoGu9ElDJQZOKjGUnvilquxBA3IQoEIOvkX/1O8xfAsnHS/oQhe2w/IXrr+w0w==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/plugin-syntax-typescript": "^7.10.1" + "@babel/helper-create-class-features-plugin": "^7.10.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-typescript": "^7.10.4" } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.1.tgz", - "integrity": "sha512-zZ0Poh/yy1d4jeDWpx/mNwbKJVwUYJX73q+gyh4bwtG0/iUlzdEu0sLMda8yuDFS6LBQlT/ST1SJAR6zYwXWgw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz", + "integrity": "sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.1.tgz", - "integrity": "sha512-Y/2a2W299k0VIUdbqYm9X2qS6fE0CUBhhiPpimK6byy7OJ/kORLlIX+J6UrjgNu5awvs62k+6RSslxhcvVw2Tw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz", + "integrity": "sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.10.1", - "@babel/helper-plugin-utils": "^7.10.1" + "@babel/helper-create-regexp-features-plugin": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/polyfill": { @@ -2462,70 +2341,74 @@ } }, "@babel/preset-env": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.10.3.tgz", - "integrity": "sha512-jHaSUgiewTmly88bJtMHbOd1bJf2ocYxb5BWKSDQIP5tmgFuS/n0gl+nhSrYDhT33m0vPxp+rP8oYYgPgMNQlg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.0.tgz", + "integrity": "sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg==", "dev": true, "requires": { - "@babel/compat-data": "^7.10.3", - "@babel/helper-compilation-targets": "^7.10.2", - "@babel/helper-module-imports": "^7.10.3", - "@babel/helper-plugin-utils": "^7.10.3", - "@babel/plugin-proposal-async-generator-functions": "^7.10.3", - "@babel/plugin-proposal-class-properties": "^7.10.1", - "@babel/plugin-proposal-dynamic-import": "^7.10.1", - "@babel/plugin-proposal-json-strings": "^7.10.1", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.1", - "@babel/plugin-proposal-numeric-separator": "^7.10.1", - "@babel/plugin-proposal-object-rest-spread": "^7.10.3", - "@babel/plugin-proposal-optional-catch-binding": "^7.10.1", - "@babel/plugin-proposal-optional-chaining": "^7.10.3", - "@babel/plugin-proposal-private-methods": "^7.10.1", - "@babel/plugin-proposal-unicode-property-regex": "^7.10.1", + "@babel/compat-data": "^7.11.0", + "@babel/helper-compilation-targets": "^7.10.4", + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-proposal-async-generator-functions": "^7.10.4", + "@babel/plugin-proposal-class-properties": "^7.10.4", + "@babel/plugin-proposal-dynamic-import": "^7.10.4", + "@babel/plugin-proposal-export-namespace-from": "^7.10.4", + "@babel/plugin-proposal-json-strings": "^7.10.4", + "@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", + "@babel/plugin-proposal-numeric-separator": "^7.10.4", + "@babel/plugin-proposal-object-rest-spread": "^7.11.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.10.4", + "@babel/plugin-proposal-optional-chaining": "^7.11.0", + "@babel/plugin-proposal-private-methods": "^7.10.4", + "@babel/plugin-proposal-unicode-property-regex": "^7.10.4", "@babel/plugin-syntax-async-generators": "^7.8.0", - "@babel/plugin-syntax-class-properties": "^7.10.1", + "@babel/plugin-syntax-class-properties": "^7.10.4", "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", - "@babel/plugin-syntax-numeric-separator": "^7.10.1", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0", - "@babel/plugin-syntax-top-level-await": "^7.10.1", - "@babel/plugin-transform-arrow-functions": "^7.10.1", - "@babel/plugin-transform-async-to-generator": "^7.10.1", - "@babel/plugin-transform-block-scoped-functions": "^7.10.1", - "@babel/plugin-transform-block-scoping": "^7.10.1", - "@babel/plugin-transform-classes": "^7.10.3", - "@babel/plugin-transform-computed-properties": "^7.10.3", - "@babel/plugin-transform-destructuring": "^7.10.1", - "@babel/plugin-transform-dotall-regex": "^7.10.1", - "@babel/plugin-transform-duplicate-keys": "^7.10.1", - "@babel/plugin-transform-exponentiation-operator": "^7.10.1", - "@babel/plugin-transform-for-of": "^7.10.1", - "@babel/plugin-transform-function-name": "^7.10.1", - "@babel/plugin-transform-literals": "^7.10.1", - "@babel/plugin-transform-member-expression-literals": "^7.10.1", - "@babel/plugin-transform-modules-amd": "^7.10.1", - "@babel/plugin-transform-modules-commonjs": "^7.10.1", - "@babel/plugin-transform-modules-systemjs": "^7.10.3", - "@babel/plugin-transform-modules-umd": "^7.10.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.3", - "@babel/plugin-transform-new-target": "^7.10.1", - "@babel/plugin-transform-object-super": "^7.10.1", - "@babel/plugin-transform-parameters": "^7.10.1", - "@babel/plugin-transform-property-literals": "^7.10.1", - "@babel/plugin-transform-regenerator": "^7.10.3", - "@babel/plugin-transform-reserved-words": "^7.10.1", - "@babel/plugin-transform-shorthand-properties": "^7.10.1", - "@babel/plugin-transform-spread": "^7.10.1", - "@babel/plugin-transform-sticky-regex": "^7.10.1", - "@babel/plugin-transform-template-literals": "^7.10.3", - "@babel/plugin-transform-typeof-symbol": "^7.10.1", - "@babel/plugin-transform-unicode-escapes": "^7.10.1", - "@babel/plugin-transform-unicode-regex": "^7.10.1", + "@babel/plugin-syntax-top-level-await": "^7.10.4", + "@babel/plugin-transform-arrow-functions": "^7.10.4", + "@babel/plugin-transform-async-to-generator": "^7.10.4", + "@babel/plugin-transform-block-scoped-functions": "^7.10.4", + "@babel/plugin-transform-block-scoping": "^7.10.4", + "@babel/plugin-transform-classes": "^7.10.4", + "@babel/plugin-transform-computed-properties": "^7.10.4", + "@babel/plugin-transform-destructuring": "^7.10.4", + "@babel/plugin-transform-dotall-regex": "^7.10.4", + "@babel/plugin-transform-duplicate-keys": "^7.10.4", + "@babel/plugin-transform-exponentiation-operator": "^7.10.4", + "@babel/plugin-transform-for-of": "^7.10.4", + "@babel/plugin-transform-function-name": "^7.10.4", + "@babel/plugin-transform-literals": "^7.10.4", + "@babel/plugin-transform-member-expression-literals": "^7.10.4", + "@babel/plugin-transform-modules-amd": "^7.10.4", + "@babel/plugin-transform-modules-commonjs": "^7.10.4", + "@babel/plugin-transform-modules-systemjs": "^7.10.4", + "@babel/plugin-transform-modules-umd": "^7.10.4", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.10.4", + "@babel/plugin-transform-new-target": "^7.10.4", + "@babel/plugin-transform-object-super": "^7.10.4", + "@babel/plugin-transform-parameters": "^7.10.4", + "@babel/plugin-transform-property-literals": "^7.10.4", + "@babel/plugin-transform-regenerator": "^7.10.4", + "@babel/plugin-transform-reserved-words": "^7.10.4", + "@babel/plugin-transform-shorthand-properties": "^7.10.4", + "@babel/plugin-transform-spread": "^7.11.0", + "@babel/plugin-transform-sticky-regex": "^7.10.4", + "@babel/plugin-transform-template-literals": "^7.10.4", + "@babel/plugin-transform-typeof-symbol": "^7.10.4", + "@babel/plugin-transform-unicode-escapes": "^7.10.4", + "@babel/plugin-transform-unicode-regex": "^7.10.4", "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.10.3", + "@babel/types": "^7.11.0", "browserslist": "^4.12.0", "core-js-compat": "^3.6.2", "invariant": "^2.2.2", @@ -2534,28 +2417,28 @@ }, "dependencies": { "@babel/helper-module-imports": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz", - "integrity": "sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", "dev": true, "requires": { - "@babel/types": "^7.10.3" + "@babel/types": "^7.10.4" } }, "@babel/helper-validator-identifier": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", - "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, "@babel/types": { - "version": "7.10.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.3.tgz", - "integrity": "sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.0.tgz", + "integrity": "sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.10.3", - "lodash": "^4.17.13", + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } }, @@ -2581,18 +2464,18 @@ } }, "@babel/preset-react": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.10.1.tgz", - "integrity": "sha512-Rw0SxQ7VKhObmFjD/cUcKhPTtzpeviEFX1E6PgP+cYOhQ98icNqtINNFANlsdbQHrmeWnqdxA4Tmnl1jy5tp3Q==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.10.4.tgz", + "integrity": "sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.1", - "@babel/plugin-transform-react-display-name": "^7.10.1", - "@babel/plugin-transform-react-jsx": "^7.10.1", - "@babel/plugin-transform-react-jsx-development": "^7.10.1", - "@babel/plugin-transform-react-jsx-self": "^7.10.1", - "@babel/plugin-transform-react-jsx-source": "^7.10.1", - "@babel/plugin-transform-react-pure-annotations": "^7.10.1" + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-transform-react-display-name": "^7.10.4", + "@babel/plugin-transform-react-jsx": "^7.10.4", + "@babel/plugin-transform-react-jsx-development": "^7.10.4", + "@babel/plugin-transform-react-jsx-self": "^7.10.4", + "@babel/plugin-transform-react-jsx-source": "^7.10.4", + "@babel/plugin-transform-react-pure-annotations": "^7.10.4" } }, "@babel/preset-typescript": { @@ -3427,9 +3310,9 @@ } }, "@types/babel__traverse": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.12.tgz", - "integrity": "sha512-t4CoEokHTfcyfb4hUaF9oOHu9RmmNWnm1CP0YmMqOOfClKascOmvlEM736vlqeScuGvBDsHkf8R2INd4DWreQA==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.13.tgz", + "integrity": "sha512-i+zS7t6/s9cdQvbqKDARrcbrPvtJGlbYsMkazo03nTAK3RX9FNrLllXys22uiTGJapPOTZTQ35nHh4ISph4SLQ==", "dev": true, "requires": { "@babel/types": "^7.3.0" @@ -3448,9 +3331,9 @@ "dev": true }, "@types/glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, "requires": { "@types/minimatch": "*", @@ -3805,9 +3688,9 @@ "dev": true }, "abab": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", - "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", + "integrity": "sha512-Eu9ELJWCz/c1e9gTiCY+FceWxcqzjYEbqMgtndnuSqZSUCOL73TWNK2mHfIj4Cw2E/ongOp+JISVNCmovt2KYQ==", "dev": true }, "accepts": { @@ -3956,9 +3839,9 @@ "dev": true }, "ajv-keywords": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.0.tgz", - "integrity": "sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true }, "alphanum-sort": { @@ -4257,14 +4140,14 @@ } }, "autoprefixer": { - "version": "9.8.4", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.4.tgz", - "integrity": "sha512-84aYfXlpUe45lvmS+HoAWKCkirI/sw4JK0/bTeeqgHYco3dcsOn0NqdejISjptsYwNji/21dnkDri9PsYKk89A==", + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", "dev": true, "requires": { "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001087", - "colorette": "^1.2.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", "postcss": "^7.0.32", @@ -4278,9 +4161,9 @@ "dev": true }, "aws4": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", - "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", + "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", "dev": true }, "axios": { @@ -4570,9 +4453,9 @@ } }, "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", + "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -4826,9 +4709,9 @@ } }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "dev": true }, "semver": { @@ -4985,6 +4868,16 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -5100,8 +4993,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browser-process-hrtime": { "version": "1.0.0", @@ -5235,15 +5127,15 @@ } }, "browserslist": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.2.tgz", - "integrity": "sha512-MfZaeYqR8StRZdstAK9hCKDd2StvePCYp5rHzQCPicUjfFliDgmuaBNPHYUTpAywBN8+Wc/d7NYVFkO0aqaBUw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.0.tgz", + "integrity": "sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001088", - "electron-to-chromium": "^1.3.483", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" + "caniuse-lite": "^1.0.30001111", + "electron-to-chromium": "^1.3.523", + "escalade": "^3.0.2", + "node-releases": "^1.1.60" } }, "bser": { @@ -5430,9 +5322,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001090", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001090.tgz", - "integrity": "sha512-QzPRKDCyp7RhjczTPZaqK3CjPA5Ht2UnXhZhCI4f7QiB5JK6KEuZBxIzyWnB3wO4hgAj4GMRxAhuiacfw0Psjg==", + "version": "1.0.30001117", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001117.tgz", + "integrity": "sha512-4tY0Fatzdx59kYjQs+bNxUwZB03ZEBgVmJ1UkFPz/Q8OLiUUbjct2EdpnXj0fvFTPej2EkbPIG0w8BWsjAyk1Q==", "dev": true }, "capture-exit": { @@ -5536,9 +5428,9 @@ } }, "chokidar": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", - "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", + "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", "dev": true, "requires": { "anymatch": "~3.1.1", @@ -5786,12 +5678,6 @@ "q": "^1.1.2" } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "codemirror": { "version": "5.53.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.53.2.tgz", @@ -5841,9 +5727,9 @@ } }, "colorette": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.0.tgz", - "integrity": "sha512-soRSroY+OF/8OdA3PTQXwaDJeMc7TfknKKrxeSCencL2a4+Tx5zhxmmv7hdpCjhKBjehzp8+bwe/T68K0hpIjw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "dev": true }, "colors": { @@ -7270,9 +7156,9 @@ "dev": true }, "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, "duplexify": { @@ -7304,16 +7190,15 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.483", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.483.tgz", - "integrity": "sha512-+05RF8S9rk8S0G8eBCqBRBaRq7+UN3lDs2DAvnG8SBSgQO3hjy0+qt4CmRk5eiuGbTcaicgXfPmBi31a+BD3lg==", + "version": "1.3.540", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.540.tgz", + "integrity": "sha512-IoGiZb8SMqTtkDYJtP8EtCdvv3VMtd1QoTlypO2RUBxRq/Wk0rU5IzhzhMckPaC9XxDqUvWsL0XKOBhTiYVN3w==", "dev": true }, "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", - "dev": true, + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -7325,10 +7210,9 @@ }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" } } }, @@ -7369,9 +7253,9 @@ } }, "enhanced-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz", - "integrity": "sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", + "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -7582,9 +7466,9 @@ } }, "escalade": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.1.tgz", - "integrity": "sha512-DR6NO3h9niOT+MZs7bjxlj2a1k+POu5RN8CLTPX2+i78bRi9eLe7+0zXgUHMnGXWybYcL61E9hGhPKqedy8tQA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", + "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==", "dev": true }, "escape-html": { @@ -8568,6 +8452,13 @@ "tslib": "^1.9.0" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "filesize": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.0.1.tgz", @@ -9076,13 +8967,27 @@ "dev": true }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "dev": true, "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, "harmony-reflect": { @@ -9212,7 +9117,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -9247,7 +9151,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -9445,12 +9348,6 @@ } } }, - "http-parser-js": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", - "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==", - "dev": true - }, "http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -9670,8 +9567,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", @@ -9803,12 +9699,6 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -9966,9 +9856,9 @@ "dev": true }, "is-docker": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", - "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", "dev": true }, "is-extendable": { @@ -10510,7 +10400,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } } } }, @@ -11034,15 +10928,6 @@ "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", "dev": true }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -11354,15 +11239,6 @@ "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", "dev": true }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -11401,17 +11277,6 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memoize-one": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", @@ -11593,14 +11458,12 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", @@ -11644,9 +11507,9 @@ } }, "minipass-pipeline": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz", - "integrity": "sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, "requires": { "minipass": "^3.0.0" @@ -11774,6 +11637,13 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "dev": true, + "optional": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -11835,9 +11705,9 @@ "dev": true }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, "next-tick": { @@ -12017,9 +11887,9 @@ } }, "node-releases": { - "version": "1.1.58", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.58.tgz", - "integrity": "sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==", + "version": "1.1.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz", + "integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==", "dev": true }, "normalize-package-data": { @@ -12093,12 +11963,6 @@ "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", "dev": true }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -12296,9 +12160,9 @@ } }, "open": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz", - "integrity": "sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.1.0.tgz", + "integrity": "sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA==", "dev": true, "requires": { "is-docker": "^2.0.0", @@ -12621,29 +12485,12 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, "p-each-series": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", @@ -12659,12 +12506,6 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -13140,14 +12981,14 @@ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" }, "portfinder": { - "version": "1.0.26", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz", - "integrity": "sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==", + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", "dev": true, "requires": { "async": "^2.6.2", "debug": "^3.1.1", - "mkdirp": "^0.5.1" + "mkdirp": "^0.5.5" }, "dependencies": { "debug": { @@ -13221,9 +13062,9 @@ } }, "postcss-calc": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.2.tgz", - "integrity": "sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.3.tgz", + "integrity": "sha512-IB/EAEmZhIMEIhG7Ov4x+l47UaXOS1n2f4FBUk/aKllQhtSCxWhTzn0nJgkqN7fo/jcWySvWTSB6Syk9L+31bA==", "dev": true, "requires": { "postcss": "^7.0.27", @@ -13726,15 +13567,15 @@ } }, "postcss-modules-local-by-default": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", - "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", "dev": true, "requires": { "icss-utils": "^4.1.1", - "postcss": "^7.0.16", + "postcss": "^7.0.32", "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.0" + "postcss-value-parser": "^4.1.0" } }, "postcss-modules-scope": { @@ -14244,12 +14085,6 @@ "react-is": "^16.8.4" } }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -14562,9 +14397,9 @@ "dev": true }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", "dev": true } } @@ -14877,9 +14712,9 @@ } }, "react-scripts": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.1.tgz", - "integrity": "sha512-JpTdi/0Sfd31mZA6Ukx+lq5j1JoKItX7qqEK4OiACjVQletM1P38g49d9/D0yTxp9FrSF+xpJFStkGgKEIRjlQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.3.tgz", + "integrity": "sha512-oSnoWmii/iKdeQiwaO6map1lUaZLmG0xIUyb/HwCVFLT7gNbj8JZ9RmpvMCZ4fB98ZUMRfNmp/ft8uy/xD1RLA==", "dev": true, "requires": { "@babel/core": "7.9.0", @@ -14928,11 +14763,11 @@ "sass-loader": "8.0.2", "semver": "6.3.0", "style-loader": "0.23.1", - "terser-webpack-plugin": "2.3.5", + "terser-webpack-plugin": "2.3.8", "ts-pnp": "1.1.6", "url-loader": "2.3.0", "webpack": "4.42.0", - "webpack-dev-server": "3.10.3", + "webpack-dev-server": "3.11.0", "webpack-manifest-plugin": "2.2.0", "workbox-webpack-plugin": "4.3.1" }, @@ -15134,13 +14969,12 @@ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regenerator-transform": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.4.tgz", - "integrity": "sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", "dev": true, "requires": { - "@babel/runtime": "^7.8.4", - "private": "^0.1.8" + "@babel/runtime": "^7.8.4" } }, "regex-not": { @@ -15332,21 +15166,21 @@ } }, "request-promise-core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", - "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", "dev": true, "requires": { - "lodash": "^4.17.15" + "lodash": "^4.17.19" } }, "request-promise-native": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", - "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", "dev": true, "requires": { - "request-promise-core": "1.1.3", + "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" } @@ -15814,10 +15648,13 @@ } }, "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } }, "serve-index": { "version": "1.9.1", @@ -16170,13 +16007,14 @@ } }, "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", "dev": true, "requires": { "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" } }, "sockjs-client": { @@ -16962,19 +16800,19 @@ } }, "terser-webpack-plugin": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.5.tgz", - "integrity": "sha512-WlWksUoq+E4+JlJ+h+U+QUzXpcsMSSNXkDy9lBVkSqDn1w23Gg29L/ary9GeJVYCGiNJJX7LnVc4bwL1N3/g1w==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz", + "integrity": "sha512-/fKw3R+hWyHfYx7Bv6oPqmk4HGQcrWLtV3X6ggvPuwPNHSnzvVV51z6OaaCOus4YLjutYGOz3pEpbhe6Up2s1w==", "dev": true, "requires": { "cacache": "^13.0.1", - "find-cache-dir": "^3.2.0", - "jest-worker": "^25.1.0", - "p-limit": "^2.2.2", - "schema-utils": "^2.6.4", - "serialize-javascript": "^2.1.2", + "find-cache-dir": "^3.3.1", + "jest-worker": "^25.4.0", + "p-limit": "^2.3.0", + "schema-utils": "^2.6.6", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", - "terser": "^4.4.3", + "terser": "^4.6.12", "webpack-sources": "^1.4.3" }, "dependencies": { @@ -17675,12 +17513,12 @@ } }, "watchpack": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz", - "integrity": "sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", + "integrity": "sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==", "dev": true, "requires": { - "chokidar": "^3.4.0", + "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", "neo-async": "^2.5.0", "watchpack-chokidar2": "^2.0.0" @@ -17729,7 +17567,11 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -17890,15 +17732,6 @@ "ajv-keywords": "^3.1.0" } }, - "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -17915,16 +17748,16 @@ } }, "terser-webpack-plugin": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz", - "integrity": "sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", "dev": true, "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^3.1.0", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", @@ -17947,9 +17780,9 @@ } }, "webpack-dev-server": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz", - "integrity": "sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -17960,31 +17793,31 @@ "debug": "^4.1.1", "del": "^4.1.1", "express": "^4.17.1", - "html-entities": "^1.2.1", + "html-entities": "^1.3.1", "http-proxy-middleware": "0.19.1", "import-local": "^2.0.0", "internal-ip": "^4.3.0", "ip": "^1.1.5", "is-absolute-url": "^3.0.3", "killable": "^1.0.1", - "loglevel": "^1.6.6", + "loglevel": "^1.6.8", "opn": "^5.5.0", "p-retry": "^3.0.1", - "portfinder": "^1.0.25", + "portfinder": "^1.0.26", "schema-utils": "^1.0.0", "selfsigned": "^1.10.7", "semver": "^6.3.0", "serve-index": "^1.9.1", - "sockjs": "0.3.19", + "sockjs": "0.3.20", "sockjs-client": "1.4.0", - "spdy": "^4.0.1", + "spdy": "^4.0.2", "strip-ansi": "^3.0.1", "supports-color": "^6.1.0", "url": "^0.11.0", "webpack-dev-middleware": "^3.7.2", "webpack-log": "^2.0.0", "ws": "^6.2.1", - "yargs": "12.0.5" + "yargs": "^13.3.2" }, "dependencies": { "ansi-regex": { @@ -18019,34 +17852,6 @@ "upath": "^1.1.1" } }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -18056,27 +17861,16 @@ "ms": "^2.1.1" } }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, "fsevents": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -18126,22 +17920,6 @@ "binary-extensions": "^1.0.0" } }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -18154,30 +17932,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, "readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", @@ -18189,12 +17943,6 @@ "readable-stream": "^2.0.2" } }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -18206,33 +17954,6 @@ "ajv-keywords": "^3.1.0" } }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -18251,38 +17972,6 @@ "has-flag": "^3.0.0" } }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - } - } - }, "ws": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", @@ -18291,36 +17980,6 @@ "requires": { "async-limiter": "~1.0.0" } - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -18378,13 +18037,11 @@ } }, "websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", "dev": true, "requires": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, @@ -18404,9 +18061,9 @@ } }, "whatwg-fetch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.1.0.tgz", - "integrity": "sha512-pgmbsVWKpH9GxLXZmtdowDIqtb/rvPyjjQv3z9wLcmgWKFHilKnZD3ldgrOlwJoPGOUluQsRPWd52yVkPfmI1A==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz", + "integrity": "sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==", "dev": true }, "whatwg-mimetype": { diff --git a/awx/ui_next/package.json b/awx/ui_next/package.json index 83818e3a86..2fc0e84108 100644 --- a/awx/ui_next/package.json +++ b/awx/ui_next/package.json @@ -49,7 +49,7 @@ "jest-websocket-mock": "^2.0.2", "mock-socket": "^9.0.3", "prettier": "^1.18.2", - "react-scripts": "^3.4.1" + "react-scripts": "^3.4.3" }, "scripts": { "start": "PORT=3001 HTTPS=true DANGEROUSLY_DISABLE_HOST_CHECK=true react-scripts start", From e4095a0c27b846b5870fc40816ca76f6c9783147 Mon Sep 17 00:00:00 2001 From: Taras Dyshkant Date: Fri, 21 Aug 2020 15:35:39 +0300 Subject: [PATCH 086/188] Fix broadcast_websocket_secret length Password lookup parameters must be within the same set of quotes. Otherwise a default value of length is used (20). --- installer/roles/kubernetes/tasks/main.yml | 2 +- installer/roles/local_docker/tasks/main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/installer/roles/kubernetes/tasks/main.yml b/installer/roles/kubernetes/tasks/main.yml index 6a9d9a2deb..9e0d6f4f7d 100644 --- a/installer/roles/kubernetes/tasks/main.yml +++ b/installer/roles/kubernetes/tasks/main.yml @@ -1,7 +1,7 @@ --- - name: Generate broadcast websocket secret set_fact: - broadcast_websocket_secret: "{{ lookup('password', '/dev/null', length=128) }}" + broadcast_websocket_secret: "{{ lookup('password', '/dev/null length=128') }}" run_once: true no_log: true when: broadcast_websocket_secret is not defined diff --git a/installer/roles/local_docker/tasks/main.yml b/installer/roles/local_docker/tasks/main.yml index aab1260a36..e2b793e50e 100644 --- a/installer/roles/local_docker/tasks/main.yml +++ b/installer/roles/local_docker/tasks/main.yml @@ -1,7 +1,7 @@ --- - name: Generate broadcast websocket secret set_fact: - broadcast_websocket_secret: "{{ lookup('password', '/dev/null', length=128) }}" + broadcast_websocket_secret: "{{ lookup('password', '/dev/null length=128') }}" run_once: true no_log: true when: broadcast_websocket_secret is not defined From 2d23748971c155707f19e84b18e9ef4ff2a3c2a4 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Fri, 21 Aug 2020 12:39:28 -0400 Subject: [PATCH 087/188] Add Remote Archive SCM Type feature to CHANGELOG.md Signed-off-by: Philip Douglass --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f79d4f57..3d6a657365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ This is a list of high-level changes for each release of AWX. A full list of com ## 14.1.0 (TBD) - AWX images can now be built on ARM64 - https://github.com/ansible/awx/pull/7607 +- Added the Remote Archive SCM Type to support using immutable artifacts and releases (such as tarballs and zip files) as projects - https://github.com/ansible/awx/issues/7954 - Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 - Added resource import/export support to the official AWX collection - https://github.com/ansible/awx/issues/7329 - Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 From 30ae0f53ec083d05bc82e536688122909ea4adf6 Mon Sep 17 00:00:00 2001 From: Philip Douglass Date: Fri, 21 Aug 2020 13:09:09 -0400 Subject: [PATCH 088/188] Reject setting scm_branch for Remote Archive projects --- awx/api/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index be6a9d640b..429bf512be 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1336,6 +1336,8 @@ class ProjectOptionsSerializer(BaseSerializer): attrs.pop('local_path', None) if 'local_path' in attrs and attrs['local_path'] not in valid_local_paths: errors['local_path'] = _('This path is already being used by another manual project.') + if attrs.get('scm_branch') and scm_type == 'archive': + errors['scm_branch'] = _('SCM branch cannot be used with archive projects.') if attrs.get('scm_refspec') and scm_type != 'git': errors['scm_refspec'] = _('SCM refspec can only be used with git projects.') From 17a40808b4461220fd176a99279e51dc12b3b4d0 Mon Sep 17 00:00:00 2001 From: nixocio Date: Fri, 21 Aug 2020 14:25:30 -0400 Subject: [PATCH 089/188] Update disassociate button variant Update disassociate button variant. See: https://github.com/ansible/awx/issues/7041 Also: https://tower-mockups.testing.ansible.com/patternfly/instance-groups/instance-groups-instances/ --- .../src/components/DisassociateButton/DisassociateButton.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx b/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx index cafde49f73..fab8186f67 100644 --- a/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx +++ b/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx @@ -61,7 +61,7 @@ function DisassociateButton({
+ + )} {template.summary_fields.user_capabilities.start && ( @@ -111,7 +126,7 @@ function TemplateListItem({ ); } return ( - ); } diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx index 4597bbd61c..06c348a8b9 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroup.jsx @@ -21,7 +21,7 @@ import JobList from '../../components/JobList'; import InstanceGroupDetails from './InstanceGroupDetails'; import InstanceGroupEdit from './InstanceGroupEdit'; -import Instances from './Instances'; +import InstanceList from './Instances/InstanceList'; function InstanceGroup({ i18n, setBreadcrumb }) { const { id } = useParams(); @@ -123,7 +123,7 @@ function InstanceGroup({ i18n, setBreadcrumb }) { - + { +describe('', () => { let wrapper; test('should have data fetched and render 3 rows', async () => { diff --git a/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceList.jsx b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceList.jsx new file mode 100644 index 0000000000..f666cecd27 --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceList.jsx @@ -0,0 +1,245 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { useLocation, useParams } from 'react-router-dom'; +import 'styled-components/macro'; + +import DataListToolbar from '../../../components/DataListToolbar'; +import PaginatedDataList, { + ToolbarAddButton, +} from '../../../components/PaginatedDataList'; +import DisassociateButton from '../../../components/DisassociateButton'; +import AssociateModal from '../../../components/AssociateModal'; +import AlertModal from '../../../components/AlertModal'; +import ErrorDetail from '../../../components/ErrorDetail'; + +import useRequest, { + useDeleteItems, + useDismissableError, +} from '../../../util/useRequest'; +import useSelected from '../../../util/useSelected'; +import { InstanceGroupsAPI, InstancesAPI } from '../../../api'; +import { getQSConfig, parseQueryString, mergeParams } from '../../../util/qs'; + +import InstanceListItem from './InstanceListItem'; + +const QS_CONFIG = getQSConfig('instance', { + page: 1, + page_size: 20, + order_by: 'hostname', +}); + +function InstanceList({ i18n }) { + const [isModalOpen, setIsModalOpen] = useState(false); + const location = useLocation(); + const { id: instanceGroupId } = useParams(); + + const { + result: { + instances, + count, + actions, + relatedSearchableKeys, + searchableKeys, + }, + error: contentError, + isLoading, + request: fetchInstances, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const [response, responseActions] = await Promise.all([ + InstanceGroupsAPI.readInstances(instanceGroupId, params), + InstanceGroupsAPI.readInstanceOptions(instanceGroupId), + ]); + return { + instances: response.data.results, + count: response.data.count, + actions: responseActions.data.actions, + relatedSearchableKeys: ( + responseActions?.data?.related_search_fields || [] + ).map(val => val.slice(0, -8)), + searchableKeys: Object.keys( + responseActions.data.actions?.GET || {} + ).filter(key => responseActions.data.actions?.GET[key].filterable), + }; + }, [location.search, instanceGroupId]), + { + instances: [], + count: 0, + actions: {}, + relatedSearchableKeys: [], + searchableKeys: [], + } + ); + + const { selected, isAllSelected, handleSelect, setSelected } = useSelected( + instances + ); + + useEffect(() => { + fetchInstances(); + }, [fetchInstances]); + + const { + isLoading: isDisassociateLoading, + deleteItems: disassociateInstances, + deletionError: disassociateError, + } = useDeleteItems( + useCallback(async () => { + return Promise.all( + selected.map(instance => + InstanceGroupsAPI.disassociateInstance(instanceGroupId, instance.id) + ) + ); + }, [instanceGroupId, selected]), + { + qsConfig: QS_CONFIG, + allItemsSelected: isAllSelected, + fetchItems: fetchInstances, + } + ); + + const { request: handleAssociate, error: associateError } = useRequest( + useCallback( + async instancesToAssociate => { + await Promise.all( + instancesToAssociate.map(instance => + InstanceGroupsAPI.associateInstance(instanceGroupId, instance.id) + ) + ); + fetchInstances(); + }, + [instanceGroupId, fetchInstances] + ) + ); + + const handleDisassociate = async () => { + await disassociateInstances(); + setSelected([]); + }; + + const { error, dismissError } = useDismissableError( + associateError || disassociateError + ); + + const canAdd = + actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); + + const fetchInstancesToAssociate = useCallback( + params => { + return InstancesAPI.read( + mergeParams(params, { not__rampart_groups__id: instanceGroupId }) + ); + }, + [instanceGroupId] + ); + + const readInstancesOptions = () => + InstanceGroupsAPI.readInstanceOptions(instanceGroupId); + + return ( + <> + ( + + setSelected(isSelected ? [...instances] : []) + } + qsConfig={QS_CONFIG} + additionalControls={[ + ...(canAdd + ? [ + setIsModalOpen(true)} + defaultLabel={i18n._(t`Associate`)} + />, + ] + : []), + , + ]} + emptyStateControls={ + canAdd ? ( + setIsModalOpen(true)} + /> + ) : null + } + /> + )} + renderItem={instance => ( + handleSelect(instance)} + isSelected={selected.some(row => row.id === instance.id)} + fetchInstances={fetchInstances} + /> + )} + /> + {isModalOpen && ( + setIsModalOpen(false)} + title={i18n._(t`Select Instances`)} + optionsRequest={readInstancesOptions} + displayKey="hostname" + /> + )} + {error && ( + + {associateError + ? i18n._(t`Failed to associate.`) + : i18n._(t`Failed to disassociate one or more instances.`)} + + + )} + + ); +} + +export default withI18n()(InstanceList); diff --git a/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceList.test.jsx b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceList.test.jsx new file mode 100644 index 0000000000..d2c7edf32d --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceList.test.jsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { Route } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; + +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; +import { InstanceGroupsAPI } from '../../../api'; + +import InstanceList from './InstanceList'; + +jest.mock('../../../api'); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + id: 1, + instanceGroupId: 2, + }), +})); + +const instances = [ + { + id: 1, + type: 'instance', + url: '/api/v2/instances/1/', + related: { + jobs: '/api/v2/instances/1/jobs/', + instance_groups: '/api/v2/instances/1/instance_groups/', + }, + uuid: '00000000-0000-0000-0000-000000000000', + hostname: 'awx', + created: '2020-07-14T19:03:49.000054Z', + modified: '2020-08-12T20:08:02.836748Z', + capacity_adjustment: '0.40', + version: '13.0.0', + capacity: 10, + consumed_capacity: 0, + percent_capacity_remaining: 60.0, + jobs_running: 0, + jobs_total: 68, + cpu: 6, + memory: 2087469056, + cpu_capacity: 24, + mem_capacity: 1, + enabled: true, + managed_by_policy: true, + }, + { + id: 2, + type: 'instance', + url: '/api/v2/instances/2/', + related: { + jobs: '/api/v2/instances/2/jobs/', + instance_groups: '/api/v2/instances/2/instance_groups/', + }, + uuid: '00000000-0000-0000-0000-000000000000', + hostname: 'foo', + created: '2020-07-14T19:03:49.000054Z', + modified: '2020-08-12T20:08:02.836748Z', + capacity_adjustment: '0.40', + version: '13.0.0', + capacity: 10, + consumed_capacity: 0, + percent_capacity_remaining: 60.0, + jobs_running: 0, + jobs_total: 68, + cpu: 6, + memory: 2087469056, + cpu_capacity: 24, + mem_capacity: 1, + enabled: true, + managed_by_policy: false, + }, + { + id: 3, + type: 'instance', + url: '/api/v2/instances/3/', + related: { + jobs: '/api/v2/instances/3/jobs/', + instance_groups: '/api/v2/instances/3/instance_groups/', + }, + uuid: '00000000-0000-0000-0000-000000000000', + hostname: 'bar', + created: '2020-07-14T19:03:49.000054Z', + modified: '2020-08-12T20:08:02.836748Z', + capacity_adjustment: '0.40', + version: '13.0.0', + capacity: 10, + consumed_capacity: 0, + percent_capacity_remaining: 60.0, + jobs_running: 0, + jobs_total: 68, + cpu: 6, + memory: 2087469056, + cpu_capacity: 24, + mem_capacity: 1, + enabled: false, + managed_by_policy: true, + }, +]; + +const options = { data: { actions: { POST: true } } }; + +describe('', () => { + let wrapper; + + beforeEach(async () => { + InstanceGroupsAPI.readInstances.mockResolvedValue({ + data: { + count: instances.length, + results: instances, + }, + }); + InstanceGroupsAPI.readInstanceOptions.mockResolvedValue(options); + const history = createMemoryHistory({ + initialEntries: ['/instance_groups/1/instances'], + }); + await act(async () => { + wrapper = mountWithContexts( + + + , + { + context: { + router: { history, route: { location: history.location } }, + }, + } + ); + }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + }); + + afterEach(() => { + jest.clearAllMocks(); + wrapper.unmount(); + }); + + test('should have data fetched', () => { + expect(wrapper.find('InstanceList').length).toBe(1); + }); + + test('should fetch instances from the api and render them in the list', () => { + expect(InstanceGroupsAPI.readInstances).toHaveBeenCalled(); + expect(InstanceGroupsAPI.readInstanceOptions).toHaveBeenCalled(); + expect(wrapper.find('InstanceListItem').length).toBe(3); + }); + + test('should show associate group modal when adding an existing group', () => { + wrapper.find('ToolbarAddButton').simulate('click'); + expect(wrapper.find('AssociateModal').length).toBe(1); + wrapper.find('ModalBoxCloseButton').simulate('click'); + expect(wrapper.find('AssociateModal').length).toBe(0); + }); +}); diff --git a/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceListItem.jsx new file mode 100644 index 0000000000..83f2fef753 --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceListItem.jsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { bool, func } from 'prop-types'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import styled from 'styled-components'; +import 'styled-components/macro'; +import { + Badge as PFBadge, + Progress, + ProgressMeasureLocation, + ProgressSize, + DataListAction, + DataListCheck, + DataListItem, + DataListItemRow, + DataListItemCells, +} from '@patternfly/react-core'; + +import _DataListCell from '../../../components/DataListCell'; +import InstanceToggle from '../../../components/InstanceToggle'; +import { Instance } from '../../../types'; + +const Unavailable = styled.span` + color: var(--pf-global--danger-color--200); +`; + +const DataListCell = styled(_DataListCell)` + white-space: nowrap; +`; + +const Badge = styled(PFBadge)` + margin-left: 8px; +`; + +const ListGroup = styled.span` + margin-left: 12px; + + &:first-of-type { + margin-left: 0; + } +`; + +function InstanceListItem({ + instance, + isSelected, + onSelect, + fetchInstances, + i18n, +}) { + const labelId = `check-action-${instance.id}`; + + function usedCapacity(item) { + if (item.enabled) { + return ( + + ); + } + return {i18n._(t`Unavailable`)}; + } + + return ( + + + + + + {instance.hostname} + , + + {i18n._(t`Type`)} + + {instance.managed_by_policy + ? i18n._(t`Auto`) + : i18n._(t`Manual`)} + + , + + + {i18n._(t`Running jobs`)} + {instance.jobs_running} + + + {i18n._(t`Total jobs`)} + {instance.jobs_total} + + , + + {usedCapacity(instance)} + , + ]} + /> + + + + + + ); +} +InstanceListItem.prototype = { + instance: Instance.isRequired, + isSelected: bool.isRequired, + onSelect: func.isRequired, +}; + +export default withI18n()(InstanceListItem); diff --git a/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceListItem.test.jsx b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceListItem.test.jsx new file mode 100644 index 0000000000..5e3a138f90 --- /dev/null +++ b/awx/ui_next/src/screens/InstanceGroup/Instances/InstanceListItem.test.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; + +import InstanceListItem from './InstanceListItem'; + +const instance = [ + { + id: 1, + type: 'instance', + url: '/api/v2/instances/1/', + related: { + jobs: '/api/v2/instances/1/jobs/', + instance_groups: '/api/v2/instances/1/instance_groups/', + }, + uuid: '00000000-0000-0000-0000-000000000000', + hostname: 'awx', + created: '2020-07-14T19:03:49.000054Z', + modified: '2020-08-12T20:08:02.836748Z', + capacity_adjustment: '0.40', + version: '13.0.0', + capacity: 10, + consumed_capacity: 0, + percent_capacity_remaining: 60.0, + jobs_running: 0, + jobs_total: 68, + cpu: 6, + memory: 2087469056, + cpu_capacity: 24, + mem_capacity: 1, + enabled: true, + managed_by_policy: true, + }, +]; + +describe('', () => { + let wrapper; + + test('should mount successfully', async () => { + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + expect(wrapper.find('InstanceListItem').length).toBe(1); + }); + + test('should render the proper data instance', async () => { + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + expect( + wrapper.find('PFDataListCell[aria-label="instance host name"]').text() + ).toBe('awx'); + expect(wrapper.find('Progress').prop('value')).toBe(40); + expect( + wrapper.find('PFDataListCell[aria-label="instance type"]').text() + ).toBe('TypeAuto'); + expect(wrapper.find('input#instances-1').prop('checked')).toBe(false); + }); + + test('should be checked', async () => { + await act(async () => { + wrapper = mountWithContexts( + {}} + /> + ); + }); + expect(wrapper.find('input#instances-1').prop('checked')).toBe(true); + }); + + test('should display instance toggle', () => { + expect(wrapper.find('InstanceToggle').length).toBe(1); + }); +}); diff --git a/awx/ui_next/src/screens/InstanceGroup/Instances/Instances.jsx b/awx/ui_next/src/screens/InstanceGroup/Instances/Instances.jsx deleted file mode 100644 index b41760edd5..0000000000 --- a/awx/ui_next/src/screens/InstanceGroup/Instances/Instances.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { Card, PageSection } from '@patternfly/react-core'; - -function Instances() { - return ( - - -
Instances
-
-
- ); -} - -export default Instances; diff --git a/awx/ui_next/src/screens/InstanceGroup/Instances/index.js b/awx/ui_next/src/screens/InstanceGroup/Instances/index.js index b018ebb049..2567e3c8e7 100644 --- a/awx/ui_next/src/screens/InstanceGroup/Instances/index.js +++ b/awx/ui_next/src/screens/InstanceGroup/Instances/index.js @@ -1 +1,2 @@ -export { default } from './Instances'; +export { default as InstanceList } from './InstanceList'; +export { default as InstanceListItem } from './InstanceListItem'; diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index 5ee55ef5d9..e1fa5a5163 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -118,6 +118,11 @@ export const InstanceGroup = shape({ name: string.isRequired, }); +export const Instance = shape({ + id: number.isRequired, + name: string.isRequired, +}); + export const Label = shape({ id: number.isRequired, name: string.isRequired, diff --git a/awx/ui_next/testUtils/enzymeHelpers.jsx b/awx/ui_next/testUtils/enzymeHelpers.jsx index bc6666a8ba..c9077ac006 100644 --- a/awx/ui_next/testUtils/enzymeHelpers.jsx +++ b/awx/ui_next/testUtils/enzymeHelpers.jsx @@ -42,6 +42,7 @@ const defaultContexts = { ansible_version: null, custom_virtualenvs: [], version: null, + me: { is_superuser: true }, toJSON: () => '/config/', }, router: { From dc7e7219685c4512636be4ff3395cc8982654bdd Mon Sep 17 00:00:00 2001 From: Daniel Sami Date: Thu, 27 Aug 2020 11:54:38 -0400 Subject: [PATCH 139/188] Change WFJT details sparkline hyperlink --- .../WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx index eb768ec5ca..2f6b99821a 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx @@ -99,7 +99,7 @@ function WorkflowJobTemplateDetail({ template, i18n }) { const canLaunch = summary_fields?.user_capabilities?.start; const recentPlaybookJobs = summary_fields.recent_jobs.map(job => ({ ...job, - type: 'job', + type: 'workflow_job', })); return ( From 007b0d841e34972e5928d8689477017e4a830ce5 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Fri, 28 Aug 2020 07:19:39 -0500 Subject: [PATCH 140/188] updated parameters and errors --- awx_collection/plugins/module_utils/tower_api.py | 2 +- awx_collection/plugins/modules/tower_job_launch.py | 2 +- awx_collection/plugins/modules/tower_job_wait.py | 7 +++++-- awx_collection/plugins/modules/tower_workflow_launch.py | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 138f523515..dfab8ad9ab 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -590,7 +590,7 @@ class TowerAPIModule(TowerModule): else: return True - def wait_on_url(self, object_name=None, object_type=None, url=None, timeout=None, interval=None): + def wait_on_url(self, url, object_name, object_type, timeout=30, interval=10): # Grab our start time to compare against for the timeout start = time.time() result = self.get_endpoint(url) diff --git a/awx_collection/plugins/modules/tower_job_launch.py b/awx_collection/plugins/modules/tower_job_launch.py index a016d1d30b..8345b55b66 100644 --- a/awx_collection/plugins/modules/tower_job_launch.py +++ b/awx_collection/plugins/modules/tower_job_launch.py @@ -247,9 +247,9 @@ def main(): # Invoke wait function results = module.wait_on_url( + url=results['json']['url'], object_name=name, object_type='job', - url=results['json']['url'], timeout=timeout, interval=interval ) diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py index edb9b718dc..888cce8381 100644 --- a/awx_collection/plugins/modules/tower_job_wait.py +++ b/awx_collection/plugins/modules/tower_job_wait.py @@ -141,14 +141,17 @@ def main(): # Invoke wait function result = module.wait_on_url( + url=job['url'], object_name=job_id, object_type='job', - url=job['url'], timeout=timeout, interval=interval ) - module.exit_json(**module.json_output) + # Format data to keep legacy compatability. + for k in ('id', 'status', 'elapsed', 'started', 'finished'): + module.json_output[k] = result['json'].get(k) + module.exit_json(**module.json_output) if __name__ == '__main__': main() diff --git a/awx_collection/plugins/modules/tower_workflow_launch.py b/awx_collection/plugins/modules/tower_workflow_launch.py index 70377fd207..b665adbf15 100644 --- a/awx_collection/plugins/modules/tower_workflow_launch.py +++ b/awx_collection/plugins/modules/tower_workflow_launch.py @@ -179,9 +179,9 @@ def main(): # Invoke wait function module.wait_on_url( + url=result['json']['url'], object_name=name, object_type='workflow_job', - url=result['json']['url'], timeout=timeout, interval=interval ) From d9713759070b2cc3db2c3bf453086409399b52f4 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Fri, 28 Aug 2020 07:35:13 -0500 Subject: [PATCH 141/188] updated to error if finished not in result --- .../plugins/module_utils/tower_api.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index dfab8ad9ab..28a4355574 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -594,17 +594,21 @@ class TowerAPIModule(TowerModule): # Grab our start time to compare against for the timeout start = time.time() result = self.get_endpoint(url) - while not result['json']['finished']: - # If we are past our time out fail with a message - if timeout and timeout < time.time() - start: - self.json_output['msg'] = 'Monitoring of {0} "{1}" aborted due to timeout'.format(object_type, object_name) - self.fail_json(**self.json_output) + if result['json']['finished'] is None: + self.json_output['msg'] = 'Monitoring of {0} "{1}" aborted due to timeout'.format(object_type, object_name) + self.fail_json(**self.json_output) + else: + while not result['json']['finished']: + # If we are past our time out fail with a message + if timeout and timeout < time.time() - start: + self.json_output['msg'] = 'Monitoring of {0} "{1}" aborted due to timeout'.format(object_type, object_name) + self.fail_json(**self.json_output) - # Put the process to sleep for our interval - time.sleep(interval) + # Put the process to sleep for our interval + time.sleep(interval) - result = self.get_endpoint(url) - self.json_output['status'] = result['json']['status'] + result = self.get_endpoint(url) + self.json_output['status'] = result['json']['status'] # If the job has failed, we want to raise a task failure for that so we get a non-zero response. if result['json']['failed']: From 7bd3f9d63c753d09dea131c94b58471b85e7aa83 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Fri, 28 Aug 2020 07:37:29 -0500 Subject: [PATCH 142/188] updated to error if finished not in result --- awx_collection/plugins/module_utils/tower_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 28a4355574..e003b76591 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -595,7 +595,7 @@ class TowerAPIModule(TowerModule): start = time.time() result = self.get_endpoint(url) if result['json']['finished'] is None: - self.json_output['msg'] = 'Monitoring of {0} "{1}" aborted due to timeout'.format(object_type, object_name) + self.json_output['msg'] = 'Finished was not returned in the request of {0}'.format(url) self.fail_json(**self.json_output) else: while not result['json']['finished']: From fd77a8aca5643d11e1f35520060f60aba3346e74 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Fri, 28 Aug 2020 08:22:44 -0500 Subject: [PATCH 143/188] updated output --- .../plugins/module_utils/tower_api.py | 33 +++++++++++-------- .../plugins/modules/tower_job_wait.py | 4 --- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index e003b76591..f6750ac3be 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -594,25 +594,32 @@ class TowerAPIModule(TowerModule): # Grab our start time to compare against for the timeout start = time.time() result = self.get_endpoint(url) - if result['json']['finished'] is None: - self.json_output['msg'] = 'Finished was not returned in the request of {0}'.format(url) - self.fail_json(**self.json_output) - else: - while not result['json']['finished']: - # If we are past our time out fail with a message - if timeout and timeout < time.time() - start: - self.json_output['msg'] = 'Monitoring of {0} "{1}" aborted due to timeout'.format(object_type, object_name) - self.fail_json(**self.json_output) + while not result['json']['finished']: + # If we are past our time out fail with a message + if timeout and timeout < time.time() - start: + self.json_output['msg'] = 'Monitoring of {0} "{1}" aborted due to timeout'.format(object_type, object_name) + # Format data to keep legacy compatability. + self.wait_output(result) + self.fail_json(**self.json_output) - # Put the process to sleep for our interval - time.sleep(interval) + # Put the process to sleep for our interval + time.sleep(interval) - result = self.get_endpoint(url) - self.json_output['status'] = result['json']['status'] + result = self.get_endpoint(url) + self.json_output['status'] = result['json']['status'] # If the job has failed, we want to raise a task failure for that so we get a non-zero response. if result['json']['failed']: self.json_output['msg'] = 'The {0} "{1}" failed'.format(object_type, object_name) + # Format data to keep legacy compatability. + self.wait_output(result) self.fail_json(**self.json_output) + self.wait_output(result) + return result + + def wait_output(self, response): + # Format data to keep legacy compatability. + for k in ('id', 'status', 'elapsed', 'started', 'finished'): + self.json_output[k] = response['json'].get(k) diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py index 888cce8381..1c65e141dc 100644 --- a/awx_collection/plugins/modules/tower_job_wait.py +++ b/awx_collection/plugins/modules/tower_job_wait.py @@ -147,10 +147,6 @@ def main(): timeout=timeout, interval=interval ) - # Format data to keep legacy compatability. - for k in ('id', 'status', 'elapsed', 'started', 'finished'): - module.json_output[k] = result['json'].get(k) - module.exit_json(**module.json_output) if __name__ == '__main__': From b3ec080e087123c874e1683394ceb92abc8b52a1 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Fri, 28 Aug 2020 08:25:21 -0500 Subject: [PATCH 144/188] updated output --- awx_collection/plugins/module_utils/tower_api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index f6750ac3be..8b619dd36e 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -598,7 +598,6 @@ class TowerAPIModule(TowerModule): # If we are past our time out fail with a message if timeout and timeout < time.time() - start: self.json_output['msg'] = 'Monitoring of {0} "{1}" aborted due to timeout'.format(object_type, object_name) - # Format data to keep legacy compatability. self.wait_output(result) self.fail_json(**self.json_output) @@ -611,7 +610,6 @@ class TowerAPIModule(TowerModule): # If the job has failed, we want to raise a task failure for that so we get a non-zero response. if result['json']['failed']: self.json_output['msg'] = 'The {0} "{1}" failed'.format(object_type, object_name) - # Format data to keep legacy compatability. self.wait_output(result) self.fail_json(**self.json_output) @@ -620,6 +618,5 @@ class TowerAPIModule(TowerModule): return result def wait_output(self, response): - # Format data to keep legacy compatability. for k in ('id', 'status', 'elapsed', 'started', 'finished'): self.json_output[k] = response['json'].get(k) From 51f4aa2b48a50ab18cc896b1aa6b788363c61eb2 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Fri, 28 Aug 2020 11:04:15 -0400 Subject: [PATCH 145/188] Adding check that we are authenticated and also have a token --- awx_collection/plugins/module_utils/tower_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 36e3d045f0..c42e6733a5 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -551,7 +551,7 @@ class TowerAPIModule(TowerModule): return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, associations=associations) def logout(self): - if self.authenticated: + if self.authenticated and self.oauth_token_id: # Attempt to delete our current token from /api/v2/tokens/ # Post to the tokens endpoint with baisc auth to try and get a token api_token_url = ( From 21330a54cbe1b7014fcfe69105aca2d62954fb06 Mon Sep 17 00:00:00 2001 From: nixocio Date: Thu, 27 Aug 2020 16:40:09 -0400 Subject: [PATCH 146/188] Update instance groups * Simplify criteria to instance group to be considered unavailable * Round values for used capacity See: https://github.com/ansible/awx/issues/7467 --- .../InstanceGroupDetails/InstanceGroupDetails.jsx | 13 ++++--------- .../InstanceGroupList/InstanceGroupListItem.jsx | 11 ++--------- .../InstanceGroup/Instances/InstanceListItem.jsx | 2 +- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx index 0d6964559a..b7c10da67d 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupDetails/InstanceGroupDetails.jsx @@ -39,13 +39,6 @@ function InstanceGroupDetails({ instanceGroup, i18n }) { const { error, dismissError } = useDismissableError(deleteError); - const isAvailable = item => { - return ( - (item.policy_instance_minimum || item.policy_instance_percentage) && - item.capacity - ); - }; - const verifyIsIsolated = item => { if (item.is_isolated) { return ( @@ -89,10 +82,12 @@ function InstanceGroupDetails({ instanceGroup, i18n }) { dataCy="instance-group-policy-instance-percentage" content={`${instanceGroup.policy_instance_percentage} %`} /> - {isAvailable(instanceGroup) ? ( + {instanceGroup.capacity ? ( ) : ( diff --git a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx index f531f8b182..9ea19a5dd2 100644 --- a/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx +++ b/awx/ui_next/src/screens/InstanceGroup/InstanceGroupList/InstanceGroupListItem.jsx @@ -60,23 +60,16 @@ function InstanceGroupListItem({ }) { const labelId = `check-action-${instanceGroup.id}`; - const isAvailable = item => { - return ( - (item.policy_instance_minimum || item.policy_instance_percentage) && - item.capacity - ); - }; - const isContainerGroup = item => { return item.is_containerized; }; function usedCapacity(item) { if (!isContainerGroup(item)) { - if (isAvailable(item)) { + if (item.capacity) { return ( Date: Fri, 28 Aug 2020 17:33:19 +0200 Subject: [PATCH 147/188] [credential_plugin/hashivault] fix typo --- awx/main/credential_plugins/hashivault.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/credential_plugins/hashivault.py b/awx/main/credential_plugins/hashivault.py index 2406623231..28f213061b 100644 --- a/awx/main/credential_plugins/hashivault.py +++ b/awx/main/credential_plugins/hashivault.py @@ -152,7 +152,7 @@ def kv_backend(**kwargs): sess = requests.Session() sess.headers['Authorization'] = 'Bearer {}'.format(token) - # Compatability header for older installs of Hashicorp Vault + # Compatibility header for older installs of Hashicorp Vault sess.headers['X-Vault-Token'] = token if api_version == 'v2': From feb9bcff4d3adbc7a884a908ccc5ec6b2518b328 Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Fri, 28 Aug 2020 12:43:33 -0400 Subject: [PATCH 148/188] Adding transaction to mock requests --- awx_collection/test/awx/conftest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py index 5db00d6325..10774b9b34 100644 --- a/awx_collection/test/awx/conftest.py +++ b/awx_collection/test/awx/conftest.py @@ -17,6 +17,8 @@ import pytest from awx.main.tests.functional.conftest import _request from awx.main.models import Organization, Project, Inventory, JobTemplate, Credential, CredentialType +from django.db import transaction + try: import tower_cli # noqa HAS_TOWER_CLI = True @@ -107,8 +109,9 @@ def run_module(request, collection_import): kwargs_copy['data'][k] = v # make request - rf = _request(method.lower()) - django_response = rf(url, user=request_user, expect=None, **kwargs_copy) + with transaction.atomic(): + rf = _request(method.lower()) + django_response = rf(url, user=request_user, expect=None, **kwargs_copy) # requests library response object is different from the Django response, but they are the same concept # this converts the Django response object into a requests response object for consumption From 4ea648307ec3cc7efc5ef57ad4f615b44dc650c2 Mon Sep 17 00:00:00 2001 From: "Christian M. Adams" Date: Wed, 26 Aug 2020 18:20:53 -0400 Subject: [PATCH 149/188] Accept all responses <300 from Insights API --- awx/main/analytics/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/analytics/core.py b/awx/main/analytics/core.py index 3ff61b82f9..bab62b4a3c 100644 --- a/awx/main/analytics/core.py +++ b/awx/main/analytics/core.py @@ -180,7 +180,8 @@ def ship(path): auth=(rh_user, rh_password), headers=s.headers, timeout=(31, 31)) - if response.status_code != 202: + # Accept 2XX status_codes + if response.status_code >= 300: return logger.exception('Upload failed with status {}, {}'.format(response.status_code, response.text)) run_now = now() From e93aa34864abf49ea5b3d09b1a470057b31802d8 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 27 Jul 2020 16:11:45 -0400 Subject: [PATCH 150/188] Adds support for a Test button on the credential form when the credential type is 'external' --- awx/ui_next/src/api/models/CredentialTypes.js | 4 + awx/ui_next/src/api/models/Credentials.js | 4 + .../Credential/shared/CredentialForm.jsx | 81 ++++++-- .../Credential/shared/CredentialForm.test.jsx | 15 ++ .../CredentialPluginTestAlert.jsx | 91 +++++++++ .../CredentialPlugins/index.js | 1 + .../Credential/shared/ExternalTestModal.jsx | 192 ++++++++++++++++++ .../shared/ExternalTestModal.test.jsx | 180 ++++++++++++++++ .../src/screens/Credential/shared/index.js | 1 + awx/ui_next/src/util/useRequest.js | 3 + 10 files changed, 554 insertions(+), 18 deletions(-) create mode 100644 awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginTestAlert.jsx create mode 100644 awx/ui_next/src/screens/Credential/shared/ExternalTestModal.jsx create mode 100644 awx/ui_next/src/screens/Credential/shared/ExternalTestModal.test.jsx diff --git a/awx/ui_next/src/api/models/CredentialTypes.js b/awx/ui_next/src/api/models/CredentialTypes.js index dab1676231..39247b5ebc 100644 --- a/awx/ui_next/src/api/models/CredentialTypes.js +++ b/awx/ui_next/src/api/models/CredentialTypes.js @@ -27,6 +27,10 @@ class CredentialTypes extends Base { .concat(nextResults) .filter(type => acceptableKinds.includes(type.kind)); } + + test(id, data) { + return this.http.post(`${this.baseUrl}${id}/test/`, data); + } } export default CredentialTypes; diff --git a/awx/ui_next/src/api/models/Credentials.js b/awx/ui_next/src/api/models/Credentials.js index 95e954fc0c..13ee1f8a9c 100644 --- a/awx/ui_next/src/api/models/Credentials.js +++ b/awx/ui_next/src/api/models/Credentials.js @@ -25,6 +25,10 @@ class Credentials extends Base { params, }); } + + test(id, data) { + return this.http.post(`${this.baseUrl}${id}/test/`, data); + } } export default Credentials; diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx index bc486bb8b1..5a76a76271 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.jsx @@ -1,16 +1,19 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Formik, useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { arrayOf, func, object, shape } from 'prop-types'; -import { Form, FormGroup } from '@patternfly/react-core'; +import { ActionGroup, Button, Form, FormGroup } from '@patternfly/react-core'; import FormField, { FormSubmitError } from '../../../components/FormField'; -import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup'; +import { + FormColumnLayout, + FormFullWidthLayout, +} from '../../../components/FormLayout'; import AnsibleSelect from '../../../components/AnsibleSelect'; import { required } from '../../../util/validators'; import OrganizationLookup from '../../../components/Lookup/OrganizationLookup'; -import { FormColumnLayout } from '../../../components/FormLayout'; import TypeInputsSubForm from './TypeInputsSubForm'; +import ExternalTestModal from './ExternalTestModal'; function CredentialFormFields({ i18n, @@ -139,6 +142,7 @@ function CredentialFormFields({ } function CredentialForm({ + i18n, credential = {}, credentialTypes, inputSources, @@ -147,6 +151,7 @@ function CredentialForm({ submitError, ...rest }) { + const [showExternalTestModal, setShowExternalTestModal] = useState(false); const initialValues = { name: credential.name || '', description: credential.description || '', @@ -205,21 +210,61 @@ function CredentialForm({ }} > {formik => ( -
- - + + + + + + + + {formik?.values?.credential_type && + credentialTypes[formik.values.credential_type]?.kind === + 'external' && ( + + )} + + + + + + {showExternalTestModal && ( + setShowExternalTestModal(false)} /> - - -
- + )} + )} ); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx index d23e5347ee..f4360e75ba 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialForm.test.jsx @@ -99,6 +99,9 @@ describe('', () => { test('should display form fields on add properly', async () => { addFieldExpects(); }); + test('should hide Test button initially', () => { + expect(wrapper.find('Button[children="Test"]').length).toBe(0); + }); test('should update form values', async () => { // name and description change await act(async () => { @@ -221,6 +224,18 @@ describe('', () => { 'There was an error parsing the file. Please check the file formatting and try again.' ); }); + test('should show Test button when external credential type is selected', async () => { + await act(async () => { + await wrapper + .find('AnsibleSelect[id="credential_type"]') + .invoke('onChange')(null, 21); + }); + wrapper.update(); + expect(wrapper.find('Button[children="Test"]').length).toBe(1); + expect(wrapper.find('Button[children="Test"]').props().isDisabled).toBe( + true + ); + }); test('should call handleCancel when Cancel button is clicked', async () => { expect(onCancel).not.toHaveBeenCalled(); wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginTestAlert.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginTestAlert.jsx new file mode 100644 index 0000000000..f1c4a4ae97 --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/CredentialPluginTestAlert.jsx @@ -0,0 +1,91 @@ +import React, { useEffect, useState } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { string, shape } from 'prop-types'; +import { + Alert, + AlertActionCloseButton, + AlertGroup, +} from '@patternfly/react-core'; + +function CredentialPluginTestAlert({ + i18n, + credentialName, + successResponse, + errorResponse, +}) { + const [testMessage, setTestMessage] = useState(''); + const [testVariant, setTestVariant] = useState(false); + + useEffect(() => { + if (errorResponse) { + if (errorResponse?.response?.data?.inputs) { + if (errorResponse.response.data.inputs.startsWith('HTTP')) { + const [ + errorCode, + errorStr, + ] = errorResponse.response.data.inputs.split('\n'); + try { + const errorJSON = JSON.parse(errorStr); + setTestMessage( + `${errorCode}${ + errorJSON?.errors[0] ? `: ${errorJSON.errors[0]}` : '' + }` + ); + } catch { + setTestMessage(errorResponse.response.data.inputs); + } + } else { + setTestMessage(errorResponse.response.data.inputs); + } + } else { + setTestMessage( + i18n._( + t`Something went wrong with the request to test this credential and metadata.` + ) + ); + } + setTestVariant('danger'); + } else if (successResponse) { + setTestMessage(i18n._(t`Test passed`)); + setTestVariant('success'); + } + }, [i18n, successResponse, errorResponse]); + + return ( + + {testMessage && testVariant && ( + { + setTestMessage(null); + setTestVariant(null); + }} + /> + } + title={ + <> + {credentialName} +

{testMessage}

+ + } + variant={testVariant} + /> + )} +
+ ); +} + +CredentialPluginTestAlert.propTypes = { + credentialName: string.isRequired, + successResponse: shape({}), + errorResponse: shape({}), +}; + +CredentialPluginTestAlert.defaultProps = { + successResponse: null, + errorResponse: null, +}; + +export default withI18n()(CredentialPluginTestAlert); diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/index.js b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/index.js index 033586567f..3799206eb4 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/index.js +++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialPlugins/index.js @@ -1,2 +1,3 @@ export { default as CredentialPluginSelected } from './CredentialPluginSelected'; export { default as CredentialPluginField } from './CredentialPluginField'; +export { default as CredentialPluginTestAlert } from './CredentialPluginTestAlert'; diff --git a/awx/ui_next/src/screens/Credential/shared/ExternalTestModal.jsx b/awx/ui_next/src/screens/Credential/shared/ExternalTestModal.jsx new file mode 100644 index 0000000000..9adc6f7124 --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/ExternalTestModal.jsx @@ -0,0 +1,192 @@ +import React, { useCallback } from 'react'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import styled from 'styled-components'; +import { func, shape } from 'prop-types'; +import { Formik } from 'formik'; +import { + Button, + Form, + FormGroup, + Modal, + Tooltip, +} from '@patternfly/react-core'; +import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; +import { CredentialsAPI, CredentialTypesAPI } from '../../../api'; +import AnsibleSelect from '../../../components/AnsibleSelect'; +import FormField from '../../../components/FormField'; +import { FormFullWidthLayout } from '../../../components/FormLayout'; +import { required } from '../../../util/validators'; +import useRequest from '../../../util/useRequest'; +import { CredentialPluginTestAlert } from './CredentialFormFields/CredentialPlugins'; + +const QuestionCircleIcon = styled(PFQuestionCircleIcon)` + margin-left: 10px; +`; + +function ExternalTestModal({ + i18n, + credential, + credentialType, + credentialFormValues, + onClose, +}) { + const { + result: testPluginSuccess, + error: testPluginError, + request: testPluginMetadata, + } = useRequest( + useCallback( + async values => { + const payload = { + inputs: credentialType.inputs.fields.reduce( + (filteredInputs, field) => { + filteredInputs[field.id] = credentialFormValues.inputs[field.id]; + return filteredInputs; + }, + {} + ), + metadata: values, + }; + + if (credential && credential.credential_type === credentialType.id) { + return CredentialsAPI.test(credential.id, payload); + } + return CredentialTypesAPI.test(credentialType.id, payload); + }, + [ + credential, + credentialType.id, + credentialType.inputs.fields, + credentialFormValues.inputs, + ] + ), + null + ); + + const handleTest = async values => { + await testPluginMetadata(values); + }; + + return ( + <> + { + if (field.type === 'string' && field.choices) { + initialValues[field.id] = field.default || field.choices[0]; + } else { + initialValues[field.id] = ''; + } + return initialValues; + }, + {} + )} + onSubmit={values => handleTest(values)} + > + {({ handleSubmit, setFieldValue }) => ( + onClose()} + variant="small" + actions={[ + , + , + ]} + > +
+ + {credentialType.inputs.metadata.map(field => { + const isRequired = credentialType.inputs?.required.includes( + field.id + ); + if (field.type === 'string') { + if (field.choices) { + return ( + + + + ) + } + isRequired={isRequired} + > + { + return { + value: choice, + key: choice, + label: choice, + }; + })} + onChange={(event, value) => { + setFieldValue(field.id, value); + }} + validate={isRequired ? required(null, i18n) : null} + /> + + ); + } + + return ( + + ); + } + + return null; + })} + +
+
+ )} +
+ + + ); +} + +ExternalTestModal.proptype = { + credential: shape({}), + credentialType: shape({}).isRequired, + credentialFormValues: shape({}).isRequired, + onClose: func.isRequired, +}; + +ExternalTestModal.defaultProps = { + credential: null, +}; + +export default withI18n()(ExternalTestModal); diff --git a/awx/ui_next/src/screens/Credential/shared/ExternalTestModal.test.jsx b/awx/ui_next/src/screens/Credential/shared/ExternalTestModal.test.jsx new file mode 100644 index 0000000000..91677795aa --- /dev/null +++ b/awx/ui_next/src/screens/Credential/shared/ExternalTestModal.test.jsx @@ -0,0 +1,180 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { CredentialsAPI, CredentialTypesAPI } from '../../../api'; +import ExternalTestModal from './ExternalTestModal'; +import credentialTypesArr from './data.credentialTypes.json'; + +jest.mock('../../../api/models/Credentials'); +jest.mock('../../../api/models/CredentialTypes'); + +const credentialType = credentialTypesArr.find( + credType => credType.namespace === 'hashivault_kv' +); + +const credentialFormValues = { + name: 'Foobar', + credential_type: credentialType.id, + inputs: { + api_version: 'v2', + token: '$encrypted$', + url: 'http://hashivault:8200', + }, +}; + +const credential = { + id: 1, + name: 'A credential', + credential_type: credentialType.id, +}; + +describe('', () => { + let wrapper; + afterEach(() => wrapper.unmount()); + test('should display metadata fields correctly', async () => { + wrapper = mountWithContexts( + + ); + expect(wrapper.find('FormField').length).toBe(5); + expect(wrapper.find('input#credential-secret_backend').length).toBe(1); + expect(wrapper.find('input#credential-secret_path').length).toBe(1); + expect(wrapper.find('input#credential-auth_path').length).toBe(1); + expect(wrapper.find('input#credential-secret_key').length).toBe(1); + expect(wrapper.find('input#credential-secret_version').length).toBe(1); + }); + test('should make the test request correctly when testing an existing credential', async () => { + wrapper = mountWithContexts( + + ); + await act(async () => { + wrapper.find('input#credential-secret_path').simulate('change', { + target: { value: '/secret/foo/bar/baz', name: 'secret_path' }, + }); + wrapper.find('input#credential-secret_key').simulate('change', { + target: { value: 'password', name: 'secret_key' }, + }); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Button[children="Run"]').simulate('click'); + }); + expect(CredentialsAPI.test).toHaveBeenCalledWith(1, { + inputs: { + api_version: 'v2', + cacert: undefined, + role_id: undefined, + secret_id: undefined, + token: '$encrypted$', + url: 'http://hashivault:8200', + }, + metadata: { + auth_path: '', + secret_backend: '', + secret_key: 'password', + secret_path: '/secret/foo/bar/baz', + secret_version: '', + }, + }); + }); + test('should make the test request correctly when testing a new credential', async () => { + wrapper = mountWithContexts( + + ); + await act(async () => { + wrapper.find('input#credential-secret_path').simulate('change', { + target: { value: '/secret/foo/bar/baz', name: 'secret_path' }, + }); + wrapper.find('input#credential-secret_key').simulate('change', { + target: { value: 'password', name: 'secret_key' }, + }); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Button[children="Run"]').simulate('click'); + }); + expect(CredentialTypesAPI.test).toHaveBeenCalledWith(21, { + inputs: { + api_version: 'v2', + cacert: undefined, + role_id: undefined, + secret_id: undefined, + token: '$encrypted$', + url: 'http://hashivault:8200', + }, + metadata: { + auth_path: '', + secret_backend: '', + secret_key: 'password', + secret_path: '/secret/foo/bar/baz', + secret_version: '', + }, + }); + }); + test('should display the alert after a successful test', async () => { + CredentialTypesAPI.test.mockResolvedValue({}); + wrapper = mountWithContexts( + + ); + await act(async () => { + wrapper.find('input#credential-secret_path').simulate('change', { + target: { value: '/secret/foo/bar/baz', name: 'secret_path' }, + }); + wrapper.find('input#credential-secret_key').simulate('change', { + target: { value: 'password', name: 'secret_key' }, + }); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Button[children="Run"]').simulate('click'); + }); + wrapper.update(); + expect(wrapper.find('Alert').length).toBe(1); + expect(wrapper.find('Alert').props().variant).toBe('success'); + }); + test('should display the alert after a failed test', async () => { + CredentialTypesAPI.test.mockRejectedValue({ + inputs: `HTTP 404 + {"errors":["no handler for route '/secret/foo/bar/baz'"]} + `, + }); + wrapper = mountWithContexts( + + ); + await act(async () => { + wrapper.find('input#credential-secret_path').simulate('change', { + target: { value: '/secret/foo/bar/baz', name: 'secret_path' }, + }); + wrapper.find('input#credential-secret_key').simulate('change', { + target: { value: 'password', name: 'secret_key' }, + }); + }); + wrapper.update(); + await act(async () => { + wrapper.find('Button[children="Run"]').simulate('click'); + }); + wrapper.update(); + expect(wrapper.find('Alert').length).toBe(1); + expect(wrapper.find('Alert').props().variant).toBe('danger'); + }); +}); diff --git a/awx/ui_next/src/screens/Credential/shared/index.js b/awx/ui_next/src/screens/Credential/shared/index.js index ad01a03c29..28dda5128a 100644 --- a/awx/ui_next/src/screens/Credential/shared/index.js +++ b/awx/ui_next/src/screens/Credential/shared/index.js @@ -1,2 +1,3 @@ export { default as mockCredentials } from './data.credentials.json'; export { default as mockCredentialType } from './data.credential_type.json'; +export { default as ExternalTestModal } from './ExternalTestModal'; diff --git a/awx/ui_next/src/util/useRequest.js b/awx/ui_next/src/util/useRequest.js index 0e95be4a69..027e82f86f 100644 --- a/awx/ui_next/src/util/useRequest.js +++ b/awx/ui_next/src/util/useRequest.js @@ -38,6 +38,9 @@ export default function useRequest(makeRequest, initialValue) { request: useCallback( async (...args) => { setIsLoading(true); + if (isMounted.current) { + setError(null); + } try { const response = await makeRequest(...args); if (isMounted.current) { From ae4f1a15d33024ac7de00ecc46e4a6508061c292 Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 26 Aug 2020 13:41:12 -0400 Subject: [PATCH 151/188] Add ID's to the buttons in the external test modal for cred form --- .../src/screens/Credential/shared/ExternalTestModal.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Credential/shared/ExternalTestModal.jsx b/awx/ui_next/src/screens/Credential/shared/ExternalTestModal.jsx index 9adc6f7124..fda8bc4492 100644 --- a/awx/ui_next/src/screens/Credential/shared/ExternalTestModal.jsx +++ b/awx/ui_next/src/screens/Credential/shared/ExternalTestModal.jsx @@ -92,13 +92,19 @@ function ExternalTestModal({ variant="small" actions={[ , - , ]} From 9d511a4c047978ceb9b26c6631885d67a79b552b Mon Sep 17 00:00:00 2001 From: mabashian Date: Fri, 28 Aug 2020 17:02:25 -0400 Subject: [PATCH 152/188] Fix id on credential select fields --- .../Credential/shared/CredentialFormFields/CredentialField.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx index aafa1c74fe..51c9dfa02d 100644 --- a/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx +++ b/awx/ui_next/src/screens/Credential/shared/CredentialFormFields/CredentialField.jsx @@ -110,7 +110,7 @@ function CredentialField({ credentialType, fieldOptions, i18n }) { > { helpers.setValue(value); From 970ecde0ea71cdb568096471d1839509528e4f8a Mon Sep 17 00:00:00 2001 From: nixocio Date: Sun, 30 Aug 2020 20:58:58 -0400 Subject: [PATCH 153/188] Update date format for project list item Update date format for project list item. See: https://github.com/ansible/awx/issues/7694 --- .../src/screens/Project/ProjectList/ProjectListItem.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index d5651a130e..e23f3339c5 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -16,7 +16,7 @@ import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; import { PencilAltIcon, SyncIcon } from '@patternfly/react-icons'; import styled from 'styled-components'; -import { timeOfDay } from '../../../util/dates'; +import { formatDateString, timeOfDay } from '../../../util/dates'; import { ProjectsAPI } from '../../../api'; import ClipboardCopyButton from '../../../components/ClipboardCopyButton'; import StatusIcon from '../../../components/StatusIcon'; @@ -72,7 +72,7 @@ function ProjectListItem({
{job.finished && (
- {i18n._(t`FINISHED:`)} {job.finished} + {i18n._(t`FINISHED:`)} {formatDateString(job.finished)}
)} From b84343d292a560b11c8a44331e8ce2b2ec0c2792 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Mon, 31 Aug 2020 10:18:08 -0400 Subject: [PATCH 154/188] correct name of tower_notification redirect --- awx_collection/meta/runtime.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/meta/runtime.yml b/awx_collection/meta/runtime.yml index d8c2535871..5fcec0d423 100644 --- a/awx_collection/meta/runtime.yml +++ b/awx_collection/meta/runtime.yml @@ -13,5 +13,5 @@ plugin_routing: deprecation: removal_date: TBD warning_text: see plugin documentation for details - tower_notifitcation: + tower_notification: redirect: tower_notification_template From ff4ed64978bbecc77509c030af4d2459dd28acef Mon Sep 17 00:00:00 2001 From: John Westcott IV Date: Mon, 31 Aug 2020 11:57:01 -0400 Subject: [PATCH 155/188] Cleaning up tower_notification references --- awx_collection/README.md | 4 ++-- .../targets/tower_job_template/tasks/main.yml | 10 +++++----- .../targets/tower_notification_template/tasks/main.yml | 2 +- .../targets/tower_workflow_job_template/tasks/main.yml | 10 +++++----- awx_collection/tests/sanity/ignore-2.10.txt | 2 +- .../tools/roles/template_galaxy/templates/README.md.j2 | 5 +++-- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/awx_collection/README.md b/awx_collection/README.md index 47b517c6e6..61480a1b3d 100644 --- a/awx_collection/README.md +++ b/awx_collection/README.md @@ -91,9 +91,9 @@ The following notes are changes that may require changes to playbooks: - Specified `tower_config` file used to handle `k=v` pairs on a single line; this is no longer supported. Please use a file formatted as `yaml`, `json` or `ini` only. - Some return values (e.g., `credential_type`) have been removed. Use of `id` is recommended. - `tower_job_template` no longer supports the deprecated `extra_vars_path` parameter, please use `extra_vars` with the lookup plugin to replace this functionality. - - The `notification_configuration` parameter of `tower_notification` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict. + - The `notification_configuration` parameter of `tower_notification_template` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict. - `tower_credential` no longer supports passing a file name to ssh_key_data. - - The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification` module. + - The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification_template` module. ## Running Unit Tests diff --git a/awx_collection/tests/integration/targets/tower_job_template/tasks/main.yml b/awx_collection/tests/integration/targets/tower_job_template/tasks/main.yml index 72711451be..a744b89464 100644 --- a/awx_collection/tests/integration/targets/tower_job_template/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_job_template/tasks/main.yml @@ -13,7 +13,7 @@ jt2: "AWX-Collection-tests-tower_job_template-jt2-{{ test_id }}" lab1: "AWX-Collection-tests-tower_job_template-lab1-{{ test_id }}" email_not: "AWX-Collection-tests-tower_job_template-email-not-{{ test_id }}" - webhook_not: "AWX-Collection-tests-tower_notification-wehbook-not-{{ test_id }}" + webhook_not: "AWX-Collection-tests-tower_notification_template-wehbook-not-{{ test_id }}" - name: Create a Demo Project tower_project: @@ -49,7 +49,7 @@ organization: Default - name: Add email notification - tower_notification: + tower_notification_template: name: "{{ email_not }}" organization: Default notification_type: email @@ -65,7 +65,7 @@ state: present - name: Add webhook notification - tower_notification: + tower_notification_template: name: "{{ webhook_not }}" organization: Default notification_type: webhook @@ -366,13 +366,13 @@ # You can't delete a label directly so no cleanup needed - name: Delete email notification - tower_notification: + tower_notification_template: name: "{{ email_not }}" organization: Default state: absent - name: Delete webhook notification - tower_notification: + tower_notification_template: name: "{{ webhook_not }}" organization: Default state: absent diff --git a/awx_collection/tests/integration/targets/tower_notification_template/tasks/main.yml b/awx_collection/tests/integration/targets/tower_notification_template/tasks/main.yml index a4d41571cf..1ab3af794c 100644 --- a/awx_collection/tests/integration/targets/tower_notification_template/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_notification_template/tasks/main.yml @@ -9,7 +9,7 @@ irc_not: "AWX-Collection-tests-tower_notification_template-irc-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - name: Test deprecation warnings with legacy name - tower_notification: + tower_notification_template: name: "{{ slack_not }}" organization: Default notification_type: slack diff --git a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml index 8a7a977164..a99281f2ba 100644 --- a/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_workflow_job_template/tasks/main.yml @@ -11,7 +11,7 @@ jt2_name: "AWX-Collection-tests-tower_workflow_job_template-jt2-{{ test_id }}" wfjt_name: "AWX-Collection-tests-tower_workflow_job_template-wfjt-{{ test_id }}" email_not: "AWX-Collection-tests-tower_job_template-email-not-{{ test_id }}" - webhook_not: "AWX-Collection-tests-tower_notification-wehbook-not-{{ test_id }}" + webhook_not: "AWX-Collection-tests-tower_notification_template-wehbook-not-{{ test_id }}" - name: Create an SCM Credential tower_credential: @@ -25,7 +25,7 @@ - "result is changed" - name: Add email notification - tower_notification: + tower_notification_template: name: "{{ email_not }}" organization: Default notification_type: email @@ -41,7 +41,7 @@ state: present - name: Add webhook notification - tower_notification: + tower_notification_template: name: "{{ webhook_not }}" organization: Default notification_type: webhook @@ -264,13 +264,13 @@ - "result is changed" - name: Delete email notification - tower_notification: + tower_notification_template: name: "{{ email_not }}" organization: Default state: absent - name: Delete webhook notification - tower_notification: + tower_notification_template: name: "{{ webhook_not }}" organization: Default state: absent diff --git a/awx_collection/tests/sanity/ignore-2.10.txt b/awx_collection/tests/sanity/ignore-2.10.txt index 93f7dd6d7b..8b5f90b44d 100644 --- a/awx_collection/tests/sanity/ignore-2.10.txt +++ b/awx_collection/tests/sanity/ignore-2.10.txt @@ -3,7 +3,7 @@ plugins/modules/tower_send.py validate-modules:deprecation-mismatch plugins/modules/tower_workflow_template.py validate-modules:deprecation-mismatch plugins/modules/tower_credential.py pylint:wrong-collection-deprecated-version-tag plugins/modules/tower_job_wait.py pylint:wrong-collection-deprecated-version-tag -plugins/modules/tower_notification.py pylint:wrong-collection-deprecated-version-tag +plugins/modules/tower_notification_template.py pylint:wrong-collection-deprecated-version-tag plugins/inventory/tower.py pylint:raise-missing-from plugins/inventory/tower.py pylint:super-with-arguments plugins/lookup/tower_schedule_rrule.py pylint:raise-missing-from diff --git a/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 b/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 index 53cc8bd076..8a5743d34f 100644 --- a/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 +++ b/awx_collection/tools/roles/template_galaxy/templates/README.md.j2 @@ -80,6 +80,7 @@ Notable releases of the `{{ collection_namespace }}.{{ collection_package }}` co The following notes are changes that may require changes to playbooks: + - The module tower_notification was renamed tower_notification_template. In ansible >= 2.10 there is a seemless redirect. Ansible 2.9 does not respect the redirect. - When a project is created, it will wait for the update/sync to finish by default; this can be turned off with the `wait` parameter, if desired. - Creating a "scan" type job template is no longer supported. - Specifying a custom certificate via the `TOWER_CERTIFICATE` environment variable no longer works. @@ -100,9 +101,9 @@ The following notes are changes that may require changes to playbooks: - Specified `tower_config` file used to handle `k=v` pairs on a single line; this is no longer supported. Please use a file formatted as `yaml`, `json` or `ini` only. - Some return values (e.g., `credential_type`) have been removed. Use of `id` is recommended. - `tower_job_template` no longer supports the deprecated `extra_vars_path` parameter, please use `extra_vars` with the lookup plugin to replace this functionality. - - The `notification_configuration` parameter of `tower_notification` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict. + - The `notification_configuration` parameter of `tower_notification_template` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict. - `tower_credential` no longer supports passing a file name to ssh_key_data. - - The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification` module. + - The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification_template` module. {% if collection_package | lower() == "awx" %} ## Running Unit Tests From 0bc927820bc57e6862ce7befed995d4e7df69e80 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 31 Aug 2020 15:24:32 -0500 Subject: [PATCH 156/188] updated legacy messages --- awx_collection/plugins/module_utils/tower_api.py | 12 ++++++++++-- awx_collection/plugins/modules/tower_job_launch.py | 2 +- awx_collection/plugins/modules/tower_job_wait.py | 2 +- .../plugins/modules/tower_workflow_launch.py | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 8b619dd36e..3e3f668401 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -597,7 +597,11 @@ class TowerAPIModule(TowerModule): while not result['json']['finished']: # If we are past our time out fail with a message if timeout and timeout < time.time() - start: - self.json_output['msg'] = 'Monitoring of {0} "{1}" aborted due to timeout'.format(object_type, object_name) + # Account for Legacy messages + if object_type is 'legacy_job_wait': + self.json_output['msg'] = 'Monitoring of Job - {1} aborted due to timeout'.format(object_name) + else: + self.json_output['msg'] = 'Monitoring of {0} - {1} aborted due to timeout'.format(object_type, object_name) self.wait_output(result) self.fail_json(**self.json_output) @@ -609,7 +613,11 @@ class TowerAPIModule(TowerModule): # If the job has failed, we want to raise a task failure for that so we get a non-zero response. if result['json']['failed']: - self.json_output['msg'] = 'The {0} "{1}" failed'.format(object_type, object_name) + # Account for Legacy messages + if object_type is 'legacy_job_wait': + self.json_output['msg'] = 'Job with id {0} failed'.format(object_name) + else: + self.json_output['msg'] = 'The {0} - {1}, failed'.format(object_type, object_name) self.wait_output(result) self.fail_json(**self.json_output) diff --git a/awx_collection/plugins/modules/tower_job_launch.py b/awx_collection/plugins/modules/tower_job_launch.py index 8345b55b66..11ebb14110 100644 --- a/awx_collection/plugins/modules/tower_job_launch.py +++ b/awx_collection/plugins/modules/tower_job_launch.py @@ -249,7 +249,7 @@ def main(): results = module.wait_on_url( url=results['json']['url'], object_name=name, - object_type='job', + object_type='Job', timeout=timeout, interval=interval ) diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py index 1c65e141dc..46bb80d007 100644 --- a/awx_collection/plugins/modules/tower_job_wait.py +++ b/awx_collection/plugins/modules/tower_job_wait.py @@ -143,7 +143,7 @@ def main(): result = module.wait_on_url( url=job['url'], object_name=job_id, - object_type='job', + object_type='legacy_job_wait', timeout=timeout, interval=interval ) diff --git a/awx_collection/plugins/modules/tower_workflow_launch.py b/awx_collection/plugins/modules/tower_workflow_launch.py index b665adbf15..8267e9352c 100644 --- a/awx_collection/plugins/modules/tower_workflow_launch.py +++ b/awx_collection/plugins/modules/tower_workflow_launch.py @@ -181,7 +181,7 @@ def main(): module.wait_on_url( url=result['json']['url'], object_name=name, - object_type='workflow_job', + object_type='Workflow Job', timeout=timeout, interval=interval ) From cd45cfec309c2c0ac7a4abadc92b17c87b01e3e5 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Mon, 31 Aug 2020 15:53:54 -0500 Subject: [PATCH 157/188] updated doc and pep8 --- awx_collection/plugins/modules/tower_job_launch.py | 5 +++-- awx_collection/plugins/modules/tower_job_wait.py | 1 + awx_collection/plugins/modules/tower_workflow_launch.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/awx_collection/plugins/modules/tower_job_launch.py b/awx_collection/plugins/modules/tower_job_launch.py index 11ebb14110..7e2dca0e38 100644 --- a/awx_collection/plugins/modules/tower_job_launch.py +++ b/awx_collection/plugins/modules/tower_job_launch.py @@ -84,7 +84,7 @@ options: wait: description: - Wait for the job to complete. - default: True + default: False type: bool interval: description: @@ -183,7 +183,7 @@ def main(): optional_args['credential_passwords'] = module.params.get('credential_passwords') wait = module.params.get('wait') interval = module.params.get('interval') - timeout = module.params.get('timeout') + timeout = module.params.get('timeout') # Create a datastructure to pass into our job launch post_data = {} @@ -259,5 +259,6 @@ def main(): 'status': results['json']['status'], }) + if __name__ == '__main__': main() diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py index 46bb80d007..6f71a1be5b 100644 --- a/awx_collection/plugins/modules/tower_job_wait.py +++ b/awx_collection/plugins/modules/tower_job_wait.py @@ -149,5 +149,6 @@ def main(): module.exit_json(**module.json_output) + if __name__ == '__main__': main() diff --git a/awx_collection/plugins/modules/tower_workflow_launch.py b/awx_collection/plugins/modules/tower_workflow_launch.py index 8267e9352c..f8ab793bad 100644 --- a/awx_collection/plugins/modules/tower_workflow_launch.py +++ b/awx_collection/plugins/modules/tower_workflow_launch.py @@ -176,7 +176,7 @@ def main(): if not wait: module.exit_json(**module.json_output) - + # Invoke wait function module.wait_on_url( url=result['json']['url'], From 0b371b4340ac33b3e75d9fdaba0cd6b927257a85 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Fri, 28 Aug 2020 22:16:34 -0400 Subject: [PATCH 158/188] Copy library folder to job private data dir Remove inventory scripts show because they no longer exist Remove reference to non-existent callback directory Remove more references to removed path --- awx/main/tasks.py | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index ec685539b9..acd7548a91 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1630,21 +1630,10 @@ class RunJob(BaseTask): return passwords - def add_ansible_venv(self, venv_path, env, isolated=False): - super(RunJob, self).add_ansible_venv(venv_path, env, isolated=isolated) - # Add awx/lib to PYTHONPATH. - env['PYTHONPATH'] = env.get('PYTHONPATH', '') + self.get_path_to('..', 'lib') + ':' - def build_env(self, job, private_data_dir, isolated=False, private_data_files=None): ''' Build environment dictionary for ansible-playbook. ''' - plugin_dir = self.get_path_to('..', 'plugins', 'callback') - plugin_dirs = [plugin_dir] - if hasattr(settings, 'AWX_ANSIBLE_CALLBACK_PLUGINS') and \ - settings.AWX_ANSIBLE_CALLBACK_PLUGINS: - plugin_dirs.extend(settings.AWX_ANSIBLE_CALLBACK_PLUGINS) - plugin_path = ':'.join(plugin_dirs) env = super(RunJob, self).build_env(job, private_data_dir, isolated=isolated, private_data_files=private_data_files) @@ -1656,19 +1645,17 @@ class RunJob(BaseTask): env['JOB_ID'] = str(job.pk) env['INVENTORY_ID'] = str(job.inventory.pk) if job.use_fact_cache: - library_path = env.get('ANSIBLE_LIBRARY') - env['ANSIBLE_LIBRARY'] = ':'.join( - filter(None, [ - library_path, - self.get_path_to('..', 'plugins', 'library') - ]) - ) + library_source = self.get_path_to('..', 'plugins', 'library') + library_dest = os.path.join(private_data_dir, 'library') + copy_tree(library_source, library_dest) + env['ANSIBLE_LIBRARY'] = library_dest if job.project: env['PROJECT_REVISION'] = job.project.scm_revision env['ANSIBLE_RETRY_FILES_ENABLED'] = "False" env['MAX_EVENT_RES'] = str(settings.MAX_EVENT_RES_DATA) if not isolated: - env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_path + if hasattr(settings, 'AWX_ANSIBLE_CALLBACK_PLUGINS') and settings.AWX_ANSIBLE_CALLBACK_PLUGINS: + env['ANSIBLE_CALLBACK_PLUGINS'] = ':'.join(settings.AWX_ANSIBLE_CALLBACK_PLUGINS) env['AWX_HOST'] = settings.TOWER_URL_BASE # Create a directory for ControlPath sockets that is unique to each @@ -2043,7 +2030,6 @@ class RunProjectUpdate(BaseTask): # like https://github.com/ansible/ansible/issues/30064 env['TMP'] = settings.AWX_PROOT_BASE_PATH env['PROJECT_UPDATE_ID'] = str(project_update.pk) - env['ANSIBLE_CALLBACK_PLUGINS'] = self.get_path_to('..', 'plugins', 'callback') if settings.GALAXY_IGNORE_CERTS: env['ANSIBLE_GALAXY_IGNORE'] = True # Set up the public Galaxy server, if enabled @@ -2455,7 +2441,7 @@ class RunInventoryUpdate(BaseTask): @property def proot_show_paths(self): - return [self.get_path_to('..', 'plugins', 'inventory'), settings.AWX_ANSIBLE_COLLECTIONS_PATHS] + return [settings.AWX_ANSIBLE_COLLECTIONS_PATHS] def build_private_data(self, inventory_update, private_data_dir): """ @@ -2762,7 +2748,6 @@ class RunAdHocCommand(BaseTask): ''' Build environment dictionary for ansible. ''' - plugin_dir = self.get_path_to('..', 'plugins', 'callback') env = super(RunAdHocCommand, self).build_env(ad_hoc_command, private_data_dir, isolated=isolated, private_data_files=private_data_files) @@ -2772,7 +2757,6 @@ class RunAdHocCommand(BaseTask): env['AD_HOC_COMMAND_ID'] = str(ad_hoc_command.pk) env['INVENTORY_ID'] = str(ad_hoc_command.inventory.pk) env['INVENTORY_HOSTVARS'] = str(True) - env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir env['ANSIBLE_LOAD_CALLBACK_PLUGINS'] = '1' env['ANSIBLE_SFTP_BATCH_MODE'] = 'False' From 50637807fcb78c30497376339fc3f6445a0f85cc Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 1 Sep 2020 10:38:15 -0500 Subject: [PATCH 159/188] fixed typo --- awx_collection/plugins/module_utils/tower_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/plugins/module_utils/tower_api.py b/awx_collection/plugins/module_utils/tower_api.py index 78df3e7337..1bc76a84b1 100644 --- a/awx_collection/plugins/module_utils/tower_api.py +++ b/awx_collection/plugins/module_utils/tower_api.py @@ -599,7 +599,7 @@ class TowerAPIModule(TowerModule): if timeout and timeout < time.time() - start: # Account for Legacy messages if object_type is 'legacy_job_wait': - self.json_output['msg'] = 'Monitoring of Job - {1} aborted due to timeout'.format(object_name) + self.json_output['msg'] = 'Monitoring of Job - {0} aborted due to timeout'.format(object_name) else: self.json_output['msg'] = 'Monitoring of {0} - {1} aborted due to timeout'.format(object_type, object_name) self.wait_output(result) From 9f3635be07a81dd4730a113d01e8a43e374f01a2 Mon Sep 17 00:00:00 2001 From: sean-m-sullivan Date: Tue, 1 Sep 2020 10:45:41 -0500 Subject: [PATCH 160/188] update test to timeout message change --- .../integration/targets/tower_workflow_launch/tasks/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx_collection/tests/integration/targets/tower_workflow_launch/tasks/main.yml b/awx_collection/tests/integration/targets/tower_workflow_launch/tasks/main.yml index bf88aecf7d..680b629473 100644 --- a/awx_collection/tests/integration/targets/tower_workflow_launch/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_workflow_launch/tasks/main.yml @@ -53,7 +53,7 @@ - assert: that: - result is failed - - "'Monitoring aborted due to timeout' in result.msg" + - "'Monitoring of Workflow Job - {{ wfjt_name1 }} aborted due to timeout' in result.msg" - name: Kick off a workflow and wait for it tower_workflow_launch: From e11040f4216171d548f7b15a4e932bfcfa8a1b98 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 15 Jul 2020 12:39:23 -0400 Subject: [PATCH 161/188] migrate to new style inv plugin --- .../migrations/0118_v380_inventory_plugins.py | 59 ++ awx/main/models/inventory.py | 708 +----------------- 2 files changed, 83 insertions(+), 684 deletions(-) create mode 100644 awx/main/migrations/0118_v380_inventory_plugins.py diff --git a/awx/main/migrations/0118_v380_inventory_plugins.py b/awx/main/migrations/0118_v380_inventory_plugins.py new file mode 100644 index 0000000000..e2c8cd2d82 --- /dev/null +++ b/awx/main/migrations/0118_v380_inventory_plugins.py @@ -0,0 +1,59 @@ +# Generated by Django 2.2.11 on 2020-07-20 19:56 + +import logging +import json + +from django.db import migrations, models + +from awx.main.models.inventory import InventorySource +from ._inventory_source_vars import FrozenInjectors + + +logger = logging.getLogger('awx.main.migrations') +BACKUP_FILENAME = '/tmp/tower_migration_inventory_source_vars.json' + + +def _get_inventory_sources(): + # TODO: Maybe pull this list from an import + return InventorySource.objects.filter(source__in=['ec2', 'gce', 'azure_rm', 'vmware', 'satellite6', 'openstack', 'rhv', 'tower']) + + +def inventory_source_vars_forward(apps, schema_editor): + source_vars_backup = dict() + + for inv_source_obj in _get_inventory_sources(): + # TODO: Log error if this is false, it shouldn't be false + if inv_source_obj.source in FrozenInjectors: + source_vars_backup[inv_source_obj.id] = dict(inv_source_obj.source_vars_dict) + with open(BACKUP_FILENAME, 'w') as fh: + json.dump(source_vars_backup, fh) + + injector = FrozenInjectors[inv_source_obj.source]() + new_inv_source_vars = injector.inventory_as_dict(inv_source_obj, None) + inv_source_obj.source_vars = new_inv_source_vars + inv_source_obj.save() + + +def inventory_source_vars_backward(apps, schema_editor): + try: + with open(BACKUP_FILENAME, 'r') as fh: + source_vars_backup = json.load(fh) + except FileNotFoundError: + print(f"Rollback file not found {BACKUP_FILENAME}") + return + + for inv_source_obj in _get_inventory_sources(): + if inv_source_obj.id in source_vars_backup: + inv_source_obj.source_vars = source_vars_backup[inv_source_obj.id] + inv_source_obj.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0117_v400_remove_cloudforms_inventory'), + ] + + operations = [ + migrations.RunPython(inventory_source_vars_forward, inventory_source_vars_backward,), + ] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 137c056111..b56f3bd03e 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -983,6 +983,24 @@ class InventorySourceOptions(BaseModel): default=1, ) + @classmethod + def get_ec2_group_by_choices(cls): + return [ + ('ami_id', _('Image ID')), + ('availability_zone', _('Availability Zone')), + ('aws_account', _('Account')), + ('instance_id', _('Instance ID')), + ('instance_state', _('Instance State')), + ('platform', _('Platform')), + ('instance_type', _('Instance Type')), + ('key_pair', _('Key Name')), + ('region', _('Region')), + ('security_group', _('Security Group')), + ('tag_keys', _('Tags')), + ('tag_none', _('Tag None')), + ('vpc_id', _('VPC ID')), + ] + @classmethod def get_ec2_region_choices(cls): ec2_region_names = getattr(settings, 'EC2_REGION_NAMES', {}) @@ -1005,24 +1023,6 @@ class InventorySourceOptions(BaseModel): regions.append((region.name, label)) return sorted(regions, key=region_sorting) - @classmethod - def get_ec2_group_by_choices(cls): - return [ - ('ami_id', _('Image ID')), - ('availability_zone', _('Availability Zone')), - ('aws_account', _('Account')), - ('instance_id', _('Instance ID')), - ('instance_state', _('Instance State')), - ('platform', _('Platform')), - ('instance_type', _('Instance Type')), - ('key_pair', _('Key Name')), - ('region', _('Region')), - ('security_group', _('Security Group')), - ('tag_keys', _('Tags')), - ('tag_none', _('Tag None')), - ('vpc_id', _('VPC ID')), - ] - @classmethod def get_gce_region_choices(self): """Return a complete list of regions in GCE, as a list of @@ -1613,26 +1613,18 @@ class PluginFileInjector(object): """ return '{0}.yml'.format(self.plugin_name) - def inventory_as_dict(self, inventory_update, private_data_dir): - """Default implementation of inventory plugin file contents. - There are some valid cases when all parameters can be obtained from - the environment variables, example "plugin: linode" is valid - ideally, however, some options should be filled from the inventory source data - """ - if self.plugin_name is None: - raise NotImplementedError('At minimum the plugin name is needed for inventory plugin use.') - proper_name = f'{self.namespace}.{self.collection}.{self.plugin_name}' - return {'plugin': proper_name} - def inventory_contents(self, inventory_update, private_data_dir): """Returns a string that is the content for the inventory file for the inventory plugin """ return yaml.safe_dump( - self.inventory_as_dict(inventory_update, private_data_dir), + inventory_update.source_vars_dict, default_flow_style=False, width=1000 ) + def inventory_as_dict(self, inventory_update, private_data_dir): + return inventory_update.source_vars_dict + def build_env(self, inventory_update, env, private_data_dir, private_data_files): injector_env = self.get_plugin_env(inventory_update, private_data_dir, private_data_files) env.update(injector_env) @@ -1690,106 +1682,6 @@ class azure_rm(PluginFileInjector): ret['ANSIBLE_JINJA2_NATIVE'] = str(True) return ret - def inventory_as_dict(self, inventory_update, private_data_dir): - ret = super(azure_rm, self).inventory_as_dict(inventory_update, private_data_dir) - - source_vars = inventory_update.source_vars_dict - - ret['fail_on_template_errors'] = False - - group_by_hostvar = { - 'location': {'prefix': '', 'separator': '', 'key': 'location'}, - 'tag': {'prefix': '', 'separator': '', 'key': 'tags.keys() | list if tags else []'}, - # Introduced with https://github.com/ansible/ansible/pull/53046 - 'security_group': {'prefix': '', 'separator': '', 'key': 'security_group'}, - 'resource_group': {'prefix': '', 'separator': '', 'key': 'resource_group'}, - # Note, os_family was not documented correctly in script, but defaulted to grouping by it - 'os_family': {'prefix': '', 'separator': '', 'key': 'os_disk.operating_system_type'} - } - # by default group by everything - # always respect user setting, if they gave it - group_by = [ - grouping_name for grouping_name in group_by_hostvar - if source_vars.get('group_by_{}'.format(grouping_name), True) - ] - ret['keyed_groups'] = [group_by_hostvar[grouping_name] for grouping_name in group_by] - if 'tag' in group_by: - # Nasty syntax to reproduce "key_value" group names in addition to "key" - ret['keyed_groups'].append({ - 'prefix': '', 'separator': '', - 'key': r'dict(tags.keys() | map("regex_replace", "^(.*)$", "\1_") | list | zip(tags.values() | list)) if tags else []' - }) - - # Compatibility content - # TODO: add proper support for instance_filters non-specific to compatibility - # TODO: add proper support for group_by non-specific to compatibility - # Dashes were not configurable in azure_rm.py script, we do not want unicode, so always use this - ret['use_contrib_script_compatible_sanitization'] = True - # use same host names as script - ret['plain_host_names'] = True - # By default the script did not filter hosts - ret['default_host_filters'] = [] - # User-given host filters - user_filters = [] - old_filterables = [ - ('resource_groups', 'resource_group'), - ('tags', 'tags') - # locations / location would be an entry - # but this would conflict with source_regions - ] - for key, loc in old_filterables: - value = source_vars.get(key, None) - if value and isinstance(value, str): - # tags can be list of key:value pairs - # e.g. 'Creator:jmarshall, peanutbutter:jelly' - # or tags can be a list of keys - # e.g. 'Creator, peanutbutter' - if key == "tags": - # grab each key value pair - for kvpair in value.split(','): - # split into key and value - kv = kvpair.split(':') - # filter out any host that does not have key - # in their tags.keys() variable - user_filters.append('"{}" not in tags.keys()'.format(kv[0].strip())) - # if a value is provided, check that the key:value pair matches - if len(kv) > 1: - user_filters.append('tags["{}"] != "{}"'.format(kv[0].strip(), kv[1].strip())) - else: - user_filters.append('{} not in {}'.format( - loc, value.split(',') - )) - if user_filters: - ret.setdefault('exclude_host_filters', []) - ret['exclude_host_filters'].extend(user_filters) - - ret['conditional_groups'] = {'azure': True} - ret['hostvar_expressions'] = { - 'provisioning_state': 'provisioning_state | title', - 'computer_name': 'name', - 'type': 'resource_type', - 'private_ip': 'private_ipv4_addresses[0] if private_ipv4_addresses else None', - 'public_ip': 'public_ipv4_addresses[0] if public_ipv4_addresses else None', - 'public_ip_name': 'public_ip_name if public_ip_name is defined else None', - 'public_ip_id': 'public_ip_id if public_ip_id is defined else None', - 'tags': 'tags if tags else None' - } - # Special functionality from script - if source_vars.get('use_private_ip', False): - ret['hostvar_expressions']['ansible_host'] = 'private_ipv4_addresses[0]' - # end compatibility content - - if inventory_update.source_regions and 'all' not in inventory_update.source_regions: - # initialize a list for this section in inventory file - ret.setdefault('exclude_host_filters', []) - # make a python list of the regions we will use - python_regions = [x.strip() for x in inventory_update.source_regions.split(',')] - # convert that list in memory to python syntax in a string - # now put that in jinja2 syntax operating on hostvar key "location" - # and put that as an entry in the exclusions list - ret['exclude_host_filters'].append("location not in {}".format(repr(python_regions))) - return ret - class ec2(PluginFileInjector): plugin_name = 'aws_ec2' @@ -1803,218 +1695,6 @@ class ec2(PluginFileInjector): ret['ANSIBLE_JINJA2_NATIVE'] = str(True) return ret - def _compat_compose_vars(self): - return { - # vars that change - 'ec2_block_devices': ( - "dict(block_device_mappings | map(attribute='device_name') | list | zip(block_device_mappings " - "| map(attribute='ebs.volume_id') | list))" - ), - 'ec2_dns_name': 'public_dns_name', - 'ec2_group_name': 'placement.group_name', - 'ec2_instance_profile': 'iam_instance_profile | default("")', - 'ec2_ip_address': 'public_ip_address', - 'ec2_kernel': 'kernel_id | default("")', - 'ec2_monitored': "monitoring.state in ['enabled', 'pending']", - 'ec2_monitoring_state': 'monitoring.state', - 'ec2_placement': 'placement.availability_zone', - 'ec2_ramdisk': 'ramdisk_id | default("")', - 'ec2_reason': 'state_transition_reason', - 'ec2_security_group_ids': "security_groups | map(attribute='group_id') | list | join(',')", - 'ec2_security_group_names': "security_groups | map(attribute='group_name') | list | join(',')", - 'ec2_tag_Name': 'tags.Name', - 'ec2_state': 'state.name', - 'ec2_state_code': 'state.code', - 'ec2_state_reason': 'state_reason.message if state_reason is defined else ""', - 'ec2_sourceDestCheck': 'source_dest_check | default(false) | lower | string', # snake_case syntax intended - 'ec2_account_id': 'owner_id', - # vars that just need ec2_ prefix - 'ec2_ami_launch_index': 'ami_launch_index | string', - 'ec2_architecture': 'architecture', - 'ec2_client_token': 'client_token', - 'ec2_ebs_optimized': 'ebs_optimized', - 'ec2_hypervisor': 'hypervisor', - 'ec2_image_id': 'image_id', - 'ec2_instance_type': 'instance_type', - 'ec2_key_name': 'key_name', - 'ec2_launch_time': r'launch_time | regex_replace(" ", "T") | regex_replace("(\+)(\d\d):(\d)(\d)$", ".\g<2>\g<3>Z")', - 'ec2_platform': 'platform | default("")', - 'ec2_private_dns_name': 'private_dns_name', - 'ec2_private_ip_address': 'private_ip_address', - 'ec2_public_dns_name': 'public_dns_name', - 'ec2_region': 'placement.region', - 'ec2_root_device_name': 'root_device_name', - 'ec2_root_device_type': 'root_device_type', - # many items need blank defaults because the script tended to keep a common schema - 'ec2_spot_instance_request_id': 'spot_instance_request_id | default("")', - 'ec2_subnet_id': 'subnet_id | default("")', - 'ec2_virtualization_type': 'virtualization_type', - 'ec2_vpc_id': 'vpc_id | default("")', - # same as ec2_ip_address, the script provided this - 'ansible_host': 'public_ip_address', - # new with https://github.com/ansible/ansible/pull/53645 - 'ec2_eventsSet': 'events | default("")', - 'ec2_persistent': 'persistent | default(false)', - 'ec2_requester_id': 'requester_id | default("")' - } - - def inventory_as_dict(self, inventory_update, private_data_dir): - ret = super(ec2, self).inventory_as_dict(inventory_update, private_data_dir) - - keyed_groups = [] - group_by_hostvar = { - 'ami_id': {'prefix': '', 'separator': '', 'key': 'image_id', 'parent_group': 'images'}, - # 2 entries for zones for same groups to establish 2 parentage trees - 'availability_zone': {'prefix': '', 'separator': '', 'key': 'placement.availability_zone', 'parent_group': 'zones'}, - 'aws_account': {'prefix': '', 'separator': '', 'key': 'ec2_account_id', 'parent_group': 'accounts'}, # composed var - 'instance_id': {'prefix': '', 'separator': '', 'key': 'instance_id', 'parent_group': 'instances'}, # normally turned off - 'instance_state': {'prefix': 'instance_state', 'key': 'ec2_state', 'parent_group': 'instance_states'}, # composed var - # ec2_platform is a composed var, but group names do not match up to hostvar exactly - 'platform': {'prefix': 'platform', 'key': 'platform | default("undefined")', 'parent_group': 'platforms'}, - 'instance_type': {'prefix': 'type', 'key': 'instance_type', 'parent_group': 'types'}, - 'key_pair': {'prefix': 'key', 'key': 'key_name', 'parent_group': 'keys'}, - 'region': {'prefix': '', 'separator': '', 'key': 'placement.region', 'parent_group': 'regions'}, - # Security requires some ninja jinja2 syntax, credit to s-hertel - 'security_group': {'prefix': 'security_group', 'key': 'security_groups | map(attribute="group_name")', 'parent_group': 'security_groups'}, - # tags cannot be parented in exactly the same way as the script due to - # https://github.com/ansible/ansible/pull/53812 - 'tag_keys': [ - {'prefix': 'tag', 'key': 'tags', 'parent_group': 'tags'}, - {'prefix': 'tag', 'key': 'tags.keys()', 'parent_group': 'tags'} - ], - # 'tag_none': None, # grouping by no tags isn't a different thing with plugin - # naming is redundant, like vpc_id_vpc_8c412cea, but intended - 'vpc_id': {'prefix': 'vpc_id', 'key': 'vpc_id', 'parent_group': 'vpcs'}, - } - # -- same-ish as script here -- - group_by = [x.strip().lower() for x in inventory_update.group_by.split(',') if x.strip()] - for choice in inventory_update.get_ec2_group_by_choices(): - value = bool((group_by and choice[0] in group_by) or (not group_by and choice[0] != 'instance_id')) - # -- end sameness to script -- - if value: - this_keyed_group = group_by_hostvar.get(choice[0], None) - # If a keyed group syntax does not exist, there is nothing we can do to get this group - if this_keyed_group is not None: - if isinstance(this_keyed_group, list): - keyed_groups.extend(this_keyed_group) - else: - keyed_groups.append(this_keyed_group) - # special case, this parentage is only added if both zones and regions are present - if not group_by or ('region' in group_by and 'availability_zone' in group_by): - keyed_groups.append({'prefix': '', 'separator': '', 'key': 'placement.availability_zone', 'parent_group': '{{ placement.region }}'}) - - source_vars = inventory_update.source_vars_dict - # This is a setting from the script, hopefully no one used it - # if true, it replaces dashes, but not in region / loc names - replace_dash = bool(source_vars.get('replace_dash_in_groups', True)) - # Compatibility content - legacy_regex = { - True: r"[^A-Za-z0-9\_]", - False: r"[^A-Za-z0-9\_\-]" # do not replace dash, dash is allowed - }[replace_dash] - list_replacer = 'map("regex_replace", "{rx}", "_") | list'.format(rx=legacy_regex) - # this option, a plugin option, will allow dashes, but not unicode - # when set to False, unicode will be allowed, but it was not allowed by script - # thus, we always have to use this option, and always use our custom regex - ret['use_contrib_script_compatible_sanitization'] = True - for grouping_data in keyed_groups: - if grouping_data['key'] in ('placement.region', 'placement.availability_zone'): - # us-east-2 is always us-east-2 according to ec2.py - # no sanitization in region-ish groups for the script standards, ever ever - continue - if grouping_data['key'] == 'tags': - # dict jinja2 transformation - grouping_data['key'] = 'dict(tags.keys() | {replacer} | zip(tags.values() | {replacer}))'.format( - replacer=list_replacer - ) - elif grouping_data['key'] == 'tags.keys()' or grouping_data['prefix'] == 'security_group': - # list jinja2 transformation - grouping_data['key'] += ' | {replacer}'.format(replacer=list_replacer) - else: - # string transformation - grouping_data['key'] += ' | regex_replace("{rx}", "_")'.format(rx=legacy_regex) - # end compatibility content - - if source_vars.get('iam_role_arn', None): - ret['iam_role_arn'] = source_vars['iam_role_arn'] - - # This was an allowed ec2.ini option, also plugin option, so pass through - if source_vars.get('boto_profile', None): - ret['boto_profile'] = source_vars['boto_profile'] - - elif not replace_dash: - # Using the plugin, but still want dashes allowed - ret['use_contrib_script_compatible_sanitization'] = True - - if source_vars.get('nested_groups') is False: - for this_keyed_group in keyed_groups: - this_keyed_group.pop('parent_group', None) - - if keyed_groups: - ret['keyed_groups'] = keyed_groups - - # Instance ID not part of compat vars, because of settings.EC2_INSTANCE_ID_VAR - compose_dict = {'ec2_id': 'instance_id'} - inst_filters = {} - - # Compatibility content - compose_dict.update(self._compat_compose_vars()) - # plugin provides "aws_ec2", but not this which the script gave - ret['groups'] = {'ec2': True} - if source_vars.get('hostname_variable') is not None: - hnames = [] - for expr in source_vars.get('hostname_variable').split(','): - if expr == 'public_dns_name': - hnames.append('dns-name') - elif not expr.startswith('tag:') and '_' in expr: - hnames.append(expr.replace('_', '-')) - else: - hnames.append(expr) - ret['hostnames'] = hnames - else: - # public_ip as hostname is non-default plugin behavior, script behavior - ret['hostnames'] = [ - 'network-interface.addresses.association.public-ip', - 'dns-name', - 'private-dns-name' - ] - # The script returned only running state by default, the plugin does not - # https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html#options - # options: pending | running | shutting-down | terminated | stopping | stopped - inst_filters['instance-state-name'] = ['running'] - # end compatibility content - - if source_vars.get('destination_variable') or source_vars.get('vpc_destination_variable'): - for fd in ('destination_variable', 'vpc_destination_variable'): - if source_vars.get(fd): - compose_dict['ansible_host'] = source_vars.get(fd) - break - - if compose_dict: - ret['compose'] = compose_dict - - if inventory_update.instance_filters: - # logic used to live in ec2.py, now it belongs to us. Yay more code? - filter_sets = [f for f in inventory_update.instance_filters.split(',') if f] - - for instance_filter in filter_sets: - # AND logic not supported, unclear how to... - instance_filter = instance_filter.strip() - if not instance_filter or '=' not in instance_filter: - continue - filter_key, filter_value = [x.strip() for x in instance_filter.split('=', 1)] - if not filter_key: - continue - inst_filters[filter_key] = filter_value - - if inst_filters: - ret['filters'] = inst_filters - - if inventory_update.source_regions and 'all' not in inventory_update.source_regions: - ret['regions'] = inventory_update.source_regions.split(',') - - return ret - class gce(PluginFileInjector): plugin_name = 'gcp_compute' @@ -2028,76 +1708,10 @@ class gce(PluginFileInjector): ret['ANSIBLE_JINJA2_NATIVE'] = str(True) return ret - def _compat_compose_vars(self): - # missing: gce_image, gce_uuid - # https://github.com/ansible/ansible/issues/51884 - return { - 'gce_description': 'description if description else None', - 'gce_machine_type': 'machineType', - 'gce_name': 'name', - 'gce_network': 'networkInterfaces[0].network.name', - 'gce_private_ip': 'networkInterfaces[0].networkIP', - 'gce_public_ip': 'networkInterfaces[0].accessConfigs[0].natIP | default(None)', - 'gce_status': 'status', - 'gce_subnetwork': 'networkInterfaces[0].subnetwork.name', - 'gce_tags': 'tags.get("items", [])', - 'gce_zone': 'zone', - 'gce_metadata': 'metadata.get("items", []) | items2dict(key_name="key", value_name="value")', - # NOTE: image hostvar is enabled via retrieve_image_info option - 'gce_image': 'image', - # We need this as long as hostnames is non-default, otherwise hosts - # will not be addressed correctly, was returned in script - 'ansible_ssh_host': 'networkInterfaces[0].accessConfigs[0].natIP | default(networkInterfaces[0].networkIP)' - } - def inventory_as_dict(self, inventory_update, private_data_dir): - ret = super(gce, self).inventory_as_dict(inventory_update, private_data_dir) - credential = inventory_update.get_cloud_credential() - - # auth related items + ret = super().inventory_as_dict(inventory_update, private_data_dir) + credential = inventory_source.get_cloud_credential() ret['projects'] = [credential.get_input('project', default='')] - ret['auth_kind'] = "serviceaccount" - - filters = [] - # TODO: implement gce group_by options - # gce never processed the group_by field, if it had, we would selectively - # apply those options here, but it did not, so all groups are added here - keyed_groups = [ - # the jinja2 syntax is duplicated with compose - # https://github.com/ansible/ansible/issues/51883 - {'prefix': 'network', 'key': 'gce_subnetwork'}, # composed var - {'prefix': '', 'separator': '', 'key': 'gce_private_ip'}, # composed var - {'prefix': '', 'separator': '', 'key': 'gce_public_ip'}, # composed var - {'prefix': '', 'separator': '', 'key': 'machineType'}, - {'prefix': '', 'separator': '', 'key': 'zone'}, - {'prefix': 'tag', 'key': 'gce_tags'}, # composed var - {'prefix': 'status', 'key': 'status | lower'}, - # NOTE: image hostvar is enabled via retrieve_image_info option - {'prefix': '', 'separator': '', 'key': 'image'}, - ] - # This will be used as the gce instance_id, must be universal, non-compat - compose_dict = {'gce_id': 'id'} - - # Compatibility content - # TODO: proper group_by and instance_filters support, irrelevant of compat mode - # The gce.py script never sanitized any names in any way - ret['use_contrib_script_compatible_sanitization'] = True - # Perform extra API query to get the image hostvar - ret['retrieve_image_info'] = True - # Add in old hostvars aliases - compose_dict.update(self._compat_compose_vars()) - # Non-default names to match script - ret['hostnames'] = ['name', 'public_ip', 'private_ip'] - # end compatibility content - - if keyed_groups: - ret['keyed_groups'] = keyed_groups - if filters: - ret['filters'] = filters - if compose_dict: - ret['compose'] = compose_dict - if inventory_update.source_regions and 'all' not in inventory_update.source_regions: - ret['zones'] = inventory_update.source_regions.split(',') return ret @@ -2107,106 +1721,6 @@ class vmware(PluginFileInjector): namespace = 'community' collection = 'vmware' - def inventory_as_dict(self, inventory_update, private_data_dir): - ret = super(vmware, self).inventory_as_dict(inventory_update, private_data_dir) - ret['strict'] = False - # Documentation of props, see - # https://github.com/ansible/ansible/blob/devel/docs/docsite/rst/scenario_guides/vmware_scenarios/vmware_inventory_vm_attributes.rst - UPPERCASE_PROPS = [ - "availableField", - "configIssue", - "configStatus", - "customValue", # optional - "datastore", - "effectiveRole", - "guestHeartbeatStatus", # optional - "layout", # optional - "layoutEx", # optional - "name", - "network", - "overallStatus", - "parentVApp", # optional - "permission", - "recentTask", - "resourcePool", - "rootSnapshot", - "snapshot", # optional - "triggeredAlarmState", - "value" - ] - NESTED_PROPS = [ - "capability", - "config", - "guest", - "runtime", - "storage", - "summary", # repeat of other properties - ] - ret['properties'] = UPPERCASE_PROPS + NESTED_PROPS - ret['compose'] = {'ansible_host': 'guest.ipAddress'} # default value - ret['compose']['ansible_ssh_host'] = ret['compose']['ansible_host'] - # the ansible_uuid was unique every host, every import, from the script - ret['compose']['ansible_uuid'] = '99999999 | random | to_uuid' - for prop in UPPERCASE_PROPS: - if prop == prop.lower(): - continue - ret['compose'][prop.lower()] = prop - ret['with_nested_properties'] = True - # ret['property_name_format'] = 'lower_case' # only dacrystal/topic/vmware-inventory-plugin-property-format - - # process custom options - vmware_opts = dict(inventory_update.source_vars_dict.items()) - if inventory_update.instance_filters: - vmware_opts.setdefault('host_filters', inventory_update.instance_filters) - if inventory_update.group_by: - vmware_opts.setdefault('groupby_patterns', inventory_update.group_by) - - alias_pattern = vmware_opts.get('alias_pattern') - if alias_pattern: - ret.setdefault('hostnames', []) - for alias in alias_pattern.split(','): # make best effort - striped_alias = alias.replace('{', '').replace('}', '').strip() # make best effort - if not striped_alias: - continue - ret['hostnames'].append(striped_alias) - - host_pattern = vmware_opts.get('host_pattern') # not working in script - if host_pattern: - stripped_hp = host_pattern.replace('{', '').replace('}', '').strip() # make best effort - ret['compose']['ansible_host'] = stripped_hp - ret['compose']['ansible_ssh_host'] = stripped_hp - - host_filters = vmware_opts.get('host_filters') - if host_filters: - ret.setdefault('filters', []) - for hf in host_filters.split(','): - striped_hf = hf.replace('{', '').replace('}', '').strip() # make best effort - if not striped_hf: - continue - ret['filters'].append(striped_hf) - else: - # default behavior filters by power state - ret['filters'] = ['runtime.powerState == "poweredOn"'] - - groupby_patterns = vmware_opts.get('groupby_patterns') - ret.setdefault('keyed_groups', []) - if groupby_patterns: - for pattern in groupby_patterns.split(','): - stripped_pattern = pattern.replace('{', '').replace('}', '').strip() # make best effort - ret['keyed_groups'].append({ - 'prefix': '', 'separator': '', - 'key': stripped_pattern - }) - else: - # default groups from script - for entry in ('config.guestId', '"templates" if config.template else "guests"'): - ret['keyed_groups'].append({ - 'prefix': '', 'separator': '', - 'key': entry - }) - - return ret - class openstack(PluginFileInjector): plugin_name = 'openstack' @@ -2243,40 +1757,6 @@ class openstack(PluginFileInjector): ) return private_data - def inventory_as_dict(self, inventory_update, private_data_dir): - def use_host_name_for_name(a_bool_maybe): - if not isinstance(a_bool_maybe, bool): - # Could be specified by user via "host" or "uuid" - return a_bool_maybe - elif a_bool_maybe: - return 'name' # plugin default - else: - return 'uuid' - - ret = super(openstack, self).inventory_as_dict(inventory_update, private_data_dir) - ret['fail_on_errors'] = True - ret['expand_hostvars'] = True - ret['inventory_hostname'] = use_host_name_for_name(False) - # Note: mucking with defaults will break import integrity - # For the plugin, we need to use the same defaults as the old script - # or else imports will conflict. To find script defaults you have - # to read source code of the script. - # - # Script Defaults Plugin Defaults - # 'use_hostnames': False, 'name' (True) - # 'expand_hostvars': True, 'no' (False) - # 'fail_on_errors': True, 'no' (False) - # - # These are, yet again, different from ansible_variables in script logic - # but those are applied inconsistently - source_vars = inventory_update.source_vars_dict - for var_name in ['expand_hostvars', 'fail_on_errors']: - if var_name in source_vars: - ret[var_name] = source_vars[var_name] - if 'use_hostnames' in source_vars: - ret['inventory_hostname'] = use_host_name_for_name(source_vars['use_hostnames']) - return ret - def get_plugin_env(self, inventory_update, private_data_dir, private_data_files): env = super(openstack, self).get_plugin_env(inventory_update, private_data_dir, private_data_files) credential = inventory_update.get_cloud_credential() @@ -2294,25 +1774,6 @@ class rhv(PluginFileInjector): namespace = 'ovirt' collection = 'ovirt' - def inventory_as_dict(self, inventory_update, private_data_dir): - ret = super(rhv, self).inventory_as_dict(inventory_update, private_data_dir) - ret['ovirt_insecure'] = False # Default changed from script - # TODO: process strict option upstream - ret['compose'] = { - 'ansible_host': '(devices.values() | list)[0][0] if devices else None' - } - ret['keyed_groups'] = [] - for key in ('cluster', 'status'): - ret['keyed_groups'].append({'prefix': key, 'separator': '_', 'key': key}) - ret['keyed_groups'].append({'prefix': 'tag', 'separator': '_', 'key': 'tags'}) - ret['ovirt_hostname_preference'] = ['name', 'fqdn'] - source_vars = inventory_update.source_vars_dict - for key, value in source_vars.items(): - if key == 'plugin': - continue - ret[key] = value - return ret - class satellite6(PluginFileInjector): plugin_name = 'foreman' @@ -2330,114 +1791,6 @@ class satellite6(PluginFileInjector): ret['FOREMAN_PASSWORD'] = credential.get_input('password', default='') return ret - def inventory_as_dict(self, inventory_update, private_data_dir): - ret = super(satellite6, self).inventory_as_dict(inventory_update, private_data_dir) - ret['validate_certs'] = False - - group_patterns = '[]' - group_prefix = 'foreman_' - want_hostcollections = False - want_ansible_ssh_host = False - want_facts = True - - foreman_opts = inventory_update.source_vars_dict.copy() - for k, v in foreman_opts.items(): - if k == 'satellite6_group_patterns' and isinstance(v, str): - group_patterns = v - elif k == 'satellite6_group_prefix' and isinstance(v, str): - group_prefix = v - elif k == 'satellite6_want_hostcollections' and isinstance(v, bool): - want_hostcollections = v - elif k == 'satellite6_want_ansible_ssh_host' and isinstance(v, bool): - want_ansible_ssh_host = v - elif k == 'satellite6_want_facts' and isinstance(v, bool): - want_facts = v - # add backwards support for ssl_verify - # plugin uses new option, validate_certs, instead - elif k == 'ssl_verify' and isinstance(v, bool): - ret['validate_certs'] = v - else: - ret[k] = str(v) - - # Compatibility content - group_by_hostvar = { - "environment": {"prefix": "{}environment_".format(group_prefix), - "separator": "", - "key": "foreman['environment_name'] | lower | regex_replace(' ', '') | " - "regex_replace('[^A-Za-z0-9_]', '_') | regex_replace('none', '')"}, - "location": {"prefix": "{}location_".format(group_prefix), - "separator": "", - "key": "foreman['location_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, - "organization": {"prefix": "{}organization_".format(group_prefix), - "separator": "", - "key": "foreman['organization_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, - "lifecycle_environment": {"prefix": "{}lifecycle_environment_".format(group_prefix), - "separator": "", - "key": "foreman['content_facet_attributes']['lifecycle_environment_name'] | " - "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, - "content_view": {"prefix": "{}content_view_".format(group_prefix), - "separator": "", - "key": "foreman['content_facet_attributes']['content_view_name'] | " - "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"} - } - - ret['legacy_hostvars'] = True # convert hostvar structure to the form used by the script - ret['want_params'] = True - ret['group_prefix'] = group_prefix - ret['want_hostcollections'] = want_hostcollections - ret['want_facts'] = want_facts - - if want_ansible_ssh_host: - ret['compose'] = {'ansible_ssh_host': "foreman['ip6'] | default(foreman['ip'], true)"} - ret['keyed_groups'] = [group_by_hostvar[grouping_name] for grouping_name in group_by_hostvar] - - def form_keyed_group(group_pattern): - """ - Converts foreman group_pattern to - inventory plugin keyed_group - - e.g. {app_param}-{tier_param}-{dc_param} - becomes - "%s-%s-%s" | format(app_param, tier_param, dc_param) - """ - if type(group_pattern) is not str: - return None - params = re.findall('{[^}]*}', group_pattern) - if len(params) == 0: - return None - - param_names = [] - for p in params: - param_names.append(p[1:-1].strip()) # strip braces and space - - # form keyed_group key by - # replacing curly braces with '%s' - # (for use with jinja's format filter) - key = group_pattern - for p in params: - key = key.replace(p, '%s', 1) - - # apply jinja filter to key - key = '"{}" | format({})'.format(key, ', '.join(param_names)) - - keyed_group = {'key': key, - 'separator': ''} - return keyed_group - - try: - group_patterns = json.loads(group_patterns) - - if type(group_patterns) is list: - for group_pattern in group_patterns: - keyed_group = form_keyed_group(group_pattern) - if keyed_group: - ret['keyed_groups'].append(keyed_group) - except json.JSONDecodeError: - logger.warning('Could not parse group_patterns. Expected JSON-formatted string, found: {}' - .format(group_patterns)) - - return ret - class tower(PluginFileInjector): plugin_name = 'tower' @@ -2445,19 +1798,6 @@ class tower(PluginFileInjector): namespace = 'awx' collection = 'awx' - def inventory_as_dict(self, inventory_update, private_data_dir): - ret = super(tower, self).inventory_as_dict(inventory_update, private_data_dir) - # Credentials injected as env vars, same as script - try: - # plugin can take an actual int type - identifier = int(inventory_update.instance_filters) - except ValueError: - # inventory_id could be a named URL - identifier = iri_to_uri(inventory_update.instance_filters) - ret['inventory_id'] = identifier - ret['include_metadata'] = True # used for license check - return ret - for cls in PluginFileInjector.__subclasses__(): InventorySourceOptions.injectors[cls.__name__] = cls From 7278e7c02502eb2069617b856dcbc19bfe613b6f Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 27 Jul 2020 10:46:42 -0400 Subject: [PATCH 162/188] remove group_by from inventory source * Does not remove group_by testing --- awx/api/metadata.py | 6 --- awx/api/serializers.py | 2 +- .../migrations/0118_v380_inventory_plugins.py | 8 ++++ awx/main/models/inventory.py | 45 ------------------- .../plugins/modules/tower_inventory_source.py | 7 +-- .../test/awx/test_inventory_source.py | 1 - awxkit/awxkit/api/pages/inventory.py | 1 - 7 files changed, 10 insertions(+), 60 deletions(-) diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 8bbfb906ef..0820902cd9 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -122,12 +122,6 @@ class Metadata(metadata.SimpleMetadata): get_regions = getattr(InventorySource, 'get_%s_region_choices' % cp) field_info['%s_region_choices' % cp] = get_regions() - # Special handling of group_by choices for EC2. - if field.field_name == 'group_by': - for cp in ('ec2',): - get_group_by_choices = getattr(InventorySource, 'get_%s_group_by_choices' % cp) - field_info['%s_group_by_choices' % cp] = get_group_by_choices() - # Special handling of notification configuration where the required properties # are conditional on the type selected. if field.field_name == 'notification_configuration': diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 6ce32b3f51..cdd03a4e93 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1937,7 +1937,7 @@ class InventorySourceOptionsSerializer(BaseSerializer): class Meta: fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential', - 'source_regions', 'instance_filters', 'group_by', 'overwrite', 'overwrite_vars', + 'source_regions', 'instance_filters', 'overwrite', 'overwrite_vars', 'custom_virtualenv', 'timeout', 'verbosity') def get_related(self, obj): diff --git a/awx/main/migrations/0118_v380_inventory_plugins.py b/awx/main/migrations/0118_v380_inventory_plugins.py index e2c8cd2d82..939922b206 100644 --- a/awx/main/migrations/0118_v380_inventory_plugins.py +++ b/awx/main/migrations/0118_v380_inventory_plugins.py @@ -56,4 +56,12 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(inventory_source_vars_forward, inventory_source_vars_backward,), + migrations.RemoveField( + model_name='inventorysource', + name='group_by', + ), + migrations.RemoveField( + model_name='inventoryupdate', + name='group_by', + ), ] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index b56f3bd03e..59e3b2d644 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -958,12 +958,6 @@ class InventorySourceOptions(BaseModel): default='', help_text=_('Comma-separated list of filter expressions (EC2 only). Hosts are imported when ANY of the filters match.'), ) - group_by = models.CharField( - max_length=1024, - blank=True, - default='', - help_text=_('Limit groups automatically created from inventory source (EC2 only).'), - ) overwrite = models.BooleanField( default=False, help_text=_('Overwrite local groups and hosts from remote inventory source.'), @@ -983,24 +977,6 @@ class InventorySourceOptions(BaseModel): default=1, ) - @classmethod - def get_ec2_group_by_choices(cls): - return [ - ('ami_id', _('Image ID')), - ('availability_zone', _('Availability Zone')), - ('aws_account', _('Account')), - ('instance_id', _('Instance ID')), - ('instance_state', _('Instance State')), - ('platform', _('Platform')), - ('instance_type', _('Instance Type')), - ('key_pair', _('Key Name')), - ('region', _('Region')), - ('security_group', _('Security Group')), - ('tag_keys', _('Tags')), - ('tag_none', _('Tag None')), - ('vpc_id', _('VPC ID')), - ] - @classmethod def get_ec2_region_choices(cls): ec2_region_names = getattr(settings, 'EC2_REGION_NAMES', {}) @@ -1189,27 +1165,6 @@ class InventorySourceOptions(BaseModel): else: return '' - def clean_group_by(self): - group_by = str(self.group_by or '') - if self.source == 'ec2': - get_choices = getattr(self, 'get_%s_group_by_choices' % self.source) - valid_choices = [x[0] for x in get_choices()] - choice_transform = lambda x: x.strip().lower() - valid_choices = [choice_transform(x) for x in valid_choices] - choices = [choice_transform(x) for x in group_by.split(',') if x.strip()] - invalid_choices = [] - for c in choices: - if c not in valid_choices and c not in invalid_choices: - invalid_choices.append(c) - if invalid_choices: - raise ValidationError(_('Invalid group by choice: %(choice)s') % - {'choice': ', '.join(invalid_choices)}) - return ','.join(choices) - elif self.source == 'vmware': - return group_by - else: - return '' - class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualEnvMixin, RelatedJobsMixin): diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index 5b0e2961df..3110c873a3 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -69,10 +69,6 @@ options: description: - Comma-separated list of filter expressions for matching hosts. type: str - group_by: - description: - - Limit groups automatically created from inventory source. - type: str overwrite: description: - Delete child groups and hosts not found in source. @@ -167,7 +163,6 @@ def main(): credential=dict(), source_regions=dict(), instance_filters=dict(), - group_by=dict(), overwrite=dict(type='bool'), overwrite_vars=dict(type='bool'), custom_virtualenv=dict(), @@ -245,7 +240,7 @@ def main(): OPTIONAL_VARS = ( 'description', 'source', 'source_path', 'source_vars', - 'source_regions', 'instance_filters', 'group_by', + 'source_regions', 'instance_filters', 'overwrite', 'overwrite_vars', 'custom_virtualenv', 'timeout', 'verbosity', 'update_on_launch', 'update_cache_timeout', 'update_on_project_update' diff --git a/awx_collection/test/awx/test_inventory_source.py b/awx_collection/test/awx/test_inventory_source.py index ab0296689b..35267bc4b3 100644 --- a/awx_collection/test/awx/test_inventory_source.py +++ b/awx_collection/test/awx/test_inventory_source.py @@ -192,7 +192,6 @@ def test_falsy_value(run_module, admin_user, base_inventory): # UoPL ? ? o - - - - - - - - - - # source_regions ? ? - o o o - - - - - - - # instance_filters ? ? - o - - o - - - - o - -# group_by ? ? - o - - o - - - - - - # source_vars* ? ? - o - o o o o o - - - # environmet vars* ? ? o - - - - - - - - - o # source_script ? ? - - - - - - - - - - r diff --git a/awxkit/awxkit/api/pages/inventory.py b/awxkit/awxkit/api/pages/inventory.py index e00f0d329a..179082e5a8 100644 --- a/awxkit/awxkit/api/pages/inventory.py +++ b/awxkit/awxkit/api/pages/inventory.py @@ -499,7 +499,6 @@ class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate): payload.source_project = project.id optional_fields = ( - 'group_by', 'instance_filters', 'source_path', 'source_regions', From f32716a0f14d1c63e9a668ed7bfba59488f5a50f Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 27 Jul 2020 10:55:36 -0400 Subject: [PATCH 163/188] remove instance_filter --- awx/api/serializers.py | 2 +- .../migrations/0118_v380_inventory_plugins.py | 8 ++ awx/main/models/inventory.py | 115 ------------------ awx/main/tests/unit/test_tasks.py | 2 - .../plugins/modules/tower_inventory_source.py | 7 +- .../test/awx/test_inventory_source.py | 1 - awxkit/awxkit/api/pages/inventory.py | 1 - tools/scripts/get_ec2_filter_names.py | 21 ---- 8 files changed, 10 insertions(+), 147 deletions(-) delete mode 100755 tools/scripts/get_ec2_filter_names.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index cdd03a4e93..3d6adc6a78 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1937,7 +1937,7 @@ class InventorySourceOptionsSerializer(BaseSerializer): class Meta: fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential', - 'source_regions', 'instance_filters', 'overwrite', 'overwrite_vars', + 'source_regions', 'overwrite', 'overwrite_vars', 'custom_virtualenv', 'timeout', 'verbosity') def get_related(self, obj): diff --git a/awx/main/migrations/0118_v380_inventory_plugins.py b/awx/main/migrations/0118_v380_inventory_plugins.py index 939922b206..3fddf729e0 100644 --- a/awx/main/migrations/0118_v380_inventory_plugins.py +++ b/awx/main/migrations/0118_v380_inventory_plugins.py @@ -64,4 +64,12 @@ class Migration(migrations.Migration): model_name='inventoryupdate', name='group_by', ), + migrations.RemoveField( + model_name='inventorysource', + name='instance_filter', + ), + migrations.RemoveField( + model_name='inventoryupdate', + name='instance_filter', + ), ] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 59e3b2d644..f34ce3d236 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -838,89 +838,6 @@ class InventorySourceOptions(BaseModel): (2, '2 (DEBUG)'), ] - # Use tools/scripts/get_ec2_filter_names.py to build this list. - INSTANCE_FILTER_NAMES = [ - "architecture", - "association.allocation-id", - "association.association-id", - "association.ip-owner-id", - "association.public-ip", - "availability-zone", - "block-device-mapping.attach-time", - "block-device-mapping.delete-on-termination", - "block-device-mapping.device-name", - "block-device-mapping.status", - "block-device-mapping.volume-id", - "client-token", - "dns-name", - "group-id", - "group-name", - "hypervisor", - "iam-instance-profile.arn", - "image-id", - "instance-id", - "instance-lifecycle", - "instance-state-code", - "instance-state-name", - "instance-type", - "instance.group-id", - "instance.group-name", - "ip-address", - "kernel-id", - "key-name", - "launch-index", - "launch-time", - "monitoring-state", - "network-interface-private-dns-name", - "network-interface.addresses.association.ip-owner-id", - "network-interface.addresses.association.public-ip", - "network-interface.addresses.primary", - "network-interface.addresses.private-ip-address", - "network-interface.attachment.attach-time", - "network-interface.attachment.attachment-id", - "network-interface.attachment.delete-on-termination", - "network-interface.attachment.device-index", - "network-interface.attachment.instance-id", - "network-interface.attachment.instance-owner-id", - "network-interface.attachment.status", - "network-interface.availability-zone", - "network-interface.description", - "network-interface.group-id", - "network-interface.group-name", - "network-interface.mac-address", - "network-interface.network-interface.id", - "network-interface.owner-id", - "network-interface.requester-id", - "network-interface.requester-managed", - "network-interface.source-destination-check", - "network-interface.status", - "network-interface.subnet-id", - "network-interface.vpc-id", - "owner-id", - "placement-group-name", - "platform", - "private-dns-name", - "private-ip-address", - "product-code", - "product-code.type", - "ramdisk-id", - "reason", - "requester-id", - "reservation-id", - "root-device-name", - "root-device-type", - "source-dest-check", - "spot-instance-request-id", - "state-reason-code", - "state-reason-message", - "subnet-id", - "tag-key", - "tag-value", - "tenancy", - "virtualization-type", - "vpc-id" - ] - class Meta: abstract = True @@ -952,12 +869,6 @@ class InventorySourceOptions(BaseModel): blank=True, default='', ) - instance_filters = models.CharField( - max_length=1024, - blank=True, - default='', - help_text=_('Comma-separated list of filter expressions (EC2 only). Hosts are imported when ANY of the filters match.'), - ) overwrite = models.BooleanField( default=False, help_text=_('Overwrite local groups and hosts from remote inventory source.'), @@ -1139,32 +1050,6 @@ class InventorySourceOptions(BaseModel): source_vars_dict = VarsDictProperty('source_vars') - def clean_instance_filters(self): - instance_filters = str(self.instance_filters or '') - if self.source == 'ec2': - invalid_filters = [] - instance_filter_re = re.compile(r'^((tag:.+)|([a-z][a-z\.-]*[a-z]))=.*$') - for instance_filter in instance_filters.split(','): - instance_filter = instance_filter.strip() - if not instance_filter: - continue - if not instance_filter_re.match(instance_filter): - invalid_filters.append(instance_filter) - continue - instance_filter_name = instance_filter.split('=', 1)[0] - if instance_filter_name.startswith('tag:'): - continue - if instance_filter_name not in self.INSTANCE_FILTER_NAMES: - invalid_filters.append(instance_filter) - if invalid_filters: - raise ValidationError(_('Invalid filter expression: %(filter)s') % - {'filter': ', '.join(invalid_filters)}) - return instance_filters - elif self.source in ('vmware', 'tower'): - return instance_filters - else: - return '' - class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualEnvMixin, RelatedJobsMixin): diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 7dcea33ccd..341fcdc82d 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -2216,7 +2216,6 @@ class TestInventoryUpdateCredentials(TestJobExecution): task = tasks.RunInventoryUpdate() tower = CredentialType.defaults['tower']() inventory_update.source = 'tower' - inventory_update.instance_filters = '12345' inputs = { 'host': 'https://tower.example.org', 'username': 'bob', @@ -2248,7 +2247,6 @@ class TestInventoryUpdateCredentials(TestJobExecution): task = tasks.RunInventoryUpdate() tower = CredentialType.defaults['tower']() inventory_update.source = 'tower' - inventory_update.instance_filters = '12345' inputs = { 'host': 'https://tower.example.org', 'username': 'bob', diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index 3110c873a3..3840806f8f 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -65,10 +65,6 @@ options: description: - Regions for cloud provider. type: str - instance_filters: - description: - - Comma-separated list of filter expressions for matching hosts. - type: str overwrite: description: - Delete child groups and hosts not found in source. @@ -162,7 +158,6 @@ def main(): source_vars=dict(type='dict'), credential=dict(), source_regions=dict(), - instance_filters=dict(), overwrite=dict(type='bool'), overwrite_vars=dict(type='bool'), custom_virtualenv=dict(), @@ -240,7 +235,7 @@ def main(): OPTIONAL_VARS = ( 'description', 'source', 'source_path', 'source_vars', - 'source_regions', 'instance_filters', + 'source_regions', 'overwrite', 'overwrite_vars', 'custom_virtualenv', 'timeout', 'verbosity', 'update_on_launch', 'update_cache_timeout', 'update_on_project_update' diff --git a/awx_collection/test/awx/test_inventory_source.py b/awx_collection/test/awx/test_inventory_source.py index 35267bc4b3..fff6cdd3c3 100644 --- a/awx_collection/test/awx/test_inventory_source.py +++ b/awx_collection/test/awx/test_inventory_source.py @@ -191,7 +191,6 @@ def test_falsy_value(run_module, admin_user, base_inventory): # update_on_launch ? ? o o o o o o o o o o o # UoPL ? ? o - - - - - - - - - - # source_regions ? ? - o o o - - - - - - - -# instance_filters ? ? - o - - o - - - - o - # source_vars* ? ? - o - o o o o o - - - # environmet vars* ? ? o - - - - - - - - - o # source_script ? ? - - - - - - - - - - r diff --git a/awxkit/awxkit/api/pages/inventory.py b/awxkit/awxkit/api/pages/inventory.py index 179082e5a8..0976b09afe 100644 --- a/awxkit/awxkit/api/pages/inventory.py +++ b/awxkit/awxkit/api/pages/inventory.py @@ -499,7 +499,6 @@ class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate): payload.source_project = project.id optional_fields = ( - 'instance_filters', 'source_path', 'source_regions', 'source_vars', diff --git a/tools/scripts/get_ec2_filter_names.py b/tools/scripts/get_ec2_filter_names.py deleted file mode 100755 index d001b1f8e7..0000000000 --- a/tools/scripts/get_ec2_filter_names.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python - -import json -import sys -import requests -from bs4 import BeautifulSoup - -response = requests.get('http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeInstances.html') -soup = BeautifulSoup(response.text) - -section_h3 = soup.find(id='query-DescribeInstances-filters') -section_div = section_h3.find_parent('div', attrs={'class': 'section'}) - -filter_names = [] -for term in section_div.select('div.variablelist dt span.term'): - filter_name = term.get_text() - if not filter_name.startswith('tag:'): - filter_names.append(filter_name) -filter_names.sort() - -json.dump(filter_names, sys.stdout, indent=4) From a8a47f314e52db0672555741dd678fb7542db63f Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 27 Jul 2020 11:04:51 -0400 Subject: [PATCH 164/188] remove source_regions --- awx/api/metadata.py | 9 +- awx/api/serializers.py | 4 +- .../migrations/0118_v380_inventory_plugins.py | 22 ++-- awx/main/models/inventory.py | 106 +----------------- awx/main/tests/unit/test_tasks.py | 3 - awx/main/tests/unit/utils/test_common.py | 8 -- awx/main/utils/common.py | 11 +- awx/settings/defaults.py | 86 -------------- .../plugins/modules/tower_inventory_source.py | 6 - .../test/awx/test_inventory_source.py | 1 - awxkit/awxkit/api/pages/inventory.py | 1 - 11 files changed, 21 insertions(+), 236 deletions(-) diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 0820902cd9..847e353890 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -23,7 +23,7 @@ from rest_framework.request import clone_request # AWX from awx.api.fields import ChoiceNullField from awx.main.fields import JSONField, ImplicitRoleField -from awx.main.models import InventorySource, NotificationTemplate +from awx.main.models import NotificationTemplate from awx.main.scheduler.kubernetes import PodManager @@ -115,13 +115,6 @@ class Metadata(metadata.SimpleMetadata): if getattr(field, 'write_only', False): field_info['write_only'] = True - # Special handling of inventory source_region choices that vary based on - # selected inventory source. - if field.field_name == 'source_regions': - for cp in ('azure_rm', 'ec2', 'gce'): - get_regions = getattr(InventorySource, 'get_%s_region_choices' % cp) - field_info['%s_region_choices' % cp] = get_regions() - # Special handling of notification configuration where the required properties # are conditional on the type selected. if field.field_name == 'notification_configuration': diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 3d6adc6a78..9cf953262b 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1937,7 +1937,7 @@ class InventorySourceOptionsSerializer(BaseSerializer): class Meta: fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential', - 'source_regions', 'overwrite', 'overwrite_vars', + 'overwrite', 'overwrite_vars', 'custom_virtualenv', 'timeout', 'verbosity') def get_related(self, obj): @@ -1957,7 +1957,7 @@ class InventorySourceOptionsSerializer(BaseSerializer): return ret def validate(self, attrs): - # TODO: Validate source, validate source_regions + # TODO: Validate source errors = {} source = attrs.get('source', self.instance and self.instance.source or '') diff --git a/awx/main/migrations/0118_v380_inventory_plugins.py b/awx/main/migrations/0118_v380_inventory_plugins.py index 3fddf729e0..d2c9a06398 100644 --- a/awx/main/migrations/0118_v380_inventory_plugins.py +++ b/awx/main/migrations/0118_v380_inventory_plugins.py @@ -3,9 +3,8 @@ import logging import json -from django.db import migrations, models +from django.db import migrations -from awx.main.models.inventory import InventorySource from ._inventory_source_vars import FrozenInjectors @@ -13,15 +12,16 @@ logger = logging.getLogger('awx.main.migrations') BACKUP_FILENAME = '/tmp/tower_migration_inventory_source_vars.json' -def _get_inventory_sources(): - # TODO: Maybe pull this list from an import +def _get_inventory_sources(InventorySource): + # TODO: Maybe pull the list of cloud sources from code return InventorySource.objects.filter(source__in=['ec2', 'gce', 'azure_rm', 'vmware', 'satellite6', 'openstack', 'rhv', 'tower']) def inventory_source_vars_forward(apps, schema_editor): + InventorySource = apps.get_model("main", "InventorySource") source_vars_backup = dict() - for inv_source_obj in _get_inventory_sources(): + for inv_source_obj in _get_inventory_sources(InventorySource): # TODO: Log error if this is false, it shouldn't be false if inv_source_obj.source in FrozenInjectors: source_vars_backup[inv_source_obj.id] = dict(inv_source_obj.source_vars_dict) @@ -66,10 +66,18 @@ class Migration(migrations.Migration): ), migrations.RemoveField( model_name='inventorysource', - name='instance_filter', + name='instance_filters', ), migrations.RemoveField( model_name='inventoryupdate', - name='instance_filter', + name='instance_filters', + ), + migrations.RemoveField( + model_name='inventorysource', + name='source_regions', + ), + migrations.RemoveField( + model_name='inventoryupdate', + name='source_regions', ), ] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index f34ce3d236..36b358a1bd 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -4,7 +4,6 @@ # Python import datetime import time -import json import logging import re import copy @@ -19,7 +18,6 @@ from django.utils.translation import ugettext_lazy as _ from django.db import transaction from django.core.exceptions import ValidationError from django.utils.timezone import now -from django.utils.encoding import iri_to_uri from django.db.models import Q # REST Framework @@ -56,7 +54,7 @@ from awx.main.models.notifications import ( JobNotificationMixin, ) from awx.main.models.credential.injectors import _openstack_data -from awx.main.utils import _inventory_updates, region_sorting +from awx.main.utils import _inventory_updates from awx.main.utils.safe_yaml import sanitize_jinja @@ -864,11 +862,6 @@ class InventorySourceOptions(BaseModel): default='', help_text=_('Inventory source variables in YAML or JSON format.'), ) - source_regions = models.CharField( - max_length=1024, - blank=True, - default='', - ) overwrite = models.BooleanField( default=False, help_text=_('Overwrite local groups and hosts from remote inventory source.'), @@ -888,79 +881,6 @@ class InventorySourceOptions(BaseModel): default=1, ) - @classmethod - def get_ec2_region_choices(cls): - ec2_region_names = getattr(settings, 'EC2_REGION_NAMES', {}) - ec2_name_replacements = { - 'us': 'US', - 'ap': 'Asia Pacific', - 'eu': 'Europe', - 'sa': 'South America', - } - import boto.ec2 - regions = [('all', 'All')] - for region in boto.ec2.regions(): - label = ec2_region_names.get(region.name, '') - if not label: - label_parts = [] - for part in region.name.split('-'): - part = ec2_name_replacements.get(part.lower(), part.title()) - label_parts.append(part) - label = ' '.join(label_parts) - regions.append((region.name, label)) - return sorted(regions, key=region_sorting) - - @classmethod - def get_gce_region_choices(self): - """Return a complete list of regions in GCE, as a list of - two-tuples. - """ - # It's not possible to get a list of regions from GCE without - # authenticating first. Therefore, use a list from settings. - regions = list(getattr(settings, 'GCE_REGION_CHOICES', [])) - regions.insert(0, ('all', 'All')) - return sorted(regions, key=region_sorting) - - @classmethod - def get_azure_rm_region_choices(self): - """Return a complete list of regions in Microsoft Azure, as a list of - two-tuples. - """ - # It's not possible to get a list of regions from Azure without - # authenticating first (someone reading these might think there's - # a pattern here!). Therefore, you guessed it, use a list from - # settings. - regions = list(getattr(settings, 'AZURE_RM_REGION_CHOICES', [])) - regions.insert(0, ('all', 'All')) - return sorted(regions, key=region_sorting) - - @classmethod - def get_vmware_region_choices(self): - """Return a complete list of regions in VMware, as a list of two-tuples - (but note that VMware doesn't actually have regions!). - """ - return [('all', 'All')] - - @classmethod - def get_openstack_region_choices(self): - """I don't think openstack has regions""" - return [('all', 'All')] - - @classmethod - def get_satellite6_region_choices(self): - """Red Hat Satellite 6 region choices (not implemented)""" - return [('all', 'All')] - - @classmethod - def get_rhv_region_choices(self): - """No region supprt""" - return [('all', 'All')] - - @classmethod - def get_tower_region_choices(self): - """No region supprt""" - return [('all', 'All')] - @staticmethod def cloud_credential_validation(source, cred): if not source: @@ -1025,28 +945,6 @@ class InventorySourceOptions(BaseModel): if cred is not None: return cred.pk - def clean_source_regions(self): - regions = self.source_regions - - if self.source in CLOUD_PROVIDERS: - get_regions = getattr(self, 'get_%s_region_choices' % self.source) - valid_regions = [x[0] for x in get_regions()] - region_transform = lambda x: x.strip().lower() - else: - return '' - all_region = region_transform('all') - valid_regions = [region_transform(x) for x in valid_regions] - regions = [region_transform(x) for x in regions.split(',') if x.strip()] - if all_region in regions: - return all_region - invalid_regions = [] - for r in regions: - if r not in valid_regions and r not in invalid_regions: - invalid_regions.append(r) - if invalid_regions: - raise ValidationError(_('Invalid %(source)s region: %(region)s') % { - 'source': self.source, 'region': ', '.join(invalid_regions)}) - return ','.join(regions) source_vars_dict = VarsDictProperty('source_vars') @@ -1550,7 +1448,7 @@ class gce(PluginFileInjector): def inventory_as_dict(self, inventory_update, private_data_dir): ret = super().inventory_as_dict(inventory_update, private_data_dir) - credential = inventory_source.get_cloud_credential() + credential = inventory_update.get_cloud_credential() ret['projects'] = [credential.get_input('project', default='')] return ret diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 341fcdc82d..cda720b6ab 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -2020,7 +2020,6 @@ class TestInventoryUpdateCredentials(TestJobExecution): task = tasks.RunInventoryUpdate() azure_rm = CredentialType.defaults['azure_rm']() inventory_update.source = 'azure_rm' - inventory_update.source_regions = 'north, south, east, west' def get_cred(): cred = Credential( @@ -2059,7 +2058,6 @@ class TestInventoryUpdateCredentials(TestJobExecution): task = tasks.RunInventoryUpdate() azure_rm = CredentialType.defaults['azure_rm']() inventory_update.source = 'azure_rm' - inventory_update.source_regions = 'all' def get_cred(): cred = Credential( @@ -2097,7 +2095,6 @@ class TestInventoryUpdateCredentials(TestJobExecution): task = tasks.RunInventoryUpdate() gce = CredentialType.defaults['gce']() inventory_update.source = 'gce' - inventory_update.source_regions = 'all' def get_cred(): cred = Credential( diff --git a/awx/main/tests/unit/utils/test_common.py b/awx/main/tests/unit/utils/test_common.py index edacfc1423..8c07020c53 100644 --- a/awx/main/tests/unit/utils/test_common.py +++ b/awx/main/tests/unit/utils/test_common.py @@ -215,11 +215,3 @@ def test_get_custom_venv_choices(): os.path.join(temp_dir, ''), os.path.join(custom_venv_1, '') ] - - -def test_region_sorting(): - s = [('Huey', 'China1'), - ('Dewey', 'UK1'), - ('Lewie', 'US1'), - ('All', 'All')] - assert [x[1] for x in sorted(s, key=common.region_sorting)] == ['All', 'US1', 'China1', 'UK1'] diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index f34cf4e4d8..a017dba61b 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -45,7 +45,7 @@ __all__ = [ 'get_object_or_400', 'camelcase_to_underscore', 'underscore_to_camelcase', 'memoize', 'memoize_delete', 'get_ansible_version', 'get_licenser', 'get_awx_http_client_headers', 'get_awx_version', 'update_scm_url', 'get_type_for_model', 'get_model_for_type', - 'copy_model_by_class', 'region_sorting', 'copy_m2m_relationships', + 'copy_model_by_class', 'copy_m2m_relationships', 'prefetch_page_capabilities', 'to_python_boolean', 'ignore_inventory_computed_fields', 'ignore_inventory_group_removal', '_inventory_updates', 'get_pk_from_dict', 'getattrd', 'getattr_dne', 'NoDefaultProvided', 'get_current_apps', 'set_current_apps', @@ -87,15 +87,6 @@ def to_python_boolean(value, allow_none=False): raise ValueError(_(u'Unable to convert "%s" to boolean') % value) -def region_sorting(region): - # python3's removal of sorted(cmp=...) is _stupid_ - if region[1].lower() == 'all': - return '' - elif region[1].lower().startswith('us'): - return region[1] - return 'ZZZ' + str(region[1]) - - def camelcase_to_underscore(s): ''' Convert CamelCase names to lowercase_with_underscore. diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 716aea3aa7..cb7dc49e46 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -672,28 +672,6 @@ INV_ENV_VARIABLE_BLOCKED = ("HOME", "USER", "_", "TERM") # -- Amazon EC2 -- # ---------------- -# AWS does not appear to provide pretty region names via any API, so store the -# list of names here. The available region IDs will be pulled from boto. -# http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region -EC2_REGION_NAMES = { - 'us-east-1': _('US East (Northern Virginia)'), - 'us-east-2': _('US East (Ohio)'), - 'us-west-2': _('US West (Oregon)'), - 'us-west-1': _('US West (Northern California)'), - 'ca-central-1': _('Canada (Central)'), - 'eu-central-1': _('EU (Frankfurt)'), - 'eu-west-1': _('EU (Ireland)'), - 'eu-west-2': _('EU (London)'), - 'ap-southeast-1': _('Asia Pacific (Singapore)'), - 'ap-southeast-2': _('Asia Pacific (Sydney)'), - 'ap-northeast-1': _('Asia Pacific (Tokyo)'), - 'ap-northeast-2': _('Asia Pacific (Seoul)'), - 'ap-south-1': _('Asia Pacific (Mumbai)'), - 'sa-east-1': _('South America (Sao Paulo)'), - 'us-gov-west-1': _('US West (GovCloud)'), - 'cn-north-1': _('China (Beijing)'), -} - # Inventory variable name/values for determining if host is active/enabled. EC2_ENABLED_VAR = 'ec2_state' EC2_ENABLED_VALUE = 'running' @@ -729,41 +707,6 @@ VMWARE_VALIDATE_CERTS = False # -- Google Compute Engine -- # --------------------------- -# It's not possible to get zones in GCE without authenticating, so we -# provide a list here. -# Source: https://developers.google.com/compute/docs/zones -GCE_REGION_CHOICES = [ - ('us-east1-b', _('US East 1 (B)')), - ('us-east1-c', _('US East 1 (C)')), - ('us-east1-d', _('US East 1 (D)')), - ('us-east4-a', _('US East 4 (A)')), - ('us-east4-b', _('US East 4 (B)')), - ('us-east4-c', _('US East 4 (C)')), - ('us-central1-a', _('US Central (A)')), - ('us-central1-b', _('US Central (B)')), - ('us-central1-c', _('US Central (C)')), - ('us-central1-f', _('US Central (F)')), - ('us-west1-a', _('US West (A)')), - ('us-west1-b', _('US West (B)')), - ('us-west1-c', _('US West (C)')), - ('europe-west1-b', _('Europe West 1 (B)')), - ('europe-west1-c', _('Europe West 1 (C)')), - ('europe-west1-d', _('Europe West 1 (D)')), - ('europe-west2-a', _('Europe West 2 (A)')), - ('europe-west2-b', _('Europe West 2 (B)')), - ('europe-west2-c', _('Europe West 2 (C)')), - ('asia-east1-a', _('Asia East (A)')), - ('asia-east1-b', _('Asia East (B)')), - ('asia-east1-c', _('Asia East (C)')), - ('asia-southeast1-a', _('Asia Southeast (A)')), - ('asia-southeast1-b', _('Asia Southeast (B)')), - ('asia-northeast1-a', _('Asia Northeast (A)')), - ('asia-northeast1-b', _('Asia Northeast (B)')), - ('asia-northeast1-c', _('Asia Northeast (C)')), - ('australia-southeast1-a', _('Australia Southeast (A)')), - ('australia-southeast1-b', _('Australia Southeast (B)')), - ('australia-southeast1-c', _('Australia Southeast (C)')), -] # Inventory variable name/value for determining whether a host is active # in Google Compute Engine. GCE_ENABLED_VAR = 'status' @@ -779,35 +722,6 @@ GCE_INSTANCE_ID_VAR = 'gce_id' # -------------------------------------- # -- Microsoft Azure Resource Manager -- # -------------------------------------- -# It's not possible to get zones in Azure without authenticating, so we -# provide a list here. -AZURE_RM_REGION_CHOICES = [ - ('eastus', _('US East')), - ('eastus2', _('US East 2')), - ('centralus', _('US Central')), - ('northcentralus', _('US North Central')), - ('southcentralus', _('US South Central')), - ('westcentralus', _('US West Central')), - ('westus', _('US West')), - ('westus2', _('US West 2')), - ('canadaeast', _('Canada East')), - ('canadacentral', _('Canada Central')), - ('brazilsouth', _('Brazil South')), - ('northeurope', _('Europe North')), - ('westeurope', _('Europe West')), - ('ukwest', _('UK West')), - ('uksouth', _('UK South')), - ('eastasia', _('Asia East')), - ('southestasia', _('Asia Southeast')), - ('australiaeast', _('Australia East')), - ('australiasoutheast', _('Australia Southeast')), - ('westindia', _('India West')), - ('southindia', _('India South')), - ('japaneast', _('Japan East')), - ('japanwest', _('Japan West')), - ('koreacentral', _('Korea Central')), - ('koreasouth', _('Korea South')), -] AZURE_RM_GROUP_FILTER = r'^.+$' AZURE_RM_HOST_FILTER = r'^.+$' AZURE_RM_ENABLED_VAR = 'powerstate' diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index 3840806f8f..d3517b3f5a 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -61,10 +61,6 @@ options: description: - Credential to use for the source. type: str - source_regions: - description: - - Regions for cloud provider. - type: str overwrite: description: - Delete child groups and hosts not found in source. @@ -157,7 +153,6 @@ def main(): source_script=dict(), source_vars=dict(type='dict'), credential=dict(), - source_regions=dict(), overwrite=dict(type='bool'), overwrite_vars=dict(type='bool'), custom_virtualenv=dict(), @@ -235,7 +230,6 @@ def main(): OPTIONAL_VARS = ( 'description', 'source', 'source_path', 'source_vars', - 'source_regions', 'overwrite', 'overwrite_vars', 'custom_virtualenv', 'timeout', 'verbosity', 'update_on_launch', 'update_cache_timeout', 'update_on_project_update' diff --git a/awx_collection/test/awx/test_inventory_source.py b/awx_collection/test/awx/test_inventory_source.py index fff6cdd3c3..b27653fb94 100644 --- a/awx_collection/test/awx/test_inventory_source.py +++ b/awx_collection/test/awx/test_inventory_source.py @@ -190,7 +190,6 @@ def test_falsy_value(run_module, admin_user, base_inventory): # overwrite_vars ? ? o o o o o o o o o o o # update_on_launch ? ? o o o o o o o o o o o # UoPL ? ? o - - - - - - - - - - -# source_regions ? ? - o o o - - - - - - - # source_vars* ? ? - o - o o o o o - - - # environmet vars* ? ? o - - - - - - - - - o # source_script ? ? - - - - - - - - - - r diff --git a/awxkit/awxkit/api/pages/inventory.py b/awxkit/awxkit/api/pages/inventory.py index 0976b09afe..ea7e70b3b8 100644 --- a/awxkit/awxkit/api/pages/inventory.py +++ b/awxkit/awxkit/api/pages/inventory.py @@ -500,7 +500,6 @@ class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate): optional_fields = ( 'source_path', - 'source_regions', 'source_vars', 'timeout', 'overwrite', From 34adbe60284832195575a5378c643b0a1b080bb5 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 28 Jul 2020 13:14:43 -0400 Subject: [PATCH 165/188] fix tests for new inv plugin behavior * Enforce plugin: --- awx/main/models/inventory.py | 21 +++++ .../plugins/azure_rm/files/azure_rm.yml | 43 --------- .../inventory/plugins/ec2/files/aws_ec2.yml | 81 ----------------- .../plugins/gce/files/gcp_compute.yml | 50 ----------- .../plugins/openstack/files/file_reference | 6 +- .../plugins/openstack/files/openstack.yml | 4 - .../inventory/plugins/rhv/files/ovirt.yml | 20 ----- .../plugins/satellite6/files/foreman.yml | 30 ------- .../inventory/plugins/tower/files/tower.yml | 3 - .../vmware/files/vmware_vm_inventory.yml | 55 ------------ .../tests/functional/api/test_inventory.py | 70 +++++++++++++-- .../tests/functional/models/test_inventory.py | 38 +++----- .../test_inventory_source_injectors.py | 88 +++---------------- awx/main/tests/unit/models/test_inventory.py | 17 ---- awx/settings/defaults.py | 2 - 15 files changed, 111 insertions(+), 417 deletions(-) delete mode 100644 awx/main/tests/data/inventory/plugins/azure_rm/files/azure_rm.yml delete mode 100644 awx/main/tests/data/inventory/plugins/ec2/files/aws_ec2.yml delete mode 100644 awx/main/tests/data/inventory/plugins/gce/files/gcp_compute.yml delete mode 100644 awx/main/tests/data/inventory/plugins/openstack/files/openstack.yml delete mode 100644 awx/main/tests/data/inventory/plugins/rhv/files/ovirt.yml delete mode 100644 awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml delete mode 100644 awx/main/tests/data/inventory/plugins/tower/files/tower.yml delete mode 100644 awx/main/tests/data/inventory/plugins/vmware/files/vmware_vm_inventory.yml diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 36b358a1bd..7d6c350274 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -7,6 +7,7 @@ import time import logging import re import copy +import json import os.path from urllib.parse import urljoin import yaml @@ -1157,6 +1158,20 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE raise ValidationError(_("Cannot set source_path if not SCM type.")) return self.source_path + def clean_source_vars(self): + injector = self.injectors.get(self.source) + source_vars = dict(self.source_vars_dict) # make a copy + if injector and self.source_vars_dict.get('plugin', '') != injector.get_proper_name(): + source_vars['plugin'] = injector.get_proper_name() + elif not injector: + source_vars = dict(self.source_vars_dict) # make a copy + collection_pattern = re.compile("^(.+)\.(.+)\.(.+)$") # noqa + if 'plugin' not in source_vars: + raise ValidationError(_("plugin: must be present and of the form namespace.collection.inv_plugin")) + elif not bool(collection_pattern.match(source_vars['plugin'])): + raise ValidationError(_("plugin: must be of the form namespace.collection.inv_plugin")) + return json.dumps(source_vars) + ''' RelatedJobsMixin ''' @@ -1344,6 +1359,12 @@ class PluginFileInjector(object): # This is InventoryOptions instance, could be source or inventory update self.ansible_version = ansible_version + @classmethod + def get_proper_name(cls): + if cls.plugin_name is None: + return None + return f'{cls.namespace}.{cls.collection}.{cls.plugin_name}' + @property def filename(self): """Inventory filename for using the inventory plugin diff --git a/awx/main/tests/data/inventory/plugins/azure_rm/files/azure_rm.yml b/awx/main/tests/data/inventory/plugins/azure_rm/files/azure_rm.yml deleted file mode 100644 index 8d6c1dbfa7..0000000000 --- a/awx/main/tests/data/inventory/plugins/azure_rm/files/azure_rm.yml +++ /dev/null @@ -1,43 +0,0 @@ -conditional_groups: - azure: true -default_host_filters: [] -exclude_host_filters: -- resource_group not in ['foo_resources', 'bar_resources'] -- '"Creator" not in tags.keys()' -- tags["Creator"] != "jmarshall" -- '"peanutbutter" not in tags.keys()' -- tags["peanutbutter"] != "jelly" -- location not in ['southcentralus', 'westus'] -fail_on_template_errors: false -hostvar_expressions: - ansible_host: private_ipv4_addresses[0] - computer_name: name - private_ip: private_ipv4_addresses[0] if private_ipv4_addresses else None - provisioning_state: provisioning_state | title - public_ip: public_ipv4_addresses[0] if public_ipv4_addresses else None - public_ip_id: public_ip_id if public_ip_id is defined else None - public_ip_name: public_ip_name if public_ip_name is defined else None - tags: tags if tags else None - type: resource_type -keyed_groups: -- key: location - prefix: '' - separator: '' -- key: tags.keys() | list if tags else [] - prefix: '' - separator: '' -- key: security_group - prefix: '' - separator: '' -- key: resource_group - prefix: '' - separator: '' -- key: os_disk.operating_system_type - prefix: '' - separator: '' -- key: dict(tags.keys() | map("regex_replace", "^(.*)$", "\1_") | list | zip(tags.values() | list)) if tags else [] - prefix: '' - separator: '' -plain_host_names: true -plugin: azure.azcollection.azure_rm -use_contrib_script_compatible_sanitization: true diff --git a/awx/main/tests/data/inventory/plugins/ec2/files/aws_ec2.yml b/awx/main/tests/data/inventory/plugins/ec2/files/aws_ec2.yml deleted file mode 100644 index 8984d4cb56..0000000000 --- a/awx/main/tests/data/inventory/plugins/ec2/files/aws_ec2.yml +++ /dev/null @@ -1,81 +0,0 @@ -boto_profile: /tmp/my_boto_stuff -compose: - ansible_host: public_dns_name - ec2_account_id: owner_id - ec2_ami_launch_index: ami_launch_index | string - ec2_architecture: architecture - ec2_block_devices: dict(block_device_mappings | map(attribute='device_name') | list | zip(block_device_mappings | map(attribute='ebs.volume_id') | list)) - ec2_client_token: client_token - ec2_dns_name: public_dns_name - ec2_ebs_optimized: ebs_optimized - ec2_eventsSet: events | default("") - ec2_group_name: placement.group_name - ec2_hypervisor: hypervisor - ec2_id: instance_id - ec2_image_id: image_id - ec2_instance_profile: iam_instance_profile | default("") - ec2_instance_type: instance_type - ec2_ip_address: public_ip_address - ec2_kernel: kernel_id | default("") - ec2_key_name: key_name - ec2_launch_time: launch_time | regex_replace(" ", "T") | regex_replace("(\+)(\d\d):(\d)(\d)$", ".\g<2>\g<3>Z") - ec2_monitored: monitoring.state in ['enabled', 'pending'] - ec2_monitoring_state: monitoring.state - ec2_persistent: persistent | default(false) - ec2_placement: placement.availability_zone - ec2_platform: platform | default("") - ec2_private_dns_name: private_dns_name - ec2_private_ip_address: private_ip_address - ec2_public_dns_name: public_dns_name - ec2_ramdisk: ramdisk_id | default("") - ec2_reason: state_transition_reason - ec2_region: placement.region - ec2_requester_id: requester_id | default("") - ec2_root_device_name: root_device_name - ec2_root_device_type: root_device_type - ec2_security_group_ids: security_groups | map(attribute='group_id') | list | join(',') - ec2_security_group_names: security_groups | map(attribute='group_name') | list | join(',') - ec2_sourceDestCheck: source_dest_check | default(false) | lower | string - ec2_spot_instance_request_id: spot_instance_request_id | default("") - ec2_state: state.name - ec2_state_code: state.code - ec2_state_reason: state_reason.message if state_reason is defined else "" - ec2_subnet_id: subnet_id | default("") - ec2_tag_Name: tags.Name - ec2_virtualization_type: virtualization_type - ec2_vpc_id: vpc_id | default("") -filters: - instance-state-name: - - running -groups: - ec2: true -hostnames: -- dns-name -iam_role_arn: arn:aws:iam::123456789012:role/test-role -keyed_groups: -- key: placement.availability_zone - parent_group: zones - prefix: '' - separator: '' -- key: instance_type | regex_replace("[^A-Za-z0-9\_]", "_") - parent_group: types - prefix: type -- key: placement.region - parent_group: regions - prefix: '' - separator: '' -- key: dict(tags.keys() | map("regex_replace", "[^A-Za-z0-9\_]", "_") | list | zip(tags.values() | map("regex_replace", "[^A-Za-z0-9\_]", "_") | list)) - parent_group: tags - prefix: tag -- key: tags.keys() | map("regex_replace", "[^A-Za-z0-9\_]", "_") | list - parent_group: tags - prefix: tag -- key: placement.availability_zone - parent_group: '{{ placement.region }}' - prefix: '' - separator: '' -plugin: amazon.aws.aws_ec2 -regions: -- us-east-2 -- ap-south-1 -use_contrib_script_compatible_sanitization: true diff --git a/awx/main/tests/data/inventory/plugins/gce/files/gcp_compute.yml b/awx/main/tests/data/inventory/plugins/gce/files/gcp_compute.yml deleted file mode 100644 index 63f8a44f64..0000000000 --- a/awx/main/tests/data/inventory/plugins/gce/files/gcp_compute.yml +++ /dev/null @@ -1,50 +0,0 @@ -auth_kind: serviceaccount -compose: - ansible_ssh_host: networkInterfaces[0].accessConfigs[0].natIP | default(networkInterfaces[0].networkIP) - gce_description: description if description else None - gce_id: id - gce_image: image - gce_machine_type: machineType - gce_metadata: metadata.get("items", []) | items2dict(key_name="key", value_name="value") - gce_name: name - gce_network: networkInterfaces[0].network.name - gce_private_ip: networkInterfaces[0].networkIP - gce_public_ip: networkInterfaces[0].accessConfigs[0].natIP | default(None) - gce_status: status - gce_subnetwork: networkInterfaces[0].subnetwork.name - gce_tags: tags.get("items", []) - gce_zone: zone -hostnames: -- name -- public_ip -- private_ip -keyed_groups: -- key: gce_subnetwork - prefix: network -- key: gce_private_ip - prefix: '' - separator: '' -- key: gce_public_ip - prefix: '' - separator: '' -- key: machineType - prefix: '' - separator: '' -- key: zone - prefix: '' - separator: '' -- key: gce_tags - prefix: tag -- key: status | lower - prefix: status -- key: image - prefix: '' - separator: '' -plugin: google.cloud.gcp_compute -projects: -- fooo -retrieve_image_info: true -use_contrib_script_compatible_sanitization: true -zones: -- us-east4-a -- us-west1-b diff --git a/awx/main/tests/data/inventory/plugins/openstack/files/file_reference b/awx/main/tests/data/inventory/plugins/openstack/files/file_reference index 895a1eb8a8..c578942ca1 100644 --- a/awx/main/tests/data/inventory/plugins/openstack/files/file_reference +++ b/awx/main/tests/data/inventory/plugins/openstack/files/file_reference @@ -1,7 +1,3 @@ -ansible: - expand_hostvars: true - fail_on_errors: true - use_hostnames: false clouds: devstack: auth: @@ -11,5 +7,5 @@ clouds: project_domain_name: fooo project_name: fooo username: fooo - private: false + private: true verify: false diff --git a/awx/main/tests/data/inventory/plugins/openstack/files/openstack.yml b/awx/main/tests/data/inventory/plugins/openstack/files/openstack.yml deleted file mode 100644 index 36e9024b54..0000000000 --- a/awx/main/tests/data/inventory/plugins/openstack/files/openstack.yml +++ /dev/null @@ -1,4 +0,0 @@ -expand_hostvars: true -fail_on_errors: true -inventory_hostname: uuid -plugin: openstack.cloud.openstack diff --git a/awx/main/tests/data/inventory/plugins/rhv/files/ovirt.yml b/awx/main/tests/data/inventory/plugins/rhv/files/ovirt.yml deleted file mode 100644 index 67a94ae6de..0000000000 --- a/awx/main/tests/data/inventory/plugins/rhv/files/ovirt.yml +++ /dev/null @@ -1,20 +0,0 @@ -base_source_var: value_of_var -compose: - ansible_host: (devices.values() | list)[0][0] if devices else None -groups: - dev: '"dev" in tags' -keyed_groups: -- key: cluster - prefix: cluster - separator: _ -- key: status - prefix: status - separator: _ -- key: tags - prefix: tag - separator: _ -ovirt_hostname_preference: -- name -- fqdn -ovirt_insecure: false -plugin: ovirt.ovirt.ovirt diff --git a/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml b/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml deleted file mode 100644 index fcad2586f6..0000000000 --- a/awx/main/tests/data/inventory/plugins/satellite6/files/foreman.yml +++ /dev/null @@ -1,30 +0,0 @@ -base_source_var: value_of_var -compose: - ansible_ssh_host: foreman['ip6'] | default(foreman['ip'], true) -group_prefix: foo_group_prefix -keyed_groups: -- key: foreman['environment_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') | regex_replace('none', '') - prefix: foo_group_prefixenvironment_ - separator: '' -- key: foreman['location_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') - prefix: foo_group_prefixlocation_ - separator: '' -- key: foreman['organization_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') - prefix: foo_group_prefixorganization_ - separator: '' -- key: foreman['content_facet_attributes']['lifecycle_environment_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') - prefix: foo_group_prefixlifecycle_environment_ - separator: '' -- key: foreman['content_facet_attributes']['content_view_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_') - prefix: foo_group_prefixcontent_view_ - separator: '' -- key: '"%s-%s-%s" | format(app, tier, color)' - separator: '' -- key: '"%s-%s" | format(app, color)' - separator: '' -legacy_hostvars: true -plugin: theforeman.foreman.foreman -validate_certs: false -want_facts: true -want_hostcollections: true -want_params: true diff --git a/awx/main/tests/data/inventory/plugins/tower/files/tower.yml b/awx/main/tests/data/inventory/plugins/tower/files/tower.yml deleted file mode 100644 index 2c41f1b55d..0000000000 --- a/awx/main/tests/data/inventory/plugins/tower/files/tower.yml +++ /dev/null @@ -1,3 +0,0 @@ -include_metadata: true -inventory_id: 42 -plugin: awx.awx.tower diff --git a/awx/main/tests/data/inventory/plugins/vmware/files/vmware_vm_inventory.yml b/awx/main/tests/data/inventory/plugins/vmware/files/vmware_vm_inventory.yml deleted file mode 100644 index ac1db9f4cf..0000000000 --- a/awx/main/tests/data/inventory/plugins/vmware/files/vmware_vm_inventory.yml +++ /dev/null @@ -1,55 +0,0 @@ -compose: - ansible_host: guest.ipAddress - ansible_ssh_host: guest.ipAddress - ansible_uuid: 99999999 | random | to_uuid - availablefield: availableField - configissue: configIssue - configstatus: configStatus - customvalue: customValue - effectiverole: effectiveRole - guestheartbeatstatus: guestHeartbeatStatus - layoutex: layoutEx - overallstatus: overallStatus - parentvapp: parentVApp - recenttask: recentTask - resourcepool: resourcePool - rootsnapshot: rootSnapshot - triggeredalarmstate: triggeredAlarmState -filters: -- config.zoo == "DC0_H0_VM0" -hostnames: -- config.foo -keyed_groups: -- key: config.asdf - prefix: '' - separator: '' -plugin: community.vmware.vmware_vm_inventory -properties: -- availableField -- configIssue -- configStatus -- customValue -- datastore -- effectiveRole -- guestHeartbeatStatus -- layout -- layoutEx -- name -- network -- overallStatus -- parentVApp -- permission -- recentTask -- resourcePool -- rootSnapshot -- snapshot -- triggeredAlarmState -- value -- capability -- config -- guest -- runtime -- storage -- summary -strict: false -with_nested_properties: true diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index e63286f7e8..8eda9cddd0 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import pytest +import json from unittest import mock from django.core.exceptions import ValidationError @@ -8,8 +9,6 @@ from awx.api.versioning import reverse from awx.main.models import InventorySource, Inventory, ActivityStream -import json - @pytest.fixture def scm_inventory(inventory, project): @@ -457,6 +456,56 @@ def test_inventory_source_vars_prohibition(post, inventory, admin_user): assert 'FOOBAR' in r.data['source_vars'][0] +@pytest.mark.django_db +@pytest.mark.parametrize('source,source_var_actual,source_var_expected,description', [ + ('ec2', {'plugin': 'blah'}, {'plugin': 'amazon.aws.aws_ec2'}, 'source plugin mismatch'), + ('ec2', {'plugin': 'amazon.aws.aws_ec2'}, {'plugin': 'amazon.aws.aws_ec2'}, 'valid plugin'), +]) +def test_inventory_source_vars_source_plugin_ok(post, inventory, admin_user, source, source_var_actual, source_var_expected, description): + r = post(reverse('api:inventory_source_list'), + {'name': 'new inv src', 'source_vars': json.dumps(source_var_actual), 'inventory': inventory.pk, 'source': source}, + admin_user, expect=201) + + assert r.data['source_vars'] == json.dumps(source_var_expected) + + +@pytest.mark.django_db +@pytest.mark.parametrize('source_var_actual,description', [ + ({'plugin': 'namespace.collection.script'}, 'valid scm user plugin'), +]) +def test_inventory_source_vars_source_plugin_scm_ok(post, inventory, admin_user, project, source_var_actual, description): + r = post(reverse('api:inventory_source_list'), + {'name': 'new inv src', + 'source_vars': json.dumps(source_var_actual), + 'inventory': inventory.pk, + 'source': 'scm', + 'source_project': project.id,}, + admin_user, expect=201) + + assert r.data['source_vars'] == json.dumps(source_var_actual) + + +@pytest.mark.django_db +@pytest.mark.parametrize('source_var_actual,err_msg,description', [ + ({'foo': 'bar'}, 'plugin: must be present and of the form namespace.collection.inv_plugin', 'no plugin line'), + ({'plugin': ''}, 'plugin: must be of the form namespace.collection.inv_plugin', 'blank plugin line'), + ({'plugin': '.'}, 'plugin: must be of the form namespace.collection.inv_plugin', 'missing namespace, collection name, and inventory plugin'), + ({'plugin': 'a.'}, 'plugin: must be of the form namespace.collection.inv_plugin', 'missing collection name and inventory plugin'), + ({'plugin': 'a.b'}, 'plugin: must be of the form namespace.collection.inv_plugin', 'missing inventory plugin'), + ({'plugin': 'a.b.'}, 'plugin: must be of the form namespace.collection.inv_plugin', 'missing inventory plugin'), +]) +def test_inventory_source_vars_source_plugin_scm_invalid(post, inventory, admin_user, project, source_var_actual, err_msg, description): + r = post(reverse('api:inventory_source_list'), + {'name': 'new inv src', + 'source_vars': json.dumps(source_var_actual), + 'inventory': inventory.pk, + 'source': 'scm', + 'source_project': project.id,}, + admin_user, expect=400) + + assert err_msg in r.data['source_vars'][0] + + @pytest.mark.django_db @pytest.mark.parametrize('role,expect', [ ('admin_role', 200), @@ -522,7 +571,8 @@ class TestInventorySourceCredential: data={ 'inventory': inventory.pk, 'name': 'fobar', 'source': 'scm', 'source_project': project.pk, 'source_path': '', - 'credential': vault_credential.pk + 'credential': vault_credential.pk, + 'source_vars': 'plugin: a.b.c', }, expect=400, user=admin_user @@ -561,7 +611,7 @@ class TestInventorySourceCredential: data={ 'inventory': inventory.pk, 'name': 'fobar', 'source': 'scm', 'source_project': project.pk, 'source_path': '', - 'credential': os_cred.pk + 'credential': os_cred.pk, 'source_vars': 'plugin: a.b.c', }, expect=201, user=admin_user @@ -636,8 +686,14 @@ class TestControlledBySCM: assert scm_inventory.inventory_sources.count() == 0 def test_adding_inv_src_ok(self, post, scm_inventory, project, admin_user): - post(reverse('api:inventory_inventory_sources_list', kwargs={'pk': scm_inventory.id}), - {'name': 'new inv src', 'source_project': project.pk, 'update_on_project_update': False, 'source': 'scm', 'overwrite_vars': True}, + post(reverse('api:inventory_inventory_sources_list', + kwargs={'pk': scm_inventory.id}), + {'name': 'new inv src', + 'source_project': project.pk, + 'update_on_project_update': False, + 'source': 'scm', + 'overwrite_vars': True, + 'source_vars': 'plugin: a.b.c'}, admin_user, expect=201) def test_adding_inv_src_prohibited(self, post, scm_inventory, project, admin_user): @@ -657,7 +713,7 @@ class TestControlledBySCM: def test_adding_inv_src_without_proj_access_prohibited(self, post, project, inventory, rando): inventory.admin_role.members.add(rando) post(reverse('api:inventory_inventory_sources_list', kwargs={'pk': inventory.id}), - {'name': 'new inv src', 'source_project': project.pk, 'source': 'scm', 'overwrite_vars': True}, + {'name': 'new inv src', 'source_project': project.pk, 'source': 'scm', 'overwrite_vars': True, 'source_vars': 'plugin: a.b.c'}, rando, expect=403) diff --git a/awx/main/tests/functional/models/test_inventory.py b/awx/main/tests/functional/models/test_inventory.py index 6765f0e73b..2b3c747868 100644 --- a/awx/main/tests/functional/models/test_inventory.py +++ b/awx/main/tests/functional/models/test_inventory.py @@ -2,7 +2,6 @@ import pytest from unittest import mock -import json from django.core.exceptions import ValidationError @@ -259,30 +258,19 @@ class TestInventorySourceInjectors: injector = InventorySource.injectors[source]('2.7.7') assert injector.filename == filename - def test_group_by_azure(self): - injector = InventorySource.injectors['azure_rm']('2.9') - inv_src = InventorySource( - name='azure source', source='azure_rm', - source_vars={'group_by_os_family': True} - ) - group_by_on = injector.inventory_as_dict(inv_src, '/tmp/foo') - # suspicious, yes, that is just what the script did - expected_groups = 6 - assert len(group_by_on['keyed_groups']) == expected_groups - inv_src.source_vars = json.dumps({'group_by_os_family': False}) - group_by_off = injector.inventory_as_dict(inv_src, '/tmp/foo') - # much better, everyone should turn off the flag and live in the future - assert len(group_by_off['keyed_groups']) == expected_groups - 1 - - def test_tower_plugin_named_url(self): - injector = InventorySource.injectors['tower']('2.9') - inv_src = InventorySource( - name='my tower source', source='tower', - # named URL pattern "inventory++organization" - instance_filters='Designer hair 읰++Cosmetic_products䵆' - ) - result = injector.inventory_as_dict(inv_src, '/tmp/foo') - assert result['inventory_id'] == 'Designer%20hair%20%EC%9D%B0++Cosmetic_products%E4%B5%86' + @pytest.mark.parametrize('source,proper_name', [ + ('ec2', 'amazon.aws.aws_ec2'), + ('openstack', 'openstack.cloud.openstack'), + ('gce', 'google.cloud.gcp_compute'), + ('azure_rm', 'azure.azcollection.azure_rm'), + ('vmware', 'community.vmware.vmware_vm_inventory'), + ('rhv', 'ovirt.ovirt.ovirt'), + ('satellite6', 'theforeman.foreman.foreman'), + ('tower', 'awx.awx.tower'), + ]) + def test_plugin_proper_names(self, source, proper_name): + injector = InventorySource.injectors[source]('2.9') + assert injector.get_proper_name() == proper_name @pytest.mark.django_db diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index 392abf8535..994c2b4c11 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -14,69 +14,6 @@ from django.conf import settings DATA = os.path.join(os.path.dirname(data.__file__), 'inventory') -TEST_SOURCE_FIELDS = { - 'vmware': { - 'instance_filters': '{{ config.name == "only_my_server" }},{{ somevar == "bar"}}', - 'group_by': 'fouo' - }, - 'ec2': { - 'instance_filters': 'foobaa', - # group_by selected to capture some non-trivial cross-interactions - 'group_by': 'availability_zone,instance_type,tag_keys,region', - 'source_regions': 'us-east-2,ap-south-1' - }, - 'gce': { - 'source_regions': 'us-east4-a,us-west1-b' # surfaced as env var - }, - 'azure_rm': { - 'source_regions': 'southcentralus,westus' - }, - 'tower': { - 'instance_filters': '42' - } -} - -INI_TEST_VARS = { - 'ec2': { - 'boto_profile': '/tmp/my_boto_stuff', - 'iam_role_arn': 'arn:aws:iam::123456789012:role/test-role', - 'hostname_variable': 'public_dns_name', - 'destination_variable': 'public_dns_name' - }, - 'gce': {}, - 'openstack': { - 'private': False, - 'use_hostnames': False, - 'expand_hostvars': True, - 'fail_on_errors': True - }, - 'tower': {}, # there are none - 'vmware': { - 'alias_pattern': "{{ config.foo }}", - 'host_filters': '{{ config.zoo == "DC0_H0_VM0" }}', - 'groupby_patterns': "{{ config.asdf }}", - # setting VMWARE_VALIDATE_CERTS is duplicated with env var - }, - 'azure_rm': { - 'use_private_ip': True, - 'resource_groups': 'foo_resources,bar_resources', - 'tags': 'Creator:jmarshall, peanutbutter:jelly' - }, - 'satellite6': { - 'satellite6_group_patterns': '["{app}-{tier}-{color}", "{app}-{color}"]', - 'satellite6_group_prefix': 'foo_group_prefix', - 'satellite6_want_hostcollections': True, - 'satellite6_want_ansible_ssh_host': True, - 'satellite6_want_facts': True - }, - 'rhv': { # options specific to the plugin - 'ovirt_insecure': False, - 'groups': { - 'dev': '"dev" in tags' - } - } -} - def generate_fake_var(element): """Given a credential type field element, makes up something acceptable. @@ -245,25 +182,21 @@ def create_reference_data(source_dir, env, content): @pytest.mark.django_db @pytest.mark.parametrize('this_kind', CLOUD_PROVIDERS) def test_inventory_update_injected_content(this_kind, inventory, fake_credential_factory): + injector = InventorySource.injectors[this_kind] + if injector.plugin_name is None: + pytest.skip('Use of inventory plugin is not enabled for this source') + src_vars = dict(base_source_var='value_of_var') - if this_kind in INI_TEST_VARS: - src_vars.update(INI_TEST_VARS[this_kind]) - extra_kwargs = {} - if this_kind in TEST_SOURCE_FIELDS: - extra_kwargs.update(TEST_SOURCE_FIELDS[this_kind]) + src_vars['plugin'] = injector.get_proper_name() inventory_source = InventorySource.objects.create( inventory=inventory, source=this_kind, source_vars=src_vars, - **extra_kwargs ) inventory_source.credentials.add(fake_credential_factory(this_kind)) inventory_update = inventory_source.create_unified_job() task = RunInventoryUpdate() - if InventorySource.injectors[this_kind].plugin_name is None: - pytest.skip('Use of inventory plugin is not enabled for this source') - def substitute_run(envvars=None, **_kw): """This method will replace run_pexpect instead of running, it will read the private data directory contents @@ -274,6 +207,12 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential assert envvars.pop('ANSIBLE_INVENTORY_ENABLED') == 'auto' set_files = bool(os.getenv("MAKE_INVENTORY_REFERENCE_FILES", 'false').lower()[0] not in ['f', '0']) env, content = read_content(private_data_dir, envvars, inventory_update) + + # Assert inventory plugin inventory file is in private_data_dir + inventory_filename = InventorySource.injectors[inventory_update.source]('2.9').filename + assert len([True for k in content.keys() if k.endswith(inventory_filename)]) > 0, \ + f"'{inventory_filename}' file not found in inventory update runtime files {content.keys()}" + env.pop('ANSIBLE_COLLECTIONS_PATHS', None) # collection paths not relevant to this test base_dir = os.path.join(DATA, 'plugins') if not os.path.exists(base_dir): @@ -283,6 +222,8 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential create_reference_data(source_dir, env, content) pytest.skip('You set MAKE_INVENTORY_REFERENCE_FILES, so this created files, unset to run actual test.') else: + source_dir = os.path.join(base_dir, this_kind) # this_kind is a global + if not os.path.exists(source_dir): raise FileNotFoundError( 'Maybe you never made reference files? ' @@ -292,9 +233,6 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential expected_file_list = os.listdir(files_dir) except FileNotFoundError: expected_file_list = [] - assert set(expected_file_list) == set(content.keys()), ( - 'Inventory update runtime environment does not have expected files' - ) for f_name in expected_file_list: with open(os.path.join(files_dir, f_name), 'r') as f: ref_content = f.read() diff --git a/awx/main/tests/unit/models/test_inventory.py b/awx/main/tests/unit/models/test_inventory.py index dc6af0e828..26ef5e1fa9 100644 --- a/awx/main/tests/unit/models/test_inventory.py +++ b/awx/main/tests/unit/models/test_inventory.py @@ -72,23 +72,6 @@ def test_invalid_kind_clean_insights_credential(): assert json.dumps(str(e.value)) == json.dumps(str([u'Assignment not allowed for Smart Inventory'])) -@pytest.mark.parametrize('source_vars,validate_certs', [ - ({'ssl_verify': True}, True), - ({'ssl_verify': False}, False), - ({'validate_certs': True}, True), - ({'validate_certs': False}, False)]) -def test_satellite_plugin_backwards_support_for_ssl_verify(source_vars, validate_certs): - injector = InventorySource.injectors['satellite6']('2.9') - inv_src = InventorySource( - name='satellite source', source='satellite6', - source_vars=source_vars - ) - - ret = injector.inventory_as_dict(inv_src, '/tmp/foo') - assert 'validate_certs' in ret - assert ret['validate_certs'] in (validate_certs, str(validate_certs)) - - class TestControlledBySCM(): def test_clean_source_path_valid(self): inv_src = InventorySource(source_path='/not_real/', diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index cb7dc49e46..93e373e401 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -8,8 +8,6 @@ from datetime import timedelta # global settings from django.conf import global_settings -# ugettext lazy -from django.utils.translation import ugettext_lazy as _ # Update this module's local settings from the global settings module. this_module = sys.modules[__name__] From 35d264d7f8217114f60c921a5801a4ba48de0e44 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 29 Jul 2020 14:29:38 -0400 Subject: [PATCH 166/188] forgot to add migration helper * move models/inventory.py plugin injector logic to a place frozen in time useable by the migration code. --- awx/main/migrations/_inventory_source_vars.py | 749 ++++++++++++++++++ 1 file changed, 749 insertions(+) create mode 100644 awx/main/migrations/_inventory_source_vars.py diff --git a/awx/main/migrations/_inventory_source_vars.py b/awx/main/migrations/_inventory_source_vars.py new file mode 100644 index 0000000000..857bb1c07e --- /dev/null +++ b/awx/main/migrations/_inventory_source_vars.py @@ -0,0 +1,749 @@ +from django.utils.translation import ugettext_lazy as _ + + +FrozenInjectors = dict() + + +class PluginFileInjector(object): + plugin_name = None # Ansible core name used to reference plugin + # every source should have collection, these are for the collection name + namespace = None + collection = None + + def inventory_as_dict(self, inventory_source, private_data_dir): + """Default implementation of inventory plugin file contents. + There are some valid cases when all parameters can be obtained from + the environment variables, example "plugin: linode" is valid + ideally, however, some options should be filled from the inventory source data + """ + if self.plugin_name is None: + raise NotImplementedError('At minimum the plugin name is needed for inventory plugin use.') + proper_name = f'{self.namespace}.{self.collection}.{self.plugin_name}' + return {'plugin': proper_name} + + +class azure_rm(PluginFileInjector): + plugin_name = 'azure_rm' + namespace = 'azure' + collection = 'azcollection' + + def inventory_as_dict(self, inventory_source, private_data_dir): + ret = super(azure_rm, self).inventory_as_dict(inventory_source, private_data_dir) + + source_vars = inventory_source.source_vars_dict + + ret['fail_on_template_errors'] = False + + group_by_hostvar = { + 'location': {'prefix': '', 'separator': '', 'key': 'location'}, + 'tag': {'prefix': '', 'separator': '', 'key': 'tags.keys() | list if tags else []'}, + # Introduced with https://github.com/ansible/ansible/pull/53046 + 'security_group': {'prefix': '', 'separator': '', 'key': 'security_group'}, + 'resource_group': {'prefix': '', 'separator': '', 'key': 'resource_group'}, + # Note, os_family was not documented correctly in script, but defaulted to grouping by it + 'os_family': {'prefix': '', 'separator': '', 'key': 'os_disk.operating_system_type'} + } + # by default group by everything + # always respect user setting, if they gave it + group_by = [ + grouping_name for grouping_name in group_by_hostvar + if source_vars.get('group_by_{}'.format(grouping_name), True) + ] + ret['keyed_groups'] = [group_by_hostvar[grouping_name] for grouping_name in group_by] + if 'tag' in group_by: + # Nasty syntax to reproduce "key_value" group names in addition to "key" + ret['keyed_groups'].append({ + 'prefix': '', 'separator': '', + 'key': r'dict(tags.keys() | map("regex_replace", "^(.*)$", "\1_") | list | zip(tags.values() | list)) if tags else []' + }) + + # Compatibility content + # TODO: add proper support for instance_filters non-specific to compatibility + # TODO: add proper support for group_by non-specific to compatibility + # Dashes were not configurable in azure_rm.py script, we do not want unicode, so always use this + ret['use_contrib_script_compatible_sanitization'] = True + # use same host names as script + ret['plain_host_names'] = True + # By default the script did not filter hosts + ret['default_host_filters'] = [] + # User-given host filters + user_filters = [] + old_filterables = [ + ('resource_groups', 'resource_group'), + ('tags', 'tags') + # locations / location would be an entry + # but this would conflict with source_regions + ] + for key, loc in old_filterables: + value = source_vars.get(key, None) + if value and isinstance(value, str): + # tags can be list of key:value pairs + # e.g. 'Creator:jmarshall, peanutbutter:jelly' + # or tags can be a list of keys + # e.g. 'Creator, peanutbutter' + if key == "tags": + # grab each key value pair + for kvpair in value.split(','): + # split into key and value + kv = kvpair.split(':') + # filter out any host that does not have key + # in their tags.keys() variable + user_filters.append('"{}" not in tags.keys()'.format(kv[0].strip())) + # if a value is provided, check that the key:value pair matches + if len(kv) > 1: + user_filters.append('tags["{}"] != "{}"'.format(kv[0].strip(), kv[1].strip())) + else: + user_filters.append('{} not in {}'.format( + loc, value.split(',') + )) + if user_filters: + ret.setdefault('exclude_host_filters', []) + ret['exclude_host_filters'].extend(user_filters) + + ret['conditional_groups'] = {'azure': True} + ret['hostvar_expressions'] = { + 'provisioning_state': 'provisioning_state | title', + 'computer_name': 'name', + 'type': 'resource_type', + 'private_ip': 'private_ipv4_addresses[0] if private_ipv4_addresses else None', + 'public_ip': 'public_ipv4_addresses[0] if public_ipv4_addresses else None', + 'public_ip_name': 'public_ip_name if public_ip_name is defined else None', + 'public_ip_id': 'public_ip_id if public_ip_id is defined else None', + 'tags': 'tags if tags else None' + } + # Special functionality from script + if source_vars.get('use_private_ip', False): + ret['hostvar_expressions']['ansible_host'] = 'private_ipv4_addresses[0]' + # end compatibility content + + if inventory_source.source_regions and 'all' not in inventory_source.source_regions: + # initialize a list for this section in inventory file + ret.setdefault('exclude_host_filters', []) + # make a python list of the regions we will use + python_regions = [x.strip() for x in inventory_source.source_regions.split(',')] + # convert that list in memory to python syntax in a string + # now put that in jinja2 syntax operating on hostvar key "location" + # and put that as an entry in the exclusions list + ret['exclude_host_filters'].append("location not in {}".format(repr(python_regions))) + return ret + +class ec2(PluginFileInjector): + plugin_name = 'aws_ec2' + namespace = 'amazon' + collection = 'aws' + + + def _get_ec2_group_by_choices(self): + return [ + ('ami_id', _('Image ID')), + ('availability_zone', _('Availability Zone')), + ('aws_account', _('Account')), + ('instance_id', _('Instance ID')), + ('instance_state', _('Instance State')), + ('platform', _('Platform')), + ('instance_type', _('Instance Type')), + ('key_pair', _('Key Name')), + ('region', _('Region')), + ('security_group', _('Security Group')), + ('tag_keys', _('Tags')), + ('tag_none', _('Tag None')), + ('vpc_id', _('VPC ID')), + ] + + def _compat_compose_vars(self): + return { + # vars that change + 'ec2_block_devices': ( + "dict(block_device_mappings | map(attribute='device_name') | list | zip(block_device_mappings " + "| map(attribute='ebs.volume_id') | list))" + ), + 'ec2_dns_name': 'public_dns_name', + 'ec2_group_name': 'placement.group_name', + 'ec2_instance_profile': 'iam_instance_profile | default("")', + 'ec2_ip_address': 'public_ip_address', + 'ec2_kernel': 'kernel_id | default("")', + 'ec2_monitored': "monitoring.state in ['enabled', 'pending']", + 'ec2_monitoring_state': 'monitoring.state', + 'ec2_placement': 'placement.availability_zone', + 'ec2_ramdisk': 'ramdisk_id | default("")', + 'ec2_reason': 'state_transition_reason', + 'ec2_security_group_ids': "security_groups | map(attribute='group_id') | list | join(',')", + 'ec2_security_group_names': "security_groups | map(attribute='group_name') | list | join(',')", + 'ec2_tag_Name': 'tags.Name', + 'ec2_state': 'state.name', + 'ec2_state_code': 'state.code', + 'ec2_state_reason': 'state_reason.message if state_reason is defined else ""', + 'ec2_sourceDestCheck': 'source_dest_check | default(false) | lower | string', # snake_case syntax intended + 'ec2_account_id': 'owner_id', + # vars that just need ec2_ prefix + 'ec2_ami_launch_index': 'ami_launch_index | string', + 'ec2_architecture': 'architecture', + 'ec2_client_token': 'client_token', + 'ec2_ebs_optimized': 'ebs_optimized', + 'ec2_hypervisor': 'hypervisor', + 'ec2_image_id': 'image_id', + 'ec2_instance_type': 'instance_type', + 'ec2_key_name': 'key_name', + 'ec2_launch_time': r'launch_time | regex_replace(" ", "T") | regex_replace("(\+)(\d\d):(\d)(\d)$", ".\g<2>\g<3>Z")', + 'ec2_platform': 'platform | default("")', + 'ec2_private_dns_name': 'private_dns_name', + 'ec2_private_ip_address': 'private_ip_address', + 'ec2_public_dns_name': 'public_dns_name', + 'ec2_region': 'placement.region', + 'ec2_root_device_name': 'root_device_name', + 'ec2_root_device_type': 'root_device_type', + # many items need blank defaults because the script tended to keep a common schema + 'ec2_spot_instance_request_id': 'spot_instance_request_id | default("")', + 'ec2_subnet_id': 'subnet_id | default("")', + 'ec2_virtualization_type': 'virtualization_type', + 'ec2_vpc_id': 'vpc_id | default("")', + # same as ec2_ip_address, the script provided this + 'ansible_host': 'public_ip_address', + # new with https://github.com/ansible/ansible/pull/53645 + 'ec2_eventsSet': 'events | default("")', + 'ec2_persistent': 'persistent | default(false)', + 'ec2_requester_id': 'requester_id | default("")' + } + + def inventory_as_dict(self, inventory_source, private_data_dir): + ret = super(ec2, self).inventory_as_dict(inventory_source, private_data_dir) + + keyed_groups = [] + group_by_hostvar = { + 'ami_id': {'prefix': '', 'separator': '', 'key': 'image_id', 'parent_group': 'images'}, + # 2 entries for zones for same groups to establish 2 parentage trees + 'availability_zone': {'prefix': '', 'separator': '', 'key': 'placement.availability_zone', 'parent_group': 'zones'}, + 'aws_account': {'prefix': '', 'separator': '', 'key': 'ec2_account_id', 'parent_group': 'accounts'}, # composed var + 'instance_id': {'prefix': '', 'separator': '', 'key': 'instance_id', 'parent_group': 'instances'}, # normally turned off + 'instance_state': {'prefix': 'instance_state', 'key': 'ec2_state', 'parent_group': 'instance_states'}, # composed var + # ec2_platform is a composed var, but group names do not match up to hostvar exactly + 'platform': {'prefix': 'platform', 'key': 'platform | default("undefined")', 'parent_group': 'platforms'}, + 'instance_type': {'prefix': 'type', 'key': 'instance_type', 'parent_group': 'types'}, + 'key_pair': {'prefix': 'key', 'key': 'key_name', 'parent_group': 'keys'}, + 'region': {'prefix': '', 'separator': '', 'key': 'placement.region', 'parent_group': 'regions'}, + # Security requires some ninja jinja2 syntax, credit to s-hertel + 'security_group': {'prefix': 'security_group', 'key': 'security_groups | map(attribute="group_name")', 'parent_group': 'security_groups'}, + # tags cannot be parented in exactly the same way as the script due to + # https://github.com/ansible/ansible/pull/53812 + 'tag_keys': [ + {'prefix': 'tag', 'key': 'tags', 'parent_group': 'tags'}, + {'prefix': 'tag', 'key': 'tags.keys()', 'parent_group': 'tags'} + ], + # 'tag_none': None, # grouping by no tags isn't a different thing with plugin + # naming is redundant, like vpc_id_vpc_8c412cea, but intended + 'vpc_id': {'prefix': 'vpc_id', 'key': 'vpc_id', 'parent_group': 'vpcs'}, + } + # -- same-ish as script here -- + group_by = [x.strip().lower() for x in inventory_source.group_by.split(',') if x.strip()] + for choice in self._get_ec2_group_by_choices(): + value = bool((group_by and choice[0] in group_by) or (not group_by and choice[0] != 'instance_id')) + # -- end sameness to script -- + if value: + this_keyed_group = group_by_hostvar.get(choice[0], None) + # If a keyed group syntax does not exist, there is nothing we can do to get this group + if this_keyed_group is not None: + if isinstance(this_keyed_group, list): + keyed_groups.extend(this_keyed_group) + else: + keyed_groups.append(this_keyed_group) + # special case, this parentage is only added if both zones and regions are present + if not group_by or ('region' in group_by and 'availability_zone' in group_by): + keyed_groups.append({'prefix': '', 'separator': '', 'key': 'placement.availability_zone', 'parent_group': '{{ placement.region }}'}) + + source_vars = inventory_source.source_vars_dict + # This is a setting from the script, hopefully no one used it + # if true, it replaces dashes, but not in region / loc names + replace_dash = bool(source_vars.get('replace_dash_in_groups', True)) + # Compatibility content + legacy_regex = { + True: r"[^A-Za-z0-9\_]", + False: r"[^A-Za-z0-9\_\-]" # do not replace dash, dash is allowed + }[replace_dash] + list_replacer = 'map("regex_replace", "{rx}", "_") | list'.format(rx=legacy_regex) + # this option, a plugin option, will allow dashes, but not unicode + # when set to False, unicode will be allowed, but it was not allowed by script + # thus, we always have to use this option, and always use our custom regex + ret['use_contrib_script_compatible_sanitization'] = True + for grouping_data in keyed_groups: + if grouping_data['key'] in ('placement.region', 'placement.availability_zone'): + # us-east-2 is always us-east-2 according to ec2.py + # no sanitization in region-ish groups for the script standards, ever ever + continue + if grouping_data['key'] == 'tags': + # dict jinja2 transformation + grouping_data['key'] = 'dict(tags.keys() | {replacer} | zip(tags.values() | {replacer}))'.format( + replacer=list_replacer + ) + elif grouping_data['key'] == 'tags.keys()' or grouping_data['prefix'] == 'security_group': + # list jinja2 transformation + grouping_data['key'] += ' | {replacer}'.format(replacer=list_replacer) + else: + # string transformation + grouping_data['key'] += ' | regex_replace("{rx}", "_")'.format(rx=legacy_regex) + # end compatibility content + + if source_vars.get('iam_role_arn', None): + ret['iam_role_arn'] = source_vars['iam_role_arn'] + + # This was an allowed ec2.ini option, also plugin option, so pass through + if source_vars.get('boto_profile', None): + ret['boto_profile'] = source_vars['boto_profile'] + + elif not replace_dash: + # Using the plugin, but still want dashes allowed + ret['use_contrib_script_compatible_sanitization'] = True + + if source_vars.get('nested_groups') is False: + for this_keyed_group in keyed_groups: + this_keyed_group.pop('parent_group', None) + + if keyed_groups: + ret['keyed_groups'] = keyed_groups + + # Instance ID not part of compat vars, because of settings.EC2_INSTANCE_ID_VAR + compose_dict = {'ec2_id': 'instance_id'} + inst_filters = {} + + # Compatibility content + compose_dict.update(self._compat_compose_vars()) + # plugin provides "aws_ec2", but not this which the script gave + ret['groups'] = {'ec2': True} + if source_vars.get('hostname_variable') is not None: + hnames = [] + for expr in source_vars.get('hostname_variable').split(','): + if expr == 'public_dns_name': + hnames.append('dns-name') + elif not expr.startswith('tag:') and '_' in expr: + hnames.append(expr.replace('_', '-')) + else: + hnames.append(expr) + ret['hostnames'] = hnames + else: + # public_ip as hostname is non-default plugin behavior, script behavior + ret['hostnames'] = [ + 'network-interface.addresses.association.public-ip', + 'dns-name', + 'private-dns-name' + ] + # The script returned only running state by default, the plugin does not + # https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html#options + # options: pending | running | shutting-down | terminated | stopping | stopped + inst_filters['instance-state-name'] = ['running'] + # end compatibility content + + if source_vars.get('destination_variable') or source_vars.get('vpc_destination_variable'): + for fd in ('destination_variable', 'vpc_destination_variable'): + if source_vars.get(fd): + compose_dict['ansible_host'] = source_vars.get(fd) + break + + if compose_dict: + ret['compose'] = compose_dict + + if inventory_source.instance_filters: + # logic used to live in ec2.py, now it belongs to us. Yay more code? + filter_sets = [f for f in inventory_source.instance_filters.split(',') if f] + + for instance_filter in filter_sets: + # AND logic not supported, unclear how to... + instance_filter = instance_filter.strip() + if not instance_filter or '=' not in instance_filter: + continue + filter_key, filter_value = [x.strip() for x in instance_filter.split('=', 1)] + if not filter_key: + continue + inst_filters[filter_key] = filter_value + + if inst_filters: + ret['filters'] = inst_filters + + if inventory_source.source_regions and 'all' not in inventory_source.source_regions: + ret['regions'] = inventory_source.source_regions.split(',') + + return ret + + +class gce(PluginFileInjector): + plugin_name = 'gcp_compute' + namespace = 'google' + collection = 'cloud' + + def _compat_compose_vars(self): + # missing: gce_image, gce_uuid + # https://github.com/ansible/ansible/issues/51884 + return { + 'gce_description': 'description if description else None', + 'gce_machine_type': 'machineType', + 'gce_name': 'name', + 'gce_network': 'networkInterfaces[0].network.name', + 'gce_private_ip': 'networkInterfaces[0].networkIP', + 'gce_public_ip': 'networkInterfaces[0].accessConfigs[0].natIP | default(None)', + 'gce_status': 'status', + 'gce_subnetwork': 'networkInterfaces[0].subnetwork.name', + 'gce_tags': 'tags.get("items", [])', + 'gce_zone': 'zone', + 'gce_metadata': 'metadata.get("items", []) | items2dict(key_name="key", value_name="value")', + # NOTE: image hostvar is enabled via retrieve_image_info option + 'gce_image': 'image', + # We need this as long as hostnames is non-default, otherwise hosts + # will not be addressed correctly, was returned in script + 'ansible_ssh_host': 'networkInterfaces[0].accessConfigs[0].natIP | default(networkInterfaces[0].networkIP)' + } + + def inventory_as_dict(self, inventory_source, private_data_dir): + ret = super(gce, self).inventory_as_dict(inventory_source, private_data_dir) + + # auth related items + ret['auth_kind'] = "serviceaccount" + + filters = [] + # TODO: implement gce group_by options + # gce never processed the group_by field, if it had, we would selectively + # apply those options here, but it did not, so all groups are added here + keyed_groups = [ + # the jinja2 syntax is duplicated with compose + # https://github.com/ansible/ansible/issues/51883 + {'prefix': 'network', 'key': 'gce_subnetwork'}, # composed var + {'prefix': '', 'separator': '', 'key': 'gce_private_ip'}, # composed var + {'prefix': '', 'separator': '', 'key': 'gce_public_ip'}, # composed var + {'prefix': '', 'separator': '', 'key': 'machineType'}, + {'prefix': '', 'separator': '', 'key': 'zone'}, + {'prefix': 'tag', 'key': 'gce_tags'}, # composed var + {'prefix': 'status', 'key': 'status | lower'}, + # NOTE: image hostvar is enabled via retrieve_image_info option + {'prefix': '', 'separator': '', 'key': 'image'}, + ] + # This will be used as the gce instance_id, must be universal, non-compat + compose_dict = {'gce_id': 'id'} + + # Compatibility content + # TODO: proper group_by and instance_filters support, irrelevant of compat mode + # The gce.py script never sanitized any names in any way + ret['use_contrib_script_compatible_sanitization'] = True + # Perform extra API query to get the image hostvar + ret['retrieve_image_info'] = True + # Add in old hostvars aliases + compose_dict.update(self._compat_compose_vars()) + # Non-default names to match script + ret['hostnames'] = ['name', 'public_ip', 'private_ip'] + # end compatibility content + + if keyed_groups: + ret['keyed_groups'] = keyed_groups + if filters: + ret['filters'] = filters + if compose_dict: + ret['compose'] = compose_dict + if inventory_source.source_regions and 'all' not in inventory_source.source_regions: + ret['zones'] = inventory_source.source_regions.split(',') + return ret + + +class vmware(PluginFileInjector): + plugin_name = 'vmware_vm_inventory' + namespace = 'community' + collection = 'vmware' + + def inventory_as_dict(self, inventory_source, private_data_dir): + ret = super(vmware, self).inventory_as_dict(inventory_source, private_data_dir) + ret['strict'] = False + # Documentation of props, see + # https://github.com/ansible/ansible/blob/devel/docs/docsite/rst/scenario_guides/vmware_scenarios/vmware_inventory_vm_attributes.rst + UPPERCASE_PROPS = [ + "availableField", + "configIssue", + "configStatus", + "customValue", # optional + "datastore", + "effectiveRole", + "guestHeartbeatStatus", # optional + "layout", # optional + "layoutEx", # optional + "name", + "network", + "overallStatus", + "parentVApp", # optional + "permission", + "recentTask", + "resourcePool", + "rootSnapshot", + "snapshot", # optional + "triggeredAlarmState", + "value" + ] + NESTED_PROPS = [ + "capability", + "config", + "guest", + "runtime", + "storage", + "summary", # repeat of other properties + ] + ret['properties'] = UPPERCASE_PROPS + NESTED_PROPS + ret['compose'] = {'ansible_host': 'guest.ipAddress'} # default value + ret['compose']['ansible_ssh_host'] = ret['compose']['ansible_host'] + # the ansible_uuid was unique every host, every import, from the script + ret['compose']['ansible_uuid'] = '99999999 | random | to_uuid' + for prop in UPPERCASE_PROPS: + if prop == prop.lower(): + continue + ret['compose'][prop.lower()] = prop + ret['with_nested_properties'] = True + # ret['property_name_format'] = 'lower_case' # only dacrystal/topic/vmware-inventory-plugin-property-format + + # process custom options + vmware_opts = dict(inventory_source.source_vars_dict.items()) + if inventory_source.instance_filters: + vmware_opts.setdefault('host_filters', inventory_source.instance_filters) + if inventory_source.group_by: + vmware_opts.setdefault('groupby_patterns', inventory_source.group_by) + + alias_pattern = vmware_opts.get('alias_pattern') + if alias_pattern: + ret.setdefault('hostnames', []) + for alias in alias_pattern.split(','): # make best effort + striped_alias = alias.replace('{', '').replace('}', '').strip() # make best effort + if not striped_alias: + continue + ret['hostnames'].append(striped_alias) + + host_pattern = vmware_opts.get('host_pattern') # not working in script + if host_pattern: + stripped_hp = host_pattern.replace('{', '').replace('}', '').strip() # make best effort + ret['compose']['ansible_host'] = stripped_hp + ret['compose']['ansible_ssh_host'] = stripped_hp + + host_filters = vmware_opts.get('host_filters') + if host_filters: + ret.setdefault('filters', []) + for hf in host_filters.split(','): + striped_hf = hf.replace('{', '').replace('}', '').strip() # make best effort + if not striped_hf: + continue + ret['filters'].append(striped_hf) + else: + # default behavior filters by power state + ret['filters'] = ['runtime.powerState == "poweredOn"'] + + groupby_patterns = vmware_opts.get('groupby_patterns') + ret.setdefault('keyed_groups', []) + if groupby_patterns: + for pattern in groupby_patterns.split(','): + stripped_pattern = pattern.replace('{', '').replace('}', '').strip() # make best effort + ret['keyed_groups'].append({ + 'prefix': '', 'separator': '', + 'key': stripped_pattern + }) + else: + # default groups from script + for entry in ('config.guestId', '"templates" if config.template else "guests"'): + ret['keyed_groups'].append({ + 'prefix': '', 'separator': '', + 'key': entry + }) + + return ret + + +class openstack(PluginFileInjector): + plugin_name = 'openstack' + namespace = 'openstack' + collection = 'cloud' + + def inventory_as_dict(self, inventory_source, private_data_dir): + def use_host_name_for_name(a_bool_maybe): + if not isinstance(a_bool_maybe, bool): + # Could be specified by user via "host" or "uuid" + return a_bool_maybe + elif a_bool_maybe: + return 'name' # plugin default + else: + return 'uuid' + + ret = super(openstack, self).inventory_as_dict(inventory_source, private_data_dir) + ret['fail_on_errors'] = True + ret['expand_hostvars'] = True + ret['inventory_hostname'] = use_host_name_for_name(False) + # Note: mucking with defaults will break import integrity + # For the plugin, we need to use the same defaults as the old script + # or else imports will conflict. To find script defaults you have + # to read source code of the script. + # + # Script Defaults Plugin Defaults + # 'use_hostnames': False, 'name' (True) + # 'expand_hostvars': True, 'no' (False) + # 'fail_on_errors': True, 'no' (False) + # + # These are, yet again, different from ansible_variables in script logic + # but those are applied inconsistently + source_vars = inventory_source.source_vars_dict + for var_name in ['expand_hostvars', 'fail_on_errors']: + if var_name in source_vars: + ret[var_name] = source_vars[var_name] + if 'use_hostnames' in source_vars: + ret['inventory_hostname'] = use_host_name_for_name(source_vars['use_hostnames']) + return ret + +class rhv(PluginFileInjector): + """ovirt uses the custom credential templating, and that is all + """ + plugin_name = 'ovirt' + initial_version = '2.9' + namespace = 'ovirt' + collection = 'ovirt' + + def inventory_as_dict(self, inventory_source, private_data_dir): + ret = super(rhv, self).inventory_as_dict(inventory_source, private_data_dir) + ret['ovirt_insecure'] = False # Default changed from script + # TODO: process strict option upstream + ret['compose'] = { + 'ansible_host': '(devices.values() | list)[0][0] if devices else None' + } + ret['keyed_groups'] = [] + for key in ('cluster', 'status'): + ret['keyed_groups'].append({'prefix': key, 'separator': '_', 'key': key}) + ret['keyed_groups'].append({'prefix': 'tag', 'separator': '_', 'key': 'tags'}) + ret['ovirt_hostname_preference'] = ['name', 'fqdn'] + source_vars = inventory_source.source_vars_dict + for key, value in source_vars.items(): + if key == 'plugin': + continue + ret[key] = value + return ret + + +class satellite6(PluginFileInjector): + plugin_name = 'foreman' + namespace = 'theforeman' + collection = 'foreman' + + def inventory_as_dict(self, inventory_source, private_data_dir): + ret = super(satellite6, self).inventory_as_dict(inventory_source, private_data_dir) + ret['validate_certs'] = False + + group_patterns = '[]' + group_prefix = 'foreman_' + want_hostcollections = False + want_ansible_ssh_host = False + want_facts = True + + foreman_opts = inventory_source.source_vars_dict.copy() + for k, v in foreman_opts.items(): + if k == 'satellite6_group_patterns' and isinstance(v, str): + group_patterns = v + elif k == 'satellite6_group_prefix' and isinstance(v, str): + group_prefix = v + elif k == 'satellite6_want_hostcollections' and isinstance(v, bool): + want_hostcollections = v + elif k == 'satellite6_want_ansible_ssh_host' and isinstance(v, bool): + want_ansible_ssh_host = v + elif k == 'satellite6_want_facts' and isinstance(v, bool): + want_facts = v + # add backwards support for ssl_verify + # plugin uses new option, validate_certs, instead + elif k == 'ssl_verify' and isinstance(v, bool): + ret['validate_certs'] = v + else: + ret[k] = str(v) + + # Compatibility content + group_by_hostvar = { + "environment": {"prefix": "{}environment_".format(group_prefix), + "separator": "", + "key": "foreman['environment_name'] | lower | regex_replace(' ', '') | " + "regex_replace('[^A-Za-z0-9_]', '_') | regex_replace('none', '')"}, + "location": {"prefix": "{}location_".format(group_prefix), + "separator": "", + "key": "foreman['location_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, + "organization": {"prefix": "{}organization_".format(group_prefix), + "separator": "", + "key": "foreman['organization_name'] | lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, + "lifecycle_environment": {"prefix": "{}lifecycle_environment_".format(group_prefix), + "separator": "", + "key": "foreman['content_facet_attributes']['lifecycle_environment_name'] | " + "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"}, + "content_view": {"prefix": "{}content_view_".format(group_prefix), + "separator": "", + "key": "foreman['content_facet_attributes']['content_view_name'] | " + "lower | regex_replace(' ', '') | regex_replace('[^A-Za-z0-9_]', '_')"} + } + + ret['legacy_hostvars'] = True # convert hostvar structure to the form used by the script + ret['want_params'] = True + ret['group_prefix'] = group_prefix + ret['want_hostcollections'] = want_hostcollections + ret['want_facts'] = want_facts + + if want_ansible_ssh_host: + ret['compose'] = {'ansible_ssh_host': "foreman['ip6'] | default(foreman['ip'], true)"} + ret['keyed_groups'] = [group_by_hostvar[grouping_name] for grouping_name in group_by_hostvar] + + def form_keyed_group(group_pattern): + """ + Converts foreman group_pattern to + inventory plugin keyed_group + + e.g. {app_param}-{tier_param}-{dc_param} + becomes + "%s-%s-%s" | format(app_param, tier_param, dc_param) + """ + if type(group_pattern) is not str: + return None + params = re.findall('{[^}]*}', group_pattern) + if len(params) == 0: + return None + + param_names = [] + for p in params: + param_names.append(p[1:-1].strip()) # strip braces and space + + # form keyed_group key by + # replacing curly braces with '%s' + # (for use with jinja's format filter) + key = group_pattern + for p in params: + key = key.replace(p, '%s', 1) + + # apply jinja filter to key + key = '"{}" | format({})'.format(key, ', '.join(param_names)) + + keyed_group = {'key': key, + 'separator': ''} + return keyed_group + + try: + group_patterns = json.loads(group_patterns) + + if type(group_patterns) is list: + for group_pattern in group_patterns: + keyed_group = form_keyed_group(group_pattern) + if keyed_group: + ret['keyed_groups'].append(keyed_group) + except json.JSONDecodeError: + logger.warning('Could not parse group_patterns. Expected JSON-formatted string, found: {}' + .format(group_patterns)) + + return ret + + +class tower(PluginFileInjector): + plugin_name = 'tower' + namespace = 'awx' + collection = 'awx' + + def inventory_as_dict(self, inventory_source, private_data_dir): + ret = super(tower, self).inventory_as_dict(inventory_source, private_data_dir) + # Credentials injected as env vars, same as script + try: + # plugin can take an actual int type + identifier = int(inventory_source.instance_filters) + except ValueError: + # inventory_id could be a named URL + identifier = iri_to_uri(inventory_source.instance_filters) + ret['inventory_id'] = identifier + ret['include_metadata'] = True # used for license check + return ret + + +for cls in PluginFileInjector.__subclasses__(): + FrozenInjectors[cls.__name__] = cls From b7efad564047e16759aca2cd1449c5117adb820d Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 29 Jul 2020 15:26:17 -0400 Subject: [PATCH 167/188] do not enforce plugin: for source=scm * InventorySource w/ source type scm point to an inventory file via source_file. source_vars are ignored. --- awx/main/models/inventory.py | 7 ----- .../tests/functional/api/test_inventory.py | 26 +++---------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 7d6c350274..7d9069e4f0 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1163,13 +1163,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE source_vars = dict(self.source_vars_dict) # make a copy if injector and self.source_vars_dict.get('plugin', '') != injector.get_proper_name(): source_vars['plugin'] = injector.get_proper_name() - elif not injector: - source_vars = dict(self.source_vars_dict) # make a copy - collection_pattern = re.compile("^(.+)\.(.+)\.(.+)$") # noqa - if 'plugin' not in source_vars: - raise ValidationError(_("plugin: must be present and of the form namespace.collection.inv_plugin")) - elif not bool(collection_pattern.match(source_vars['plugin'])): - raise ValidationError(_("plugin: must be of the form namespace.collection.inv_plugin")) return json.dumps(source_vars) ''' diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index 8eda9cddd0..a04a68c9bb 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -471,7 +471,10 @@ def test_inventory_source_vars_source_plugin_ok(post, inventory, admin_user, sou @pytest.mark.django_db @pytest.mark.parametrize('source_var_actual,description', [ - ({'plugin': 'namespace.collection.script'}, 'valid scm user plugin'), + ({'plugin': 'namespace.collection.script'}, 'scm source type source_vars are ignored valid'), + ({'plugin': 'namespace.collection.script'}, 'scm source type source_vars are ignored invalid'), + ({'plugin': ''}, 'scm source type source_vars are ignored blank'), + ({}, 'scm source type source_vars are ignored non-existent'), ]) def test_inventory_source_vars_source_plugin_scm_ok(post, inventory, admin_user, project, source_var_actual, description): r = post(reverse('api:inventory_source_list'), @@ -485,27 +488,6 @@ def test_inventory_source_vars_source_plugin_scm_ok(post, inventory, admin_user, assert r.data['source_vars'] == json.dumps(source_var_actual) -@pytest.mark.django_db -@pytest.mark.parametrize('source_var_actual,err_msg,description', [ - ({'foo': 'bar'}, 'plugin: must be present and of the form namespace.collection.inv_plugin', 'no plugin line'), - ({'plugin': ''}, 'plugin: must be of the form namespace.collection.inv_plugin', 'blank plugin line'), - ({'plugin': '.'}, 'plugin: must be of the form namespace.collection.inv_plugin', 'missing namespace, collection name, and inventory plugin'), - ({'plugin': 'a.'}, 'plugin: must be of the form namespace.collection.inv_plugin', 'missing collection name and inventory plugin'), - ({'plugin': 'a.b'}, 'plugin: must be of the form namespace.collection.inv_plugin', 'missing inventory plugin'), - ({'plugin': 'a.b.'}, 'plugin: must be of the form namespace.collection.inv_plugin', 'missing inventory plugin'), -]) -def test_inventory_source_vars_source_plugin_scm_invalid(post, inventory, admin_user, project, source_var_actual, err_msg, description): - r = post(reverse('api:inventory_source_list'), - {'name': 'new inv src', - 'source_vars': json.dumps(source_var_actual), - 'inventory': inventory.pk, - 'source': 'scm', - 'source_project': project.id,}, - admin_user, expect=400) - - assert err_msg in r.data['source_vars'][0] - - @pytest.mark.django_db @pytest.mark.parametrize('role,expect', [ ('admin_role', 200), From 2eec1317bded2da851c3fb7f0f8e5081415c9917 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Mon, 3 Aug 2020 09:50:02 -0400 Subject: [PATCH 168/188] safer migrations --- .../migrations/0118_v380_inventory_plugins.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/awx/main/migrations/0118_v380_inventory_plugins.py b/awx/main/migrations/0118_v380_inventory_plugins.py index d2c9a06398..5ac36b6c1f 100644 --- a/awx/main/migrations/0118_v380_inventory_plugins.py +++ b/awx/main/migrations/0118_v380_inventory_plugins.py @@ -5,6 +5,8 @@ import json from django.db import migrations +from awx.main.models.inventory import InventorySourceOptions + from ._inventory_source_vars import FrozenInjectors @@ -12,8 +14,20 @@ logger = logging.getLogger('awx.main.migrations') BACKUP_FILENAME = '/tmp/tower_migration_inventory_source_vars.json' +class InventorySourceOptionsWrapper(InventorySourceOptions): + ''' + InventorySource inherits from InventorySourceOptions but that is not + "recorded" by Django's app registry model tracking. This will, effectively, + reintroduce the inheritance. + ''' + def __init__(self, *args, **kw): + self.target = kw.pop('target') + super().__init__(self, *args, **kw) + def __getattr__(self, attr): + return getattr(self.target, attr) + + def _get_inventory_sources(InventorySource): - # TODO: Maybe pull the list of cloud sources from code return InventorySource.objects.filter(source__in=['ec2', 'gce', 'azure_rm', 'vmware', 'satellite6', 'openstack', 'rhv', 'tower']) @@ -22,7 +36,7 @@ def inventory_source_vars_forward(apps, schema_editor): source_vars_backup = dict() for inv_source_obj in _get_inventory_sources(InventorySource): - # TODO: Log error if this is false, it shouldn't be false + inv_source_obj = InventorySourceOptionsWrapper(target=inv_source_obj) if inv_source_obj.source in FrozenInjectors: source_vars_backup[inv_source_obj.id] = dict(inv_source_obj.source_vars_dict) with open(BACKUP_FILENAME, 'w') as fh: From dce946e93fe55f3e280f6cfa667d18ab4371c341 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 29 Jul 2020 16:39:05 -0400 Subject: [PATCH 169/188] Delete inventory source fields --- .../sources/add/sources-add.controller.js | 107 --------------- .../sources/edit/sources-edit.controller.js | 122 ------------------ .../related/sources/sources.form.js | 40 ------ .../related/sources/sources.service.js | 18 --- 4 files changed, 287 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js index 9e254fd7e5..0ee7b8392b 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js @@ -24,49 +24,6 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars const virtualEnvs = ConfigData.custom_virtualenvs || []; $scope.custom_virtualenvs_options = virtualEnvs; - GetChoices({ - scope: $scope, - field: 'source_regions', - variable: 'rax_regions', - choice_name: 'rax_region_choices', - options: inventorySourcesOptions - }); - - GetChoices({ - scope: $scope, - field: 'source_regions', - variable: 'ec2_regions', - choice_name: 'ec2_region_choices', - options: inventorySourcesOptions - }); - - GetChoices({ - scope: $scope, - field: 'source_regions', - variable: 'gce_regions', - choice_name: 'gce_region_choices', - options: inventorySourcesOptions - }); - - GetChoices({ - scope: $scope, - field: 'source_regions', - variable: 'azure_regions', - choice_name: 'azure_rm_region_choices', - options: inventorySourcesOptions - }); - - // Load options for group_by - GetChoices({ - scope: $scope, - field: 'group_by', - variable: 'ec2_group_by', - choice_name: 'ec2_group_by_choices', - options: inventorySourcesOptions - }); - - initRegionSelect(); - GetChoices({ scope: $scope, field: 'verbosity', @@ -205,20 +162,11 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars $scope.projectBasePath = GetBasePath('projects') + '?not__status=never updated'; } - // reset fields - $scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null; - // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint - $scope.source_region_choices = source === 'azure_rm' ? $scope.azure_regions : $scope[source + '_regions']; $scope.cloudCredentialRequired = source !== '' && source !== 'scm' && source !== 'custom' && source !== 'ec2' ? true : false; - $scope.source_regions = null; $scope.credential = null; $scope.credential_name = null; - $scope.group_by = null; - $scope.group_by_choices = []; $scope.overwrite_vars = false; - initRegionSelect(); }; - // region / source options callback $scope.$on('sourceTypeOptionsReady', function() { CreateSelect2({ @@ -227,57 +175,6 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars }); }); - function initRegionSelect(){ - CreateSelect2({ - element: '#inventory_source_source_regions', - multiple: true - }); - - let add_new = false; - if( _.get($scope, 'source') === 'ec2' || _.get($scope.source, 'value') === 'ec2') { - $scope.group_by_choices = $scope.ec2_group_by; - $scope.groupByPopOver = "

" + i18n._("Select which groups to create automatically. ") + - $rootScope.BRAND_NAME + i18n._(" will create group names similar to the following examples based on the options selected:") + "

    " + - "
  • " + i18n._("Availability Zone:") + "zones » us-east-1b
  • " + - "
  • " + i18n._("Image ID:") + "images » ami-b007ab1e
  • " + - "
  • " + i18n._("Instance ID:") + "instances » i-ca11ab1e
  • " + - "
  • " + i18n._("Instance Type:") + "types » type_m1_medium
  • " + - "
  • " + i18n._("Key Name:") + "keys » key_testing
  • " + - "
  • " + i18n._("Region:") + "regions » us-east-1
  • " + - "
  • " + i18n._("Security Group:") + "security_groups » security_group_default
  • " + - "
  • " + i18n._("Tags:") + "tags » tag_Name » tag_Name_host1
  • " + - "
  • " + i18n._("VPC ID:") + "vpcs » vpc-5ca1ab1e
  • " + - "
  • " + i18n._("Tag None:") + "tags » tag_none
  • " + - "

" + i18n._("If blank, all groups above are created except") + "" + i18n._("Instance ID") + ".

"; - - $scope.instanceFilterPopOver = "

" + i18n._("Provide a comma-separated list of filter expressions. ") + - i18n._("Hosts are imported to ") + $rootScope.BRAND_NAME + i18n._(" when ") + "" + i18n._("ANY") + "" + i18n._(" of the filters match.") + "

" + - i18n._("Limit to hosts having a tag:") + "
\n" + - "
tag-key=TowerManaged
\n" + - i18n._("Limit to hosts using either key pair:") + "
\n" + - "
key-name=staging, key-name=production
\n" + - i18n._("Limit to hosts where the Name tag begins with ") + "" + i18n._("test") + ":
\n" + - "
tag:Name=test*
\n" + - "

" + i18n._("View the ") + "" + i18n._("Describe Instances documentation") + " " + - i18n._("for a complete list of supported filters.") + "

"; - } - if( _.get($scope, 'source') === 'vmware' || _.get($scope.source, 'value') === 'vmware') { - add_new = true; - $scope.group_by_choices = []; - $scope.group_by = $scope.group_by_choices; - $scope.groupByPopOver = i18n._("Specify which groups to create automatically. Group names will be created similar to the options selected. If blank, all groups above are created. Refer to Ansible Tower documentation for more detail."); - $scope.instanceFilterPopOver = i18n._("Provide a comma-separated list of filter expressions. Hosts are imported when all of the filters match. Refer to Ansible Tower documentation for more detail."); - } - if( _.get($scope, 'source') === 'tower' || _.get($scope.source, 'value') === 'tower') { - $scope.instanceFilterPopOver = i18n._("Provide the named URL encoded name or id of the remote Tower inventory to be imported."); - } - CreateSelect2({ - element: '#inventory_source_group_by', - multiple: true, - addNew: add_new - }); - } - $scope.formCancel = function() { $state.go('^'); }; @@ -289,7 +186,6 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars name: $scope.name, description: $scope.description, inventory: inventoryData.id, - instance_filters: $scope.instance_filters, source_script: $scope.inventory_script, credential: $scope.credential, overwrite: $scope.overwrite, @@ -298,9 +194,6 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars verbosity: $scope.verbosity.value, update_cache_timeout: $scope.update_cache_timeout || 0, custom_virtualenv: $scope.custom_virtualenv || null, - // comma-delimited strings - group_by: SourcesService.encodeGroupBy($scope.source, $scope.group_by), - source_regions: _.map($scope.source_regions, 'value').join(','), }; if ($scope.source) { diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js index 40dc4fc970..1bbaecaf4b 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js @@ -34,7 +34,6 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange', {overwrite_vars: inventorySourceData.overwrite_vars}, {update_on_launch: inventorySourceData.update_on_launch}, {update_cache_timeout: inventorySourceData.update_cache_timeout}, - {instance_filters: inventorySourceData.instance_filters}, {inventory_script: inventorySourceData.source_script}, {verbosity: inventorySourceData.verbosity}); @@ -100,56 +99,6 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange', scope: $scope, variable: 'source_type_options' }); - GetChoices({ - scope: $scope, - field: 'source_regions', - variable: 'rax_regions', - choice_name: 'rax_region_choices', - options: inventorySourcesOptions - }); - GetChoices({ - scope: $scope, - field: 'source_regions', - variable: 'ec2_regions', - choice_name: 'ec2_region_choices', - options: inventorySourcesOptions - }); - GetChoices({ - scope: $scope, - field: 'source_regions', - variable: 'gce_regions', - choice_name: 'gce_region_choices', - options: inventorySourcesOptions - }); - GetChoices({ - scope: $scope, - field: 'source_regions', - variable: 'azure_regions', - choice_name: 'azure_rm_region_choices', - options: inventorySourcesOptions - }); - GetChoices({ - scope: $scope, - field: 'group_by', - variable: 'ec2_group_by', - choice_name: 'ec2_group_by_choices', - options: inventorySourcesOptions - }); - - var source = $scope.source === 'azure_rm' ? 'azure' : $scope.source; - var regions = inventorySourceData.source_regions.split(','); - // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint - $scope.source_region_choices = $scope[source + '_regions']; - - // the API stores azure regions as all-lowercase strings - but the azure regions received from OPTIONS are Snake_Cased - if (source === 'azure') { - $scope.source_regions = _.map(regions, (region) => _.find($scope[source + '_regions'], (o) => o.value.toLowerCase() === region)); - } - // all other regions are 1-1 - else { - $scope.source_regions = _.map(regions, (region) => _.find($scope[source + '_regions'], (o) => o.value === region)); - } - initRegionSelect(); GetChoices({ scope: $scope, @@ -236,63 +185,6 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange', } } - function initRegionSelect() { - CreateSelect2({ - element: '#inventory_source_source_regions', - multiple: true - }); - - let add_new = false; - if( _.get($scope, 'source') === 'ec2' || _.get($scope.source, 'value') === 'ec2') { - $scope.group_by_choices = $scope.ec2_group_by; - let group_by = inventorySourceData.group_by.split(','); - $scope.group_by = _.map(group_by, (item) => _.find($scope.ec2_group_by, { value: item })); - - $scope.groupByPopOver = "

" + i18n._("Select which groups to create automatically. ") + - $rootScope.BRAND_NAME + i18n._(" will create group names similar to the following examples based on the options selected:") + "

    " + - "
  • " + i18n._("Availability Zone:") + "zones » us-east-1b
  • " + - "
  • " + i18n._("Image ID:") + "images » ami-b007ab1e
  • " + - "
  • " + i18n._("Instance ID:") + "instances » i-ca11ab1e
  • " + - "
  • " + i18n._("Instance Type:") + "types » type_m1_medium
  • " + - "
  • " + i18n._("Key Name:") + "keys » key_testing
  • " + - "
  • " + i18n._("Region:") + "regions » us-east-1
  • " + - "
  • " + i18n._("Security Group:") + "security_groups » security_group_default
  • " + - "
  • " + i18n._("Tags:") + "tags » tag_Name » tag_Name_host1
  • " + - "
  • " + i18n._("VPC ID:") + "vpcs » vpc-5ca1ab1e
  • " + - "
  • " + i18n._("Tag None:") + "tags » tag_none
  • " + - "

" + i18n._("If blank, all groups above are created except") + "" + i18n._("Instance ID") + ".

"; - - - $scope.instanceFilterPopOver = "

" + i18n._("Provide a comma-separated list of filter expressions. ") + - i18n._("Hosts are imported to ") + $rootScope.BRAND_NAME + i18n._(" when ") + "" + i18n._("ANY") + "" + i18n._(" of the filters match.") + "

" + - i18n._("Limit to hosts having a tag:") + "
\n" + - "
tag-key=TowerManaged
\n" + - i18n._("Limit to hosts using either key pair:") + "
\n" + - "
key-name=staging, key-name=production
\n" + - i18n._("Limit to hosts where the Name tag begins with ") + "" + i18n._("test") + ":
\n" + - "
tag:Name=test*
\n" + - "

" + i18n._("View the ") + "" + i18n._("Describe Instances documentation") + " " + - i18n._("for a complete list of supported filters.") + "

"; - - } - if( _.get($scope, 'source') === 'vmware' || _.get($scope.source, 'value') === 'vmware') { - add_new = true; - $scope.group_by_choices = (inventorySourceData.group_by) ? inventorySourceData.group_by.split(',') - .map((i) => ({name: i, label: i, value: i})) : []; - $scope.group_by = $scope.group_by_choices; - $scope.groupByPopOver = i18n._(`Specify which groups to create automatically. Group names will be created similar to the options selected. If blank, all groups above are created. Refer to Ansible Tower documentation for more detail.`); - $scope.instanceFilterPopOver = i18n._(`Provide a comma-separated list of filter expressions. Hosts are imported when all of the filters match. Refer to Ansible Tower documentation for more detail.`); - } - if( _.get($scope, 'source') === 'tower' || _.get($scope.source, 'value') === 'tower') { - $scope.instanceFilterPopOver = i18n._(`Provide the named URL encoded name or id of the remote Tower inventory to be imported.`); - } - CreateSelect2({ - element: '#inventory_source_group_by', - multiple: true, - addNew: add_new - }); - } - $scope.lookupProject = function(){ $state.go('.project', { project_search: { @@ -346,7 +238,6 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange', name: $scope.name, description: $scope.description, inventory: inventoryData.id, - instance_filters: $scope.instance_filters, source_script: $scope.inventory_script, credential: $scope.credential, overwrite: $scope.overwrite, @@ -355,9 +246,6 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange', update_cache_timeout: $scope.update_cache_timeout || 0, verbosity: $scope.verbosity.value, custom_virtualenv: $scope.custom_virtualenv || null, - // comma-delimited strings - group_by: SourcesService.encodeGroupBy($scope.source, $scope.group_by), - source_regions: _.map($scope.source_regions, 'value').join(',') }; if ($scope.source) { @@ -417,20 +305,10 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange', }); } - // reset fields - $scope.group_by_choices = source === 'ec2' ? $scope.ec2_group_by : null; - // azure_rm regions choices are keyed as "azure" in an OPTIONS request to the inventory_sources endpoint - $scope.source_region_choices = source === 'azure_rm' ? $scope.azure_regions : $scope[source + '_regions']; $scope.cloudCredentialRequired = source !== '' && source !== 'scm' && source !== 'custom' && source !== 'ec2' ? true : false; - $scope.source_regions = null; $scope.credential = null; $scope.credential_name = null; - $scope.group_by = null; - $scope.group_by_choices = []; $scope.overwrite_vars = false; - - initRegionSelect(); - }; } ]; diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js index 3c76dd2e61..2594ad29da 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js @@ -126,46 +126,6 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ includeInventoryFileNotFoundError: true, subForm: 'sourceSubForm' }, - source_regions: { - label: i18n._('Regions'), - type: 'select', - ngOptions: 'source.label for source in source_region_choices track by source.value', - multiSelect: true, - ngShow: "source && (source.value == 'rax' || source.value == 'ec2' || source.value == 'gce' || source.value == 'azure_rm')", - dataTitle: i18n._('Source Regions'), - dataPlacement: 'right', - awPopOver: "

" + i18n._("Click on the regions field to see a list of regions for your cloud provider. You can select multiple regions, or choose") + - "" + i18n._("All") + " " + i18n._("to include all regions. Only Hosts associated with the selected regions will be updated.") + "

", - dataContainer: 'body', - ngDisabled: '!(inventory_source_obj.summary_fields.user_capabilities.edit || canAdd)', - subForm: 'sourceSubForm' - }, - instance_filters: { - label: i18n._("Instance Filters"), - type: 'text', - ngShow: "source && (source.value == 'ec2' || source.value == 'vmware' || source.value == 'tower')", - dataTitle: i18n._('Instance Filters'), - dataPlacement: 'right', - awPopOverWatch: 'instanceFilterPopOver', - awPopOver: '{{ instanceFilterPopOver }}', - dataContainer: 'body', - ngDisabled: '!(inventory_source_obj.summary_fields.user_capabilities.edit || canAdd)', - subForm: 'sourceSubForm' - }, - group_by: { - label: i18n._('Only Group By'), - type: 'select', - ngShow: "source && (source.value == 'ec2' || source.value == 'vmware')", - ngOptions: 'source.label for source in group_by_choices track by source.value', - multiSelect: true, - dataTitle: i18n._("Only Group By"), - dataPlacement: 'right', - awPopOverWatch: 'groupByPopOver', - awPopOver: '{{ groupByPopOver }}', - dataContainer: 'body', - ngDisabled: '!(inventory_source_obj.summary_fields.user_capabilities.edit || canAdd)', - subForm: 'sourceSubForm' - }, inventory_script: { label : i18n._("Custom Inventory Script"), type: 'lookup', diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.service.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.service.js index ce65a7dae5..a42ac51097 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.service.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.service.js @@ -116,24 +116,6 @@ export default .catch(this.error.bind(this)) .finally(Wait('stop')); }, - encodeGroupBy(source, group_by){ - source = source && source.value ? source.value : ''; - if(source === 'ec2'){ - return _.map(group_by, 'value').join(','); - } - - if(source === 'vmware'){ - group_by = _.map(group_by, (i) => {return i.value;}); - $("#inventory_source_group_by").siblings(".select2").first().find(".select2-selection__choice").each(function(optionIndex, option){ - group_by.push(option.title); - }); - group_by = (Array.isArray(group_by)) ? _.uniq(group_by).join() : ""; - return group_by; - } - else { - return; - } - }, deleteHosts(id) { this.url = GetBasePath('inventory_sources') + id + '/hosts/'; Rest.setUrl(this.url); From 42e70bc85207e0409925f37eccf7ebcc9cc85b9e Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 29 Jul 2020 18:33:46 -0400 Subject: [PATCH 170/188] Delete inventory source fields --- .../InventorySourceDetail.jsx | 57 +--- .../InventorySourceDetail.test.jsx | 26 -- .../Inventory/shared/InventorySourceForm.jsx | 6 - .../InventorySourceSubForms/AzureSubForm.jsx | 14 +- .../AzureSubForm.test.jsx | 10 +- .../CloudFormsSubForm.test.jsx | 3 - .../InventorySourceSubForms/EC2SubForm.jsx | 25 +- .../EC2SubForm.test.jsx | 15 +- .../InventorySourceSubForms/GCESubForm.jsx | 9 +- .../GCESubForm.test.jsx | 10 +- .../OpenStackSubForm.test.jsx | 3 - .../SCMSubForm.test.jsx | 3 - .../SatelliteSubForm.test.jsx | 3 - .../InventorySourceSubForms/SharedFields.jsx | 249 +----------------- .../InventorySourceSubForms/TowerSubForm.jsx | 7 +- .../TowerSubForm.test.jsx | 4 - .../InventorySourceSubForms/VMwareSubForm.jsx | 10 +- .../VMwareSubForm.test.jsx | 11 +- .../VirtualizationSubForm.test.jsx | 3 - .../shared/data.inventory_source.json | 3 - 20 files changed, 16 insertions(+), 455 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx index 42fad28421..badd28fb26 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx @@ -3,10 +3,9 @@ import { Link, useHistory } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Button, Chip, List, ListItem } from '@patternfly/react-core'; +import { Button, List, ListItem } from '@patternfly/react-core'; import AlertModal from '../../../components/AlertModal'; import { CardBody, CardActionsRow } from '../../../components/Card'; -import ChipGroup from '../../../components/ChipGroup'; import { VariablesDetail } from '../../../components/CodeMirrorInput'; import ContentError from '../../../components/ContentError'; import ContentLoading from '../../../components/ContentLoading'; @@ -28,16 +27,13 @@ function InventorySourceDetail({ inventorySource, i18n }) { created, custom_virtualenv, description, - group_by, id, - instance_filters, modified, name, overwrite, overwrite_vars, source, source_path, - source_regions, source_vars, update_cache_timeout, update_on_launch, @@ -233,57 +229,6 @@ function InventorySourceDetail({ inventorySource, i18n }) { ))} /> )} - {source_regions && ( - - {source_regions.split(',').map(region => ( - - {region} - - ))} - - } - /> - )} - {instance_filters && ( - - {instance_filters.split(',').map(filter => ( - - {filter} - - ))} - - } - /> - )} - {group_by && ( - - {group_by.split(',').map(group => ( - - {group} - - ))} - - } - /> - )} {optionsList && ( )} diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx index c8905143ee..0d7f9a9b79 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.test.jsx @@ -64,32 +64,6 @@ describe('InventorySourceDetail', () => { assertDetail(wrapper, 'Inventory file', 'foo'); assertDetail(wrapper, 'Verbosity', '2 (Debug)'); assertDetail(wrapper, 'Cache timeout', '2 seconds'); - expect( - wrapper - .find('Detail[label="Regions"]') - .containsAllMatchingElements([ - us-east-1, - us-east-2, - ]) - ).toEqual(true); - expect( - wrapper - .find('Detail[label="Instance filters"]') - .containsAllMatchingElements([ - filter1, - filter2, - filter3, - ]) - ).toEqual(true); - expect( - wrapper - .find('Detail[label="Only group by"]') - .containsAllMatchingElements([ - group1, - group2, - group3, - ]) - ).toEqual(true); expect(wrapper.find('CredentialChip').text()).toBe('Cloud: mock cred'); expect(wrapper.find('VariablesDetail').prop('value')).toEqual( '---\nfoo: bar' diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx index a694d15243..08b32f1f1a 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx @@ -75,14 +75,11 @@ const InventorySourceFormFields = ({ sourceOptions, i18n }) => { } else { const defaults = { credential: null, - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source: sourceType, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, @@ -200,15 +197,12 @@ const InventorySourceForm = ({ credential: source?.summary_fields?.credential || null, custom_virtualenv: source?.custom_virtualenv || '', description: source?.description || '', - group_by: source?.group_by || '', - instance_filters: source?.instance_filters || '', name: source?.name || '', overwrite: source?.overwrite || false, overwrite_vars: source?.overwrite_vars || false, source: source?.source || '', source_path: source?.source_path === '' ? '/ (project root)' : '', source_project: source?.summary_fields?.source_project || null, - source_regions: source?.source_regions || '', source_script: source?.summary_fields?.source_script || null, source_vars: source?.source_vars || '---\n', update_cache_timeout: source?.update_cache_timeout || 0, diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx index 7bc6b49975..99b83ca6b5 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx @@ -3,14 +3,9 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { - OptionsField, - RegionsField, - SourceVarsField, - VerbosityField, -} from './SharedFields'; +import { OptionsField, SourceVarsField, VerbosityField } from './SharedFields'; -const AzureSubForm = ({ i18n, sourceOptions }) => { +const AzureSubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( 'credential' ); @@ -29,11 +24,6 @@ const AzureSubForm = ({ i18n, sourceOptions }) => { value={credentialField.value} required /> - diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.test.jsx index a7dae124fd..b363f7f42b 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.test.jsx @@ -10,13 +10,10 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, custom_virtualenv: '', - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, @@ -27,11 +24,7 @@ const initialValues = { const mockSourceOptions = { actions: { - POST: { - source_regions: { - azure_rm_region_choices: [], - }, - }, + POST: {}, }, }; @@ -58,7 +51,6 @@ describe('', () => { test('should render subform fields', () => { expect(wrapper.find('FormGroup[label="Credential"]')).toHaveLength(1); - expect(wrapper.find('FormGroup[label="Regions"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Verbosity"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Update options"]')).toHaveLength(1); expect( diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.test.jsx index e46ac8d8fa..8e46b042fd 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.test.jsx @@ -10,13 +10,10 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, custom_virtualenv: '', - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx index d74e770895..32fc581742 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx @@ -3,24 +3,10 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { - GroupByField, - InstanceFiltersField, - OptionsField, - RegionsField, - SourceVarsField, - VerbosityField, -} from './SharedFields'; +import { OptionsField, SourceVarsField, VerbosityField } from './SharedFields'; -const EC2SubForm = ({ i18n, sourceOptions }) => { +const EC2SubForm = ({ i18n }) => { const [credentialField, , credentialHelpers] = useField('credential'); - const groupByOptionsObj = Object.assign( - {}, - ...sourceOptions?.actions?.POST?.group_by?.ec2_group_by_choices.map( - ([key, val]) => ({ [key]: val }) - ) - ); - return ( <> { credentialHelpers.setValue(value); }} /> - - - diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.test.jsx index fc15d03ea9..7a41471ec2 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.test.jsx @@ -10,13 +10,10 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, custom_virtualenv: '', - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, @@ -27,14 +24,7 @@ const initialValues = { const mockSourceOptions = { actions: { - POST: { - source_regions: { - ec2_region_choices: [], - }, - group_by: { - ec2_group_by_choices: [], - }, - }, + POST: {}, }, }; @@ -61,9 +51,6 @@ describe('', () => { test('should render subform fields', () => { expect(wrapper.find('FormGroup[label="Credential"]')).toHaveLength(1); - expect(wrapper.find('FormGroup[label="Regions"]')).toHaveLength(1); - expect(wrapper.find('FormGroup[label="Instance filters"]')).toHaveLength(1); - expect(wrapper.find('FormGroup[label="Only group by"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Verbosity"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Update options"]')).toHaveLength(1); expect( diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx index 0451e80b86..fcbbffa040 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx @@ -3,9 +3,9 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { OptionsField, RegionsField, VerbosityField } from './SharedFields'; +import { OptionsField, VerbosityField } from './SharedFields'; -const GCESubForm = ({ i18n, sourceOptions }) => { +const GCESubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( 'credential' ); @@ -24,11 +24,6 @@ const GCESubForm = ({ i18n, sourceOptions }) => { value={credentialField.value} required /> - diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.test.jsx index a7845972c1..4655d0ad3f 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.test.jsx @@ -10,13 +10,10 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, custom_virtualenv: '', - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, @@ -27,11 +24,7 @@ const initialValues = { const mockSourceOptions = { actions: { - POST: { - source_regions: { - gce_region_choices: [], - }, - }, + POST: {}, }, }; @@ -58,7 +51,6 @@ describe('', () => { test('should render subform fields', () => { expect(wrapper.find('FormGroup[label="Credential"]')).toHaveLength(1); - expect(wrapper.find('FormGroup[label="Regions"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Verbosity"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Update options"]')).toHaveLength(1); expect( diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.test.jsx index b4be2c1aff..f912186816 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.test.jsx @@ -10,13 +10,10 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, custom_virtualenv: '', - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx index 6d763b78a7..fdd4fdb317 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx @@ -11,13 +11,10 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, custom_virtualenv: '', - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SatelliteSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SatelliteSubForm.test.jsx index 5da6c02db6..2934390896 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SatelliteSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SatelliteSubForm.test.jsx @@ -10,13 +10,10 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, custom_virtualenv: '', - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx index dcfc4b70fc..3ca698641b 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx @@ -1,16 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { withI18n } from '@lingui/react'; -import { t, Trans } from '@lingui/macro'; +import { t } from '@lingui/macro'; import { useField } from 'formik'; -import { - FormGroup, - Select, - SelectOption, - SelectVariant, -} from '@patternfly/react-core'; -import { arrayToString, stringToArray } from '../../../../util/strings'; +import { FormGroup } from '@patternfly/react-core'; import { minMaxValue } from '../../../../util/validators'; -import { BrandName } from '../../../../variables'; import AnsibleSelect from '../../../../components/AnsibleSelect'; import { VariablesField } from '../../../../components/CodeMirrorInput'; import FormField, { @@ -32,196 +25,6 @@ export const SourceVarsField = withI18n()(({ i18n }) => ( )); -export const RegionsField = withI18n()(({ i18n, regionOptions }) => { - const [field, meta, helpers] = useField('source_regions'); - const [isOpen, setIsOpen] = useState(false); - const options = Object.assign( - {}, - ...regionOptions.map(([key, val]) => ({ [key]: val })) - ); - const selected = stringToArray(field?.value) - .filter(i => options[i]) - .map(val => options[val]); - - return ( - - Click on the regions field to see a list of regions for your cloud - provider. You can select multiple regions, or choose - All to include all regions. Only Hosts associated with - the selected regions will be updated. - - } - /> - } - > - - - ); -}); - -export const GroupByField = withI18n()( - ({ i18n, fixedOptions, isCreatable = false }) => { - const [field, meta, helpers] = useField('group_by'); - const fixedOptionLabels = fixedOptions && Object.values(fixedOptions); - const selections = fixedOptions - ? stringToArray(field.value).map(o => fixedOptions[o]) - : stringToArray(field.value); - const [options, setOptions] = useState(selections); - const [isOpen, setIsOpen] = useState(false); - - const renderOptions = opts => { - return opts.map(option => ( - - {option} - - )); - }; - - const handleFilter = event => { - const str = event.target.value.toLowerCase(); - let matches; - if (fixedOptions) { - matches = fixedOptionLabels.filter(o => o.toLowerCase().includes(str)); - } else { - matches = options.filter(o => o.toLowerCase().includes(str)); - } - return renderOptions(matches); - }; - - const handleSelect = (e, option) => { - let selectedValues; - if (selections.includes(option)) { - selectedValues = selections.filter(o => o !== option); - } else { - selectedValues = selections.concat(option); - } - if (fixedOptions) { - selectedValues = selectedValues.map(val => - Object.keys(fixedOptions).find(key => fixedOptions[key] === val) - ); - } - helpers.setValue(arrayToString(selectedValues)); - }; - - return ( - - Select which groups to create automatically. AWX will create - group names similar to the following examples based on the - options selected: -
-
-
    -
  • - Availability Zone: zones » us-east-1b -
  • -
  • - Image ID: images » ami-b007ab1e -
  • -
  • - Instance ID: instances » i-ca11ab1e -
  • -
  • - Instance Type: types » type_m1_medium -
  • -
  • - Key Name: keys » key_testing -
  • -
  • - Region: regions » us-east-1 -
  • -
  • - Security Group:{' '} - - security_groups » security_group_default - -
  • -
  • - Tags: tags » tag_Name_host1 -
  • -
  • - VPC ID: vpcs » vpc-5ca1ab1e -
  • -
  • - Tag None: tags » tag_none -
  • -
-
- If blank, all groups above are created except{' '} - Instance ID. - - } - /> - } - > - -
- ); - } -); - export const VerbosityField = withI18n()(({ i18n }) => { const [field, meta, helpers] = useField('verbosity'); const isValid = !(meta.touched && meta.error); @@ -351,49 +154,3 @@ export const OptionsField = withI18n()( ); } ); - -export const InstanceFiltersField = withI18n()(({ i18n }) => { - // Setting BrandName to a variable here is necessary to get the jest tests - // passing. Attempting to use BrandName in the template literal results - // in failing tests. - const brandName = BrandName; - return ( - - Provide a comma-separated list of filter expressions. Hosts are - imported to {brandName} when ANY of the filters match. -
-
- Limit to hosts having a tag: -
- tag-key=TowerManaged -
-
- Limit to hosts using either key pair: -
- key-name=staging, key-name=production -
-
- Limit to hosts where the Name tag begins with test:
- tag:Name=test* -
-
- View the - - {' '} - Describe Instances documentation{' '} - - for a complete list of supported filters. - - } - /> - ); -}); diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx index f3fe26d3a9..3af0f9a5c6 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx @@ -3,11 +3,7 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { - InstanceFiltersField, - OptionsField, - VerbosityField, -} from './SharedFields'; +import { OptionsField, VerbosityField } from './SharedFields'; const TowerSubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( @@ -28,7 +24,6 @@ const TowerSubForm = ({ i18n }) => { value={credentialField.value} required /> - diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.test.jsx index 71bc801823..fd7ee0488a 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.test.jsx @@ -10,13 +10,10 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, custom_virtualenv: '', - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, @@ -48,7 +45,6 @@ describe('', () => { test('should render subform fields', () => { expect(wrapper.find('FormGroup[label="Credential"]')).toHaveLength(1); - expect(wrapper.find('FormGroup[label="Instance filters"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Verbosity"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Update options"]')).toHaveLength(1); expect( diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx index e975e789b1..555b0498e3 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx @@ -3,13 +3,7 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { - InstanceFiltersField, - GroupByField, - OptionsField, - SourceVarsField, - VerbosityField, -} from './SharedFields'; +import { OptionsField, SourceVarsField, VerbosityField } from './SharedFields'; const VMwareSubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( @@ -30,8 +24,6 @@ const VMwareSubForm = ({ i18n }) => { value={credentialField.value} required /> - - diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.test.jsx index ba4777733d..e86bc49d50 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.test.jsx @@ -10,13 +10,10 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, custom_virtualenv: '', - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, @@ -27,11 +24,7 @@ const initialValues = { const mockSourceOptions = { actions: { - POST: { - source_regions: { - gce_region_choices: [], - }, - }, + POST: {}, }, }; @@ -58,8 +51,6 @@ describe('', () => { test('should render subform fields', () => { expect(wrapper.find('FormGroup[label="Credential"]')).toHaveLength(1); - expect(wrapper.find('FormGroup[label="Instance filters"]')).toHaveLength(1); - expect(wrapper.find('FormGroup[label="Only group by"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Verbosity"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Update options"]')).toHaveLength(1); expect( diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.test.jsx index 1d1526a42d..35f3933bb9 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.test.jsx @@ -10,13 +10,10 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, custom_virtualenv: '', - group_by: '', - instance_filters: '', overwrite: false, overwrite_vars: false, source_path: '', source_project: null, - source_regions: '', source_script: null, source_vars: '---\n', update_cache_timeout: 0, diff --git a/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json b/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json index c6fbf26365..ad1e313611 100644 --- a/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json +++ b/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json @@ -96,9 +96,6 @@ "source_script": "Mock Script", "source_vars":"---\nfoo: bar", "credential": 8, - "source_regions": "us-east-1,us-east-2", - "instance_filters": "filter1,filter2,filter3", - "group_by": "group1,group2,group3", "overwrite":true, "overwrite_vars":true, "custom_virtualenv":"/venv/custom", From b253540047a17b90989c4317b3335546e3177cdd Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 4 Aug 2020 16:04:04 -0400 Subject: [PATCH 171/188] Add new common inventory source fields --- .../sources/add/sources-add.controller.js | 3 ++ .../sources/edit/sources-edit.controller.js | 5 ++++ .../related/sources/sources.form.js | 30 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js index 0ee7b8392b..05bfe07718 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js @@ -194,6 +194,9 @@ export default ['$state', 'ConfigData', '$scope', 'SourcesFormDefinition', 'Pars verbosity: $scope.verbosity.value, update_cache_timeout: $scope.update_cache_timeout || 0, custom_virtualenv: $scope.custom_virtualenv || null, + enabled_var: $scope.enabled_var, + enabled_value: $scope.enabled_value, + host_filter: $scope.host_filter }; if ($scope.source) { diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js index 1bbaecaf4b..6da60e6b3b 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js @@ -233,6 +233,8 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange', $scope.formSave = function() { var params; + console.log($scope); + params = { id: inventorySourceData.id, name: $scope.name, @@ -246,6 +248,9 @@ export default ['$state', '$scope', 'ParseVariableString', 'ParseTypeChange', update_cache_timeout: $scope.update_cache_timeout || 0, verbosity: $scope.verbosity.value, custom_virtualenv: $scope.custom_virtualenv || null, + enabled_var: $scope.enabled_var, + enabled_value: $scope.enabled_value, + host_filter: $scope.host_filter }; if ($scope.source) { diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js index 2594ad29da..c49904b2e3 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js @@ -300,6 +300,36 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ ngDisabled: '!(inventory_source_obj.summary_fields.user_capabilities.edit || canAdd)', subForm: 'sourceSubForm' }, + host_filter: { + label: i18n._("Host Filter"), + type: 'text', + dataTitle: i18n._('Host Filter'), + dataPlacement: 'right', + awPopOver: "

" + i18n._("Regular expression where only matching hosts will be imported.") + "

", + dataContainer: 'body', + ngDisabled: '!(inventory_source_obj.summary_fields.user_capabilities.edit || canAdd)', + subForm: 'sourceSubForm' + }, + enabled_var: { + label: i18n._("Enabled Variable"), + type: 'text', + dataTitle: i18n._('Enabled Variable'), + dataPlacement: 'right', + awPopOver: "

" + i18n._("Retrieve the enabled state from the given dict of host variables. The enabled variable may be specified using dot notation, e.g: 'foo.bar'") + "

", + dataContainer: 'body', + ngDisabled: '!(inventory_source_obj.summary_fields.user_capabilities.edit || canAdd)', + subForm: 'sourceSubForm' + }, + enabled_value: { + label: i18n._("Enabled Value"), + type: 'text', + dataTitle: i18n._('Enabled Value'), + dataPlacement: 'right', + awPopOver: "

" + i18n._("If the enabled variable matches this value, the host will be enabled on import.") + "

", + dataContainer: 'body', + ngDisabled: '!(inventory_source_obj.summary_fields.user_capabilities.edit || canAdd)', + subForm: 'sourceSubForm' + }, checkbox_group: { label: i18n._('Update Options'), type: 'checkbox_group', From 7d4493e109f344a70d7b1cd7f239338f99414cbf Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 4 Aug 2020 10:54:28 -0400 Subject: [PATCH 172/188] rename inventory migration --- .../{0118_v380_inventory_plugins.py => 0118_inventory_plugins.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename awx/main/migrations/{0118_v380_inventory_plugins.py => 0118_inventory_plugins.py} (100%) diff --git a/awx/main/migrations/0118_v380_inventory_plugins.py b/awx/main/migrations/0118_inventory_plugins.py similarity index 100% rename from awx/main/migrations/0118_v380_inventory_plugins.py rename to awx/main/migrations/0118_inventory_plugins.py From 12cf607e8aa2f81ebb5a51351c0088f07a1fa840 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Tue, 4 Aug 2020 16:40:15 -0400 Subject: [PATCH 173/188] simplify InventorySourceOption inheritance hack --- awx/main/migrations/0118_inventory_plugins.py | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/awx/main/migrations/0118_inventory_plugins.py b/awx/main/migrations/0118_inventory_plugins.py index 5ac36b6c1f..e69a3b7740 100644 --- a/awx/main/migrations/0118_inventory_plugins.py +++ b/awx/main/migrations/0118_inventory_plugins.py @@ -2,10 +2,11 @@ import logging import json +import yaml from django.db import migrations -from awx.main.models.inventory import InventorySourceOptions +from awx.main.models.base import VarsDictProperty from ._inventory_source_vars import FrozenInjectors @@ -14,29 +15,23 @@ logger = logging.getLogger('awx.main.migrations') BACKUP_FILENAME = '/tmp/tower_migration_inventory_source_vars.json' -class InventorySourceOptionsWrapper(InventorySourceOptions): - ''' - InventorySource inherits from InventorySourceOptions but that is not - "recorded" by Django's app registry model tracking. This will, effectively, - reintroduce the inheritance. - ''' - def __init__(self, *args, **kw): - self.target = kw.pop('target') - super().__init__(self, *args, **kw) - def __getattr__(self, attr): - return getattr(self.target, attr) - - def _get_inventory_sources(InventorySource): return InventorySource.objects.filter(source__in=['ec2', 'gce', 'azure_rm', 'vmware', 'satellite6', 'openstack', 'rhv', 'tower']) def inventory_source_vars_forward(apps, schema_editor): InventorySource = apps.get_model("main", "InventorySource") + ''' + The Django app registry does not keep track of model inheritance. The + source_vars_dict property comes from InventorySourceOptions via inheritance. + This adds that property. Luckily, other properteries and functionality from + InventorySourceOptions is not needed by the injector logic. + ''' + setattr(InventorySource, 'source_vars_dict', VarsDictProperty('source_vars')) source_vars_backup = dict() for inv_source_obj in _get_inventory_sources(InventorySource): - inv_source_obj = InventorySourceOptionsWrapper(target=inv_source_obj) + if inv_source_obj.source in FrozenInjectors: source_vars_backup[inv_source_obj.id] = dict(inv_source_obj.source_vars_dict) with open(BACKUP_FILENAME, 'w') as fh: @@ -44,11 +39,12 @@ def inventory_source_vars_forward(apps, schema_editor): injector = FrozenInjectors[inv_source_obj.source]() new_inv_source_vars = injector.inventory_as_dict(inv_source_obj, None) - inv_source_obj.source_vars = new_inv_source_vars + inv_source_obj.source_vars = yaml.dump(new_inv_source_vars) inv_source_obj.save() def inventory_source_vars_backward(apps, schema_editor): + InventorySource = apps.get_model("main", "InventorySource") try: with open(BACKUP_FILENAME, 'r') as fh: source_vars_backup = json.load(fh) @@ -56,7 +52,7 @@ def inventory_source_vars_backward(apps, schema_editor): print(f"Rollback file not found {BACKUP_FILENAME}") return - for inv_source_obj in _get_inventory_sources(): + for inv_source_obj in _get_inventory_sources(InventorySource): if inv_source_obj.id in source_vars_backup: inv_source_obj.source_vars = source_vars_backup[inv_source_obj.id] inv_source_obj.save() From 2fdeba47a5ff90bab4f89b8097209493415f0048 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Tue, 4 Aug 2020 17:35:12 -0400 Subject: [PATCH 174/188] Add new common inventory source fields --- .../InventorySourceDetail.jsx | 6 +++ .../Inventory/shared/InventorySourceForm.jsx | 6 +++ .../InventorySourceSubForms/AzureSubForm.jsx | 12 +++++- .../CloudFormsSubForm.jsx | 12 +++++- .../InventorySourceSubForms/EC2SubForm.jsx | 12 +++++- .../InventorySourceSubForms/GCESubForm.jsx | 11 ++++- .../OpenStackSubForm.jsx | 12 +++++- .../InventorySourceSubForms/SCMSubForm.jsx | 12 +++++- .../SatelliteSubForm.jsx | 12 +++++- .../InventorySourceSubForms/SharedFields.jsx | 41 +++++++++++++++++++ .../InventorySourceSubForms/TowerSubForm.jsx | 11 ++++- .../InventorySourceSubForms/VMwareSubForm.jsx | 12 +++++- .../VirtualizationSubForm.jsx | 11 ++++- 13 files changed, 160 insertions(+), 10 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx index badd28fb26..a67c3c2064 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx @@ -39,6 +39,9 @@ function InventorySourceDetail({ inventorySource, i18n }) { update_on_launch, update_on_project_update, verbosity, + enabled_var, + enabled_value, + host_filter, summary_fields: { created_by, credentials, @@ -220,6 +223,9 @@ function InventorySourceDetail({ inventorySource, i18n }) { label={i18n._(t`Cache timeout`)} value={`${update_cache_timeout} ${i18n._(t`seconds`)}`} /> + + + {credentials?.length > 0 && ( { update_on_launch: false, update_on_project_update: false, verbosity: 1, + enabled_var: '', + enabled_value: '', + host_filter: '', }; Object.keys(defaults).forEach(label => { setFieldValue(label, defaults[label]); @@ -209,6 +212,9 @@ const InventorySourceForm = ({ update_on_launch: source?.update_on_launch || false, update_on_project_update: source?.update_on_project_update || false, verbosity: source?.verbosity || 1, + enabled_var: source?.enabled_var || '', + enabled_value: source?.enabled_value || '', + host_filter: source?.host_filter || '', }; const { diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx index 99b83ca6b5..535b364691 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.jsx @@ -3,7 +3,14 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { OptionsField, SourceVarsField, VerbosityField } from './SharedFields'; +import { + OptionsField, + SourceVarsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, +} from './SharedFields'; const AzureSubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( @@ -25,6 +32,9 @@ const AzureSubForm = ({ i18n }) => { required /> + + + diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.jsx index 68aeed4d76..7db4431fdd 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/CloudFormsSubForm.jsx @@ -3,7 +3,14 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { OptionsField, SourceVarsField, VerbosityField } from './SharedFields'; +import { + OptionsField, + SourceVarsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, +} from './SharedFields'; const CloudFormsSubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( @@ -25,6 +32,9 @@ const CloudFormsSubForm = ({ i18n }) => { required /> + + + diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx index 32fc581742..33447a2229 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/EC2SubForm.jsx @@ -3,7 +3,14 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { OptionsField, SourceVarsField, VerbosityField } from './SharedFields'; +import { + OptionsField, + SourceVarsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, +} from './SharedFields'; const EC2SubForm = ({ i18n }) => { const [credentialField, , credentialHelpers] = useField('credential'); @@ -18,6 +25,9 @@ const EC2SubForm = ({ i18n }) => { }} /> + + + diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx index fcbbffa040..a6c8ce5cbd 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.jsx @@ -3,7 +3,13 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { OptionsField, VerbosityField } from './SharedFields'; +import { + OptionsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, +} from './SharedFields'; const GCESubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( @@ -25,6 +31,9 @@ const GCESubForm = ({ i18n }) => { required /> + + + ); diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.jsx index 7c61fb5d16..55a142e936 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.jsx @@ -3,7 +3,14 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { OptionsField, SourceVarsField, VerbosityField } from './SharedFields'; +import { + OptionsField, + SourceVarsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, +} from './SharedFields'; const OpenStackSubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( @@ -25,6 +32,9 @@ const OpenStackSubForm = ({ i18n }) => { required /> + + + diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx index b338088a8d..858e209cc5 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.jsx @@ -11,7 +11,14 @@ import AnsibleSelect from '../../../../components/AnsibleSelect'; import { FieldTooltip } from '../../../../components/FormField'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; import ProjectLookup from '../../../../components/Lookup/ProjectLookup'; -import { VerbosityField, OptionsField, SourceVarsField } from './SharedFields'; +import { + OptionsField, + SourceVarsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, +} from './SharedFields'; const SCMSubForm = ({ i18n }) => { const [credentialField, , credentialHelpers] = useField('credential'); @@ -121,6 +128,9 @@ const SCMSubForm = ({ i18n }) => { /> + + + diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SatelliteSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SatelliteSubForm.jsx index 641539f978..573be61679 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SatelliteSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SatelliteSubForm.jsx @@ -3,7 +3,14 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { OptionsField, SourceVarsField, VerbosityField } from './SharedFields'; +import { + OptionsField, + SourceVarsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, +} from './SharedFields'; const SatelliteSubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( @@ -25,6 +32,9 @@ const SatelliteSubForm = ({ i18n }) => { required /> + + + diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx index 3ca698641b..14e0bbd896 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx @@ -154,3 +154,44 @@ export const OptionsField = withI18n()( ); } ); + +export const EnabledVarField = withI18n()(({ i18n }) => { + return ( + + ); +}); + +export const EnabledValueField = withI18n()(({ i18n }) => { + return ( + + ); +}); + +export const HostFilterField = withI18n()(({ i18n }) => { + return ( + + ); +}); diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx index 3af0f9a5c6..0dd6d7be52 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/TowerSubForm.jsx @@ -3,7 +3,13 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { OptionsField, VerbosityField } from './SharedFields'; +import { + OptionsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, +} from './SharedFields'; const TowerSubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( @@ -25,6 +31,9 @@ const TowerSubForm = ({ i18n }) => { required /> + + + ); diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx index 555b0498e3..08f1342cfe 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.jsx @@ -3,7 +3,14 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { OptionsField, SourceVarsField, VerbosityField } from './SharedFields'; +import { + OptionsField, + SourceVarsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, +} from './SharedFields'; const VMwareSubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( @@ -25,6 +32,9 @@ const VMwareSubForm = ({ i18n }) => { required /> + + + diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.jsx index 4d558b74a6..fda2a58ec9 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.jsx @@ -3,7 +3,13 @@ import { useField } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import CredentialLookup from '../../../../components/Lookup/CredentialLookup'; -import { OptionsField, VerbosityField } from './SharedFields'; +import { + OptionsField, + VerbosityField, + EnabledVarField, + EnabledValueField, + HostFilterField, +} from './SharedFields'; const VirtualizationSubForm = ({ i18n }) => { const [credentialField, credentialMeta, credentialHelpers] = useField( @@ -25,6 +31,9 @@ const VirtualizationSubForm = ({ i18n }) => { required /> + + + ); From c7794fc3e4084d3322720f970f31cc9355abdb32 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 5 Aug 2020 09:38:55 -0400 Subject: [PATCH 175/188] fix missed import --- awx/main/migrations/_inventory_source_vars.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/awx/main/migrations/_inventory_source_vars.py b/awx/main/migrations/_inventory_source_vars.py index 857bb1c07e..edd2e6827a 100644 --- a/awx/main/migrations/_inventory_source_vars.py +++ b/awx/main/migrations/_inventory_source_vars.py @@ -1,3 +1,5 @@ +import json + from django.utils.translation import ugettext_lazy as _ From 48fb1e973c9c2e7e13f7808de37d56208186fb1e Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 5 Aug 2020 10:02:18 -0400 Subject: [PATCH 176/188] overwrite `plugin:` at runtime * Before, we were re-writing `plugin:` when users updated the InventorySource via the API. Now, we just override at run-time. This makes for a more sane API interaction --- awx/main/models/inventory.py | 20 ++++++------ .../tests/functional/api/test_inventory.py | 32 ------------------- 2 files changed, 11 insertions(+), 41 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 7d9069e4f0..5121f081ed 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1158,13 +1158,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE raise ValidationError(_("Cannot set source_path if not SCM type.")) return self.source_path - def clean_source_vars(self): - injector = self.injectors.get(self.source) - source_vars = dict(self.source_vars_dict) # make a copy - if injector and self.source_vars_dict.get('plugin', '') != injector.get_proper_name(): - source_vars['plugin'] = injector.get_proper_name() - return json.dumps(source_vars) - ''' RelatedJobsMixin ''' @@ -1369,13 +1362,21 @@ class PluginFileInjector(object): """Returns a string that is the content for the inventory file for the inventory plugin """ return yaml.safe_dump( - inventory_update.source_vars_dict, + self.inventory_as_dict(inventory_update, private_data_dir), default_flow_style=False, width=1000 ) def inventory_as_dict(self, inventory_update, private_data_dir): - return inventory_update.source_vars_dict + source_vars = dict(inventory_update.source_vars_dict) # make a copy + proper_name = self.get_proper_name() + ''' + None conveys that we should use the user-provided plugin. + Note that a plugin value of '' should still be overridden. + ''' + if proper_name is not None: + source_vars['plugin'] = proper_name + return source_vars def build_env(self, inventory_update, env, private_data_dir, private_data_files): injector_env = self.get_plugin_env(inventory_update, private_data_dir, private_data_files) @@ -1463,6 +1464,7 @@ class gce(PluginFileInjector): def inventory_as_dict(self, inventory_update, private_data_dir): ret = super().inventory_as_dict(inventory_update, private_data_dir) credential = inventory_update.get_cloud_credential() + # TODO: Align precedence of ENV vs. inventory config w/ Ansible behavior ret['projects'] = [credential.get_input('project', default='')] return ret diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index a04a68c9bb..5bad1b6f30 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -456,38 +456,6 @@ def test_inventory_source_vars_prohibition(post, inventory, admin_user): assert 'FOOBAR' in r.data['source_vars'][0] -@pytest.mark.django_db -@pytest.mark.parametrize('source,source_var_actual,source_var_expected,description', [ - ('ec2', {'plugin': 'blah'}, {'plugin': 'amazon.aws.aws_ec2'}, 'source plugin mismatch'), - ('ec2', {'plugin': 'amazon.aws.aws_ec2'}, {'plugin': 'amazon.aws.aws_ec2'}, 'valid plugin'), -]) -def test_inventory_source_vars_source_plugin_ok(post, inventory, admin_user, source, source_var_actual, source_var_expected, description): - r = post(reverse('api:inventory_source_list'), - {'name': 'new inv src', 'source_vars': json.dumps(source_var_actual), 'inventory': inventory.pk, 'source': source}, - admin_user, expect=201) - - assert r.data['source_vars'] == json.dumps(source_var_expected) - - -@pytest.mark.django_db -@pytest.mark.parametrize('source_var_actual,description', [ - ({'plugin': 'namespace.collection.script'}, 'scm source type source_vars are ignored valid'), - ({'plugin': 'namespace.collection.script'}, 'scm source type source_vars are ignored invalid'), - ({'plugin': ''}, 'scm source type source_vars are ignored blank'), - ({}, 'scm source type source_vars are ignored non-existent'), -]) -def test_inventory_source_vars_source_plugin_scm_ok(post, inventory, admin_user, project, source_var_actual, description): - r = post(reverse('api:inventory_source_list'), - {'name': 'new inv src', - 'source_vars': json.dumps(source_var_actual), - 'inventory': inventory.pk, - 'source': 'scm', - 'source_project': project.id,}, - admin_user, expect=201) - - assert r.data['source_vars'] == json.dumps(source_var_actual) - - @pytest.mark.django_db @pytest.mark.parametrize('role,expect', [ ('admin_role', 200), From d518891520a3e04d99b8f9b752c273ceb78d19ba Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 5 Aug 2020 12:06:37 -0400 Subject: [PATCH 177/188] inventory plugin inventory parameter take precedence over env vars --- awx/main/models/inventory.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 5121f081ed..6e3a22434c 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1464,8 +1464,9 @@ class gce(PluginFileInjector): def inventory_as_dict(self, inventory_update, private_data_dir): ret = super().inventory_as_dict(inventory_update, private_data_dir) credential = inventory_update.get_cloud_credential() - # TODO: Align precedence of ENV vs. inventory config w/ Ansible behavior - ret['projects'] = [credential.get_input('project', default='')] + # InventorySource.source_vars take precedence over ENV vars + if 'projects' not in ret: + ret['projects'] = [credential.get_input('project', default='')] return ret From a9cdf076904f1782ddfcbbe48bff7b068b40d934 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 5 Aug 2020 13:33:24 -0400 Subject: [PATCH 178/188] push global invsource fields onto invsource obj --- awx/api/serializers.py | 2 +- awx/main/migrations/0118_inventory_plugins.py | 32 ++++++++++++++- awx/main/models/inventory.py | 34 ++++++++++++++- awx/main/tasks.py | 30 ++++++-------- awx/settings/defaults.py | 41 +------------------ 5 files changed, 79 insertions(+), 60 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 9cf953262b..83575025e7 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1937,7 +1937,7 @@ class InventorySourceOptionsSerializer(BaseSerializer): class Meta: fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential', - 'overwrite', 'overwrite_vars', + 'enabled_var', 'enabled_value', 'host_filter', 'overwrite', 'overwrite_vars', 'custom_virtualenv', 'timeout', 'verbosity') def get_related(self, obj): diff --git a/awx/main/migrations/0118_inventory_plugins.py b/awx/main/migrations/0118_inventory_plugins.py index e69a3b7740..da21337d69 100644 --- a/awx/main/migrations/0118_inventory_plugins.py +++ b/awx/main/migrations/0118_inventory_plugins.py @@ -4,7 +4,7 @@ import logging import json import yaml -from django.db import migrations +from django.db import migrations, models from awx.main.models.base import VarsDictProperty @@ -90,4 +90,34 @@ class Migration(migrations.Migration): model_name='inventoryupdate', name='source_regions', ), + migrations.AddField( + model_name='inventorysource', + name='enabled_value', + field=models.TextField(blank=True, default='', help_text='Only used when enabled_var is set. Value when the host is considered enabled. For example if enabled_var="status.power_state"and enabled_value="powered_on" with host variables:{ "status": { "power_state": "powered_on", "created": "2020-08-04T18:13:04+00:00", "healthy": true }, "name": "foobar", "ip_address": "192.168.2.1"}The host would be marked enabled. If power_state where any value other than powered_on then the host would be disabled when imported into Tower. If the key is not found then the host will be enabled'), + ), + migrations.AddField( + model_name='inventorysource', + name='enabled_var', + field=models.TextField(blank=True, default='', help_text='Retrieve the enabled state from the given dict of host variables. The enabled variable may be specified as "foo.bar", in which case the lookup will traverse into nested dicts, equivalent to: from_dict.get("foo", {}).get("bar", default)'), + ), + migrations.AddField( + model_name='inventorysource', + name='host_filter', + field=models.TextField(blank=True, default='', help_text='Regex where only matching hosts will be imported into Tower.'), + ), + migrations.AddField( + model_name='inventoryupdate', + name='enabled_value', + field=models.TextField(blank=True, default='', help_text='Only used when enabled_var is set. Value when the host is considered enabled. For example if enabled_var="status.power_state"and enabled_value="powered_on" with host variables:{ "status": { "power_state": "powered_on", "created": "2020-08-04T18:13:04+00:00", "healthy": true }, "name": "foobar", "ip_address": "192.168.2.1"}The host would be marked enabled. If power_state where any value other than powered_on then the host would be disabled when imported into Tower. If the key is not found then the host will be enabled'), + ), + migrations.AddField( + model_name='inventoryupdate', + name='enabled_var', + field=models.TextField(blank=True, default='', help_text='Retrieve the enabled state from the given dict of host variables. The enabled variable may be specified as "foo.bar", in which case the lookup will traverse into nested dicts, equivalent to: from_dict.get("foo", {}).get("bar", default)'), + ), + migrations.AddField( + model_name='inventoryupdate', + name='host_filter', + field=models.TextField(blank=True, default='', help_text='Regex where only matching hosts will be imported into Tower.'), + ), ] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 6e3a22434c..79568ce2a2 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -7,7 +7,6 @@ import time import logging import re import copy -import json import os.path from urllib.parse import urljoin import yaml @@ -863,6 +862,39 @@ class InventorySourceOptions(BaseModel): default='', help_text=_('Inventory source variables in YAML or JSON format.'), ) + enabled_var = models.TextField( + blank=True, + default='', + help_text=_('Retrieve the enabled state from the given dict of host ' + 'variables. The enabled variable may be specified as "foo.bar", ' + 'in which case the lookup will traverse into nested dicts, ' + 'equivalent to: from_dict.get("foo", {}).get("bar", default)'), + ) + enabled_value = models.TextField( + blank=True, + default='', + help_text=_('Only used when enabled_var is set. Value when the host is ' + 'considered enabled. For example if enabled_var="status.power_state"' + 'and enabled_value="powered_on" with host variables:' + '{' + ' "status": {' + ' "power_state": "powered_on",' + ' "created": "2020-08-04T18:13:04+00:00",' + ' "healthy": true' + ' },' + ' "name": "foobar",' + ' "ip_address": "192.168.2.1"' + '}' + 'The host would be marked enabled. If power_state where any ' + 'value other than powered_on then the host would be disabled ' + 'when imported into Tower. If the key is not found then the ' + 'host will be enabled'), + ) + host_filter = models.TextField( + blank=True, + default='', + help_text=_('Regex where only matching hosts will be imported into Tower.'), + ) overwrite = models.BooleanField( default=False, help_text=_('Overwrite local groups and hosts from remote inventory source.'), diff --git a/awx/main/tasks.py b/awx/main/tasks.py index acd7548a91..4809d2c136 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -23,6 +23,7 @@ import fcntl from pathlib import Path from uuid import uuid4 import urllib.parse as urlparse +import shlex # Django from django.conf import settings @@ -2559,23 +2560,18 @@ class RunInventoryUpdate(BaseTask): args.extend(['--venv', inventory_update.ansible_virtualenv_path]) src = inventory_update.source - # Add several options to the shell arguments based on the - # inventory-source-specific setting in the AWX configuration. - # These settings are "per-source"; it's entirely possible that - # they will be different between cloud providers if an AWX user - # actively uses more than one. - if getattr(settings, '%s_ENABLED_VAR' % src.upper(), False): - args.extend(['--enabled-var', - getattr(settings, '%s_ENABLED_VAR' % src.upper())]) - if getattr(settings, '%s_ENABLED_VALUE' % src.upper(), False): - args.extend(['--enabled-value', - getattr(settings, '%s_ENABLED_VALUE' % src.upper())]) - if getattr(settings, '%s_GROUP_FILTER' % src.upper(), False): - args.extend(['--group-filter', - getattr(settings, '%s_GROUP_FILTER' % src.upper())]) - if getattr(settings, '%s_HOST_FILTER' % src.upper(), False): - args.extend(['--host-filter', - getattr(settings, '%s_HOST_FILTER' % src.upper())]) + if inventory_update.enabled_var: + args.extend(['--enabled-var', shlex.quote(inventory_update.enabled_var)]) + args.extend(['--enabled-value', shlex.quote(inventory_update.enabled_value)]) + else: + if getattr(settings, '%s_ENABLED_VAR' % src.upper(), False): + args.extend(['--enabled-var', + getattr(settings, '%s_ENABLED_VAR' % src.upper())]) + if getattr(settings, '%s_ENABLED_VALUE' % src.upper(), False): + args.extend(['--enabled-value', + getattr(settings, '%s_ENABLED_VALUE' % src.upper())]) + if inventory_update.host_filter: + args.extend(['--host-filter', shlex.quote(inventory_update.host_filter)]) if getattr(settings, '%s_EXCLUDE_EMPTY_GROUPS' % src.upper()): args.append('--exclude-empty-groups') if getattr(settings, '%s_INSTANCE_ID_VAR' % src.upper(), False): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 93e373e401..5aa0b834ea 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -669,59 +669,32 @@ INV_ENV_VARIABLE_BLOCKED = ("HOME", "USER", "_", "TERM") # ---------------- # -- Amazon EC2 -- # ---------------- - -# Inventory variable name/values for determining if host is active/enabled. EC2_ENABLED_VAR = 'ec2_state' EC2_ENABLED_VALUE = 'running' - -# Inventory variable name containing unique instance ID. EC2_INSTANCE_ID_VAR = 'ec2_id' - -# Filter for allowed group/host names when importing inventory from EC2. -EC2_GROUP_FILTER = r'^.+$' -EC2_HOST_FILTER = r'^.+$' EC2_EXCLUDE_EMPTY_GROUPS = True - # ------------ # -- VMware -- # ------------ -# Inventory variable name/values for determining whether a host is -# active in vSphere. VMWARE_ENABLED_VAR = 'guest.gueststate' VMWARE_ENABLED_VALUE = 'running' - -# Inventory variable name containing the unique instance ID. VMWARE_INSTANCE_ID_VAR = 'config.instanceUuid, config.instanceuuid' - -# Filter for allowed group and host names when importing inventory -# from VMware. -VMWARE_GROUP_FILTER = r'^.+$' -VMWARE_HOST_FILTER = r'^.+$' VMWARE_EXCLUDE_EMPTY_GROUPS = True VMWARE_VALIDATE_CERTS = False + # --------------------------- # -- Google Compute Engine -- # --------------------------- - -# Inventory variable name/value for determining whether a host is active -# in Google Compute Engine. GCE_ENABLED_VAR = 'status' GCE_ENABLED_VALUE = 'running' - -# Filter for allowed group and host names when importing inventory from -# Google Compute Engine. -GCE_GROUP_FILTER = r'^.+$' -GCE_HOST_FILTER = r'^.+$' GCE_EXCLUDE_EMPTY_GROUPS = True GCE_INSTANCE_ID_VAR = 'gce_id' # -------------------------------------- # -- Microsoft Azure Resource Manager -- # -------------------------------------- -AZURE_RM_GROUP_FILTER = r'^.+$' -AZURE_RM_HOST_FILTER = r'^.+$' AZURE_RM_ENABLED_VAR = 'powerstate' AZURE_RM_ENABLED_VALUE = 'running' AZURE_RM_INSTANCE_ID_VAR = 'id' @@ -732,8 +705,6 @@ AZURE_RM_EXCLUDE_EMPTY_GROUPS = True # --------------------- OPENSTACK_ENABLED_VAR = 'status' OPENSTACK_ENABLED_VALUE = 'ACTIVE' -OPENSTACK_GROUP_FILTER = r'^.+$' -OPENSTACK_HOST_FILTER = r'^.+$' OPENSTACK_EXCLUDE_EMPTY_GROUPS = True OPENSTACK_INSTANCE_ID_VAR = 'openstack.id' @@ -742,8 +713,6 @@ OPENSTACK_INSTANCE_ID_VAR = 'openstack.id' # --------------------- RHV_ENABLED_VAR = 'status' RHV_ENABLED_VALUE = 'up' -RHV_GROUP_FILTER = r'^.+$' -RHV_HOST_FILTER = r'^.+$' RHV_EXCLUDE_EMPTY_GROUPS = True RHV_INSTANCE_ID_VAR = 'id' @@ -752,8 +721,6 @@ RHV_INSTANCE_ID_VAR = 'id' # --------------------- TOWER_ENABLED_VAR = 'remote_tower_enabled' TOWER_ENABLED_VALUE = 'true' -TOWER_GROUP_FILTER = r'^.+$' -TOWER_HOST_FILTER = r'^.+$' TOWER_EXCLUDE_EMPTY_GROUPS = True TOWER_INSTANCE_ID_VAR = 'remote_tower_id' @@ -762,8 +729,6 @@ TOWER_INSTANCE_ID_VAR = 'remote_tower_id' # --------------------- SATELLITE6_ENABLED_VAR = 'foreman.enabled' SATELLITE6_ENABLED_VALUE = 'True' -SATELLITE6_GROUP_FILTER = r'^.+$' -SATELLITE6_HOST_FILTER = r'^.+$' SATELLITE6_EXCLUDE_EMPTY_GROUPS = True SATELLITE6_INSTANCE_ID_VAR = 'foreman.id' # SATELLITE6_GROUP_PREFIX and SATELLITE6_GROUP_PATTERNS defined in source vars @@ -773,8 +738,6 @@ SATELLITE6_INSTANCE_ID_VAR = 'foreman.id' # --------------------- #CUSTOM_ENABLED_VAR = #CUSTOM_ENABLED_VALUE = -CUSTOM_GROUP_FILTER = r'^.+$' -CUSTOM_HOST_FILTER = r'^.+$' CUSTOM_EXCLUDE_EMPTY_GROUPS = False #CUSTOM_INSTANCE_ID_VAR = @@ -783,8 +746,6 @@ CUSTOM_EXCLUDE_EMPTY_GROUPS = False # --------------------- #SCM_ENABLED_VAR = #SCM_ENABLED_VALUE = -SCM_GROUP_FILTER = r'^.+$' -SCM_HOST_FILTER = r'^.+$' SCM_EXCLUDE_EMPTY_GROUPS = False #SCM_INSTANCE_ID_VAR = From f04aff81c49f33f0e3d0ded9a88256ba2304771e Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 27 Aug 2020 11:42:55 -0400 Subject: [PATCH 179/188] Add details to inv source field help text --- .../inventories/related/sources/sources.form.js | 4 ++-- .../Inventory/shared/InventorySourceSubForms/SharedFields.jsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js index c49904b2e3..66b6693d58 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js @@ -305,7 +305,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ type: 'text', dataTitle: i18n._('Host Filter'), dataPlacement: 'right', - awPopOver: "

" + i18n._("Regular expression where only matching hosts will be imported.") + "

", + awPopOver: "

" + i18n._("Regular expression where only matching host names will be imported. The filter is applied as a post-processing step after any inventory plugin filters are applied.") + "

", dataContainer: 'body', ngDisabled: '!(inventory_source_obj.summary_fields.user_capabilities.edit || canAdd)', subForm: 'sourceSubForm' @@ -325,7 +325,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n){ type: 'text', dataTitle: i18n._('Enabled Value'), dataPlacement: 'right', - awPopOver: "

" + i18n._("If the enabled variable matches this value, the host will be enabled on import.") + "

", + awPopOver: "

" + i18n._("This field is ignored unless an Enabled Variable is set. If the enabled variable matches this value, the host will be enabled on import.") + "

", dataContainer: 'body', ngDisabled: '!(inventory_source_obj.summary_fields.user_capabilities.edit || canAdd)', subForm: 'sourceSubForm' diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx index 14e0bbd896..8150a9a45f 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx @@ -174,7 +174,7 @@ export const EnabledValueField = withI18n()(({ i18n }) => { id="inventory-enabled-value" label={i18n._(t`Enabled Value`)} tooltip={i18n._( - t`If the enabled variable matches this value, the host will be enabled on import.` + t`This field is ignored unless an Enabled Variable is set. If the enabled variable matches this value, the host will be enabled on import.` )} name="enabled_value" type="text" @@ -188,7 +188,7 @@ export const HostFilterField = withI18n()(({ i18n }) => { id="host-filter" label={i18n._(t`Host Filter`)} tooltip={i18n._( - t`Regular expression where only matching hosts will be imported.` + t`Regular expression where only matching host names will be imported. The filter is applied as a post-processing step after any inventory plugin filters are applied.` )} name="host_filter" type="text" From dcf5917a4e996c306005a9641857d7b01b695773 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Thu, 27 Aug 2020 12:13:40 -0400 Subject: [PATCH 180/188] Check that host_filter is regexp --- .../shared/InventorySourceSubForms/SharedFields.jsx | 3 ++- awx/ui_next/src/util/validators.jsx | 11 +++++++++++ awx/ui_next/src/util/validators.test.js | 10 ++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx index 8150a9a45f..35e2c3fc30 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SharedFields.jsx @@ -3,7 +3,7 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { useField } from 'formik'; import { FormGroup } from '@patternfly/react-core'; -import { minMaxValue } from '../../../../util/validators'; +import { minMaxValue, regExp } from '../../../../util/validators'; import AnsibleSelect from '../../../../components/AnsibleSelect'; import { VariablesField } from '../../../../components/CodeMirrorInput'; import FormField, { @@ -192,6 +192,7 @@ export const HostFilterField = withI18n()(({ i18n }) => { )} name="host_filter" type="text" + validate={regExp(i18n)} /> ); }); diff --git a/awx/ui_next/src/util/validators.jsx b/awx/ui_next/src/util/validators.jsx index ee97cd4702..035b4dfb56 100644 --- a/awx/ui_next/src/util/validators.jsx +++ b/awx/ui_next/src/util/validators.jsx @@ -88,3 +88,14 @@ export function combine(validators) { return undefined; }; } + +export function regExp(i18n) { + return value => { + try { + RegExp(value); + } catch { + return i18n._(t`This field must be a regular expression`); + } + return undefined; + }; +} diff --git a/awx/ui_next/src/util/validators.test.js b/awx/ui_next/src/util/validators.test.js index f3e37c6cde..11b1a3bfd9 100644 --- a/awx/ui_next/src/util/validators.test.js +++ b/awx/ui_next/src/util/validators.test.js @@ -5,6 +5,7 @@ import { noWhiteSpace, integer, combine, + regExp, } from './validators'; const i18n = { _: val => val }; @@ -128,4 +129,13 @@ describe('validators', () => { }); expect(combine(validators)('ok')).toBeUndefined(); }); + + test('regExp rejects invalid regular expression', () => { + expect(regExp(i18n)('[')).toEqual({ + id: 'This field must be a regular expression', + }); + expect(regExp(i18n)('')).toBeUndefined(); + expect(regExp(i18n)('ok')).toBeUndefined(); + expect(regExp(i18n)('[^a-zA-Z]')).toBeUndefined(); + }); }); From 03ad1aa1412a22b98a14f54c83fe840065d259af Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 27 Aug 2020 14:32:17 -0400 Subject: [PATCH 181/188] remove backwords migraiton support for inv plugins * Do not write out inventory source_vars to a file on disk as they _may_ contain sensitive information. This also removes support for backwards migrations. This is fine, backwards migration is really only useful during development. --- awx/main/migrations/0118_inventory_plugins.py | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/awx/main/migrations/0118_inventory_plugins.py b/awx/main/migrations/0118_inventory_plugins.py index da21337d69..991de733d3 100644 --- a/awx/main/migrations/0118_inventory_plugins.py +++ b/awx/main/migrations/0118_inventory_plugins.py @@ -1,7 +1,6 @@ # Generated by Django 2.2.11 on 2020-07-20 19:56 import logging -import json import yaml from django.db import migrations, models @@ -12,7 +11,6 @@ from ._inventory_source_vars import FrozenInjectors logger = logging.getLogger('awx.main.migrations') -BACKUP_FILENAME = '/tmp/tower_migration_inventory_source_vars.json' def _get_inventory_sources(InventorySource): @@ -34,8 +32,6 @@ def inventory_source_vars_forward(apps, schema_editor): if inv_source_obj.source in FrozenInjectors: source_vars_backup[inv_source_obj.id] = dict(inv_source_obj.source_vars_dict) - with open(BACKUP_FILENAME, 'w') as fh: - json.dump(source_vars_backup, fh) injector = FrozenInjectors[inv_source_obj.source]() new_inv_source_vars = injector.inventory_as_dict(inv_source_obj, None) @@ -43,21 +39,6 @@ def inventory_source_vars_forward(apps, schema_editor): inv_source_obj.save() -def inventory_source_vars_backward(apps, schema_editor): - InventorySource = apps.get_model("main", "InventorySource") - try: - with open(BACKUP_FILENAME, 'r') as fh: - source_vars_backup = json.load(fh) - except FileNotFoundError: - print(f"Rollback file not found {BACKUP_FILENAME}") - return - - for inv_source_obj in _get_inventory_sources(InventorySource): - if inv_source_obj.id in source_vars_backup: - inv_source_obj.source_vars = source_vars_backup[inv_source_obj.id] - inv_source_obj.save() - - class Migration(migrations.Migration): dependencies = [ @@ -65,7 +46,7 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(inventory_source_vars_forward, inventory_source_vars_backward,), + migrations.RunPython(inventory_source_vars_forward), migrations.RemoveField( model_name='inventorysource', name='group_by', From 99aff939309ea05948acfea83dc25ac0e75b2e16 Mon Sep 17 00:00:00 2001 From: Alan Rominger Date: Thu, 27 Aug 2020 14:23:50 -0400 Subject: [PATCH 182/188] Get rid of ansible version checking --- awx/main/management/commands/inventory_import.py | 14 ++++---------- awx/main/models/inventory.py | 4 ---- awx/main/tasks.py | 14 ++++---------- awx/main/tests/unit/test_tasks.py | 7 ------- awx/main/utils/common.py | 10 +++------- 5 files changed, 11 insertions(+), 38 deletions(-) diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index 2e51283445..f0aa2feff2 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -39,7 +39,6 @@ from awx.main.utils import ( build_proot_temp_dir, get_licenser ) -from awx.main.utils.common import _get_ansible_version from awx.main.signals import disable_activity_stream from awx.main.constants import STANDARD_INVENTORY_UPDATE_ENV from awx.main.utils.pglock import advisory_lock @@ -136,15 +135,10 @@ class AnsibleInventoryLoader(object): # inside of /venv/ansible, so we override the specified interpreter # https://github.com/ansible/ansible/issues/50714 bargs = ['python', ansible_inventory_path, '-i', self.source] - ansible_version = _get_ansible_version(ansible_inventory_path[:-len('-inventory')]) - if ansible_version != 'unknown': - this_version = Version(ansible_version) - if this_version >= Version('2.5'): - bargs.extend(['--playbook-dir', self.source_dir]) - if this_version >= Version('2.8'): - if self.verbosity: - # INFO: -vvv, DEBUG: -vvvvv, for inventory, any more than 3 makes little difference - bargs.append('-{}'.format('v' * min(5, self.verbosity * 2 + 1))) + bargs.extend(['--playbook-dir', self.source_dir]) + if self.verbosity: + # INFO: -vvv, DEBUG: -vvvvv, for inventory, any more than 3 makes little difference + bargs.append('-{}'.format('v' * min(5, self.verbosity * 2 + 1))) logger.debug('Using base command: {}'.format(' '.join(bargs))) return bargs diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 79568ce2a2..5305e6e532 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1373,10 +1373,6 @@ class PluginFileInjector(object): collection = None collection_migration = '2.9' # Starting with this version, we use collections - def __init__(self, ansible_version): - # This is InventoryOptions instance, could be source or inventory update - self.ansible_version = ansible_version - @classmethod def get_proper_name(cls): if cls.plugin_name is None: diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 4809d2c136..ac1a9dca04 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -73,7 +73,7 @@ from awx.main.utils import (update_scm_url, ignore_inventory_group_removal, extract_ansible_vars, schedule_task_manager, get_awx_version) from awx.main.utils.ansible import read_ansible_config -from awx.main.utils.common import _get_ansible_version, get_custom_venv_choices +from awx.main.utils.common import get_custom_venv_choices from awx.main.utils.external_logging import reconfigure_rsyslog from awx.main.utils.safe_yaml import safe_dump, sanitize_jinja from awx.main.utils.reload import stop_local_services @@ -841,12 +841,6 @@ class BaseTask(object): logger.error('Failed to update %s after %d retries.', self.model._meta.object_name, _attempt) - def get_ansible_version(self, instance): - if not hasattr(self, '_ansible_version'): - self._ansible_version = _get_ansible_version( - ansible_path=self.get_path_to_ansible(instance, executable='ansible')) - return self._ansible_version - def get_path_to(self, *args): ''' Return absolute path relative to this file. @@ -2460,7 +2454,7 @@ class RunInventoryUpdate(BaseTask): If no private data is needed, return None. """ if inventory_update.source in InventorySource.injectors: - injector = InventorySource.injectors[inventory_update.source](self.get_ansible_version(inventory_update)) + injector = InventorySource.injectors[inventory_update.source]() return injector.build_private_data(inventory_update, private_data_dir) def build_env(self, inventory_update, private_data_dir, isolated, private_data_files=None): @@ -2488,7 +2482,7 @@ class RunInventoryUpdate(BaseTask): injector = None if inventory_update.source in InventorySource.injectors: - injector = InventorySource.injectors[inventory_update.source](self.get_ansible_version(inventory_update)) + injector = InventorySource.injectors[inventory_update.source]() if injector is not None: env = injector.build_env(inventory_update, env, private_data_dir, private_data_files) @@ -2601,7 +2595,7 @@ class RunInventoryUpdate(BaseTask): injector = None if inventory_update.source in InventorySource.injectors: - injector = InventorySource.injectors[src](self.get_ansible_version(inventory_update)) + injector = InventorySource.injectors[src]() if injector is not None: content = injector.inventory_contents(inventory_update, private_data_dir) diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index cda720b6ab..5ce64894e6 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -1880,13 +1880,6 @@ class TestProjectUpdateCredentials(TestJobExecution): assert env['FOO'] == 'BAR' -@pytest.fixture -def mock_ansible_version(): - with mock.patch('awx.main.tasks._get_ansible_version', mock.MagicMock(return_value='2.10')) as _fixture: - yield _fixture - - -@pytest.mark.usefixtures("mock_ansible_version") class TestInventoryUpdateCredentials(TestJobExecution): @pytest.fixture def inventory_update(self): diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index a017dba61b..a65120c8e8 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -162,13 +162,14 @@ def memoize_delete(function_name): return cache.delete(function_name) -def _get_ansible_version(ansible_path): +@memoize() +def get_ansible_version(): ''' Return Ansible version installed. Ansible path needs to be provided to account for custom virtual environments ''' try: - proc = subprocess.Popen([ansible_path, '--version'], + proc = subprocess.Popen(['ansible', '--version'], stdout=subprocess.PIPE) result = smart_str(proc.communicate()[0]) return result.split('\n')[0].replace('ansible', '').strip() @@ -176,11 +177,6 @@ def _get_ansible_version(ansible_path): return 'unknown' -@memoize() -def get_ansible_version(): - return _get_ansible_version('ansible') - - def get_awx_version(): ''' Return AWX version as reported by setuptools. From a6712cfd60a6de5f874c043a416707b8bb36b0e1 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 27 Aug 2020 16:20:06 -0400 Subject: [PATCH 183/188] bump inv plugin migration to avoid conflict --- .../{0118_inventory_plugins.py => 0119_inventory_plugins.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename awx/main/migrations/{0118_inventory_plugins.py => 0119_inventory_plugins.py} (98%) diff --git a/awx/main/migrations/0118_inventory_plugins.py b/awx/main/migrations/0119_inventory_plugins.py similarity index 98% rename from awx/main/migrations/0118_inventory_plugins.py rename to awx/main/migrations/0119_inventory_plugins.py index 991de733d3..670fb7887b 100644 --- a/awx/main/migrations/0118_inventory_plugins.py +++ b/awx/main/migrations/0119_inventory_plugins.py @@ -42,7 +42,7 @@ def inventory_source_vars_forward(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('main', '0117_v400_remove_cloudforms_inventory'), + ('main', '0118_add_remote_archive_scm_type'), ] operations = [ From 043a7f8599dd79f4b7ef10e613deeef91b9fb55f Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 27 Aug 2020 16:38:00 -0400 Subject: [PATCH 184/188] more get_ansible_version removal --- .../functional/test_inventory_source_injectors.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index 994c2b4c11..4601668d25 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -209,7 +209,7 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential env, content = read_content(private_data_dir, envvars, inventory_update) # Assert inventory plugin inventory file is in private_data_dir - inventory_filename = InventorySource.injectors[inventory_update.source]('2.9').filename + inventory_filename = InventorySource.injectors[inventory_update.source]().filename assert len([True for k in content.keys() if k.endswith(inventory_filename)]) > 0, \ f"'{inventory_filename}' file not found in inventory update runtime files {content.keys()}" @@ -252,8 +252,7 @@ def test_inventory_update_injected_content(this_kind, inventory, fake_credential with mock.patch('awx.main.queue.CallbackQueueDispatcher.dispatch', lambda self, obj: None): # Also do not send websocket status updates with mock.patch.object(UnifiedJob, 'websocket_emit_status', mock.Mock()): - with mock.patch.object(task, 'get_ansible_version', return_value='2.13'): - # The point of this test is that we replace run with assertions - with mock.patch('awx.main.tasks.ansible_runner.interface.run', substitute_run): - # so this sets up everything for a run and then yields control over to substitute_run - task.run(inventory_update.pk) + # The point of this test is that we replace run with assertions + with mock.patch('awx.main.tasks.ansible_runner.interface.run', substitute_run): + # so this sets up everything for a run and then yields control over to substitute_run + task.run(inventory_update.pk) From 72fc314da1b143b54c829f18ee3368a81c502379 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 27 Aug 2020 16:57:43 -0400 Subject: [PATCH 185/188] flake8 fix --- awx/main/management/commands/inventory_import.py | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/main/management/commands/inventory_import.py b/awx/main/management/commands/inventory_import.py index f0aa2feff2..53a1660c4f 100644 --- a/awx/main/management/commands/inventory_import.py +++ b/awx/main/management/commands/inventory_import.py @@ -12,7 +12,6 @@ import sys import time import traceback import shutil -from distutils.version import LooseVersion as Version # Django from django.conf import settings From 924273f5892768270b8c7f38e6674f72641b5eff Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 27 Aug 2020 19:05:40 -0400 Subject: [PATCH 186/188] more get_ansible_version test failure fixes --- awx/main/tests/functional/models/test_inventory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/tests/functional/models/test_inventory.py b/awx/main/tests/functional/models/test_inventory.py index 2b3c747868..04b92d5a1d 100644 --- a/awx/main/tests/functional/models/test_inventory.py +++ b/awx/main/tests/functional/models/test_inventory.py @@ -255,7 +255,7 @@ class TestInventorySourceInjectors: are named correctly, because Ansible will reject files that do not have these exact names """ - injector = InventorySource.injectors[source]('2.7.7') + injector = InventorySource.injectors[source]() assert injector.filename == filename @pytest.mark.parametrize('source,proper_name', [ @@ -269,7 +269,7 @@ class TestInventorySourceInjectors: ('tower', 'awx.awx.tower'), ]) def test_plugin_proper_names(self, source, proper_name): - injector = InventorySource.injectors[source]('2.9') + injector = InventorySource.injectors[source]() assert injector.get_proper_name() == proper_name From 059999c7c3076438880ed9302a595a661dbd2733 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 1 Sep 2020 13:15:07 -0400 Subject: [PATCH 187/188] update the inventory source module to respect new fields --- .../plugins/modules/tower_inventory_source.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index d3517b3f5a..4755670291 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -57,6 +57,18 @@ options: description: - The variables or environment fields to apply to this source type. type: dict + enabled_var: + description: + - The variable to use to determine enabled state e.g., "status.power_state" + type: str + enabled_value: + description: + - Value when the host is considered enabled, e.g., "powered_on" + type: str + host_filter: + description: + - If specified, AWX will only import hosts that match this regular expression. + type: str credential: description: - Credential to use for the source. @@ -152,6 +164,9 @@ def main(): source_path=dict(), source_script=dict(), source_vars=dict(type='dict'), + enabled_var=dict(), + enabled_value=dict(), + host_filter=dict(), credential=dict(), overwrite=dict(type='bool'), overwrite_vars=dict(type='bool'), @@ -232,7 +247,7 @@ def main(): 'description', 'source', 'source_path', 'source_vars', 'overwrite', 'overwrite_vars', 'custom_virtualenv', 'timeout', 'verbosity', 'update_on_launch', 'update_cache_timeout', - 'update_on_project_update' + 'update_on_project_update', 'enabled_var', 'enabled_value', 'host_filter', ) # Layer in all remaining optional information From 40ac719d6da220be403b5016f9a38bcbf134fcdf Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 1 Sep 2020 13:30:11 -0400 Subject: [PATCH 188/188] add a changelog note about upgrade issues from 13.0 -> 14.0 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c929aefc9..ffad4cfb70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This is a list of high-level changes for each release of AWX. A full list of com - Deprecated official support for Mercurial-based project updates - https://github.com/ansible/awx/issues/7932 - Added resource import/export support to the official AWX collection - https://github.com/ansible/awx/issues/7329 - Added the ability to import YAML-based resources (instead of just JSON) when using the AWX CLI - https://github.com/ansible/awx/pull/7808 +- Users upgrading from older versions of AWX may encounter an issue that causes their postgres container to restart in a loop (https://github.com/ansible/awx/issues/7854) - if you encounter this, bring your containers down and then back up (e.g., `docker-compose down && docker-compose up -d`) after upgrading to 14.1.0. - Updated the AWX CLI to export labels associated with Workflow Job Templates - https://github.com/ansible/awx/pull/7847 - Updated to the latest python-ldap to address a bug - https://github.com/ansible/awx/issues/7868 - Upgraded git-python to fix a bug that caused workflows to sometimes fail - https://github.com/ansible/awx/issues/6119