diff --git a/LICENSE-BSL.txt b/LICENSE-BSL.txt new file mode 100644 index 0000000000..63159efdfa --- /dev/null +++ b/LICENSE-BSL.txt @@ -0,0 +1,65 @@ +Business Source License +======================= + +License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. +“Business Source License” is a trademark of MariaDB Corporation Ab. + +Parameters + +Licensor: Artillery Software Inc +Licensed Works: Azure-related code in Artillery. The Licensed Work is + (c) 2024 Artillery Software Inc +Additional Use Grant: You may make use of the Licensed Work strictly for + evaluation and/or non-production use only. Your use + does not include offering the Licensed Work to third + parties on a hosted or embedded basis. +Change Date: Four years from the date the Licensed Work is published +Change License: MPL 2.0 + +For information about commercial licensing arrangements for the Licensed Work, +please contact sales@artillery.io. + +Notice + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. \ No newline at end of file diff --git a/README.md b/README.md index 3cd2a84b5a..996cc75d51 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,12 @@ - **Test anything**. HTTP, WebSocket, Socket.io, gRPC, Kinesis, and more. - **Powerful workload modeling**. Emulate complex user behavior with request chains, multiple steps, transactions, and more. - **Extensible & hackable**. Artillery has a plugin API to allow extending and customization. -- **Open source**. Permissive open source license to let you build on top of Artillery without worry. + +## License + +* Most of the code in this repository is licensed under the terms of the [MPL 2.0](https://www.mozilla.org/en-US/MPL/2.0/) license. +* Some Azure-specific modules are licensed under the terms of the [BSL license](https://mariadb.com/bsl-faq-adopting/). See [LICENSE-BSL.txt](./LICENSE-BSL.txt) for details. You may use Artillery on Azure for evaluation and proof-of-concept purposes, but commercial and/or production usage requires a commercial license. + → [Learn more](./packages/artillery#readme) diff --git a/package-lock.json b/package-lock.json index 8308ab656a..ccfdf77741 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "artillery", + "name": "artillery-monorepo", "lockfileVersion": 3, "requires": true, "packages": { @@ -13,7 +13,7 @@ "lint-staged": "^13.2.3", "prettier": "^2.8.8", "simple-git-hooks": "^2.8.1", - "turbo": "1.10.7" + "turbo": "^2.0.11" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1434,6 +1434,402 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/arm-containerinstance": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@azure/arm-containerinstance/-/arm-containerinstance-9.1.0.tgz", + "integrity": "sha512-N9T3/HJwWXvJuz7tin+nO+DYYCTGHILJ5Die3TtdF8Wd1ITfXGqB0vY/wOnspUu/AGojhaIKGmawAfPdw2kX8w==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.7.0", + "@azure/core-lro": "^2.5.0", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", + "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz", + "integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http-compat/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.3.tgz", + "integrity": "sha512-VxLk4AHLyqcHsfKe4MZ6IQ+D+ShuByy+RfStKfSjxJoL3WBWq17VNmrz8aT8etKzqc2nAeIyLxScjpzsS4fz8w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.9.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz", + "integrity": "sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.2.tgz", + "integrity": "sha512-l1Qrqhi4x1aekkV+OlcqsJa4AnAkj5p0JV8omgwjaV9OAbP41lvrMvs+CptfetKkeEaGRGSzby7sjPZEX7+kkQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-xml": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.4.3.tgz", + "integrity": "sha512-D6G7FEmDiTctPKuWegX2WTrS1enKZwqYwdKTO6ZN6JMigcCehlT0/CYl+zWpI9vQ9frwwp7GQT3/owaEXgnOsA==", + "dependencies": { + "fast-xml-parser": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.4.1.tgz", + "integrity": "sha512-DwnG4cKFEM7S3T+9u05NstXU/HN0dk45kPOinUyNKsn5VWwpXd9sbPKEg6kgJzGbm1lMuhx9o31PVbCtM5sfBA==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.14.0", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity/node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/@azure/identity/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@azure/identity/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz", + "integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.20.0.tgz", + "integrity": "sha512-ErsxbfCGIwdqD8jipqdxpfAGiUEQS7MWUe39Rjhl0ZVPsb1JEe9bZCe2+0g23HDH6DGyCAtnTNN9scPtievrMQ==", + "dependencies": { + "@azure/msal-common": "14.14.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.14.0.tgz", + "integrity": "sha512-OxcOk9H1/1fktHh6//VCORgSNJc2dCQObTm6JNmL824Z6iZSO6eFo/Bttxe0hETn9B+cr7gDouTQtsRq3YPuSQ==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.12.0.tgz", + "integrity": "sha512-jmk5Im5KujRA2AcyCb0awA3buV8niSrwXZs+NBJWIvxOz76RvNlusGIqi43A0h45BPUy93Qb+CPdpJn82NFTIg==", + "dependencies": { + "@azure/msal-common": "14.14.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.24.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.24.0.tgz", + "integrity": "sha512-l8cmWM4C7RoNCBOImoFMxhTXe1Lr+8uQ/IgnhRNMpfoA9bAFWoLG4XrWm6O5rKXortreVQuD+fc1hbzWklOZbw==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-client": "^1.6.2", + "@azure/core-http-compat": "^2.0.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.10.1", + "@azure/core-tracing": "^1.1.2", + "@azure/core-util": "^1.6.1", + "@azure/core-xml": "^1.3.2", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/storage-blob/node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/@azure/storage-queue": { + "version": "12.23.0", + "resolved": "https://registry.npmjs.org/@azure/storage-queue/-/storage-queue-12.23.0.tgz", + "integrity": "sha512-koVDpx/lXl3bx6GiyitIsLZ4rtywpTlfwKXiuTDif+dY6PhgSyN9mrq9AsHXaHQnx2CCpmoIzRSV5n4GoQGcmg==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-client": "^1.6.2", + "@azure/core-http-compat": "^2.0.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-rest-pipeline": "^1.10.1", + "@azure/core-tracing": "^1.1.2", + "@azure/core-util": "^1.6.1", + "@azure/core-xml": "^1.3.2", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.16.7", "dev": true, @@ -10726,6 +11122,14 @@ "node": ">=10" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -12555,6 +12959,27 @@ "fastest-levenshtein": "^1.0.7" } }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "license": "MIT", @@ -17093,6 +17518,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/opener": { "version": "1.5.2", "license": "(WTFPL OR MIT)", @@ -19711,6 +20152,15 @@ "node": ">=8.0.0" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, "node_modules/stream-browserify": { "version": "2.0.2", "dev": true, @@ -21220,27 +21670,26 @@ } }, "node_modules/turbo": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.7.tgz", - "integrity": "sha512-xm0MPM28TWx1e6TNC3wokfE5eaDqlfi0G24kmeHupDUZt5Wd0OzHFENEHMPqEaNKJ0I+AMObL6nbSZonZBV2HA==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.0.11.tgz", + "integrity": "sha512-imDlFFAvitbCm1JtDFJ6eG882qwxHUmVT2noPb3p2jq5o5DuXOchMbkVS9kUeC3/4WpY5N0GBZ3RvqNyjHZw1Q==", "dev": true, - "hasInstallScript": true, "bin": { "turbo": "bin/turbo" }, "optionalDependencies": { - "turbo-darwin-64": "1.10.7", - "turbo-darwin-arm64": "1.10.7", - "turbo-linux-64": "1.10.7", - "turbo-linux-arm64": "1.10.7", - "turbo-windows-64": "1.10.7", - "turbo-windows-arm64": "1.10.7" + "turbo-darwin-64": "2.0.11", + "turbo-darwin-arm64": "2.0.11", + "turbo-linux-64": "2.0.11", + "turbo-linux-arm64": "2.0.11", + "turbo-windows-64": "2.0.11", + "turbo-windows-arm64": "2.0.11" } }, "node_modules/turbo-darwin-64": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.10.7.tgz", - "integrity": "sha512-N2MNuhwrl6g7vGuz4y3fFG2aR1oCs0UZ5HKl8KSTn/VC2y2YIuLGedQ3OVbo0TfEvygAlF3QGAAKKtOCmGPNKA==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.0.11.tgz", + "integrity": "sha512-YlHEEhcm+jI1BSZoLugGHUWDfRXaNaQIv7tGQBfadYjo9kixBnqoTOU6s1ubOrQMID+lizZZQs79GXwqM6vohg==", "cpu": [ "x64" ], @@ -21251,9 +21700,9 @@ ] }, "node_modules/turbo-darwin-arm64": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.7.tgz", - "integrity": "sha512-WbJkvjU+6qkngp7K4EsswOriO3xrNQag7YEGRtfLoDdMTk4O4QTeU6sfg2dKfDsBpTidTvEDwgIYJhYVGzrz9Q==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.0.11.tgz", + "integrity": "sha512-K/YW+hWzRQ/wGmtffxllH4M1tgy8OlwgXODrIiAGzkSpZl9+pIsem/F86UULlhsIeavBYK/LS5+dzV3DPMjJ9w==", "cpu": [ "arm64" ], @@ -21264,9 +21713,9 @@ ] }, "node_modules/turbo-linux-64": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.10.7.tgz", - "integrity": "sha512-x1CF2CDP1pDz/J8/B2T0hnmmOQI2+y11JGIzNP0KtwxDM7rmeg3DDTtDM/9PwGqfPotN9iVGgMiMvBuMFbsLhg==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.0.11.tgz", + "integrity": "sha512-mv8CwGP06UPweMh1Vlp6PI6OWnkuibxfIJ4Vlof7xqjohAaZU5FLqeOeHkjQflH/6YrCVuS9wrK0TFOu+meTtA==", "cpu": [ "x64" ], @@ -21277,9 +21726,9 @@ ] }, "node_modules/turbo-linux-arm64": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.10.7.tgz", - "integrity": "sha512-JtnBmaBSYbs7peJPkXzXxsRGSGBmBEIb6/kC8RRmyvPAMyqF8wIex0pttsI+9plghREiGPtRWv/lfQEPRlXnNQ==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.0.11.tgz", + "integrity": "sha512-wLE5tl4oriTmHbuayc0ki0csaCplmVLj+uCWtecM/mfBuZgNS9ICNM9c4sB+Cfl5tlBBFeepqRNgvRvn8WeVZg==", "cpu": [ "arm64" ], @@ -21290,9 +21739,9 @@ ] }, "node_modules/turbo-windows-64": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.10.7.tgz", - "integrity": "sha512-7A/4CByoHdolWS8dg3DPm99owfu1aY/W0V0+KxFd0o2JQMTQtoBgIMSvZesXaWM57z3OLsietFivDLQPuzE75w==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.0.11.tgz", + "integrity": "sha512-tja3zvVCSWu3HizOoeQv0qDJ+GeWGWRFOOM6a8i3BYnXLgGKAaDZFcjwzgC50tWiAw4aowIVR4OouwIyRhLBaQ==", "cpu": [ "x64" ], @@ -21303,9 +21752,9 @@ ] }, "node_modules/turbo-windows-arm64": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.10.7.tgz", - "integrity": "sha512-D36K/3b6+hqm9IBAymnuVgyePktwQ+F0lSXr2B9JfAdFPBktSqGmp50JNC7pahxhnuCLj0Vdpe9RqfnJw5zATA==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.0.11.tgz", + "integrity": "sha512-sYjXP6k94Bqh99R+y3M1Ks6LRIEZybMz+7enA8GKl6JJ2ZFaXxTnS6q+/2+ii1+rRwxohj5OBb4gxODcF8Jd4w==", "cpu": [ "arm64" ], @@ -22708,6 +23157,10 @@ "@artilleryio/int-commons": "*", "@artilleryio/int-core": "*", "@aws-sdk/credential-providers": "^3.127.0", + "@azure/arm-containerinstance": "^9.1.0", + "@azure/identity": "^4.4.1", + "@azure/storage-blob": "^12.24.0", + "@azure/storage-queue": "^12.23.0", "@oclif/core": "^2.8.11", "@oclif/plugin-help": "^5.2.11", "@oclif/plugin-not-found": "^2.3.1", diff --git a/package.json b/package.json index 3cd3c67076..8862283683 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,6 @@ { + "name": "artillery-monorepo", + "packageManager": "^npm@10.8.2", "workspaces": [ "packages/*" ], @@ -16,7 +18,7 @@ "lint-staged": "^13.2.3", "prettier": "^2.8.8", "simple-git-hooks": "^2.8.1", - "turbo": "1.10.7" + "turbo": "2.0.11" }, "simple-git-hooks": { "commit-msg": "npx commitlint --edit $1", diff --git a/packages/artillery/README.md b/packages/artillery/README.md index 3d7eedc188..cabb28a9d5 100644 --- a/packages/artillery/README.md +++ b/packages/artillery/README.md @@ -49,9 +49,4 @@ We maintain a list of official and community-built [integrations and plugins](ht ### Example tests -You can find a list of ready-to-run Artillery examples under [`examples/`](https://github.com/artilleryio/artillery/tree/master/examples#readme). - - -## License - -**Artillery** is open-source software distributed under the terms of the [MPLv2](https://www.mozilla.org/en-US/MPL/2.0/) license. +You can find a list of ready-to-run Artillery examples under [`examples/`](https://github.com/artilleryio/artillery/tree/master/examples#readme). \ No newline at end of file diff --git a/packages/artillery/lib/cmds/run-aci.js b/packages/artillery/lib/cmds/run-aci.js new file mode 100644 index 0000000000..3530f1ae8f --- /dev/null +++ b/packages/artillery/lib/cmds/run-aci.js @@ -0,0 +1,103 @@ +// Copyright (c) Artillery Software Inc. +// SPDX-License-Identifier: BUSL-1.1 +// +// Non-evaluation use of Artillery on Azure requires a commercial license + +const { Command, Flags, Args } = require('@oclif/core'); +const { CommonRunFlags } = require('../cli/common-flags'); +const { DefaultAzureCredential } = require('@azure/identity'); +const { BlobServiceClient } = require('@azure/storage-blob'); + +const RunCommand = require('./run'); + +class RunACICommand extends Command { + static aliases = ['run:aci']; + static strict = false; + + async run() { + const { flags, argv, args } = await this.parse(RunACICommand); + flags.platform = 'az:aci'; + + flags['platform-opt'] = [ + `region=${flags.region}`, + `count=${flags.count}`, + `cpu=${flags.cpu}`, + `memory=${flags.memory}`, + `tenant-id=${flags['tenant-id']}`, + `subscription-id=${flags['subscription-id']}`, + `storage-account=${flags['storage-account']}`, + `blob-container=${flags['blob-container']}`, + `resource-group=${flags['resource-group']}`, + `client-id=${flags['client-id']}`, + `client-secret=${flags['client-secret']}` + ]; + + RunCommand.runCommandImplementation(flags, argv, args); + } +} + +RunACICommand.description = `launch a test using Azure ACI +Launch a test on Azure ACI + +Examples: + + To run a test script in my-test.yml on Azure ACI in eastus region + with 10 workers: + + $ artillery run:aci --region eastus --count 10 my-test.yml +`; +RunACICommand.flags = { + ...CommonRunFlags, + count: Flags.string({ + default: '1' + }), + region: Flags.string({ + description: 'Azure region to run the test in', + default: 'eastus' + }), + cpu: Flags.string({ + description: + 'Number of CPU cores per worker (defaults to 4 CPUs). A number between 1-4.' + }), + memory: Flags.string({ + description: + 'Memory in GB per worker (defaults to 8 GB). A number between 1-16.' + }), + 'tenant-id': Flags.string({ + description: + 'Azure tenant ID. May also be set via AZURE_TENANT_ID environment variable.' + }), + 'subscription-id': Flags.string({ + description: + 'Azure subscription ID. May also be set via AZURE_SUBSCRIPTION_ID environment variable.' + }), + 'storage-account': Flags.string({ + description: + 'Azure Blob Storage account name. May also be set via AZURE_STORAGE_ACCOUNT environment variable.' + }), + 'blob-container': Flags.string({ + description: + 'Azure Blob Storage container name. May also be set via AZURE_STORAGE_BLOB_CONTAINER environment variable.' + }), + 'resource-group': Flags.string({ + description: + 'Azure Resource Group name. May also be set via AZURE_RESOURCE_GROUP environment variable.' + }), + 'client-id': Flags.string({ + description: + 'Azure app client ID. May also be set via AZURE_CLIENT_ID environment variable.' + }), + 'client-secret': Flags.string({ + description: + 'Azure app client secret. May also be set via AZURE_CLIENT_SECRET environment variable.' + }) +}; + +RunACICommand.args = { + script: Args.string({ + name: 'script', + required: true + }) +}; + +module.exports = RunACICommand; diff --git a/packages/artillery/lib/cmds/run.js b/packages/artillery/lib/cmds/run.js index f3c0d5757f..e29e67591b 100644 --- a/packages/artillery/lib/cmds/run.js +++ b/packages/artillery/lib/cmds/run.js @@ -89,11 +89,11 @@ RunCommand.flags = { platform: Flags.string({ description: 'Runtime platform', default: 'local', - options: ['local', 'aws:lambda'] + options: ['local', 'aws:lambda', 'az:aci'] }), 'platform-opt': Flags.string({ description: - 'Set a platform-specific option, e.g. --platform region=eu-west-1 for AWS Lambda', + 'Set a platform-specific option, e.g. --platform-opt region=eu-west-1 for AWS Lambda', multiple: true }), count: Flags.string({ @@ -233,6 +233,12 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) { runnerOpts, launcherOpts ); + + if (!launcher) { + console.log('Failed to create launcher'); + await gracefulShutdown({ exitCode: 1 }); + } + let intermediates = []; const metricsToSuppress = getPluginMetricsToSuppress(script); diff --git a/packages/artillery/lib/launch-platform.js b/packages/artillery/lib/launch-platform.js index 498601e2af..5308af0cfc 100644 --- a/packages/artillery/lib/launch-platform.js +++ b/packages/artillery/lib/launch-platform.js @@ -14,13 +14,22 @@ const _ = require('lodash'); const PlatformLocal = require('./platform/local'); const PlatformLambda = require('./platform/aws-lambda'); +const PlatformAzureACI = require('./platform/az/aci'); async function createLauncher(script, payload, opts, launcherOpts) { launcherOpts = launcherOpts || { platform: 'local', mode: 'distribute' }; - return new Launcher(script, payload, opts, launcherOpts); + let l; + try { + l = new Launcher(script, payload, opts, launcherOpts); + } catch (err) { + console.log(err); + return null; + } + + return l; } class Launcher { constructor(script, payload, opts, launcherOpts) { @@ -48,6 +57,10 @@ class Launcher { this.platform = new PlatformLocal(script, payload, opts, launcherOpts); } else if (launcherOpts.platform === 'aws:lambda') { this.platform = new PlatformLambda(script, payload, opts, launcherOpts); + } else if (launcherOpts.platform === 'az:aci') { + this.platform = new PlatformAzureACI(script, payload, opts, launcherOpts); + } else { + throw new Error('Unknown platform: ' + launcherOpts.platform); } this.phaseStartedEventsSeen = {}; diff --git a/packages/artillery/lib/platform/aws-ecs/legacy/create-test.js b/packages/artillery/lib/platform/aws-ecs/legacy/create-test.js index 82d0fde2ee..c764cb0f79 100644 --- a/packages/artillery/lib/platform/aws-ecs/legacy/create-test.js +++ b/packages/artillery/lib/platform/aws-ecs/legacy/create-test.js @@ -46,29 +46,48 @@ async function createTest(scriptPath, options, callback) { context.configPath = absoluteConfigPath; } - await setDefaultAWSCredentials(); - - A.waterfall( - [ - A.constant(context), - async function (context) { - context.s3Bucket = await getBucketName(); - return context; - }, - prepareManifest, - printManifest, - syncS3, - writeTestMetadata - ], - function (err, context) { - if (err) { - console.log(err); - return; - } + if (options.customSyncClient) { + context.customSyncClient = options.customSyncClient; + } - callback(err, context); - } - ); + if (!options.customSyncClient) { + await setDefaultAWSCredentials(); + } + + return new Promise((resolve, reject) => { + A.waterfall( + [ + A.constant(context), + async function (context) { + if (!context.customSyncClient) { + context.s3Bucket = await getBucketName(); + return context; + } else { + context.s3Bucket = 'S3_BUCKET_ARGUMENT_NOT_USED_ON_AZURE'; + return context; + } + }, + prepareManifest, + printManifest, + syncS3, + writeTestMetadata + ], + function (err, context) { + if (err) { + console.log(err); + return; + } + + if (callback) { + callback(err, context); + } else if (err) { + reject(err); + } else { + resolve(context); + } + } + ); + }); } function prepareManifest(context, callback) { @@ -103,7 +122,13 @@ function printManifest(context, callback) { } function syncS3(context, callback) { - const plainS3 = createS3Client(); + let plainS3; + if (context.customSyncClient) { + plainS3 = context.customSyncClient; + } else { + plainS3 = createS3Client(); + } + const prefix = `tests/${context.name}`; context.s3Prefix = prefix; @@ -182,7 +207,13 @@ function writeTestMetadata(context, callback) { debug('metadata', metadata); - const s3 = createS3Client(); + let s3 = null; + if (context.customSyncClient) { + s3 = context.customSyncClient; + } else { + s3 = createS3Client(); + } + const key = context.s3Prefix + '/metadata.json'; // TODO: Rename to something less likely to clash debug('metadata location:', `${context.s3Bucket}/${key}`); s3.putObject( diff --git a/packages/artillery/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js b/packages/artillery/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js new file mode 100644 index 0000000000..d65b740426 --- /dev/null +++ b/packages/artillery/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js @@ -0,0 +1,29 @@ +// Copyright (c) Artillery Software Inc. +// SPDX-License-Identifier: BUSL-1.1 +// +// Non-evaluation use of Artillery on Azure requires a commercial license + +const { QueueClient } = require('@azure/storage-queue'); +const { DefaultAzureCredential } = require('@azure/identity'); + +function getAQS() { + return new QueueClient( + process.env.AZURE_STORAGE_QUEUE_URL, + new DefaultAzureCredential() + ); +} + +function sendMessage(queue, body, tags) { + const payload = JSON.stringify({ + payload: body, + // attributes: this.tags + attributes: tags.reduce((acc, tag) => { + acc[tag.key] = tag.value; + return acc; + }, {}) + }); + + return queue.sendMessage(payload); +} + +module.exports = { getAQS, sendMessage }; diff --git a/packages/artillery/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js b/packages/artillery/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js index 3e8b2cf2ba..0503a0f840 100644 --- a/packages/artillery/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js +++ b/packages/artillery/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js @@ -5,6 +5,7 @@ const AWS = require('aws-sdk'); const debug = require('debug')('plugin:sqsReporter'); const uuid = require('node:crypto').randomUUID; +const { getAQS, sendMessage } = require('./azure-aqs'); module.exports = { Plugin: ArtillerySQSPlugin, @@ -36,13 +37,23 @@ function ArtillerySQSPlugin(script, events) { this.messageAttributes = messageAttributes; - this.sqs = new AWS.SQS({ - region: - process.env.SQS_REGION || script.config.plugins['sqs-reporter'].region - }); + this.sqs = null; + this.aqs = null; + + if (process.env.SQS_QUEUE_URL) { + this.sqs = new AWS.SQS({ + region: + process.env.SQS_REGION || script.config.plugins['sqs-reporter'].region + }); - this.queueUrl = - process.env.SQS_QUEUE_URL || script.config.plugins['sqs-reporter'].queueUrl; + this.queueUrl = + process.env.SQS_QUEUE_URL || + script.config.plugins['sqs-reporter'].queueUrl; + } + + if (process.env.AZURE_STORAGE_QUEUE_URL) { + this.aqs = getAQS(); + } events.on('stats', (statsOriginal) => { let body; @@ -51,105 +62,54 @@ function ArtillerySQSPlugin(script, events) { event: 'workerStats', stats: serialized }; - body = JSON.stringify(body); - - debug('Prepared messsage body'); - debug(body); - - this.unsent++; - // TODO: Check that body is not longer than 255kb - const params = { - MessageBody: body, - QueueUrl: this.queueUrl, - MessageAttributes: this.messageAttributes, - MessageDeduplicationId: uuid(), - MessageGroupId: this.testId - }; - - this.sqs.sendMessage(params, (err, data) => { - if (err) { - console.error(err); - } - this.unsent--; - }); + this.sendMessage(body); }); //TODO: reconcile some of this code with how lambda does sqs reporting events.on('phaseStarted', (phaseContext) => { - this.unsent++; - const body = JSON.stringify({ + const body = { event: 'phaseStarted', phase: phaseContext - }); - - const params = { - MessageBody: body, - QueueUrl: this.queueUrl, - MessageAttributes: this.messageAttributes, - MessageDeduplicationId: uuid(), - MessageGroupId: this.testId }; - this.sqs.sendMessage(params, (err, data) => { - if (err) { - console.error(err); - } - - this.unsent--; - }); + this.sendMessage(body); }); //TODO: reconcile some of this code with how lambda does sqs reporting events.on('phaseCompleted', (phaseContext) => { - this.unsent++; - const body = JSON.stringify({ + const body = { event: 'phaseCompleted', phase: phaseContext - }); - - const params = { - MessageBody: body, - QueueUrl: this.queueUrl, - MessageAttributes: this.messageAttributes, - MessageDeduplicationId: uuid(), - MessageGroupId: this.testId }; - - this.sqs.sendMessage(params, (err, data) => { - if (err) { - console.error(err); - } - - this.unsent--; - }); + this.sendMessage(body); }); events.on('done', (_stats) => { - this.unsent++; - const body = JSON.stringify({ - event: 'done' - }); - - const params = { - MessageBody: body, - QueueUrl: this.queueUrl, - MessageAttributes: this.messageAttributes, - MessageDeduplicationId: uuid(), - MessageGroupId: this.testId + const body = { + event: 'done', + stats: global.artillery.__SSMS.serializeMetrics(_stats) }; + this.sendMessage(body); + }); - this.sqs.sendMessage(params, (err, data) => { - if (err) { - console.error(err); - } - - this.unsent--; - }); + global.artillery.globalEvents.on('log', (opts, ...args) => { + if (process.env.SHIP_LOGS) { + const body = { + event: 'artillery.log', + log: { + opts, + args: [...args] + } + }; + + this.sendMessage(body); + } }); return this; } + ArtillerySQSPlugin.prototype.cleanup = function (done) { const interval = setInterval(() => { if (this.unsent <= 0) { @@ -158,3 +118,44 @@ ArtillerySQSPlugin.prototype.cleanup = function (done) { } }, 200).unref(); }; + +ArtillerySQSPlugin.prototype.sendMessage = function (body) { + if (this.sqs) { + this.sendSQS(body); + } else { + this.sendAQS(body); + } +}; + +ArtillerySQSPlugin.prototype.sendSQS = function (body) { + this.unsent++; + + const payload = JSON.stringify(body); + + const params = { + MessageBody: payload, + QueueUrl: this.queueUrl, + MessageAttributes: this.messageAttributes, + MessageDeduplicationId: uuid(), + MessageGroupId: this.testId + }; + + this.sqs.sendMessage(params, (err, _data) => { + if (err) { + console.error(err); + } + this.unsent--; + }); +}; + +ArtillerySQSPlugin.prototype.sendAQS = async function (body) { + this.unsent++; + sendMessage(this.aqs, body, this.tags) + .then((_res) => { + this.unsent--; + }) + .catch((err) => { + console.error(err); + this.unsent--; + }); +}; diff --git a/packages/artillery/lib/platform/aws-ecs/worker/Dockerfile b/packages/artillery/lib/platform/aws-ecs/worker/Dockerfile index d8c8dd6bcc..a644cef5dc 100644 --- a/packages/artillery/lib/platform/aws-ecs/worker/Dockerfile +++ b/packages/artillery/lib/platform/aws-ecs/worker/Dockerfile @@ -17,6 +17,8 @@ RUN apt-get update && apt-get install -y \ libtool \ awscli +RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash + ARG WORKER_VERSION ENV WORKER_VERSION=$WORKER_VERSION @@ -39,8 +41,6 @@ COPY ./packages/artillery/lib/platform/aws-ecs/worker/helpers.sh /artillery/help # Install dependencies RUN npm install -w artillery --ignore-scripts --omit=dev RUN npm install aws-lambda-ric -# TODO: add artillery-plugin-sqs-reporter as a depedency instead of installing it here -RUN npm install artillery-plugin-sqs-reporter RUN npm cache clean --force \ && rm -rf /root/.cache \ diff --git a/packages/artillery/lib/platform/aws-ecs/worker/helpers.sh b/packages/artillery/lib/platform/aws-ecs/worker/helpers.sh index 892b640c4e..2d3c02d986 100644 --- a/packages/artillery/lib/platform/aws-ecs/worker/helpers.sh +++ b/packages/artillery/lib/platform/aws-ecs/worker/helpers.sh @@ -1,5 +1,12 @@ #!/usr/bin/env bash +ARTIFACTORY_AUTH="${ARTIFACTORY_AUTH:-"null"}" +ARTIFACTORY_EMAIL="${ARTIFACTORY_EMAIL:-"null"}" +NPM_REGISTRY="${NPM_REGISTRY:-"null"}" +NPM_TOKEN="${NPM_TOKEN:-"null"}" +NPM_SCOPE="${NPM_SCOPE:-"null"}" +NPMRC="${NPMRC:-"null"}" + generate_npmrc () { if [[ "$ARTIFACTORY_AUTH" != "null" ]] && [[ "$ARTIFACTORY_EMAIL" != "null" ]] ; then echo "_auth=$ARTIFACTORY_AUTH" diff --git a/packages/artillery/lib/platform/aws-ecs/worker/loadgen-worker b/packages/artillery/lib/platform/aws-ecs/worker/loadgen-worker index 823f30c4e6..199d669eb9 100644 --- a/packages/artillery/lib/platform/aws-ecs/worker/loadgen-worker +++ b/packages/artillery/lib/platform/aws-ecs/worker/loadgen-worker @@ -44,6 +44,9 @@ CLI_PID= CLEANING_UP="no" #mode="${MODE:-run}" # "run" or "bootstrap" +is_azure= +azure_storage_container_name= + s3_test_data_path= cli_args=() cli_args_encoded= @@ -53,23 +56,46 @@ test_run_id= s3_run_data_base_path= s3_run_data_path= -taskArn=$(curl -s "$ECS_CONTAINER_METADATA_URI_V4/task" \ - | jq -r ".TaskARN" \ - | cut -d "/" -f 3) - -worker_id=${taskArn:-worker-$(pwgen -A 12 1)} -export WORKER_ID="$worker_id" # make available to Artillery custom scripts/environment +# This is set once we know if we're on Azure or AWS +worker_id= is_leader=${IS_LEADER:-false} # true or false -progress "Worker starting up, ID = $worker_id, version = ${WORKER_VERSION:-unknown}, leader = $is_leader" - -declare -r DEPENDENCIES=(jq aws pwgen node npm yarn) +declare -r DEPENDENCIES=(jq aws az pwgen node npm yarn tree) send_message () { + local body="$1" # body of the message, a string + local type="$2" # typeo of the message: debug, leader, ensure + + if [[ "$is_azure" = "yes" ]] ; then + send_message_aqs "$body" "$type" + else + send_message_sqs "$body" "$type" + fi +} + +send_message_aqs () { + set +e + set +o pipefail + + local body="$1" + local type="$2" + + local aqs_message_payload="{\"msg\":\"$body\",\"type\":\"$type\"}" + local aqs_message_attributes="{\"testId\": \"${test_run_id}\", \"workerId\": \"${worker_id}\"}" + + >/dev/null az storage message put \ + --content "{ \"payload\": $aqs_message_payload, \"attributes\": $aqs_message_attributes }" \ + --queue-name "$AQS_QUEUE_NAME" \ + --account-name "$AZURE_STORAGE_ACCOUNT" || true + + set -e + set -o pipefail +} + +send_message_sqs () { set +e set +o pipefail - # NOTE: no quotes to avoid quoting issues local body="$1" local type="$2" @@ -88,6 +114,49 @@ send_message () { set -o pipefail } +send_event () { + set +e + set +o pipefail + + local payload="$1" + + if [[ "$is_azure" = "yes" ]] ; then + send_event_aqs "$payload" + else + send_event_sqs "$payload" + fi + + set -e + set -o pipefail +} + +send_event_sqs () { + local payload="$1" + + local sqs_message_attributes="{\"testId\": {\"StringValue\": \"${test_run_id}\", \"DataType\": \"String\"}, \"workerId\": {\"StringValue\": \"${worker_id}\", \"DataType\": \"String\"}}" + + >/dev/null aws sqs send-message \ + --queue-url "${sqs_queue_url}" \ + --message-body "$payload" \ + --message-attributes "$sqs_message_attributes" \ + --message-group-id "${test_run_id}" \ + --message-deduplication-id "$(pwgen -A 32 1)" \ + --region "$aws_region" +} + +send_event_aqs () { + local payload="$1" + + local aqs_message_attributes="{\"testId\": \"${test_run_id}\", \"workerId\": \"${worker_id}\"}" + + >/dev/null az storage message put \ + --content "{ \"payload\": $payload, \"attributes\": $aqs_message_attributes }" \ + --queue-name "$AQS_QUEUE_NAME" \ + --account-name "$AZURE_STORAGE_ACCOUNT" +} + + + install_npm_dependencies () { if [[ -f "$TEST_DATA/package.json" ]] ; then echo "Installing dependencies in package.json" @@ -134,14 +203,33 @@ check_dependencies () { } sync_test_data () { - mkdir "$TEST_DATA" + mkdir "$TEST_DATA" || true pushd "$TEST_DATA" >/dev/null - aws s3 sync --exclude node_modules_stream.zip "$s3_test_data_path" . >/dev/null + + echo "is_azure: $is_azure" + + if [[ "$is_azure" = "yes" ]] ; then + sync_test_data_azure + else + sync_test_data_s3 + fi debug "$(pwd)" debug "$(ls -a)" } +sync_test_data_azure () { + # TODO: Exclude node_modules_stream.zip + # This recreates the directory structure in the container, i.e. we'll have tests/$test_run_id here with all files under it + # So we need to move them all up two levels to the current directory + az storage blob download-batch -d . --account-name "$AZURE_STORAGE_ACCOUNT" -s "$azure_storage_container_name" --pattern "tests/$test_run_id/*" + mv "tests/$test_run_id"/* . +} + +sync_test_data_s3 () { + aws s3 sync --exclude node_modules_stream.zip "$s3_test_data_path" . >/dev/null +} + check_test_data () { file_count=$(find . -maxdepth 1 -name "*" | grep -v '^.$' -c) if [[ ! $file_count -gt 0 ]]; then @@ -173,17 +261,29 @@ install_dependencies () { touch node_modules/.artillery fi - zip -r -q - node_modules | aws s3 cp - "$s3_test_data_path/node_modules_stream.zip" - + zip -r -q node_modules.zip node_modules # | aws s3 cp - "$s3_test_data_path/node_modules_stream.zip" echo "Modules pre-packaged" - aws s3 mv "$s3_test_data_path/node_modules_stream.zip" "$s3_test_data_path/node_modules.zip" + + # aws s3 mv "$s3_test_data_path/node_modules_stream.zip" "$s3_test_data_path/node_modules.zip" + + if [[ "$is_azure" = "yes" ]] ; then + az storage blob upload --overwrite --account-name "$AZURE_STORAGE_ACCOUNT" --container-name "$azure_storage_container_name" --file node_modules.zip --name "tests/$test_run_id/node_modules.zip" + else + aws s3 cp node_modules.zip "$s3_test_data_path/node_modules.zip" + fi + send_message "leader npm prepack end `date +%s`" "debug" send_message "prepack_end" "leader" else # wait until node_modules.zip is available and unzip, or timeout # TODO: use aws s3api wait object-exists with a custom timeout send_message "follower npm prepack wait start `date +%s`" "debug" - wait_for_go "$s3_test_data_path/node_modules.zip" + + if [[ "$is_azure" = "yes" ]] ; then + wait_for_go "tests/$test_run_id/node_modules.zip" + else + wait_for_go "$s3_test_data_path/node_modules.zip" + fi unzip -o -q node_modules.zip send_message "follower npm prepack wait end `date +%s`" "debug" fi @@ -194,9 +294,21 @@ install_dependencies () { signal_ready () { local synced_filename="synced_${worker_id}.json" echo "{ \"worker_id\": \"${worker_id}\" }" >> "$synced_filename" - local synced_dest="${s3_run_data_path}/${synced_filename}" - aws s3 cp "$synced_filename" "$synced_dest" 1>/dev/null 2>/dev/null - cp_status=$? + local synced_dest= + local cp_status= + + if [[ "$is_azure" = "yes" ]] ; then + + send_event "{\"event\": \"workerReady\"}" + + synced_dest="${azure_storage_container_name}/$synced_filename" + az storage blob upload --overwrite --account-name "$AZURE_STORAGE_ACCOUNT" --container-name "$azure_storage_container_name" --file "$synced_filename" --name "tests/$test_run_id/$synced_filename" + cp_status=$? + else + synced_dest="${s3_run_data_path}/${synced_filename}" + aws s3 cp "$synced_filename" "$synced_dest" 1>/dev/null 2>/dev/null + cp_status=$? + fi if [[ $cp_status -ne 0 ]]; then echo "could not send synced signal (to: $synced_dest)" @@ -210,13 +322,25 @@ signal_ready () { wait_for_go () { local SLEEP=2 local slept=0 - local objpath="${1:-$s3_run_data_path/go.json}" + local objpath= + + if [[ "$is_azure" = "yes" ]] ; then + objpath="${1:-tests/$test_run_id/go.json}" + else + objpath="${1:-$s3_run_data_path/go.json}" + fi echo "Waiting... ($objpath)" while true ; do set +e - aws s3 cp "$objpath" . 1>/dev/null 2>/dev/null + + if [[ "$is_azure" = "yes" ]] ; then + az storage blob download --account-name "$AZURE_STORAGE_ACCOUNT" --container-name "$azure_storage_container_name" --name "$objpath" --file "$(basename $objpath)" 1>/dev/null 2>/dev/null + else + aws s3 cp "$objpath" . 1>/dev/null 2>/dev/null + fi + local cp_exit_code=$? set -e @@ -257,12 +381,17 @@ run_a9 () { export NODE_PATH="$TEST_DATA/node_modules:${NODE_PATH:-""}" export DEBUG=${DEBUG:-"debug:mode:off"} # can set via --launch-config if needed - export ARTILLERY_PLUGIN_PATH=/artillery/packages/artillery/lib/platform/aws-ecs/legacy/plugins + export ARTILLERY_PLUGIN_PATH=${ARTILLERY_PLUGIN_PATH:-""}:/artillery/packages/artillery/lib/platform/aws-ecs/legacy/plugins export ARTILLERY_PLUGINS="{\"sqs-reporter\":{\"region\": \"${aws_region}\"},\"inspect-script\":{}}" export SQS_TAGS="[{\"key\": \"testId\", \"value\": \"${test_run_id}\"},{\"key\":\"workerId\", \"value\":\"${worker_id}\"}]" - export SQS_QUEUE_URL=$sqs_queue_url - export SQS_REGION=$aws_region + + if [[ "$is_azure" = "yes" ]] ; then + export AZURE_STORAGE_QUEUE_URL=$sqs_queue_url + else + export SQS_QUEUE_URL=$sqs_queue_url + export SQS_REGION=$aws_region + fi export ARTILLERY_DISABLE_ENSURE=true @@ -274,7 +403,7 @@ run_a9 () { MAX_OLD_SPACE_SIZE=${MAX_OLD_SPACE_SIZE:-12288} export NODE_OPTIONS="--max-http-header-size=32768 --max-old-space-size=$MAX_OLD_SPACE_SIZE ${NODE_OPTIONS:-""}" - (set +eu ; artillery "${cli_args[@]}" ; echo $? > exitCode ; set -eu) | tee output.txt & + (set +eu ; ${ARTILLERY_BINARY:-"artillery"} "${cli_args[@]}" ; echo $? > exitCode ; set -eu) | tee output.txt & debug "node processes:" debug "$(pgrep -lfa node)" sleep 5 @@ -310,8 +439,11 @@ run_a9 () { ;; esac - aws s3 cp output.txt "${s3_run_data_path}/worker-log-${worker_id}.txt" - echo "log: ${s3_run_data_path}/worker-log-${worker_id}.txt" + # TODO: Upload to Storage Blob if on Azure + if [[ "$is_azure" != "yes" ]] ; then + aws s3 cp output.txt "${s3_run_data_path}/worker-log-${worker_id}.txt" + echo "log: ${s3_run_data_path}/worker-log-${worker_id}.txt" + fi if [[ $CLI_STATUS -eq 0 ]] ; then EXIT_CODE=0 @@ -352,13 +484,16 @@ usage: $0 - run worker EOF } -while getopts "p:a:r:q:i:d:t:h?" OPTION +while getopts "z:p:a:r:q:i:d:t:h?" OPTION do case $OPTION in h) usage exit 0 ;; + z) + is_azure="$OPTARG" + ;; p) s3_test_data_path="$OPTARG" ;; @@ -369,6 +504,7 @@ do aws_region="$OPTARG" ;; q) + # Can also be AQS queue URL sqs_queue_url="$OPTARG" ;; i) @@ -399,12 +535,34 @@ if [[ ! $# -eq 0 ]] ; then exit fi -if [[ -z $s3_test_data_path || -z $cli_args_encoded || -z $aws_region || -z $test_run_id || -z $s3_run_data_base_path ]] ; then +if [[ -z $s3_test_data_path || -z $cli_args_encoded || -z $test_run_id ]] ; then echo "Some required argument(s) not provided, aborting" >&2 EXIT_CODE=$ERR_ARGS exit fi +if [[ "$is_azure" = "yes" ]] ; then + # Remap for convenience + azure_storage_container_name="$s3_test_data_path" + + az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID +fi + +if [[ "$is_azure" != "yes" ]] ; then + taskArn=$(curl -s "$ECS_CONTAINER_METADATA_URI_V4/task" \ + | jq -r ".TaskARN" \ + | cut -d "/" -f 3) +fi + +worker_id=${WORKER_ID_OVERRIDE:-$(pwgen -A 12 1)} +worker_id=${taskArn:-$worker_id} +# make available to Artillery custom scripts/environment +export WORKER_ID="$worker_id" + +progress "============================" +progress "Worker starting up, ID = $worker_id, version = ${WORKER_VERSION:-unknown}, leader = $is_leader" +progress "============================" + cleanup () { local sig="$1" @@ -414,6 +572,17 @@ cleanup () { if [[ $CLEANING_UP = "no" ]] ; then CLEANING_UP="yes" + if [[ "$is_azure" = "yes" ]] ; then + if [[ -z "${AZURE_RETAIN_BLOBS:-""}" ]] ; then + # This exits with 0 regardless of whether the pattern matches any + # blobs or not so it's OK to run this multiple times + az storage blob delete-batch \ + --account-name "$AZURE_STORAGE_ACCOUNT" \ + -s "$azure_storage_container_name" \ + --pattern "tests/$test_run_id/*" + fi + fi + # Abnormal exit: if [[ $CLI_RUNNING = "yes" ]] ; then printf "Interrupted with %s, stopping\n" "$sig" @@ -443,18 +612,9 @@ cleanup () { sqs_message_body="{\"event\": \"workerError\", \"exitCode\": \"$EXIT_CODE\" }" fi - sqs_message_attributes="{\"testId\": {\"StringValue\": \"${test_run_id}\", \"DataType\": \"String\"}, \"workerId\": {\"StringValue\": \"${worker_id}\", \"DataType\": \"String\"}}" - - >/dev/null aws sqs send-message \ - --queue-url "${sqs_queue_url}" \ - --message-body "$sqs_message_body" \ - --message-attributes "$sqs_message_attributes" \ - --message-group-id "${test_run_id}" \ - --message-deduplication-id "$(pwgen -A 32 1)" \ - --region "$aws_region" + send_event "$sqs_message_body" debug "Message body: $sqs_message_body" - debug "Message attributes: $sqs_message_attributes" exit $EXIT_CODE else if [[ ! $sig = "EXIT" ]] ; then diff --git a/packages/artillery/lib/platform/az/aci.js b/packages/artillery/lib/platform/az/aci.js new file mode 100644 index 0000000000..a201686b6a --- /dev/null +++ b/packages/artillery/lib/platform/az/aci.js @@ -0,0 +1,552 @@ +// Copyright (c) Artillery Software Inc. +// SPDX-License-Identifier: BUSL-1.1 +// +// Non-evaluation use of Artillery on Azure requires a commercial license + +const { QueueConsumer } = require('./aqs-queue-consumer'); +const { SQS_QUEUES_NAME_PREFIX } = require('../aws/constants'); +const { DefaultAzureCredential } = require('@azure/identity'); +const { QueueClient } = require('@azure/storage-queue'); +const { + ContainerInstanceManagementClient +} = require('@azure/arm-containerinstance'); +const { BlobServiceClient } = require('@azure/storage-blob'); +const { createTest } = require('../aws-ecs/legacy/create-test'); +const util = require('../aws-ecs/legacy/util'); +const generateId = require('../../util/generate-id'); +const EventEmitter = require('eventemitter3'); +const debug = require('debug')('platform:azure-aci'); +const { IMAGE_VERSION } = require('../aws-ecs/legacy/constants'); +const { regionNames } = require('./regions'); +const path = require('path'); + +class PlatformAzureACI { + constructor(script, variablePayload, opts, platformOpts) { + this.script = script; + this.variablePayload = variablePayload; + this.opts = opts; + this.platformOpts = platformOpts; + + this.events = new EventEmitter(); + + this.testRunId = platformOpts.testRunId; + + this.workers = {}; + this.count = 0; + this.waitingReadyCount = 0; + this.artilleryArgs = []; + + this.azureTenantId = + process.env.AZURE_TENANT_ID || platformOpts.platformConfig['tenant-id']; + this.azureSubscriptionId = + process.env.AZURE_SUBSCRIPTION_ID || + platformOpts.platformConfig['subscription-id']; + this.azureClientId = + process.env.AZURE_CLIENT_ID || platformOpts.platformConfig['client-id']; + this.azureClientSecret = + process.env.AZURE_CLIENT_SECRET || + platformOpts.platformConfig['client-secret']; + + this.storageAccount = + process.env.AZURE_STORAGE_ACCOUNT || + platformOpts.platformConfig['storage-account']; + this.blobContainerName = + process.env.AZURE_STORAGE_BLOB_CONTAINER || + platformOpts.platformConfig['blob-container']; + this.resourceGroupName = + process.env.AZURE_RESOURCE_GROUP || + platformOpts.platformConfig['resource-group']; + + this.cpu = parseInt(platformOpts.platformConfig.cpu, 10) || 4; + this.memory = parseInt(platformOpts.platformConfig.memory, 10) || 8; + this.region = platformOpts.platformConfig.region || 'eastus'; + + if (!regionNames.includes(this.region)) { + const err = new Error(`Invalid region: ${this.region}`); + err.code = 'INVALID_REGION'; + err.url = 'https://docs.art/az/regions'; + throw err; + } + + if ( + !this.azureTenantId || + !this.azureSubscriptionId || + !this.azureClientId || + !this.azureClientSecret + ) { + const err = new Error('Azure credentials not found'); + err.code = 'AZURE_CREDENTIALS_NOT_FOUND'; + err.url = 'https://docs.art/az/credentials'; + throw err; + } + + if ( + !this.storageAccount || + !this.blobContainerName || + !this.resourceGroupName + ) { + const err = new Error('Azure configuration not found'); + err.code = 'AZURE_CONFIG_NOT_FOUND'; + err.url = 'https://docs.art/az/configuration'; + throw err; + } + + this.containerInstances = []; + + return this; + } + + async init() { + const credential = new DefaultAzureCredential(); + + artillery.log('Tenant ID:', this.azureTenantId); + artillery.log('Subscription ID:', this.azureSubscriptionId); + artillery.log('Storage account:', this.storageAccount); + artillery.log('Blob container:', this.blobContainerName); + artillery.log('Resource group:', this.resourceGroupName); + + // + // Upload test bundle + // + + this.blobServiceClient = new BlobServiceClient( + `https://${this.storageAccount}.blob.core.windows.net`, + credential + ); + this.blobContainerClient = this.blobServiceClient.getContainerClient( + this.blobContainerName + ); + const { manifest } = await createTest(this.opts.absoluteScriptPath, { + flags: this.platformOpts.cliArgs, + name: this.testRunId, + customSyncClient: { + putObject: ({ _Bucket, Key, Body }, callback) => { + const blockBlobClient = + this.blobContainerClient.getBlockBlobClient(Key); + blockBlobClient + .upload(Body, Body.length) + .then((res) => { + callback(null, res); + }) + .catch((err) => { + callback(err, null); + }); + } + } + }); + + // + // Create the queue + // + this.queueName = `${SQS_QUEUES_NAME_PREFIX}_${this.testRunId}.` + .replaceAll('_', '-') + .slice(0, 63); + this.queueUrl = + process.env.AZURE_STORAGE_QUEUE_URL || + `https://${this.storageAccount}.queue.core.windows.net/${this.queueName}`; + const queueClient = new QueueClient(this.queueUrl, credential); + await queueClient.create(); + this.aqsClient = queueClient; + + // Construct CLI args for the container + + this.artilleryArgs = []; + this.artilleryArgs.push('run'); + + if (this.platformOpts.cliArgs.environment) { + this.artilleryArgs.push('-e'); + this.artilleryArgs.push(this.platformOpts.cliArgs.environment); + } + if (this.platformOpts.cliArgs.solo) { + this.artilleryArgs.push('--solo'); + } + + if (this.platformOpts.cliArgs.target) { + this.artilleryArgs.push('--target'); + this.artilleryArgs.push(this.platformOpts.cliArgs.target); + } + + if (this.platformOpts.cliArgs.variables) { + this.artilleryArgs.push('-v'); + this.artilleryArgs.push(this.platformOpts.cliArgs.variables); + } + + if (this.platformOpts.cliArgs.overrides) { + this.artilleryArgs.push('--overrides'); + this.artilleryArgs.push(this.platformOpts.cliArgs.overrides); + } + + if (this.platformOpts.cliArgs.dotenv) { + this.artilleryArgs.push('--dotenv'); + this.artilleryArgs.push(path.basename(this.platformOpts.cliArgs.dotenv)); + } + + if (this.platformOpts.cliArgs['scenario-name']) { + this.artilleryArgs.push('--scenario-name'); + this.artilleryArgs.push(this.platformOpts.cliArgs['scenario-name']); + } + + if (this.platformOpts.cliArgs.config) { + this.artilleryArgs.push('--config'); + const p = manifest.files.filter( + (x) => x.orig === this.opts.absoluteConfigPath + )[0]; + this.artilleryArgs.push(p.noPrefixPosix); + } + + if (this.platformOpts.cliArgs.quiet) { + this.artilleryArgs.push('--quiet'); + } + + // This needs to be the last argument for now: + const p = manifest.files.filter( + (x) => x.orig === this.opts.absoluteScriptPath + )[0]; + this.artilleryArgs.push(p.noPrefixPosix); + + const consumer = new QueueConsumer( + { poolSize: Infinity }, + { + queueUrl: process.env.AZURE_STORAGE_QUEUE_URL || this.queueUrl, + handleMessage: async (message) => { + let payload = null; + let attributes = null; + try { + const result = JSON.parse(message.Body); + payload = result.payload; + attributes = result.attributes; + } catch (parseErr) { + console.error(parseErr); + console.error(message.Body); + } + + if (process.env.LOG_QUEUE_MESSAGES) { + console.log(message); + } + + if (!payload) { + throw new Error('AQS message with an empty body'); + } + + if (!attributes || !attributes.testId || !attributes.workerId) { + throw new Error('AQS message with no testId or workerId'); + } + + if (this.testRunId !== attributes.testId) { + throw new Error('AQS message for an unknown testId'); + } + + const workerId = attributes.workerId; + if (payload.event === 'workerStats') { + this.events.emit('stats', workerId, payload); + } else if (payload.event === 'artillery.log') { + console.log(payload.log); + } else if (payload.event === 'done') { + // 'done' handler in Launcher exects the message argument to have an "id" and "report" fields + payload.id = workerId; + payload.report = payload.stats; + this.events.emit('done', workerId, payload); + } else if ( + payload.event === 'phaseStarted' || + payload.event === 'phaseCompleted' + ) { + payload.id = workerId; + this.events.emit(payload.event, workerId, { phase: payload.phase }); + } else if (payload.event === 'workerError') { + global.artillery.suggestedExitCode = payload.exitCode || 1; + + if (payload.exitCode != 21) { + this.events.emit(payload.event, workerId, { + id: workerId, + error: new Error( + `A worker has exited with an error. Reason: ${payload.reason}` + ), + level: 'error', + aggregatable: false, + logs: payload.logs + }); + } + } else if (payload.event == 'workerReady') { + this.events.emit(payload.event, workerId); + this.waitingReadyCount++; + + // TODO: Do this only for batches of workers with "wait" option set + if (this.waitingReadyCount === this.count) { + await this.sendGoSignal(); + } + } else { + debug(payload); + } + } + } + ); + + consumer.on('error', (err) => { + console.error(err); + }); + + consumer.start(); + this.queueConsumer = consumer; + + const metadata = { + region: this.region, + platformConfig: { + memory: this.memory, + cpu: this.cpu + } + }; + global.artillery.globalEvents.emit('metadata', metadata); + } + + getDesiredWorkerCount() { + return this.platformOpts.count; + } + + async startJob() { + await this.init(); + + console.log('Creating container instances...'); + + // Create & run the leader: + const { workerId } = await this.createWorker(); + this.workers[workerId] = { workerId }; + await this.runWorker(workerId, { isLeader: true }); + + // Run the rest of the containers we need: + for (let i = 0; i < this.platformOpts.count - 1; i++) { + const { workerId } = await this.createWorker(); + this.workers[workerId] = { workerId }; + await this.runWorker(workerId); + } + + let instancesCreated = false; + console.log('Waiting for Azure ACI to create container instances...'); + this.workerStatusInterval = setInterval(async () => { + const containerInstanceClient = new ContainerInstanceManagementClient( + new DefaultAzureCredential(), + this.azureSubscriptionId + ); + const containerGroupListResult = + containerInstanceClient.containerGroups.listByResourceGroup( + this.resourceGroupName + ); + + const containerGroupsInTestRun = []; + for await (const containerGroup of containerGroupListResult) { + if (containerGroup.name.indexOf(this.testRunId) > 0) { + containerGroupsInTestRun.push(containerGroup); + } + } + const byStatus = containerGroupsInTestRun.reduce((acc, cg) => { + if (!acc[cg.provisioningState]) { + acc[cg.provisioningState] = 0; + } + acc[cg.provisioningState]++; + return acc; + }, {}); + + if ( + (byStatus['Succeeded'] || 0) + (byStatus['Running'] || 0) === + this.count + ) { + if (!instancesCreated) + console.log( + 'Container instances created. Waiting for workers to start...' + ); + instancesCreated = true; + clearInterval(this.workerStatusInterval); + } + }, 10 * 1000).unref(); + } + + async shutdown() { + this.queueConsumer.stop(); + try { + await this.aqsClient.delete(); + } catch (_err) {} + + const credential = new DefaultAzureCredential(); + + if (process.env.RETAIN_CONTAINER_INSTANCES !== 'true') { + const containerInstanceClient = new ContainerInstanceManagementClient( + credential, + this.azureSubscriptionId + ); + + const containerGroupListResult = + containerInstanceClient.containerGroups.listByResourceGroup( + this.resourceGroupName + ); + + for await (const containerGroup of containerGroupListResult) { + if (containerGroup.name.indexOf(this.testRunId) > 0) { + try { + await containerInstanceClient.containerGroups.beginDeleteAndWait( + this.resourceGroupName, + containerGroup.name + ); + } catch (err) { + console.log(err); + } + } + } + } + } + + async sendGoSignal() { + const Key = `tests/${this.testRunId}/go.json`; + const blockBlobClient = this.blobContainerClient.getBlockBlobClient(Key); + const res = await blockBlobClient.upload('', 0); + } + + async createWorker() { + const workerId = generateId('worker'); + return { workerId }; + } + + async runWorker(workerId, opts = { isLeader: false }) { + const credential = new DefaultAzureCredential(); + + const imageVersion = + process.env.ARTILLERY_WORKER_IMAGE_VERSION || IMAGE_VERSION; + const defaultArchitecture = 'x86_64'; + const containerImageURL = `public.ecr.aws/d8a4z9o5/artillery-worker:${imageVersion}-${defaultArchitecture}`; + + const client = new ContainerInstanceManagementClient( + credential, + this.azureSubscriptionId + ); + + const environmentVariables = [ + { + name: 'WORKER_ID_OVERRIDE', + value: workerId + }, + { + name: 'ARTILLERY_TEST_RUN_ID', + value: this.testRunId + }, + // { + // name: 'DEBUGX', + // value: 'true', + // }, + { + name: 'DEBUG', + value: 'cloud' + }, + { + name: 'IS_LEADER', + value: String(opts.isLeader) + }, + { + name: 'AQS_QUEUE_NAME', + value: this.queueName + }, + { + name: 'AZURE_STORAGE_ACCOUNT', + value: this.storageAccount + }, + { + name: 'AZURE_SUBSCRIPTION_ID', + secureValue: this.azureSubscriptionId + }, + { + name: 'AZURE_TENANT_ID', + secureValue: this.azureTenantId + }, + { + name: 'AZURE_CLIENT_ID', + secureValue: this.azureClientId + }, + { + name: 'AZURE_CLIENT_SECRET', + secureValue: this.azureClientSecret + }, + { + name: 'AZURE_STORAGE_AUTH_MODE', + value: 'login' + } + ]; + + const cloudKey = + process.env.ARTILLERY_CLOUD_API_KEY || this.platformOpts.cliArgs.key; + + if (cloudKey) { + environmentVariables.push({ + name: 'ARTILLERY_CLOUD_API_KEY', + secureValue: cloudKey + }); + } + + const cloudEndpoint = process.env.ARTILLERY_CLOUD_ENDPOINT; + if (cloudEndpoint) { + environmentVariables.push({ + name: 'ARTILLERY_CLOUD_ENDPOINT', + secureValue: cloudEndpoint + }); + } + + const containerGroup = { + location: this.region, + containers: [ + { + name: 'artillery-worker', + image: containerImageURL, + resources: { + requests: { + cpu: this.cpu, + memoryInGB: this.memory + } + }, + command: [ + '/artillery/loadgen-worker', + '-z', + 'yes', // yes for Azure + '-q', + this.queueUrl, + '-p', + this.blobContainerName, + '-a', + util.btoa(JSON.stringify(this.artilleryArgs)), + '-i', + this.testRunId, + '-d', + 'NOT_USED_ON_AZURE', + '-r', + 'NOT_USED_ON_AZURE' + ], + environmentVariables + } + ], + osType: 'Linux', + restartPolicy: 'Never' + }; + + if (!this.ts) { + this.ts = Date.now(); + } + + const containerGroupName = `artillery-test-${this.ts}-${this.testRunId}-${this.count}`; + try { + const containerInstance = + await client.containerGroups.beginCreateOrUpdate( + this.resourceGroupName, + containerGroupName, + containerGroup + ); + + this.containerInstances.push(containerInstance); + + this.count++; + } catch (err) { + // TODO: Make this better + console.log(err.code); + console.log(err.details?.error?.message); + throw err; + } + } + + async stopWorker(_workerId) {} +} + +module.exports = PlatformAzureACI; diff --git a/packages/artillery/lib/platform/az/aqs-queue-consumer.js b/packages/artillery/lib/platform/az/aqs-queue-consumer.js new file mode 100644 index 0000000000..13db04d6da --- /dev/null +++ b/packages/artillery/lib/platform/az/aqs-queue-consumer.js @@ -0,0 +1,74 @@ +// Copyright (c) Artillery Software Inc. +// SPDX-License-Identifier: BUSL-1.1 +// +// Non-evaluation use of Artillery on Azure requires a commercial license +// + +const EventEmitter = require('eventemitter3'); + +const { QueueClient } = require('@azure/storage-queue'); +const { DefaultAzureCredential } = require('@azure/identity'); + +class AzureQueueConsumer extends EventEmitter { + constructor( + {} = { poolSize: 30 }, + { queueUrl, visibilityTimeout = 60, batchSize = 32, handleMessage } + ) { + super(); + this.queueUrl = queueUrl; + this.batchSize = batchSize; + this.visibilityTimeout = visibilityTimeout; + this.handleMessage = handleMessage; + + // TODO: Implement this + this.poolSize = this.poolSize; + return this; + } + + async start() { + const credential = new DefaultAzureCredential(); + this.queueClient = new QueueClient(this.queueUrl, credential); + + this.pollInterval = setInterval(async () => { + const messages = await this.queueClient.receiveMessages({ + numberOfMessages: this.batchSize, + visibilityTimeout: this.visibilityTimeout + }); + + // TODO: Handle errors - no auth, no queue, network etc + + for (const messageItem of messages.receivedMessageItems) { + const message = { + Body: messageItem.messageText + }; + + let processed = false; + try { + await this.handleMessage(message); + processed = true; + } catch (err) { + console.log(err); + } + + if (processed) { + try { + await this.queueClient.deleteMessage( + messageItem.messageId, + messageItem.popReceipt + ); + } catch (_err) {} + } + } + }, 5 * 1000); + } + + async stop() { + if (this.pollInterval) { + clearInterval(this.pollInterval); + } + } + + // TODO: events: error, empty +} + +module.exports = { QueueConsumer: AzureQueueConsumer }; diff --git a/packages/artillery/lib/platform/az/regions.js b/packages/artillery/lib/platform/az/regions.js new file mode 100644 index 0000000000..22bcd5246f --- /dev/null +++ b/packages/artillery/lib/platform/az/regions.js @@ -0,0 +1,52 @@ +const regionNames = [ + 'australiacentral', + 'australiacentral2', + 'australiaeast', + 'australiasoutheast', + 'brazilsouth', + 'canadacentral', + 'canadaeast', + 'centralindia', + 'centralus', + 'eastasia', + 'eastus', + 'eastus2', + 'francecentral', + 'francesouth', + 'germanynorth', + 'germanywestcentral', + 'israelcentral', + 'italynorth', + 'japaneast', + 'japanwest', + 'jioindiawest', + 'koreacentral', + 'koreasouth', + 'mexicocentral', + 'northcentralus', + 'northeurope', + 'norwayeast', + 'norwaywest', + 'polandcentral', + 'qatarcentral', + 'southafricanorth', + 'southafricawest', + 'southcentralus', + 'southeastasia', + 'southindia', + 'spaincentral', + 'swedencentral', + 'switzerlandnorth', + 'switzerlandwest', + 'uaecentral', + 'uaenorth', + 'uksouth', + 'ukwest', + 'westcentralus', + 'westeurope', + 'westindia', + 'westus', + 'westus2' +]; + +module.exports = { regionNames }; diff --git a/packages/artillery/lib/util/generate-id.js b/packages/artillery/lib/util/generate-id.js index 54f7701025..a77dcabbf6 100644 --- a/packages/artillery/lib/util/generate-id.js +++ b/packages/artillery/lib/util/generate-id.js @@ -1,9 +1,9 @@ const { customAlphabet } = require('nanoid'); -function generateId(prefix) { +function generateId(prefix = '') { const idf = customAlphabet('3456789abcdefghjkmnpqrtwxyz'); - const testRunId = `${prefix}${idf(4)}_${idf(29)}_${idf(4)}`; + const testRunId = `${prefix}${idf(4)}_${idf(29)}_${idf(4)}`; return testRunId; } -module.exports = generateId; \ No newline at end of file +module.exports = generateId; diff --git a/packages/artillery/package.json b/packages/artillery/package.json index 989f35bb8a..8d2edbed6e 100644 --- a/packages/artillery/package.json +++ b/packages/artillery/package.json @@ -96,6 +96,10 @@ "@artilleryio/int-commons": "*", "@artilleryio/int-core": "*", "@aws-sdk/credential-providers": "^3.127.0", + "@azure/arm-containerinstance": "^9.1.0", + "@azure/identity": "^4.2.0", + "@azure/storage-blob": "^12.18.0", + "@azure/storage-queue": "^12.22.0", "@oclif/core": "^2.8.11", "@oclif/plugin-help": "^5.2.11", "@oclif/plugin-not-found": "^2.3.1", diff --git a/turbo.json b/turbo.json index 278412eb72..72b9c7115f 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,6 @@ { "$schema": "https://turbo.build/schema.json", - "pipeline": { + "tasks": { "build": { "dependsOn": ["^build"], "outputs": [".next/**"]