Repository: walmartlabs/concord Branch: master Commit: 20fc58d4827b Files: 4414 Total size: 11.0 MB Directory structure: gitextract_43zvyq75/ ├── .codex ├── .gitattributes ├── .github/ │ ├── settings.xml │ └── workflows/ │ ├── build.yml │ └── docker-multiarch.yml ├── .gitignore ├── .insights.yml ├── .looper/ │ ├── render-settings.sh │ └── settings.xml ├── .looper.yml ├── .mvn/ │ └── wrapper/ │ └── maven-wrapper.properties ├── .sentinelpolicy ├── AGENTS.md ├── CHANGELOG.md ├── LICENSE ├── NOTES.md ├── README.md ├── SECURITY.md ├── agent/ │ ├── pom.xml │ └── src/ │ ├── assembly/ │ │ ├── default.conf │ │ ├── dist.xml │ │ └── start.sh │ ├── main/ │ │ ├── filtered-resources/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── agent/ │ │ │ ├── cfg/ │ │ │ │ ├── runnerV1.properties │ │ │ │ └── runnerV2.properties │ │ │ └── executors/ │ │ │ └── runner/ │ │ │ └── default-dependencies │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── agent/ │ │ │ ├── Agent.java │ │ │ ├── AgentAuthTokenProvider.java │ │ │ ├── AgentConstants.java │ │ │ ├── AgentModule.java │ │ │ ├── CommandHandler.java │ │ │ ├── ConfiguredJobRequest.java │ │ │ ├── DefaultStateFetcher.java │ │ │ ├── ExecutionException.java │ │ │ ├── JobInstance.java │ │ │ ├── JobRequest.java │ │ │ ├── Main.java │ │ │ ├── OneShotRunner.java │ │ │ ├── RepositoryManager.java │ │ │ ├── StateFetcher.java │ │ │ ├── Utils.java │ │ │ ├── Worker.java │ │ │ ├── WorkerFactory.java │ │ │ ├── cfg/ │ │ │ │ ├── AgentConfiguration.java │ │ │ │ ├── DockerConfiguration.java │ │ │ │ ├── GitConfiguration.java │ │ │ │ ├── GitHubConfiguration.java │ │ │ │ ├── ImportConfiguration.java │ │ │ │ ├── PreForkConfiguration.java │ │ │ │ ├── RepositoryCacheConfiguration.java │ │ │ │ ├── RuntimeConfiguration.java │ │ │ │ ├── ServerConfiguration.java │ │ │ │ └── Utils.java │ │ │ ├── docker/ │ │ │ │ └── OrphanSweeper.java │ │ │ ├── executors/ │ │ │ │ ├── JobExecutor.java │ │ │ │ ├── JobExecutorFactory.java │ │ │ │ └── runner/ │ │ │ │ ├── DefaultDependencies.java │ │ │ │ ├── JobDependencies.java │ │ │ │ ├── ProcessPool.java │ │ │ │ ├── RunnerCommandBuilder.java │ │ │ │ ├── RunnerJob.java │ │ │ │ ├── RunnerJobExecutor.java │ │ │ │ └── RunnerLog.java │ │ │ ├── guice/ │ │ │ │ ├── AgentDependencyManagerConfigurationProvider.java │ │ │ │ ├── AgentImportManager.java │ │ │ │ ├── AgentImportManagerProvider.java │ │ │ │ └── WorkerModule.java │ │ │ ├── logging/ │ │ │ │ ├── AbstractProcessLog.java │ │ │ │ ├── CombinedLogAppender.java │ │ │ │ ├── LocalProcessLog.java │ │ │ │ ├── LogAppender.java │ │ │ │ ├── LogSegmentStats.java │ │ │ │ ├── ProcessLog.java │ │ │ │ ├── ProcessLogFactory.java │ │ │ │ ├── RedirectedProcessLog.java │ │ │ │ ├── RemoteLogAppender.java │ │ │ │ ├── RemoteProcessLog.java │ │ │ │ ├── SegmentHeaderParser.java │ │ │ │ ├── SegmentedLogsConsumer.java │ │ │ │ └── StdOutLogAppender.java │ │ │ ├── mmode/ │ │ │ │ ├── MaintenanceModeListener.java │ │ │ │ └── MaintenanceModeNotifier.java │ │ │ └── remote/ │ │ │ ├── ApiClientFactory.java │ │ │ ├── AttachmentsUploader.java │ │ │ ├── ProcessStatusUpdater.java │ │ │ └── QueueClientProvider.java │ │ └── resources/ │ │ ├── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── agent/ │ │ │ └── logback.xml │ │ ├── concord-agent.conf │ │ └── logback.xml │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── agent/ │ │ ├── AgentAuthTokenProviderTest.java │ │ └── executors/ │ │ └── runner/ │ │ ├── JobDependenciesTest.java │ │ ├── SegmentHeaderParserTest.java │ │ └── SegmentedLogsConsumerTest.java │ └── resources/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── agent/ │ └── executors/ │ └── runner/ │ └── versions.properties ├── agent-operator/ │ ├── README.md │ ├── deploy/ │ │ ├── cluster_role.yml │ │ ├── cluster_role_binding.yml │ │ ├── crds/ │ │ │ ├── agentpools.concord.walmartlabs.com-v1.yml │ │ │ └── example.agentpool.yml │ │ ├── operator.yml │ │ └── service_account.yml │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── agentoperator/ │ │ │ ├── HashUtils.java │ │ │ ├── Operator.java │ │ │ ├── PodLabels.java │ │ │ ├── agent/ │ │ │ │ ├── AgentClient.java │ │ │ │ ├── AgentClientFactory.java │ │ │ │ ├── DefaultAgentClient.java │ │ │ │ └── NopAgentClient.java │ │ │ ├── crd/ │ │ │ │ ├── AgentPool.java │ │ │ │ ├── AgentPoolConfiguration.java │ │ │ │ └── AgentPoolList.java │ │ │ ├── planner/ │ │ │ │ ├── Change.java │ │ │ │ ├── CreateConfigMapChange.java │ │ │ │ ├── CreatePodChange.java │ │ │ │ ├── DeleteConfigMapChange.java │ │ │ │ ├── Planner.java │ │ │ │ ├── TagForRemovalChange.java │ │ │ │ └── TryToDeletePodChange.java │ │ │ ├── processqueue/ │ │ │ │ ├── ProcessQueueClient.java │ │ │ │ └── ProcessQueueEntry.java │ │ │ ├── resources/ │ │ │ │ ├── AgentConfigMap.java │ │ │ │ ├── AgentPod.java │ │ │ │ └── Resources.java │ │ │ └── scheduler/ │ │ │ ├── AgentPoolInstance.java │ │ │ ├── AutoScaler.java │ │ │ ├── AutoScalerFactory.java │ │ │ ├── DefaultAutoScaler.java │ │ │ ├── Event.java │ │ │ ├── LinearAutoScaler.java │ │ │ ├── QueueSelector.java │ │ │ └── Scheduler.java │ │ └── resources/ │ │ ├── logback.xml │ │ └── prestop-hook.sh │ └── test/ │ └── java/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── agentoperator/ │ ├── processqueue/ │ │ └── ProcessQueueClientTest.java │ └── scheduler/ │ ├── DefaultAutoScalerTest.java │ └── LinearAutoScalerTest.java ├── checkstyle.xml ├── cli/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── filtered-resources/ │ │ │ ├── defaultCfg.yml │ │ │ └── project.properties │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── cli/ │ │ │ ├── AbortException.java │ │ │ ├── App.java │ │ │ ├── CliConfig.java │ │ │ ├── CliExitCodes.java │ │ │ ├── CliPaths.java │ │ │ ├── Confirmation.java │ │ │ ├── GitIgnoreFilter.java │ │ │ ├── Lint.java │ │ │ ├── LocalCliRuntime.java │ │ │ ├── LocalFormInputs.java │ │ │ ├── LocalFormPrompts.java │ │ │ ├── LocalFormSession.java │ │ │ ├── LocalFormState.java │ │ │ ├── LocalSuspendPersistence.java │ │ │ ├── LocalSuspendPrinter.java │ │ │ ├── Main.java │ │ │ ├── PromptSupport.java │ │ │ ├── RemoteRun.java │ │ │ ├── Resume.java │ │ │ ├── Run.java │ │ │ ├── SelfUpdate.java │ │ │ ├── Verbosity.java │ │ │ ├── Version.java │ │ │ ├── lint/ │ │ │ │ ├── DummyImportsNormalizer.java │ │ │ │ ├── ExpressionLinter.java │ │ │ │ ├── FlowElementLinter.java │ │ │ │ ├── LintResult.java │ │ │ │ ├── Linter.java │ │ │ │ ├── TaskCallLinter.java │ │ │ │ └── Utils.java │ │ │ ├── runner/ │ │ │ │ ├── ApiKey.java │ │ │ │ ├── CliApiClientProvider.java │ │ │ │ ├── CliCheckpointService.java │ │ │ │ ├── CliDockerService.java │ │ │ │ ├── CliImportsListener.java │ │ │ │ ├── CliImportsNormalizer.java │ │ │ │ ├── CliLockService.java │ │ │ │ ├── CliRepositoryExporter.java │ │ │ │ ├── CliServicesModule.java │ │ │ │ ├── DependencyResolver.java │ │ │ │ ├── FlowStepLogger.java │ │ │ │ ├── TaskParamsLogger.java │ │ │ │ └── VaultProvider.java │ │ │ └── secrets/ │ │ │ ├── CliSecretService.java │ │ │ ├── FileSecretsProvider.java │ │ │ ├── RemoteSecretsProvider.java │ │ │ ├── SecretsProvider.java │ │ │ ├── SecretsProviderRef.java │ │ │ └── UncheckedIO.java │ │ └── resources/ │ │ ├── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── cli/ │ │ │ └── defaultCliConfig.yaml │ │ ├── default-vars.json │ │ └── logback.xml │ └── test/ │ ├── filtered-resources/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── cli/ │ │ └── defaultCfg/ │ │ ├── concord.yml │ │ └── defaults.yml │ ├── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── cli/ │ │ ├── AbstractTest.java │ │ ├── CliConfigTest.java │ │ ├── GitIgnoreFilterTest.java │ │ ├── LintTest.java │ │ ├── ResumeTest.java │ │ └── RunTest.java │ └── resources/ │ ├── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── cli/ │ │ ├── cliCheckpointService/ │ │ │ └── concord.yml │ │ ├── configWithDefaults.yaml │ │ ├── defaultTaskVars/ │ │ │ ├── concord.yml │ │ │ └── defaultTaskVars.json │ │ ├── fileForm/ │ │ │ └── concord.yml │ │ ├── fileRetryForm/ │ │ │ └── concord.yml │ │ ├── form/ │ │ │ └── concord.yml │ │ ├── lintV1/ │ │ │ ├── concord/ │ │ │ │ └── extra.concord.yml │ │ │ └── concord.yml │ │ ├── lintV2/ │ │ │ ├── concord/ │ │ │ │ └── extra.concord.yml │ │ │ └── concord.yml │ │ ├── mixedFormEvent/ │ │ │ └── concord.yml │ │ ├── multiContextConfig.yaml │ │ ├── parallelForms/ │ │ │ └── concord.yml │ │ ├── passwordRetry/ │ │ │ └── concord.yml │ │ ├── passwordSuspend/ │ │ │ └── concord.yml │ │ ├── processProjectInfo/ │ │ │ └── concord.yml │ │ ├── profileDeps/ │ │ │ └── concord.yml │ │ ├── resourceTask/ │ │ │ └── concord.yml │ │ ├── secretResume/ │ │ │ └── concord.yml │ │ ├── simple/ │ │ │ └── concord.yml │ │ ├── suspend/ │ │ │ └── concord.yml │ │ ├── testConfig.yaml │ │ └── validatedForm/ │ │ └── concord.yml │ └── logback-test.xml ├── client2/ │ ├── README.md │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── client2/ │ │ │ ├── ApiClientConfiguration.java │ │ │ ├── ApiClientFactory.java │ │ │ ├── ClientUtils.java │ │ │ ├── ConcordApiClient.java │ │ │ ├── CreateSecretRequest.java │ │ │ ├── DefaultApiClientFactory.java │ │ │ ├── ProcessDataInclude.java │ │ │ ├── ProcessListFilter.java │ │ │ ├── ProcessUtils.java │ │ │ ├── SecretClient.java │ │ │ ├── SecretNotFoundException.java │ │ │ ├── UpdateSecretRequest.java │ │ │ └── impl/ │ │ │ ├── ByteArrayBuffer.java │ │ │ ├── ContentType.java │ │ │ ├── Headers.java │ │ │ ├── HttpEntity.java │ │ │ ├── MultipartBuilder.java │ │ │ ├── MultipartRequestBodyHandler.java │ │ │ ├── NameValuePair.java │ │ │ ├── OffsetDateTimeDeserializer.java │ │ │ ├── OffsetDateTimeSerializer.java │ │ │ ├── RequestBody.java │ │ │ ├── RequestBodyHandler.java │ │ │ ├── ResponseBodyHandler.java │ │ │ └── auth/ │ │ │ ├── ApiKey.java │ │ │ ├── Authentication.java │ │ │ └── SessionToken.java │ │ └── template/ │ │ ├── README.md │ │ └── libraries/ │ │ └── native/ │ │ ├── ApiClient.mustache │ │ ├── api.mustache │ │ ├── api.mustache.orig │ │ ├── pojo.mustache │ │ └── pojo.mustache.orig │ └── test/ │ └── java/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── client2/ │ ├── ApiClientJsonTest.java │ ├── ProcessApiTest.java │ ├── SecretClientTest.java │ └── impl/ │ └── MultipartRequestBodyHandlerTest.java ├── common/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── common/ │ │ │ ├── AllowNulls.java │ │ │ ├── AuthTokenProvider.java │ │ │ ├── ConfigurationUtils.java │ │ │ ├── CycleChecker.java │ │ │ ├── DateTimeUtils.java │ │ │ ├── DockerProcessBuilder.java │ │ │ ├── DynamicTaskRegistry.java │ │ │ ├── ExceptionUtils.java │ │ │ ├── ExternalAuthToken.java │ │ │ ├── FileVisitor.java │ │ │ ├── GrepUtils.java │ │ │ ├── IOUtils.java │ │ │ ├── LogUtils.java │ │ │ ├── Matcher.java │ │ │ ├── MemoSupplier.java │ │ │ ├── ObjectInputStreamWithClassLoader.java │ │ │ ├── ObjectMapperProvider.java │ │ │ ├── PathUtils.java │ │ │ ├── Posix.java │ │ │ ├── PrivilegedAction.java │ │ │ ├── ReflectionUtils.java │ │ │ ├── StringUtils.java │ │ │ ├── TemporaryPath.java │ │ │ ├── ThreadLocalStack.java │ │ │ ├── ToStringHelper.java │ │ │ ├── TruncBufferedReader.java │ │ │ ├── ZipUtils.java │ │ │ ├── cfg/ │ │ │ │ ├── MappingAuthConfig.java │ │ │ │ └── OauthTokenConfig.java │ │ │ ├── form/ │ │ │ │ ├── ConcordFormFields.java │ │ │ │ ├── ConcordFormValidator.java │ │ │ │ ├── ConcordFormValidatorLocale.java │ │ │ │ └── DefaultConcordFormValidatorLocale.java │ │ │ ├── secret/ │ │ │ │ ├── BinaryDataSecret.java │ │ │ │ ├── HashAlgorithm.java │ │ │ │ ├── KeyPair.java │ │ │ │ ├── SecretEncryptedByType.java │ │ │ │ ├── SecretUtils.java │ │ │ │ └── UsernamePassword.java │ │ │ └── validation/ │ │ │ ├── ConcordId.java │ │ │ └── ConcordKey.java │ │ └── resources/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── common/ │ │ └── dockerPasswd │ └── test/ │ └── java/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── common/ │ ├── AuthTokenProviderTest.java │ ├── ConfigurationUtilsTest.java │ ├── CycleCheckerTest.java │ ├── DateTimeUtilsTest.java │ ├── ExternalAuthTokenTest.java │ ├── LogUtilsTest.java │ ├── MatcherTest.java │ ├── PathUtilsTest.java │ ├── StringUtilsTest.java │ ├── TruncBufferedReaderTest.java │ └── ZipUtilsTest.java ├── config/ │ ├── README.md │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── config/ │ ├── Config.java │ ├── ConfigExtractor.java │ ├── ConfigExtractors.java │ ├── ConfigModule.java │ ├── ListExtractor.java │ └── ListExtractors.java ├── console2/ │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── cfg.d.ts │ ├── index.html │ ├── npm.sh │ ├── package/ │ │ └── META-INF/ │ │ └── concord/ │ │ └── webapp.properties │ ├── package.json │ ├── pom.xml │ ├── public/ │ │ ├── cfg.js │ │ └── manifest.json │ ├── react-json-view.d.ts │ ├── src/ │ │ ├── App.tsx │ │ ├── api/ │ │ │ ├── __tests__/ │ │ │ │ ├── common.deepMerge.test.ts │ │ │ │ ├── common.parseNestedQueryParams.test.ts │ │ │ │ ├── common.parseQueryParams.test.ts │ │ │ │ └── common.queryParams.test.ts │ │ │ ├── audit/ │ │ │ │ └── index.ts │ │ │ ├── common.ts │ │ │ ├── noderoster/ │ │ │ │ └── index.ts │ │ │ ├── org/ │ │ │ │ ├── index.ts │ │ │ │ ├── jsonstore/ │ │ │ │ │ └── index.ts │ │ │ │ ├── project/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── repository/ │ │ │ │ │ └── index.ts │ │ │ │ ├── secret/ │ │ │ │ │ └── index.ts │ │ │ │ └── team/ │ │ │ │ └── index.ts │ │ │ ├── process/ │ │ │ │ ├── ansible/ │ │ │ │ │ └── index.ts │ │ │ │ ├── attachment/ │ │ │ │ │ └── index.ts │ │ │ │ ├── checkpoint/ │ │ │ │ │ └── index.ts │ │ │ │ ├── event/ │ │ │ │ │ └── index.ts │ │ │ │ ├── form/ │ │ │ │ │ └── index.ts │ │ │ │ ├── history/ │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── log/ │ │ │ │ │ ├── fetchLogAsBlobURL.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── wait/ │ │ │ │ └── index.ts │ │ │ ├── profile/ │ │ │ │ ├── api_token/ │ │ │ │ │ └── index.tsx │ │ │ │ └── user/ │ │ │ │ └── index.tsx │ │ │ ├── secret/ │ │ │ │ └── store/ │ │ │ │ └── index.ts │ │ │ ├── server/ │ │ │ │ └── index.ts │ │ │ ├── service/ │ │ │ │ ├── console/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── user/ │ │ │ │ │ └── index.ts │ │ │ │ └── custom_form/ │ │ │ │ └── index.ts │ │ │ ├── usePolling.ts │ │ │ └── user/ │ │ │ └── index.ts │ │ ├── components/ │ │ │ ├── atoms/ │ │ │ │ ├── ClassIcon.tsx │ │ │ │ ├── ColumnSort/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── FormikCheckbox/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── FormikDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── FormikFileInput/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── FormikInput/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── LogFileFromBlob/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ReactJson/ │ │ │ │ │ └── index.ts │ │ │ │ ├── RefreshButton/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Scrollable.tsx │ │ │ │ ├── TableSearchFilter/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── Truncate.tsx │ │ │ │ └── index.ts │ │ │ ├── molecules/ │ │ │ │ ├── BreadcrumbSegment/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── BulkProcessActionDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ButtonWithConfirmation/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── CreateNewEntityButton/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── DropdownWithAddition/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── EditProjectForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── EntityId/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── EntityOwnerChangeForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── EntityOwnerPopup/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── EntityRenameForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── FormWizardAction/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── GitHubLink/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── GlobalNavMenu/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Highlighter/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── HumanizedDuration/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── LoadingEditor/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── LocalTimestamp/ │ │ │ │ │ ├── __test__/ │ │ │ │ │ │ └── LocalTimestamp.test.tsx │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ └── LocalTimestamp.test.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── LogSegment/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── MainToolbar/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── NewAPITokenForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NewProjectForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NewSecretForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NewStorageForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NewTeamForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── PaginationToolBar/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── usePagination.tsx │ │ │ │ ├── ProcessActionDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessActionList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessAttachmentsList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessElementList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessHistoryList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessLastErrorModal/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessList/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ProcessListWithSearch/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessLogContainer/ │ │ │ │ │ ├── LogContainer.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessLogViewer/ │ │ │ │ │ ├── datetime.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ProcessStatusIcon/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessStatusTable/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessToolbar/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ProcessWaitList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectConfiguration/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ProjectRenameForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── RepositoryForm/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── RepositoryList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── RequestErrorMessage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SingleOperationPopup/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamAccessDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamAccessList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamRoleDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── WithCopyToClipboard/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ansible/ │ │ │ │ │ ├── AnsibleHostList/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── AnsibleTaskList/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── organisms/ │ │ │ │ ├── APITokenDeleteActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── APITokenList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── AuditLogActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── BreadcrumbsToolbar/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── BulkCancelProcessPopup/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── CancelProcessPopup/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── CheckpointView/ │ │ │ │ │ ├── ActionBar/ │ │ │ │ │ │ ├── ActiveFilters.tsx │ │ │ │ │ │ ├── CancelButton.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── styles.tsx │ │ │ │ │ ├── CheckpointErrorBoundry/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── CheckpointGroup/ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── styles.tsx │ │ │ │ │ ├── CheckpointPopup/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Container/ │ │ │ │ │ │ ├── __mocks__/ │ │ │ │ │ │ │ └── checkpointUtils.mocks.ts │ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ │ │ │ └── checkpointUtils.test.ts.snap │ │ │ │ │ │ │ ├── checkpointUtils.test.ts │ │ │ │ │ │ │ └── useQueryParams.test.tsx │ │ │ │ │ │ ├── checkpointUtils.ts │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── useForm.ts │ │ │ │ │ │ ├── usePopup.ts │ │ │ │ │ │ └── useQueryParams.ts │ │ │ │ │ ├── MetaFilterForm/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── NoCheckpointsMessage/ │ │ │ │ │ │ ├── NoCheckpointsMessge.test.tsx │ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ │ └── NoCheckpointsMessge.test.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ProcessCheckpoint/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ProcessCheckpointView.tsx │ │ │ │ │ ├── ProcessList/ │ │ │ │ │ │ ├── LeftContent.tsx │ │ │ │ │ │ ├── RightContent.tsx │ │ │ │ │ │ └── styles.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── shared/ │ │ │ │ │ ├── Labels.tsx │ │ │ │ │ ├── Layout.tsx │ │ │ │ │ ├── types.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── DeleteRepositoryPopup/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── DisableProcessPopup/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── EditProjectActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── EditRepositoryActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── EncryptValueActivity/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── FindLdapGroupField/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── FindOrganizationsField/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── FindTeamDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── FindUserField2/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── Login2/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── NewAPITokenActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NewProjectActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NewSecretActivity/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── NewStorageActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NewTeamActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── OrganizationActivity/ │ │ │ │ │ ├── OrganizationProcesses.tsx │ │ │ │ │ ├── OrganizationSettings.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── OrganizationList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── OrganizationOwnerChangeActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessActivity/ │ │ │ │ │ ├── Toolbar.tsx │ │ │ │ │ ├── favicon.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ProcessAttachmentsActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessCheckpointActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessChildrenActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessEventsActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessFormActivity/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── processFormNavigation.ts │ │ │ │ ├── ProcessHistoryActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessListActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessLogActivity/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ProcessLogActivityV2/ │ │ │ │ │ ├── LogSegmentActivity.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ProcessRestoreActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessStatusActivity/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ProcessWaitActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessWizard/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── useProcessWizard.ts │ │ │ │ ├── ProjectActivity/ │ │ │ │ │ ├── ProjectCheckpoints.tsx │ │ │ │ │ ├── ProjectProcesses.tsx │ │ │ │ │ ├── ProjectRepositories.tsx │ │ │ │ │ ├── ProjectSettings.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectConfigurationActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectDeleteActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectListActivity/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ProjectOrganizationChangeActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectOutVariablesModeActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectOwnerChangeActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectProcessExecModeActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectRawPayloadModeActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectRenameActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectSearch/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectSearchFormField/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectTeamAccessActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProtectedRoute/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── PublicKeyPopup/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── RedirectButton/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── RefreshRepositoryPopup/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── RepositoryActionDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── RequestErrorActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── RestartProcessPopup/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretDeleteActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretListActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretOrganizationChangeActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretOwnerChangeActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretProjectActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretRenameActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretSearch/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretStoreDropdown/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretTeamAccessActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretVisibilityActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ServerVersion/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── StartRepositoryPopup/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── TaskCallDetails/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamDeleteActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamLdapGroupList2/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamList/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamListActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamMemberList2/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamRenameActivity/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TopBar/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TriggeredByPopup/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── UserInfo/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── UserProcessActivity/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ValidateRepositoryPopup/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ansible/ │ │ │ │ │ ├── AnsibleTaskActivity/ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── ProcessAnsibleActivity/ │ │ │ │ │ ├── PlayInfoList.tsx │ │ │ │ │ ├── PlaybookChooser.tsx │ │ │ │ │ ├── PlaybookStats.tsx │ │ │ │ │ ├── TaskProgress.tsx │ │ │ │ │ ├── TaskProgressLegend.tsx │ │ │ │ │ ├── TaskStats.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── styles.css │ │ │ │ │ └── types.tsx │ │ │ │ └── index.ts │ │ │ ├── pages/ │ │ │ │ ├── APITokensListPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── AboutPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── AddRepositoryPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── CustomResourcePage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── JsonStorePage/ │ │ │ │ │ ├── EditStoreQueryActivity.tsx │ │ │ │ │ ├── EditStoreQueryForm.tsx │ │ │ │ │ ├── EditStoreQueryPage.tsx │ │ │ │ │ ├── ExecuteQueryResult.tsx │ │ │ │ │ ├── NewStorageQueryActivity.tsx │ │ │ │ │ ├── NewStorageQueryForm.tsx │ │ │ │ │ ├── NewStorageQueryPage.tsx │ │ │ │ │ ├── NewStorePage.tsx │ │ │ │ │ ├── StoreDataDeleteActivity.tsx │ │ │ │ │ ├── StoreDataList.tsx │ │ │ │ │ ├── StoreDeleteActivity.tsx │ │ │ │ │ ├── StoreListActivity.tsx │ │ │ │ │ ├── StoreOrganizationChangeActivity.tsx │ │ │ │ │ ├── StoreOwnerChangeActivity.tsx │ │ │ │ │ ├── StoreQueryDeleteActivity.tsx │ │ │ │ │ ├── StoreQueryList.tsx │ │ │ │ │ ├── StoreSettings.tsx │ │ │ │ │ ├── StoreTeamAccessActivity.tsx │ │ │ │ │ ├── StoreVisibilityActivity.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── LoginPage/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── LogoutPage/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── NewAPITokenPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NewProjectPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NewSecretPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NewTeamPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── NodeRoster/ │ │ │ │ │ ├── HostArtifacts.tsx │ │ │ │ │ ├── HostFacts.tsx │ │ │ │ │ ├── HostPage.tsx │ │ │ │ │ ├── HostProcesses.tsx │ │ │ │ │ ├── NodeRosterArtifactsList.tsx │ │ │ │ │ ├── NodeRosterHostsList.tsx │ │ │ │ │ └── NodeRosterPage.tsx │ │ │ │ ├── NotFoundPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── OrganizationListPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── OrganizationPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessCardFormPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessFormPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessListPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProcessWizardPage/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── ProfilePage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProjectPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── RepositoryPage/ │ │ │ │ │ ├── RepositoryEventsActivity.tsx │ │ │ │ │ ├── RepositoryTriggersActivity.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── SecretPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── TeamPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── UnauthorizedPage/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.css │ │ │ │ ├── UserActivityPage/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── UserInfoPage/ │ │ │ │ │ └── index.tsx │ │ │ │ └── index.ts │ │ │ └── templates/ │ │ │ ├── Layout/ │ │ │ │ ├── index.tsx │ │ │ │ └── styles.css │ │ │ ├── OrgActivityPage/ │ │ │ │ └── index.tsx │ │ │ └── index.ts │ │ ├── hooks/ │ │ │ ├── useApi.tsx │ │ │ └── useThrottle.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── reducers/ │ │ │ └── loading.ts │ │ ├── router.tsx │ │ ├── session.ts │ │ ├── state/ │ │ │ └── data/ │ │ │ ├── orgs/ │ │ │ │ └── types.ts │ │ │ └── processes/ │ │ │ └── logs/ │ │ │ ├── processors.ts │ │ │ └── types.ts │ │ ├── utils.ts │ │ ├── validation.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── types.d.ts │ ├── vite.config.ts │ └── wallaby.js ├── dependency-manager/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── filtered-resources/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── dependencymanager/ │ │ │ └── version.properties │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── dependencymanager/ │ │ ├── DependencyEntity.java │ │ ├── DependencyManager.java │ │ ├── DependencyManagerConfiguration.java │ │ ├── DependencyManagerException.java │ │ ├── DependencyManagerRepositories.java │ │ ├── MavenProxy.java │ │ ├── MavenRepository.java │ │ ├── MavenRepositoryConfiguration.java │ │ ├── MavenRepositoryPolicy.java │ │ ├── ProgressListener.java │ │ ├── RepositorySystemFactory.java │ │ ├── ResolveExceptionConverter.java │ │ ├── RetryUtils.java │ │ └── Version.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── dependencymanager/ │ │ └── DependencyManagerTest.java │ └── resources/ │ ├── __files/ │ │ └── repository/ │ │ └── concord-cli-1.82.0.pom │ └── logback.xml ├── docker-images/ │ ├── agent/ │ │ ├── oss/ │ │ │ └── debian/ │ │ │ └── Dockerfile │ │ └── pom.xml │ ├── agent-operator/ │ │ ├── oss/ │ │ │ └── Dockerfile │ │ └── pom.xml │ ├── ansible/ │ │ ├── galaxy_requirements.yml │ │ ├── oss/ │ │ │ └── debian/ │ │ │ └── Dockerfile │ │ └── pom.xml │ ├── base/ │ │ ├── get_arch.sh │ │ ├── get_jdk_url.sh │ │ ├── oss/ │ │ │ └── debian/ │ │ │ └── Dockerfile │ │ └── pom.xml │ ├── compose/ │ │ ├── README.md │ │ ├── concord.conf │ │ └── docker-compose.yml │ ├── docker-bake.hcl │ ├── mvn.json │ ├── pom.xml │ ├── push.sh │ ├── run_dev.sh │ ├── server/ │ │ ├── oss/ │ │ │ └── Dockerfile │ │ └── pom.xml │ ├── server.conf │ └── stop.sh ├── examples/ │ ├── README.md │ ├── ansible/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_docker/ │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_dynamic_inventory/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── my_inventory.sh │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_form/ │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_form_as_inventory/ │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_kerberos/ │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_limit/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ ├── hello.limit │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_out_vars/ │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_project/ │ │ ├── README.md │ │ ├── inventory.ini │ │ ├── inventory.py │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── request.json │ ├── ansible_remote/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_retry/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_roles/ │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_stats/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_template/ │ │ ├── playbook/ │ │ │ └── hello.yml │ │ ├── request.json │ │ └── run.sh │ ├── ansible_vault/ │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ ├── group_vars/ │ │ │ │ └── local.yml │ │ │ └── hello.yml │ │ └── run.sh │ ├── ansible_windows/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── inventory.ini │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── approval/ │ │ ├── concord.yml │ │ └── run.sh │ ├── context_injection/ │ │ ├── concord.yml │ │ ├── run.sh │ │ └── tasks/ │ │ └── test.groovy │ ├── custom_form/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── forms/ │ │ │ └── myForm/ │ │ │ ├── data.js │ │ │ └── index.html │ │ └── run.sh │ ├── custom_form_basic/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── forms/ │ │ │ └── myForm/ │ │ │ ├── data.js │ │ │ └── index.html │ │ └── run.sh │ ├── custom_task/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── pom.xml │ │ ├── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── examples/ │ │ │ │ └── customtask/ │ │ │ │ ├── CustomTask.java │ │ │ │ └── CustomTaskV2.java │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── examples/ │ │ │ └── customtask/ │ │ │ └── CustomTaskTest.java │ │ ├── test-v2.yml │ │ ├── test.sh │ │ └── test.yml │ ├── datetime/ │ │ ├── concord.yml │ │ └── run.sh │ ├── docker/ │ │ ├── README.md │ │ ├── ansible.cfg │ │ ├── concord.yml │ │ ├── inventory.ini │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── docker_simple/ │ │ ├── concord.yml │ │ └── run.sh │ ├── dynamic_form_fields/ │ │ ├── concord.yml │ │ └── run.sh │ ├── dynamic_form_values/ │ │ ├── concord.yml │ │ ├── forms/ │ │ │ └── myForm/ │ │ │ ├── data.js │ │ │ └── index.html │ │ └── run.sh │ ├── dynamic_forms/ │ │ ├── concord.yml │ │ └── run.sh │ ├── dynamic_tasks/ │ │ ├── concord.yml │ │ ├── run.sh │ │ ├── runtime-v2/ │ │ │ ├── concord.yml │ │ │ ├── run.sh │ │ │ └── tasks/ │ │ │ └── test.groovy │ │ └── tasks/ │ │ └── test.groovy │ ├── error_handling/ │ │ ├── concord.yml │ │ └── run.sh │ ├── external_script/ │ │ ├── concord.yml │ │ ├── example.js │ │ └── run.sh │ ├── fork/ │ │ ├── concord.yml │ │ └── run.sh │ ├── fork_join/ │ │ ├── concord.yml │ │ └── run.sh │ ├── form_and_long_process/ │ │ ├── concord.yml │ │ └── run.sh │ ├── form_l10n/ │ │ ├── concord.yml │ │ ├── forms/ │ │ │ └── myOtherForm/ │ │ │ ├── index.html │ │ │ └── locale.properties │ │ ├── locale.properties │ │ └── run.sh │ ├── forms/ │ │ ├── README.md │ │ ├── concord.yml │ │ └── run.sh │ ├── forms_multi_group/ │ │ ├── concord.yml │ │ └── run.sh │ ├── forms_override/ │ │ ├── concord.yml │ │ └── run.sh │ ├── forms_wizard/ │ │ ├── concord.yml │ │ ├── forms/ │ │ │ ├── shared/ │ │ │ │ └── common.js │ │ │ ├── userData/ │ │ │ │ └── index.html │ │ │ └── userWarning/ │ │ │ └── index.html │ │ └── run.sh │ ├── generic_triggers/ │ │ ├── README.md │ │ └── concord.yml │ ├── git/ │ │ ├── concord.yml │ │ └── run.sh │ ├── groovy/ │ │ ├── concord.yml │ │ └── run.sh │ ├── groovy_grape/ │ │ ├── concord.yml │ │ ├── run.sh │ │ └── test.groovy │ ├── groovy_rest/ │ │ ├── concord.yml │ │ └── run.sh │ ├── hello_initiator/ │ │ ├── concord.yml │ │ └── run.sh │ ├── hello_world/ │ │ ├── concord.yml │ │ └── run.sh │ ├── hello_world2/ │ │ ├── concord.yml │ │ └── run.sh │ ├── http/ │ │ ├── README.md │ │ ├── concord.yml │ │ └── run.sh │ ├── imports/ │ │ ├── concord.yml │ │ └── run.sh │ ├── in_variables/ │ │ ├── concord.yml │ │ └── run.sh │ ├── inventory/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── create_inventory.sh │ │ ├── isp.s01160.ca_ansiblefacts.json │ │ ├── isp.s05505.us_ansiblefacts.json │ │ ├── playbook/ │ │ │ └── hello.yml │ │ ├── query.sql │ │ ├── run.sh │ │ └── rxp.s00524.us_ansiblefacts.json │ ├── inventory_lookup/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── create_inventory.sh │ │ ├── playbook/ │ │ │ └── hello.yml │ │ ├── query.sql │ │ └── run.sh │ ├── jira/ │ │ ├── concord.yml │ │ └── run.sh │ ├── juel_java_streams/ │ │ ├── concord.yml │ │ └── run.sh │ ├── ldap/ │ │ ├── concord.yml │ │ └── run.sh │ ├── logback_config/ │ │ ├── _agent.json │ │ ├── concord.yml │ │ ├── my_logback.xml │ │ └── run.sh │ ├── loglevel/ │ │ ├── concord.yml │ │ └── run.sh │ ├── long_running/ │ │ ├── concord.yml │ │ └── run.sh │ ├── loops/ │ │ ├── concord.yml │ │ └── run.sh │ ├── mocking/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── mocks/ │ │ │ └── github.groovy │ │ └── run.sh │ ├── multiple_flows/ │ │ ├── concord.yml │ │ └── run.sh │ ├── noderoster/ │ │ ├── concord.yml │ │ └── run.sh │ ├── out/ │ │ ├── concord.yml │ │ └── run.sh │ ├── out_groovy/ │ │ ├── concord.yml │ │ └── run.sh │ ├── parsing_yaml_json/ │ │ ├── concord.yml │ │ ├── my.json │ │ ├── my.yml │ │ └── run.sh │ ├── process_card_htmx/ │ │ ├── README.md │ │ ├── data.js │ │ └── index.html │ ├── process_card_jquery/ │ │ ├── README.md │ │ ├── data.js │ │ └── index.html │ ├── process_from_a_process/ │ │ ├── concord.yml │ │ └── run.sh │ ├── process_from_a_process2/ │ │ ├── concord.yml │ │ └── run.sh │ ├── process_from_a_process3/ │ │ ├── concord.yml │ │ ├── example/ │ │ │ ├── concord.yml │ │ │ └── file.txt │ │ └── run.sh │ ├── process_meta/ │ │ ├── concord.yml │ │ └── run.sh │ ├── profiles/ │ │ ├── concord/ │ │ │ └── concord.yml │ │ ├── concord.yml │ │ └── run.sh │ ├── project_file/ │ │ ├── _main.json │ │ ├── concord.yml │ │ └── run.sh │ ├── python_script/ │ │ ├── concord.yml │ │ ├── example.py │ │ ├── my_module.py │ │ └── run.sh │ ├── ruby/ │ │ ├── concord.yml │ │ └── run.sh │ ├── runtime-v2/ │ │ ├── a_basic_example/ │ │ │ ├── README.md │ │ │ ├── concord/ │ │ │ │ └── example.concord.yml │ │ │ ├── concord.yml │ │ │ └── run.sh │ │ ├── ansible_out_vars/ │ │ │ ├── concord.yml │ │ │ ├── playbook/ │ │ │ │ └── hello.yml │ │ │ └── run.sh │ │ ├── demo-flow/ │ │ │ ├── README.md │ │ │ ├── concord/ │ │ │ │ ├── forms.concord.yml │ │ │ │ └── test.concord.yml │ │ │ ├── concord.yml │ │ │ ├── run.sh │ │ │ └── scripts/ │ │ │ └── test-script.groovy │ │ ├── mocks/ │ │ │ ├── main.concord.yaml │ │ │ ├── run-tests.sh │ │ │ ├── tests/ │ │ │ │ └── main.tests.concord.yaml │ │ │ └── tests-runner.concord.yaml │ │ ├── out_groovy/ │ │ │ ├── concord.yml │ │ │ └── run.sh │ │ ├── out_js/ │ │ │ ├── concord.yml │ │ │ └── run.sh │ │ ├── out_python/ │ │ │ ├── concord.yml │ │ │ └── run.sh │ │ ├── out_ruby/ │ │ │ ├── concord.yml │ │ │ └── run.sh │ │ ├── parallel_execution/ │ │ │ ├── concord.yml │ │ │ └── run.sh │ │ └── python_script/ │ │ ├── concord.yml │ │ ├── example.py │ │ ├── my_module.py │ │ └── run.sh │ ├── script_url/ │ │ ├── concord.yml │ │ ├── example.groovy │ │ └── run.sh │ ├── secret_files/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── myFileA.txt │ │ ├── myFileB.txt │ │ └── run.sh │ ├── secret_lookup/ │ │ ├── concord.yml │ │ ├── playbook/ │ │ │ └── hello.yml │ │ └── run.sh │ ├── secrets/ │ │ ├── README.md │ │ ├── concord.yml │ │ └── run.sh │ ├── slack/ │ │ ├── concord.yml │ │ └── run.sh │ ├── slackChannel/ │ │ ├── concord.yml │ │ └── run.sh │ ├── smtp/ │ │ ├── README.md │ │ ├── concord.yml │ │ ├── first.txt │ │ ├── mail.mustache │ │ ├── run.sh │ │ └── second.txt │ └── smtp_html/ │ ├── README.md │ ├── concord.yml │ ├── mail.mustache.html │ └── run.sh ├── forms/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── forms/ │ ├── Constants.java │ ├── DefaultFormValidator.java │ ├── DefaultFormValidatorLocale.java │ ├── Form.java │ ├── FormField.java │ ├── FormFields.java │ ├── FormOptions.java │ ├── FormUtils.java │ ├── FormValidator.java │ ├── FormValidatorLocale.java │ └── ValidationError.java ├── github-app-installation/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── github/ │ │ └── appinstallation/ │ │ ├── AccessTokenProvider.java │ │ ├── GitHubAppAuthCacheKey.java │ │ ├── GitHubAppAuthConfig.java │ │ ├── GitHubAppInstallation.java │ │ ├── GitHubInstallationToken.java │ │ ├── Utils.java │ │ ├── cfg/ │ │ │ └── GitHubAppInstallationConfig.java │ │ └── exception/ │ │ ├── GitHubAppException.java │ │ └── RepoExtractionException.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── github/ │ │ └── appinstallation/ │ │ ├── AccessTokenProviderTest.java │ │ ├── GitHubAppAuthConfigTest.java │ │ ├── GitHubAppInstallationTest.java │ │ ├── RepoExtractionTest.java │ │ ├── TestConstants.java │ │ ├── UtilsTest.java │ │ └── cfg/ │ │ └── ConfigTest.java │ └── resources/ │ └── logback-test.xml ├── idea-code-style.xml ├── imports/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── imports/ │ ├── DefaultImportManager.java │ ├── DirectoryProcessor.java │ ├── Import.java │ ├── ImportManager.java │ ├── ImportManagerFactory.java │ ├── ImportProcessingException.java │ ├── ImportProcessor.java │ ├── Imports.java │ ├── ImportsListener.java │ ├── MvnProcessor.java │ ├── NoopImportManager.java │ ├── RepositoryExporter.java │ ├── RepositoryProcessor.java │ └── package-info.java ├── it/ │ ├── README.md │ ├── common/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── filtered-resources/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── common/ │ │ │ └── version.properties │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── it/ │ │ └── common/ │ │ ├── ForbiddenException.java │ │ ├── GitHubUtils.java │ │ ├── GitUtils.java │ │ ├── ITUtils.java │ │ ├── JGitUtils.java │ │ ├── MockGitSshServer.java │ │ ├── OffsetDateTimeDeserializer.java │ │ ├── ServerClient.java │ │ ├── ServerCompatModule.java │ │ └── Version.java │ ├── compat/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── test/ │ │ ├── filtered-resources/ │ │ │ └── testcontainers.properties │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── compat/ │ │ │ ├── ITConstants.java │ │ │ ├── LocalModeIT.java │ │ │ └── OldAgentIT.java │ │ └── resources/ │ │ └── logback.xml │ ├── console/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── console/ │ │ │ ├── Base.java │ │ │ ├── Concord.java │ │ │ ├── ConcordConsoleRule.java │ │ │ ├── ConcordServerRule.java │ │ │ ├── CustomFormsIT.java │ │ │ ├── FormsIT.java │ │ │ ├── LoginIT.java │ │ │ ├── ProcessAnsibleIT.java │ │ │ ├── ProfileNavigationIT.java │ │ │ ├── ProjectTeamAccessIT.java │ │ │ ├── RepositoryRunIT.java │ │ │ ├── SecretIT.java │ │ │ ├── TeamIT.java │ │ │ ├── TryingIT.java │ │ │ ├── Utils.java │ │ │ └── WebDriverRule.java │ │ └── resources/ │ │ ├── agent.conf │ │ ├── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── console/ │ │ │ ├── customForm/ │ │ │ │ ├── concord.yml │ │ │ │ └── forms/ │ │ │ │ └── testForm/ │ │ │ │ └── index.html │ │ │ ├── dateTimeField/ │ │ │ │ └── concord.yml │ │ │ ├── processAnsible/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook.yml │ │ │ ├── repositoryRun/ │ │ │ │ └── concord.yml │ │ │ └── stringValues/ │ │ │ └── concord.yml │ │ ├── console.conf │ │ ├── logback.xml │ │ ├── mvn.json │ │ └── server.conf │ ├── pom.xml │ ├── runtime-v1/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── test/ │ │ ├── filtered-resources/ │ │ │ ├── testcontainers.properties │ │ │ └── version.properties │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── runtime/ │ │ │ └── v1/ │ │ │ ├── ConcordConfiguration.java │ │ │ ├── ITConstants.java │ │ │ └── ProcessIT.java │ │ └── resources/ │ │ ├── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── runtime/ │ │ │ └── v1/ │ │ │ ├── activeProfiles/ │ │ │ │ └── concord.yml │ │ │ ├── customJvmArgs/ │ │ │ │ └── concord.yml │ │ │ ├── defaultEntryPoint/ │ │ │ │ └── concord.yml │ │ │ ├── disableProcess/ │ │ │ │ └── concord.yml │ │ │ ├── emptyExclusiveGroup/ │ │ │ │ └── concord.yml │ │ │ ├── errorHandling/ │ │ │ │ ├── _main.json │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── eventBatchingTimer/ │ │ │ │ └── concord.yml │ │ │ ├── example/ │ │ │ │ ├── _main.json │ │ │ │ ├── processes/ │ │ │ │ │ └── test.yml │ │ │ │ └── something.txt │ │ │ ├── fileupload/ │ │ │ │ └── concord.yml │ │ │ ├── interpolateWithVars/ │ │ │ │ └── concord.yml │ │ │ ├── interpolation/ │ │ │ │ ├── _main.json │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── killCascade/ │ │ │ │ └── concord.yml │ │ │ ├── multipart/ │ │ │ │ ├── .concord.yml │ │ │ │ └── _main.json │ │ │ ├── onFailureVars/ │ │ │ │ └── concord.yml │ │ │ ├── onFailureVars2/ │ │ │ │ └── concord.yml │ │ │ ├── processMetadataAfterExecution/ │ │ │ │ └── concord.yml │ │ │ ├── processMetadataWithItems/ │ │ │ │ └── concord.yml │ │ │ ├── processWithChildSuspend/ │ │ │ │ ├── concord.yml │ │ │ │ └── myPayload/ │ │ │ │ └── concord.yml │ │ │ ├── processWithChildSuspendWithoutOut/ │ │ │ │ ├── concord.yml │ │ │ │ └── myPayload/ │ │ │ │ └── concord.yml │ │ │ ├── processWithChildren/ │ │ │ │ └── concord.yml │ │ │ ├── processWithChildrenSuspend/ │ │ │ │ └── concord.yml │ │ │ ├── runnerLogLevel/ │ │ │ │ └── concord.yml │ │ │ ├── startupProblem/ │ │ │ │ ├── _main.json │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── switchCase/ │ │ │ │ └── concord.yml │ │ │ ├── tags/ │ │ │ │ └── concord.yml │ │ │ ├── throwBpmnError/ │ │ │ │ └── concord.yml │ │ │ ├── throwRuntime/ │ │ │ │ └── concord.yml │ │ │ ├── timeout/ │ │ │ │ ├── _main.json │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── workDir/ │ │ │ │ ├── .concord.yml │ │ │ │ ├── test1.txt │ │ │ │ └── test2.txt │ │ │ └── yamlRootFile/ │ │ │ ├── concord/ │ │ │ │ └── extra.yaml │ │ │ └── concord.yaml │ │ └── logback.xml │ ├── runtime-v2/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── test/ │ │ ├── filtered-resources/ │ │ │ └── testcontainers.properties │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── runtime/ │ │ │ └── v2/ │ │ │ ├── AbstractTest.java │ │ │ ├── ConcordConfiguration.java │ │ │ ├── ConcordTaskIT.java │ │ │ ├── CryptoIT.java │ │ │ ├── FlowEventsIT.java │ │ │ ├── FormIT.java │ │ │ ├── GitHubTriggersV2IT.java │ │ │ ├── ITConstants.java │ │ │ ├── ImportsIT.java │ │ │ ├── JsonStoreIT.java │ │ │ ├── KvTaskIT.java │ │ │ ├── NodeRosterIT.java │ │ │ ├── ProcessIT.java │ │ │ ├── ProfilesIT.java │ │ │ ├── ResourceTaskIT.java │ │ │ ├── RestartIT.java │ │ │ ├── SessionStateFilesIT.java │ │ │ ├── SmtpIT.java │ │ │ ├── TaskSchemaValidationIT.java │ │ │ ├── TemplateIT.java │ │ │ └── Utils.java │ │ └── resources/ │ │ ├── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── runtime/ │ │ │ └── v2/ │ │ │ ├── args/ │ │ │ │ └── concord.yml │ │ │ ├── checkpointClasses/ │ │ │ │ └── concord.yml │ │ │ ├── checkpointState/ │ │ │ │ └── concord.yml │ │ │ ├── checkpoints/ │ │ │ │ └── concord.yml │ │ │ ├── checkpointsParallel/ │ │ │ │ └── concord.yml │ │ │ ├── concord/ │ │ │ │ ├── concordOutVars/ │ │ │ │ │ ├── concord.yml │ │ │ │ │ └── myPayload/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── concordSubDryRun/ │ │ │ │ │ ├── concord.yml │ │ │ │ │ └── myPayload/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── concordSubIgnoreFail/ │ │ │ │ │ ├── concord.yml │ │ │ │ │ └── myPayload/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── concordTaskApiKey/ │ │ │ │ │ ├── concord.yml │ │ │ │ │ └── payload/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── concordTaskForkSuspend/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── concordTaskForkWithArguments/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── concordTaskSuspendParentProcess/ │ │ │ │ │ ├── concord.yml │ │ │ │ │ └── payload/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── createApiKey/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── createOrUpdateApiKey/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── projectTask/ │ │ │ │ │ └── concord.yml │ │ │ │ └── repositoryRefreshTask/ │ │ │ │ └── concord.yml │ │ │ ├── crypto/ │ │ │ │ └── concord.yml │ │ │ ├── crypto-masked/ │ │ │ │ └── concord.yml │ │ │ ├── cryptoCreate/ │ │ │ │ └── concord.yml │ │ │ ├── customFormValues/ │ │ │ │ ├── concord.yml │ │ │ │ └── forms/ │ │ │ │ └── myForm/ │ │ │ │ └── index.html │ │ │ ├── dirImport/ │ │ │ │ └── other.concord.yml │ │ │ ├── dryRun/ │ │ │ │ └── concord.yaml │ │ │ ├── emptyExclusiveGroup/ │ │ │ │ └── concord.yml │ │ │ ├── eventBatchingParallel/ │ │ │ │ └── concord.yml │ │ │ ├── eventBatchingTimer/ │ │ │ │ └── concord.yml │ │ │ ├── exitWithMeta/ │ │ │ │ └── concord.yml │ │ │ ├── failProcess/ │ │ │ │ └── concord.yml │ │ │ ├── flowEvents/ │ │ │ │ └── concord.yml │ │ │ ├── forkAfterForm/ │ │ │ │ └── concord.yml │ │ │ ├── forkCheckpoints/ │ │ │ │ └── concord.yml │ │ │ ├── form/ │ │ │ │ └── concord.yml │ │ │ ├── formOnCancel/ │ │ │ │ └── concord.yml │ │ │ ├── formWithTimeout/ │ │ │ │ └── concord.yml │ │ │ ├── jsonStore/ │ │ │ │ └── concord.yml │ │ │ ├── kv/ │ │ │ │ └── concord.yml │ │ │ ├── logExpression/ │ │ │ │ └── concord.yml │ │ │ ├── meta/ │ │ │ │ └── concord.yml │ │ │ ├── metaAfterSuspend/ │ │ │ │ ├── concord.yml │ │ │ │ └── payload/ │ │ │ │ └── concord.yml │ │ │ ├── noderoster/ │ │ │ │ ├── ansible.yml │ │ │ │ ├── noderoster.yml │ │ │ │ └── playbook.yml │ │ │ ├── nullCallInputParam/ │ │ │ │ └── concord.yml │ │ │ ├── out/ │ │ │ │ └── concord.yml │ │ │ ├── outForFailed/ │ │ │ │ └── concord.yml │ │ │ ├── parallelExceptionPayload/ │ │ │ │ └── concord.yml │ │ │ ├── processMetadataSend/ │ │ │ │ ├── concord.yml │ │ │ │ └── debug_logback.xml │ │ │ ├── processMetadataWithItems/ │ │ │ │ └── concord.yml │ │ │ ├── profileFlow/ │ │ │ │ └── concord.yml │ │ │ ├── profileForm/ │ │ │ │ └── concord.yml │ │ │ ├── projectInfo/ │ │ │ │ └── concord.yml │ │ │ ├── resourcePrintJson/ │ │ │ │ └── concord.yml │ │ │ ├── resourceReadAsJson/ │ │ │ │ ├── concord.yml │ │ │ │ └── sample.json │ │ │ ├── resourceReadAsString/ │ │ │ │ ├── concord.yml │ │ │ │ └── sample.txt │ │ │ ├── resourceReadFromJsonString/ │ │ │ │ └── concord.yml │ │ │ ├── resourceWriteAsJson/ │ │ │ │ └── concord.yml │ │ │ ├── resourceWriteAsString/ │ │ │ │ └── concord.yml │ │ │ ├── resourceWriteAsYaml/ │ │ │ │ └── concord.yml │ │ │ ├── restartWithDeletedRepo/ │ │ │ │ └── concord.yml │ │ │ ├── scriptGroovy/ │ │ │ │ └── concord.yml │ │ │ ├── scriptJs/ │ │ │ │ └── concord.yml │ │ │ ├── scriptRuby/ │ │ │ │ └── concord.yml │ │ │ ├── sessionFileAccess/ │ │ │ │ └── concord.yml │ │ │ ├── smtp/ │ │ │ │ └── concord.yml │ │ │ ├── taskSchemaValidation/ │ │ │ │ ├── concordTaskInvalid/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── concordTaskValid/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── invalidInputFail/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── invalidInputWarn/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── invalidOutputFail/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── invalidOutputWarn/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── invalidSchemaFail/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── invalidSchemaWarn/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── multipleErrors/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── noSchema/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── validInput/ │ │ │ │ │ └── concord.yml │ │ │ │ └── validationDisabled/ │ │ │ │ └── concord.yml │ │ │ ├── template/ │ │ │ │ ├── _main.js │ │ │ │ └── concord/ │ │ │ │ └── hello.concord.yml │ │ │ ├── throwWithPayload/ │ │ │ │ └── concord.yml │ │ │ ├── triggers/ │ │ │ │ └── github/ │ │ │ │ ├── events/ │ │ │ │ │ ├── direct_branch_push.json │ │ │ │ │ ├── direct_branch_push_commit_id.json │ │ │ │ │ ├── pr_close.json │ │ │ │ │ └── pr_open.json │ │ │ │ └── repos/ │ │ │ │ └── v2/ │ │ │ │ ├── allParamsTrigger/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── defaultTrigger/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── defaultTriggerWithSender/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── filesTrigger/ │ │ │ │ │ └── concord.yml │ │ │ │ └── useEventCommitIdTrigger/ │ │ │ │ └── concord.yml │ │ │ ├── usernameSignature/ │ │ │ │ └── concord.yml │ │ │ └── yamlRootFile/ │ │ │ ├── concord/ │ │ │ │ └── extra.concord.yaml │ │ │ └── concord.yaml │ │ └── logback-test.xml │ ├── server/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── server/ │ │ │ ├── AbstractGeneralTriggerIT.java │ │ │ ├── AbstractGitHubTriggersIT.java │ │ │ ├── AbstractOneOpsTriggerIT.java │ │ │ ├── AbstractServerIT.java │ │ │ ├── AnsibleEventIT.java │ │ │ ├── AnsibleEventProcessorIT.java │ │ │ ├── AnsibleIT.java │ │ │ ├── AnsibleLookupIT.java │ │ │ ├── AnsiblePolicyIT.java │ │ │ ├── AnsiblePolicyVerboseLimitIT.java │ │ │ ├── AnsibleProjectIT.java │ │ │ ├── AnsibleRetryIT.java │ │ │ ├── ApiKeyIT.java │ │ │ ├── AttachmentRbacIT.java │ │ │ ├── CheckpointsIT.java │ │ │ ├── ClasspathIsolationIT.java │ │ │ ├── ClasspathRepoIT.java │ │ │ ├── ConcordTaskForkFromGitRepoIT.java │ │ │ ├── ConcordTaskIT.java │ │ │ ├── ConfigurableResourcesIT.java │ │ │ ├── CronIT.java │ │ │ ├── CrudIT.java │ │ │ ├── CryptoIT.java │ │ │ ├── DefaultProcessVariablesIT.java │ │ │ ├── DependenciesIT.java │ │ │ ├── DependencyManagerIT.java │ │ │ ├── DispatcherIT.java │ │ │ ├── DockerAnsibleIT.java │ │ │ ├── DockerIT.java │ │ │ ├── DynamicFormIT.java │ │ │ ├── DynamicTaskIT.java │ │ │ ├── EntityOwnerPolicyIT.java │ │ │ ├── EscapeGitCommitMessageIT.java │ │ │ ├── ExclusiveProcessIT.java │ │ │ ├── ExpressionResolveOrderIT.java │ │ │ ├── ExternalImportsIT.java │ │ │ ├── FailureHandlingIT.java │ │ │ ├── FilePermissionsIT.java │ │ │ ├── ForceSuspendIT.java │ │ │ ├── FormIT.java │ │ │ ├── GeneralTriggerIT.java │ │ │ ├── GeneralTriggerV2IT.java │ │ │ ├── GitBranchesIT.java │ │ │ ├── GitHubNonOrgEventIt.java │ │ │ ├── GitHubTriggersV2IT.java │ │ │ ├── GitRepositoryIT.java │ │ │ ├── GroovyIT.java │ │ │ ├── HttpTaskIT.java │ │ │ ├── ITConstants.java │ │ │ ├── InitiatorIT.java │ │ │ ├── InventoryIT.java │ │ │ ├── InventoryQueryIT.java │ │ │ ├── JsonStoreIT.java │ │ │ ├── JsonStoreTaskIT.java │ │ │ ├── KvPolicyIT.java │ │ │ ├── KvServiceIT.java │ │ │ ├── LdapIT.java │ │ │ ├── MavenRepoIT.java │ │ │ ├── MultipleProjectFilesIT.java │ │ │ ├── NodeRosterIT.java │ │ │ ├── OneOpsTriggerIT.java │ │ │ ├── OneOpsTriggerITV2.java │ │ │ ├── OutVariablesIT.java │ │ │ ├── OutVariablesProjectIT.java │ │ │ ├── PermissionIT.java │ │ │ ├── PolicyIT.java │ │ │ ├── PortalIT.java │ │ │ ├── PrincipalPermissionIT.java │ │ │ ├── ProcessCardIT.java │ │ │ ├── ProcessContainerIT.java │ │ │ ├── ProcessCountIT.java │ │ │ ├── ProcessEventsIT.java │ │ │ ├── ProcessExecModeIT.java │ │ │ ├── ProcessIT.java │ │ │ ├── ProcessLocksIT.java │ │ │ ├── ProcessMetadataIT.java │ │ │ ├── ProcessRbacIT.java │ │ │ ├── ProcessStateIT.java │ │ │ ├── ProjectDeleteIT.java │ │ │ ├── ProjectFileIT.java │ │ │ ├── ProjectIT.java │ │ │ ├── ProjectInfoIT.java │ │ │ ├── ProjectTaskIT.java │ │ │ ├── PublicFlowsIT.java │ │ │ ├── RawPayloadProjectIT.java │ │ │ ├── RepositoryRefreshIT.java │ │ │ ├── RequirementsIT.java │ │ │ ├── ResourceIT.java │ │ │ ├── RunAsIT.java │ │ │ ├── SecretIT.java │ │ │ ├── SecretProjectsIT.java │ │ │ ├── SecretsTaskIT.java │ │ │ ├── SerializationIT.java │ │ │ ├── SimpleIT.java │ │ │ ├── SmtpIT.java │ │ │ ├── StandardAuthenticationHandlersIT.java │ │ │ ├── SuspendIT.java │ │ │ ├── TaskRetryIT.java │ │ │ ├── TeamRbacIT.java │ │ │ ├── TemplateIT.java │ │ │ ├── TemplateMergeIT.java │ │ │ ├── ThrowExceptionTaskIT.java │ │ │ ├── TimeoutHandlingIT.java │ │ │ ├── TriggerIT.java │ │ │ ├── TriggersRefreshIT.java │ │ │ ├── UserManagementIT.java │ │ │ ├── UserResourceIT.java │ │ │ ├── UserResourceV2IT.java │ │ │ ├── ValidationIT.java │ │ │ ├── VariablesIT.java │ │ │ ├── VariablesInjectionIT.java │ │ │ ├── WithItemsIT.java │ │ │ └── WorkspacePolicyIT.java │ │ └── resources/ │ │ ├── agent.conf │ │ ├── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── server/ │ │ │ ├── ProcessDisabledRepo/ │ │ │ │ └── concord.yml │ │ │ ├── ansible/ │ │ │ │ ├── _main.json │ │ │ │ ├── playbook/ │ │ │ │ │ └── hello.yml │ │ │ │ └── processes/ │ │ │ │ └── main.yml │ │ │ ├── ansibleBadStrings/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ ├── blns.base64.json │ │ │ │ └── hello.yml │ │ │ ├── ansibleConfigFile/ │ │ │ │ ├── concord.yml │ │ │ │ ├── myInventory.ini │ │ │ │ ├── playbook/ │ │ │ │ │ └── hello.yml │ │ │ │ └── user.cfg │ │ │ ├── ansibleEvent/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleEventProcessor/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ ├── hello.yml │ │ │ │ ├── large_play_and_task_names.yml │ │ │ │ └── unicode_sanitization.yml │ │ │ ├── ansibleExternalPlaybook/ │ │ │ │ ├── payload/ │ │ │ │ │ └── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleExtraVarsFiles/ │ │ │ │ ├── concord.yml │ │ │ │ ├── extra.json │ │ │ │ ├── extra.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleFailedHosts/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook.yml │ │ │ ├── ansibleGroupVars/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleIgnoredFailures/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleInventoryMix/ │ │ │ │ ├── concord.yml │ │ │ │ ├── inventory.ini │ │ │ │ └── playbook.yml │ │ │ ├── ansibleInventoryName/ │ │ │ │ ├── concord.yml │ │ │ │ ├── inventory'$( whoami )'&123.ini │ │ │ │ └── playbook.yml │ │ │ ├── ansibleLargeVerbose/ │ │ │ │ ├── concord.yml │ │ │ │ ├── inventory_large.ini │ │ │ │ ├── inventory_limit.ini │ │ │ │ ├── inventory_small.ini │ │ │ │ ├── more_tasks.yml │ │ │ │ ├── playbook_include.yml │ │ │ │ ├── playbook_multi.yml │ │ │ │ └── playbook_single.yml │ │ │ ├── ansibleLimitWithMultipleHost/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleLogFiltering/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook.yml │ │ │ ├── ansibleLookupPublicKey/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleLookupSecret/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleLookupSecretData/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleLookupSecretDataNoPassword/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleLookupSecretDataValue/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleMergeDefaults/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleMultiInventory/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleMultiInventoryFile/ │ │ │ │ ├── aaa.ini │ │ │ │ ├── bbb.ini │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleOutVars/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansiblePolicyTaskDeny/ │ │ │ │ ├── concord.yml │ │ │ │ ├── playbook/ │ │ │ │ │ └── hello.yml │ │ │ │ └── test-policy.json │ │ │ ├── ansibleRawStrings/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleSaveRetry/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleSkipTags/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleStats/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleTemplateArgs/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleVault/ │ │ │ │ ├── concord.yml │ │ │ │ ├── get_password.py │ │ │ │ └── playbook/ │ │ │ │ ├── group_vars/ │ │ │ │ │ └── all.yml │ │ │ │ └── hello.yml │ │ │ ├── ansibleVaultMultiplePasswordFiles/ │ │ │ │ ├── concord.yml │ │ │ │ ├── get_all_password.py │ │ │ │ ├── get_local_password.py │ │ │ │ └── playbook/ │ │ │ │ ├── group_vars/ │ │ │ │ │ ├── all.yml │ │ │ │ │ └── local.yml │ │ │ │ └── hello.yml │ │ │ ├── ansibleVaultMultiplePasswords/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ ├── group_vars/ │ │ │ │ │ ├── all.yml │ │ │ │ │ └── local.yml │ │ │ │ └── hello.yml │ │ │ ├── ansibleWithForm/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleWithItems/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleWithPostFormSuspension/ │ │ │ │ ├── payload/ │ │ │ │ │ └── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── ansibleproject/ │ │ │ │ ├── git/ │ │ │ │ │ └── playbook/ │ │ │ │ │ └── hello.yml │ │ │ │ ├── inventory.ini │ │ │ │ ├── request.json │ │ │ │ ├── requestFailure.json │ │ │ │ └── requestInline.json │ │ │ ├── arrayInterpolation/ │ │ │ │ └── concord.yml │ │ │ ├── brokenDeps/ │ │ │ │ └── concord.yml │ │ │ ├── cancelHandling/ │ │ │ │ └── .concord.yml │ │ │ ├── cancelSuspendAfterTwoForms/ │ │ │ │ └── concord.yml │ │ │ ├── cancelSuspendHandling/ │ │ │ │ └── concord.yml │ │ │ ├── checkpointExpressions/ │ │ │ │ └── concord.yml │ │ │ ├── checkpoints/ │ │ │ │ └── concord.yml │ │ │ ├── checkpointsWithArgs/ │ │ │ │ └── concord.yml │ │ │ ├── checkpointsWithError/ │ │ │ │ └── concord.yml │ │ │ ├── checkpointsWithEventName/ │ │ │ │ └── concord.yml │ │ │ ├── checkpointsWithEventNameV2/ │ │ │ │ └── concord.yml │ │ │ ├── concordDirTask/ │ │ │ │ ├── concord.yml │ │ │ │ ├── myPayload/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── rootFile.txt │ │ │ │ └── someDir/ │ │ │ │ └── subFile.txt │ │ │ ├── concordOutVars/ │ │ │ │ ├── concord.yml │ │ │ │ └── myPayload/ │ │ │ │ └── concord.yml │ │ │ ├── concordProjectTask/ │ │ │ │ └── concord.yml │ │ │ ├── concordStartAtTask/ │ │ │ │ ├── concord.yml │ │ │ │ └── myPayload/ │ │ │ │ └── concord.yml │ │ │ ├── concordSubFail/ │ │ │ │ ├── concord.yml │ │ │ │ └── myPayload/ │ │ │ │ └── concord.yml │ │ │ ├── concordSubIgnoreFail/ │ │ │ │ ├── concord.yml │ │ │ │ └── myPayload/ │ │ │ │ └── concord.yml │ │ │ ├── concordSubWithNullArg/ │ │ │ │ ├── concord.yml │ │ │ │ └── myPayload/ │ │ │ │ └── concord.yml │ │ │ ├── concordTask/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskApiKey/ │ │ │ │ ├── concord.yml │ │ │ │ └── payload/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskFailChild/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskFork/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskForkAsyncGrabOutVars/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskForkSuspend/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskForkWithArguments/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskForkWithForm/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskForkWithItems/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskForkWithItemsWithOut/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskForkWithRequirements/ │ │ │ │ └── concord.yml │ │ │ ├── concordTaskSuspendParentProcess/ │ │ │ │ ├── concord.yml │ │ │ │ └── payload/ │ │ │ │ └── concord.yml │ │ │ ├── configurableFlowsDirectory/ │ │ │ │ ├── concord.yml │ │ │ │ └── myFlows/ │ │ │ │ └── external.yml │ │ │ ├── configurableProfilesDirectory/ │ │ │ │ ├── .concord.yml │ │ │ │ ├── _main.json │ │ │ │ └── myProfiles/ │ │ │ │ └── test.yml │ │ │ ├── cronProfiles/ │ │ │ │ └── concord.yml │ │ │ ├── cronRunAs/ │ │ │ │ └── concord.yml │ │ │ ├── crypto/ │ │ │ │ ├── _main.json │ │ │ │ └── flows/ │ │ │ │ └── test.yml │ │ │ ├── cryptoFile/ │ │ │ │ └── concord.yml │ │ │ ├── cryptoFileWithOrg/ │ │ │ │ └── concord.yml │ │ │ ├── cryptoPlain/ │ │ │ │ └── .concord.yml │ │ │ ├── cryptoPwd/ │ │ │ │ └── .concord.yml │ │ │ ├── cryptoWithoutPassword/ │ │ │ │ └── concord.yml │ │ │ ├── currentOrgCrypto/ │ │ │ │ └── concord.yml │ │ │ ├── decryptString/ │ │ │ │ └── concord.yml │ │ │ ├── decryptStringTooBig/ │ │ │ │ └── concord.yml │ │ │ ├── defaultEntryPoint/ │ │ │ │ └── concord.yml │ │ │ ├── defaultVars/ │ │ │ │ └── concord.yml │ │ │ ├── delegateOut/ │ │ │ │ └── concord.yml │ │ │ ├── dependencyManager/ │ │ │ │ └── concord.yml │ │ │ ├── deps/ │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── disableProfilesDirectory/ │ │ │ │ ├── _main.json │ │ │ │ ├── concord.yml │ │ │ │ └── profiles/ │ │ │ │ └── broken.yml │ │ │ ├── docker/ │ │ │ │ └── concord.yml │ │ │ ├── dockerAnsible/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── dockerLogWithStdErr/ │ │ │ │ └── concord.yml │ │ │ ├── dockerLogWithoutStdOut/ │ │ │ │ └── concord.yml │ │ │ ├── dockerNoLogWithStdOut/ │ │ │ │ └── concord.yml │ │ │ ├── dockerOut/ │ │ │ │ └── concord.yml │ │ │ ├── dockerPullRetry/ │ │ │ │ └── concord.yml │ │ │ ├── dockerTaskSyntaxOut/ │ │ │ │ └── concord.yml │ │ │ ├── dynamicFormFields/ │ │ │ │ └── concord.yml │ │ │ ├── dynamicFormWithGroovy/ │ │ │ │ └── concord.yml │ │ │ ├── dynamicTask/ │ │ │ │ ├── concord.yml │ │ │ │ └── tasks/ │ │ │ │ └── test.groovy │ │ │ ├── effectiveYaml/ │ │ │ │ └── concord.yml │ │ │ ├── encryptString/ │ │ │ │ └── .concord.yml │ │ │ ├── escapeCommitMessage/ │ │ │ │ └── .concord.yml │ │ │ ├── example/ │ │ │ │ ├── _main.json │ │ │ │ ├── processes/ │ │ │ │ │ └── test.yml │ │ │ │ └── something.txt │ │ │ ├── exclusiveCancelOld/ │ │ │ │ └── concord.yml │ │ │ ├── externalImport/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportFailHandler/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportMain/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportMainFailed/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportMainStateTest/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportMainWithDeps/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportMainWithExclude/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportMainWithFlow/ │ │ │ │ ├── concord/ │ │ │ │ │ └── concord.yml │ │ │ │ └── concord.yml │ │ │ ├── externalImportMainWithForks/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportMainWithPath/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportMainWithVersion/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportSymlink/ │ │ │ │ └── concord.txt │ │ │ ├── externalImportTriggerReference/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportWithConfiguration/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportWithDeps/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportWithDir/ │ │ │ │ ├── concord.yml │ │ │ │ └── dir/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportWithForks/ │ │ │ │ └── concord.yml │ │ │ ├── externalImportWithForm/ │ │ │ │ └── concord.yml │ │ │ ├── externalWithItems/ │ │ │ │ └── concord.yml │ │ │ ├── extraDeps/ │ │ │ │ └── concord.yml │ │ │ ├── failureHandling/ │ │ │ │ └── .concord.yml │ │ │ ├── failureHandlingError/ │ │ │ │ └── .concord.yml │ │ │ ├── filePerm/ │ │ │ │ ├── concord.yml │ │ │ │ └── test.sh │ │ │ ├── forkDepth/ │ │ │ │ └── concord.yml │ │ │ ├── forkInitiator/ │ │ │ │ └── concord.yml │ │ │ ├── forkOnFailure/ │ │ │ │ └── concord.yml │ │ │ ├── form/ │ │ │ │ ├── _main.json │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── formCallWithExpression/ │ │ │ │ └── .concord.yml │ │ │ ├── formExternal/ │ │ │ │ ├── concord.yml │ │ │ │ └── flows/ │ │ │ │ └── external.yml │ │ │ ├── formLabelExpression/ │ │ │ │ └── concord.yml │ │ │ ├── formMultiValue/ │ │ │ │ └── .concord.yml │ │ │ ├── formOptionalFileTypeField/ │ │ │ │ └── .concord.yml │ │ │ ├── formReadonlyField/ │ │ │ │ └── .concord.yml │ │ │ ├── formSingleAllowedValue/ │ │ │ │ └── concord.yml │ │ │ ├── formValues/ │ │ │ │ └── .concord.yml │ │ │ ├── formValuesSubmit/ │ │ │ │ └── concord.yml │ │ │ ├── formsWithItems/ │ │ │ │ └── concord.yml │ │ │ ├── generalExclusiveTrigger/ │ │ │ │ └── concord.yml │ │ │ ├── generalExclusiveTriggerv2/ │ │ │ │ └── concord.yml │ │ │ ├── generalTriggerWithExclusiveCfg/ │ │ │ │ └── concord.yml │ │ │ ├── generalTriggerWithExclusiveCfgv2/ │ │ │ │ └── concord.yml │ │ │ ├── generalTriggerWithExclusiveOverride/ │ │ │ │ └── concord.yml │ │ │ ├── generalTriggerWithExclusiveOverridev2/ │ │ │ │ └── concord.yml │ │ │ ├── getVar/ │ │ │ │ └── concord.yml │ │ │ ├── gitBranches/ │ │ │ │ ├── dev/ │ │ │ │ │ └── concord.yml │ │ │ │ └── qa/ │ │ │ │ └── concord.yml │ │ │ ├── gitRepository/ │ │ │ │ └── concord.yml │ │ │ ├── githubNonRepoEvent/ │ │ │ │ ├── concord.yml │ │ │ │ └── event.json │ │ │ ├── githubTests/ │ │ │ │ ├── events/ │ │ │ │ │ ├── direct_branch_push.json │ │ │ │ │ ├── direct_branch_push_delete.json │ │ │ │ │ ├── empty_push.json │ │ │ │ │ ├── pr_close.json │ │ │ │ │ └── pr_open.json │ │ │ │ └── repos/ │ │ │ │ └── v2/ │ │ │ │ ├── allParamsTrigger/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── anyRepoWithSender/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── defaultTrigger/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── files/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── groupByBranchTrigger/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── groupByEventAttrTrigger/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── ignoreEmptyPushTrigger/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── queryParams/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── requestInfo/ │ │ │ │ │ └── concord.yml │ │ │ │ └── useInitiatorTrigger/ │ │ │ │ └── concord.yml │ │ │ ├── groovy/ │ │ │ │ └── concord.yml │ │ │ ├── httpFollowRedirects/ │ │ │ │ └── concord.yml │ │ │ ├── httpGet/ │ │ │ │ └── concord.yml │ │ │ ├── httpGetAsDefaultMethod/ │ │ │ │ └── concord.yml │ │ │ ├── httpGetAsString/ │ │ │ │ └── concord.yml │ │ │ ├── httpGetEmpty/ │ │ │ │ └── concord.yml │ │ │ ├── httpGetWithAuthUsingPassword/ │ │ │ │ └── concord.yml │ │ │ ├── httpGetWithAuthUsingToken/ │ │ │ │ └── concord.yml │ │ │ ├── httpGetWithHeaders/ │ │ │ │ └── concord.yml │ │ │ ├── httpGetWithIgnoreErrors/ │ │ │ │ └── concord.yml │ │ │ ├── httpGetWithInvalidUrl/ │ │ │ │ └── concord.yml │ │ │ ├── httpGetWithQueryParams/ │ │ │ │ └── concord.yml │ │ │ ├── httpPatch/ │ │ │ │ └── concord.yml │ │ │ ├── httpPost/ │ │ │ │ └── concord.yml │ │ │ ├── httpPostArray/ │ │ │ │ └── concord.yml │ │ │ ├── httpPostWithAuthUsingToken/ │ │ │ │ └── concord.yml │ │ │ ├── httpPostWithDebug/ │ │ │ │ └── concord.yml │ │ │ ├── httpPostWithFormUrlEncoded/ │ │ │ │ └── concord.yml │ │ │ ├── importATrigger/ │ │ │ │ └── concord.yml │ │ │ ├── initiator/ │ │ │ │ ├── _main.json │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── inject/ │ │ │ │ └── .concord.yml │ │ │ ├── interpolation/ │ │ │ │ ├── _main.json │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── invalidResourcesPath/ │ │ │ │ └── concord.yml │ │ │ ├── invalidTriggers/ │ │ │ │ └── concord.yml │ │ │ ├── invalidTriggersBrokenProcess/ │ │ │ │ ├── a/ │ │ │ │ │ ├── concord.yml │ │ │ │ │ └── makepolicyfail.txt │ │ │ │ ├── b/ │ │ │ │ │ └── concord.yml │ │ │ │ └── policy.json │ │ │ ├── inventoryQuery/ │ │ │ │ └── concord.yml │ │ │ ├── jsonStoreTask/ │ │ │ │ └── concord.yml │ │ │ ├── jsonStoreTaskStoreTest/ │ │ │ │ └── concord.yml │ │ │ ├── kvInc/ │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── kvInvalidKeys/ │ │ │ │ └── concord.yml │ │ │ ├── kvPolicy/ │ │ │ │ ├── concord.yml │ │ │ │ ├── test-policy-relaxed.json │ │ │ │ └── test-policy.json │ │ │ ├── kvScript/ │ │ │ │ └── concord.yml │ │ │ ├── kvSpecialString/ │ │ │ │ └── concord.yml │ │ │ ├── ldapFormRunAs/ │ │ │ │ └── concord.yml │ │ │ ├── ldapInitiator/ │ │ │ │ └── concord.yml │ │ │ ├── multiProjectTemplate/ │ │ │ │ ├── template/ │ │ │ │ │ ├── concord/ │ │ │ │ │ │ └── test.yml │ │ │ │ │ └── test.txt │ │ │ │ └── user/ │ │ │ │ └── concord.yml │ │ │ ├── multipart/ │ │ │ │ ├── .concord.yml │ │ │ │ └── _main.json │ │ │ ├── multipleWithItems/ │ │ │ │ └── concord.yml │ │ │ ├── mvnDeps/ │ │ │ │ └── concord.yml │ │ │ ├── nodeRoster/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook.yml │ │ │ ├── nodeRosterMultiFacts/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook.yml │ │ │ ├── nodeRosterTask/ │ │ │ │ └── concord.yml │ │ │ ├── nonSerializableTest/ │ │ │ │ └── concord.yml │ │ │ ├── onFailureDependencies/ │ │ │ │ └── concord.yml │ │ │ ├── oneCheckpoint/ │ │ │ │ └── concord.yml │ │ │ ├── oneopsTests/ │ │ │ │ ├── events/ │ │ │ │ │ ├── oneops_deployment_complete.json │ │ │ │ │ └── oneops_deployment_qa.json │ │ │ │ └── trigger/ │ │ │ │ └── concord.yml │ │ │ ├── out/ │ │ │ │ └── concord.yml │ │ │ ├── parentInstanceId/ │ │ │ │ ├── concord.yml │ │ │ │ └── myPayload/ │ │ │ │ └── concord.yml │ │ │ ├── policyCfg/ │ │ │ │ └── concord.yml │ │ │ ├── portal/ │ │ │ │ └── .concord.yml │ │ │ ├── principalPermission/ │ │ │ │ ├── concord.yml │ │ │ │ └── payload/ │ │ │ │ └── concord.yml │ │ │ ├── process/ │ │ │ │ └── concord.yml │ │ │ ├── processContainer/ │ │ │ │ └── concord.yml │ │ │ ├── processCount/ │ │ │ │ └── concord.yml │ │ │ ├── processLocks/ │ │ │ │ └── concord.yml │ │ │ ├── processMetadata/ │ │ │ │ └── concord.yml │ │ │ ├── processModeExec/ │ │ │ │ └── concord.yml │ │ │ ├── processRbac/ │ │ │ │ └── concord.yml │ │ │ ├── processRequirements/ │ │ │ │ └── concord.yml │ │ │ ├── processWithChildren/ │ │ │ │ └── concord.yml │ │ │ ├── project/ │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── project-commit-id/ │ │ │ │ ├── 1/ │ │ │ │ │ └── processes/ │ │ │ │ │ └── test.yml │ │ │ │ └── 2/ │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── project-triggers/ │ │ │ │ └── concord.yml │ │ │ ├── projectEntryPoint/ │ │ │ │ └── .concord.yml │ │ │ ├── projectInfo/ │ │ │ │ └── concord.yml │ │ │ ├── projectTask/ │ │ │ │ └── concord.yml │ │ │ ├── projectfile/ │ │ │ │ ├── altname/ │ │ │ │ │ └── concord.yml │ │ │ │ ├── deps/ │ │ │ │ │ └── .template.yml │ │ │ │ ├── expr/ │ │ │ │ │ ├── .concord.yml │ │ │ │ │ └── _main.json │ │ │ │ ├── expressionscript/ │ │ │ │ │ ├── concord.yml │ │ │ │ │ └── scripts/ │ │ │ │ │ └── test.js │ │ │ │ ├── externalprofile/ │ │ │ │ │ ├── .concord.yml │ │ │ │ │ ├── _main.json │ │ │ │ │ └── profiles/ │ │ │ │ │ ├── another.yml │ │ │ │ │ └── test.yml │ │ │ │ ├── externalscript/ │ │ │ │ │ ├── .concord.yml │ │ │ │ │ ├── _main.json │ │ │ │ │ └── scripts/ │ │ │ │ │ └── test.js │ │ │ │ ├── overrideflow/ │ │ │ │ │ ├── .concord.yml │ │ │ │ │ └── _main.json │ │ │ │ ├── scriptWithErrorBlock/ │ │ │ │ │ ├── .concord.yml │ │ │ │ │ ├── _main.json │ │ │ │ │ └── scripts/ │ │ │ │ │ └── myscript.groovy │ │ │ │ ├── singleprofile/ │ │ │ │ │ ├── .concord.yml │ │ │ │ │ └── _main.json │ │ │ │ └── singleprofilecfg/ │ │ │ │ ├── .concord.yml │ │ │ │ └── _main.json │ │ │ ├── publicFlowsInProfiles/ │ │ │ │ ├── concord/ │ │ │ │ │ ├── a.yml │ │ │ │ │ └── b.yml │ │ │ │ └── concord.yml │ │ │ ├── repositoryRefresh/ │ │ │ │ └── concord.yml │ │ │ ├── repositoryValidation/ │ │ │ │ └── concord.yml │ │ │ ├── repositoryValidationEmptyFlow/ │ │ │ │ └── concord.yml │ │ │ ├── repositoryValidationEmptyForm/ │ │ │ │ └── concord.yml │ │ │ ├── repositoryValidationTemplateRef/ │ │ │ │ └── concord.yml │ │ │ ├── resolveOrder/ │ │ │ │ └── concord.yml │ │ │ ├── resourcePrintJson/ │ │ │ │ └── concord.yml │ │ │ ├── resourceReadAsJson/ │ │ │ │ ├── concord.yml │ │ │ │ └── sample.json │ │ │ ├── resourceReadAsString/ │ │ │ │ ├── concord.yml │ │ │ │ └── sample.txt │ │ │ ├── resourceReadFromJsonString/ │ │ │ │ └── concord.yml │ │ │ ├── resourceWriteAsJson/ │ │ │ │ └── concord.yml │ │ │ ├── resourceWriteAsString/ │ │ │ │ └── concord.yml │ │ │ ├── resourceWriteAsYaml/ │ │ │ │ └── concord.yml │ │ │ ├── runAsMultipleUsers/ │ │ │ │ └── concord.yml │ │ │ ├── runAsPayload/ │ │ │ │ └── concord.yml │ │ │ ├── runas/ │ │ │ │ └── concord.yml │ │ │ ├── runnerEvents/ │ │ │ │ └── concord.yml │ │ │ ├── secretProjects/ │ │ │ │ └── concord.yml │ │ │ ├── secretsTask/ │ │ │ │ └── concord.yml │ │ │ ├── serialization/ │ │ │ │ └── .concord.yml │ │ │ ├── sessionToken/ │ │ │ │ └── concord.yml │ │ │ ├── sessionTokenAsUsername/ │ │ │ │ └── concord.yml │ │ │ ├── setVar/ │ │ │ │ └── concord.yml │ │ │ ├── setVarNested/ │ │ │ │ └── concord.yml │ │ │ ├── setVarNested2/ │ │ │ │ └── concord.yml │ │ │ ├── simple/ │ │ │ │ └── concord.yml │ │ │ ├── smtp/ │ │ │ │ ├── _main.json │ │ │ │ ├── processes/ │ │ │ │ │ └── main.yml │ │ │ │ └── test.mustache │ │ │ ├── stateSingleFile/ │ │ │ │ ├── concord.yml │ │ │ │ └── dir/ │ │ │ │ └── test.txt │ │ │ ├── suspend/ │ │ │ │ ├── _main.json │ │ │ │ └── processes/ │ │ │ │ └── main.yml │ │ │ ├── suspendForCompletion/ │ │ │ │ ├── concord.yml │ │ │ │ └── payload/ │ │ │ │ └── concord.yml │ │ │ ├── suspendForForkedProcesses/ │ │ │ │ └── concord.yml │ │ │ ├── suspendTask/ │ │ │ │ └── concord.yml │ │ │ ├── taskOut/ │ │ │ │ └── concord.yml │ │ │ ├── taskRetry/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── taskRetryWithExpression/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── templateMerge/ │ │ │ │ ├── process/ │ │ │ │ │ └── .concord.yml │ │ │ │ └── template/ │ │ │ │ ├── _main.js │ │ │ │ └── flows/ │ │ │ │ └── main.yml │ │ │ ├── testTrigger/ │ │ │ │ └── concord.yml │ │ │ ├── throwExceptionMessage/ │ │ │ │ └── concord.yml │ │ │ ├── throwExceptionTask/ │ │ │ │ └── concord.yml │ │ │ ├── timeout/ │ │ │ │ ├── _main.json │ │ │ │ └── processes/ │ │ │ │ └── test.yml │ │ │ ├── timeoutHandling/ │ │ │ │ └── .concord.yml │ │ │ ├── triggerActiveProfiles/ │ │ │ │ └── concord.yml │ │ │ ├── triggerRepo/ │ │ │ │ ├── concord.yml │ │ │ │ └── new_concord.yml │ │ │ ├── twoAnsible/ │ │ │ │ ├── concord.yml │ │ │ │ └── playbook/ │ │ │ │ └── hello.yml │ │ │ ├── unknownFlavor/ │ │ │ │ └── concord.yml │ │ │ ├── variables/ │ │ │ │ ├── .concord.yml │ │ │ │ └── _main.json │ │ │ ├── withDelay/ │ │ │ │ └── concord.yml │ │ │ ├── withForm/ │ │ │ │ └── concord.yml │ │ │ └── workspacePolicy/ │ │ │ ├── concord.yml │ │ │ ├── test-policy-relaxed.json │ │ │ └── test-policy.json │ │ ├── default_vars.yml │ │ ├── logback.xml │ │ ├── mvn.json │ │ └── server.conf │ ├── tasks/ │ │ ├── broken-deps/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── tasks/ │ │ │ └── brokendeps/ │ │ │ └── BrokenDepsTask.java │ │ ├── dependency-manager-test/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── tasks/ │ │ │ └── dependencymanagertest/ │ │ │ └── DependencyManagerTestTask.java │ │ ├── schema-test/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── it/ │ │ │ │ └── tasks/ │ │ │ │ └── schematest/ │ │ │ │ ├── InvalidSchemaTask.java │ │ │ │ └── SchemaTestTask.java │ │ │ └── resources/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── tasks/ │ │ │ └── schematest/ │ │ │ ├── invalidSchema.schema.json │ │ │ └── schemaTest.schema.json │ │ ├── serialization-test/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── it/ │ │ │ └── tasks/ │ │ │ └── serializationtest/ │ │ │ ├── CustomBean.java │ │ │ ├── CustomBeanTask.java │ │ │ ├── NonSerializableThingy.java │ │ │ └── SerializationTestTask.java │ │ └── suspend-test/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── it/ │ │ └── tasks/ │ │ └── suspendtest/ │ │ └── SuspendTestTask.java │ └── testing-server/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── it/ │ │ └── testingserver/ │ │ ├── TestingConcordAgent.java │ │ └── TestingConcordServer.java │ └── test/ │ └── java/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── it/ │ └── testingserver/ │ └── TestingConcordIT.java ├── mvnw ├── mvnw.cmd ├── plugins/ │ ├── pom.xml │ ├── tasks/ │ │ ├── ansible/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── walmartlabs/ │ │ │ │ │ └── concord/ │ │ │ │ │ └── plugins/ │ │ │ │ │ └── ansible/ │ │ │ │ │ ├── AnsibleAuth.java │ │ │ │ │ ├── AnsibleAuthFactory.java │ │ │ │ │ ├── AnsibleCallbacks.java │ │ │ │ │ ├── AnsibleConfig.java │ │ │ │ │ ├── AnsibleContext.java │ │ │ │ │ ├── AnsibleEnv.java │ │ │ │ │ ├── AnsibleInventory.java │ │ │ │ │ ├── AnsibleLibs.java │ │ │ │ │ ├── AnsibleLookup.java │ │ │ │ │ ├── AnsibleRoles.java │ │ │ │ │ ├── AnsibleTask.java │ │ │ │ │ ├── AnsibleVaultId.java │ │ │ │ │ ├── ArgUtils.java │ │ │ │ │ ├── ConfigSection.java │ │ │ │ │ ├── DefaultPlaybookProcessRunner.java │ │ │ │ │ ├── DeprecatedArgsProcessor.java │ │ │ │ │ ├── DockerExtraHosts.java │ │ │ │ │ ├── DockerPlaybookProcessRunner.java │ │ │ │ │ ├── EventSender.java │ │ │ │ │ ├── GroupVarsProcessor.java │ │ │ │ │ ├── KerberosAuth.java │ │ │ │ │ ├── NopAuth.java │ │ │ │ │ ├── OutVarsProcessor.java │ │ │ │ │ ├── PlaybookProcessRunner.java │ │ │ │ │ ├── PlaybookProcessRunnerFactory.java │ │ │ │ │ ├── PlaybookScriptBuilder.java │ │ │ │ │ ├── PrivateKeyAuth.java │ │ │ │ │ ├── Resources.java │ │ │ │ │ ├── Secret.java │ │ │ │ │ ├── TaskParams.java │ │ │ │ │ ├── Utils.java │ │ │ │ │ ├── Virtualenv.java │ │ │ │ │ ├── docker/ │ │ │ │ │ │ └── AnsibleDockerService.java │ │ │ │ │ ├── secrets/ │ │ │ │ │ │ ├── AnsibleSecretService.java │ │ │ │ │ │ ├── KeyPair.java │ │ │ │ │ │ └── UsernamePassword.java │ │ │ │ │ ├── v1/ │ │ │ │ │ │ ├── AnsibleDockerServiceV1.java │ │ │ │ │ │ ├── AnsibleSecretServiceV1.java │ │ │ │ │ │ ├── AnsibleTaskV1.java │ │ │ │ │ │ └── RunPlaybookTask2.java │ │ │ │ │ └── v2/ │ │ │ │ │ ├── AnsibleDockerServiceV2.java │ │ │ │ │ ├── AnsibleSecretServiceV2.java │ │ │ │ │ └── AnsibleTaskV2.java │ │ │ │ └── resources/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── ansible/ │ │ │ │ ├── callback/ │ │ │ │ │ ├── concord_default_module_args.py │ │ │ │ │ ├── concord_events.py │ │ │ │ │ ├── concord_out_vars.py │ │ │ │ │ ├── concord_protectdata.py │ │ │ │ │ ├── concord_strategy_patch.py │ │ │ │ │ ├── concord_task_executor_patch.py │ │ │ │ │ └── concord_trace.py │ │ │ │ ├── inventory.sh │ │ │ │ ├── lib/ │ │ │ │ │ ├── concord_ansible_stats.py │ │ │ │ │ ├── process_cfg_policy.py │ │ │ │ │ └── task_policy.py │ │ │ │ └── lookup/ │ │ │ │ ├── concord_data_secret.py │ │ │ │ ├── concord_inventory.py │ │ │ │ ├── concord_public_key_secret.py │ │ │ │ └── concord_secret.py │ │ │ └── test/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── ansible/ │ │ │ │ ├── AbstractTest.java │ │ │ │ ├── AnsibleConfigTest.java │ │ │ │ └── KerberosTest.java │ │ │ └── resources/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── ansible/ │ │ │ └── ansible.cfg │ │ ├── asserts/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── asserts/ │ │ │ └── AssertsTask.java │ │ ├── concord/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── walmartlabs/ │ │ │ │ │ └── concord/ │ │ │ │ │ └── client/ │ │ │ │ │ ├── AbstractConcordTask.java │ │ │ │ │ ├── ConcordTask.java │ │ │ │ │ ├── ConcordTaskCommon.java │ │ │ │ │ ├── ConcordTaskParams.java │ │ │ │ │ ├── InventoryTask.java │ │ │ │ │ ├── JsonStoreTask.java │ │ │ │ │ ├── JsonStoreTaskCommon.java │ │ │ │ │ ├── Keys.java │ │ │ │ │ ├── ProjectTask.java │ │ │ │ │ ├── ProjectTaskCommon.java │ │ │ │ │ ├── ProjectTaskParams.java │ │ │ │ │ ├── RepositoryRefreshTaskCommon.java │ │ │ │ │ ├── RepositoryRefreshTaskParams.java │ │ │ │ │ ├── ResumePayload.java │ │ │ │ │ ├── SecretsTask.java │ │ │ │ │ ├── SecretsTaskCommon.java │ │ │ │ │ ├── SecretsTaskParams.java │ │ │ │ │ ├── model/ │ │ │ │ │ │ ├── EventRepository.java │ │ │ │ │ │ ├── PushEvent.java │ │ │ │ │ │ └── RefreshEvent.java │ │ │ │ │ ├── v1/ │ │ │ │ │ │ └── ContextBackedVariables.java │ │ │ │ │ └── v2/ │ │ │ │ │ ├── ConcordTaskV2.java │ │ │ │ │ ├── JsonStoreTaskV2.java │ │ │ │ │ ├── ProjectTaskV2.java │ │ │ │ │ ├── RepositoryRefreshTaskV2.java │ │ │ │ │ └── SecretsTaskV2.java │ │ │ │ └── resources/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── client/ │ │ │ │ └── v2/ │ │ │ │ └── concord.schema.json │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── client/ │ │ │ ├── ConcordTaskParamsTest.java │ │ │ └── RepositoryRefreshCommonTest.java │ │ ├── crypto/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── crypto/ │ │ │ ├── CryptoTask.java │ │ │ ├── CryptoTaskV2.java │ │ │ └── TaskParams.java │ │ ├── docker/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── docker/ │ │ │ ├── DockerConstants.java │ │ │ ├── DockerTask.java │ │ │ ├── DockerTaskCommon.java │ │ │ ├── DockerTaskV2.java │ │ │ └── TaskParams.java │ │ ├── dynamic-tasks/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── dynamic/ │ │ │ ├── LoadTasksTask.java │ │ │ ├── LoadTasksTaskV2.java │ │ │ ├── TaskLoader.java │ │ │ └── TaskRegistry.java │ │ ├── example/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── example/ │ │ │ ├── ExampleBean.java │ │ │ ├── ExampleDelegate.java │ │ │ └── ExampleTask.java │ │ ├── files/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── file/ │ │ │ └── v2/ │ │ │ └── FilesTaskV2.java │ │ ├── http/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ ├── filtered-resources/ │ │ │ │ │ └── com/ │ │ │ │ │ └── walmartlabs/ │ │ │ │ │ └── concord/ │ │ │ │ │ └── plugins/ │ │ │ │ │ └── http/ │ │ │ │ │ └── version.properties │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── http/ │ │ │ │ ├── Configuration.java │ │ │ │ ├── HttpTask.java │ │ │ │ ├── HttpTaskUtils.java │ │ │ │ ├── HttpTaskV2.java │ │ │ │ ├── SimpleHttpClient.java │ │ │ │ ├── Version.java │ │ │ │ ├── exception/ │ │ │ │ │ ├── RequestTimeoutException.java │ │ │ │ │ └── UnauthorizedException.java │ │ │ │ └── request/ │ │ │ │ ├── HttpTaskRequest.java │ │ │ │ └── Request.java │ │ │ └── test/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── http/ │ │ │ │ ├── HttpTaskTest.java │ │ │ │ ├── HttpTaskV2Test.java │ │ │ │ ├── SimpleHttpClientTest.java │ │ │ │ └── WiremockTest.java │ │ │ └── resources/ │ │ │ └── logback.xml │ │ ├── kv/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── kv/ │ │ │ ├── Constants.java │ │ │ ├── KvTask.java │ │ │ ├── KvTaskUtils.java │ │ │ └── KvTaskV2.java │ │ ├── locale/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── locale/ │ │ │ ├── LocaleTask.java │ │ │ └── LocaleTaskV2.java │ │ ├── lock/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── lock/ │ │ │ ├── Constants.java │ │ │ ├── LockTask.java │ │ │ ├── LockTaskCommon.java │ │ │ ├── TaskParams.java │ │ │ └── v2/ │ │ │ ├── LockTaskV2.java │ │ │ └── UnlockTaskV2.java │ │ ├── log/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── log/ │ │ │ ├── LogDebugTask.java │ │ │ ├── LogErrorTask.java │ │ │ ├── LogUtils.java │ │ │ ├── LogWarnTask.java │ │ │ ├── LoggingTask.java │ │ │ └── LoggingTaskV2.java │ │ ├── misc/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── misc/ │ │ │ ├── Base64TaskV2.java │ │ │ ├── CollectionsTaskV2.java │ │ │ ├── DateTimeTask.java │ │ │ ├── DateTimeTaskV2.java │ │ │ ├── EnvTaskV2.java │ │ │ ├── MiscTask.java │ │ │ └── MiscTaskV2.java │ │ ├── mock/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── mock/ │ │ │ │ ├── InputSanitizer.java │ │ │ │ ├── Invocation.java │ │ │ │ ├── Invocations.java │ │ │ │ ├── InvocationsCollector.java │ │ │ │ ├── InvocationsCollectorParams.java │ │ │ │ ├── MockDefinition.java │ │ │ │ ├── MockDefinitionContext.java │ │ │ │ ├── MockDefinitionProvider.java │ │ │ │ ├── MockModule.java │ │ │ │ ├── MockTask.java │ │ │ │ ├── MockTaskMethodResolver.java │ │ │ │ ├── MockTaskProvider.java │ │ │ │ ├── MockUtilsTask.java │ │ │ │ ├── VerifierBeanMethodResolver.java │ │ │ │ ├── VerifyTask.java │ │ │ │ └── matcher/ │ │ │ │ ├── AbstractMatcher.java │ │ │ │ ├── ArgsMatcher.java │ │ │ │ ├── CollectionMatcher.java │ │ │ │ ├── MapMatcher.java │ │ │ │ ├── Matcher.java │ │ │ │ ├── StringValueMatcher.java │ │ │ │ ├── TypeReference.java │ │ │ │ └── ValueMatcher.java │ │ │ └── test/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── mock/ │ │ │ │ ├── MockDefinitionMatcherTest.java │ │ │ │ ├── MockTest.java │ │ │ │ ├── TestTask.java │ │ │ │ ├── VerifyTest.java │ │ │ │ └── matcher/ │ │ │ │ └── ArgsMatcherTest.java │ │ │ └── resources/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── mock/ │ │ │ ├── method-mock/ │ │ │ │ └── concord.yml │ │ │ ├── method-mock-with-any/ │ │ │ │ └── concord.yml │ │ │ ├── method-mock-with-flow-execute/ │ │ │ │ └── concord.yml │ │ │ ├── simple/ │ │ │ │ └── concord.yml │ │ │ ├── simple-verify/ │ │ │ │ └── concord.yaml │ │ │ ├── task-mock-with-flow-execute/ │ │ │ │ └── concord.yml │ │ │ └── verify-mocked-task/ │ │ │ └── concord.yaml │ │ ├── noderoster/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── noderoster/ │ │ │ ├── Constants.java │ │ │ ├── NodeRosterTask.java │ │ │ ├── NodeRosterTaskUtils.java │ │ │ ├── NodeRosterTaskV2.java │ │ │ └── Result.java │ │ ├── resource/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── resource/ │ │ │ │ ├── Evaluator.java │ │ │ │ ├── FileService.java │ │ │ │ ├── ResourceTask.java │ │ │ │ ├── ResourceTaskCommon.java │ │ │ │ └── v2/ │ │ │ │ └── ResourceTaskV2.java │ │ │ └── test/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── resource/ │ │ │ │ └── ResourceTaskCommonTest.java │ │ │ └── resources/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── resource/ │ │ │ └── test.properties │ │ ├── slack/ │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── slack/ │ │ │ │ ├── ContextVariables.java │ │ │ │ ├── Slack.java │ │ │ │ ├── SlackChannelTask.java │ │ │ │ ├── SlackChannelTaskCommon.java │ │ │ │ ├── SlackChannelTaskParams.java │ │ │ │ ├── SlackClient.java │ │ │ │ ├── SlackConfiguration.java │ │ │ │ ├── SlackConfigurationParams.java │ │ │ │ ├── SlackTask.java │ │ │ │ ├── SlackTaskCommon.java │ │ │ │ ├── SlackTaskParams.java │ │ │ │ ├── Utils.java │ │ │ │ └── v2/ │ │ │ │ ├── SlackChannelTaskV2.java │ │ │ │ └── SlackTaskV2.java │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── slack/ │ │ │ ├── SlackClientTest.java │ │ │ ├── SlackTaskTest.java │ │ │ └── TestParams.java │ │ ├── sleep/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── sleep/ │ │ │ ├── Constants.java │ │ │ ├── SleepTask.java │ │ │ ├── SleepTaskCommon.java │ │ │ ├── Suspender.java │ │ │ ├── TaskParams.java │ │ │ └── v2/ │ │ │ └── SleepTaskV2.java │ │ ├── smtp/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── smtp/ │ │ │ │ ├── Constants.java │ │ │ │ ├── SmtpTask.java │ │ │ │ ├── SmtpTaskUtils.java │ │ │ │ └── SmtpTaskV2.java │ │ │ └── test/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── plugins/ │ │ │ │ └── smtp/ │ │ │ │ ├── SmtpTaskTest.java │ │ │ │ └── SmtpTaskV2Test.java │ │ │ └── resources/ │ │ │ ├── attahcment.txt │ │ │ ├── logback.xml │ │ │ └── test.mustache │ │ ├── throw/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── plugins/ │ │ │ └── throwex/ │ │ │ ├── ConcordException.java │ │ │ ├── ThrowExceptionTask.java │ │ │ └── ThrowExceptionTaskV2.java │ │ └── variables/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── plugins/ │ │ └── variables/ │ │ ├── VariablesTask.java │ │ ├── VariablesTaskCommon.java │ │ └── v2/ │ │ └── VariablesTaskV2.java │ └── templates/ │ └── ansible/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── filtered-resources/ │ ├── META-INF/ │ │ └── concord/ │ │ └── template.properties │ ├── _main.js │ └── processes/ │ └── main.yml ├── policy-engine/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── policyengine/ │ │ ├── AttachmentsPolicy.java │ │ ├── AttachmentsRule.java │ │ ├── CheckResult.java │ │ ├── ConcurrentProcessPolicy.java │ │ ├── ConcurrentProcessRule.java │ │ ├── ContainerPolicy.java │ │ ├── ContainerRule.java │ │ ├── CronTriggerPolicy.java │ │ ├── CronTriggerRule.java │ │ ├── DependencyPolicy.java │ │ ├── DependencyRewritePolicy.java │ │ ├── DependencyRewriteRule.java │ │ ├── DependencyRule.java │ │ ├── DependencyVersionsPolicy.java │ │ ├── EffectiveYamlPolicy.java │ │ ├── EffectiveYamlRule.java │ │ ├── EntityPolicy.java │ │ ├── EntityRule.java │ │ ├── FilePolicy.java │ │ ├── FileRule.java │ │ ├── ForkDepthPolicy.java │ │ ├── ForkDepthRule.java │ │ ├── JsonStorePolicy.java │ │ ├── JsonStoreRule.java │ │ ├── KvPolicy.java │ │ ├── KvRule.java │ │ ├── PolicyEngine.java │ │ ├── PolicyEngineRules.java │ │ ├── PolicyRules.java │ │ ├── ProcessCfgPolicy.java │ │ ├── ProcessTimeoutPolicy.java │ │ ├── ProcessTimeoutRule.java │ │ ├── ProtectedTasksPolicy.java │ │ ├── ProtectedTasksRule.java │ │ ├── QueueRule.java │ │ ├── RawPayloadPolicy.java │ │ ├── RawPayloadRule.java │ │ ├── RuntimePolicy.java │ │ ├── RuntimeRule.java │ │ ├── StatePolicy.java │ │ ├── StateRule.java │ │ ├── TaskPolicy.java │ │ ├── TaskRule.java │ │ ├── Utils.java │ │ ├── WorkspacePolicy.java │ │ ├── WorkspaceRule.java │ │ └── package-info.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── policyengine/ │ │ ├── ContainerPolicyTest.java │ │ ├── DependencyPolicyTest.java │ │ ├── DependencyRewritePolicyTest.java │ │ ├── EffectiveYamlPolicyTest.java │ │ ├── PolicyEngineRulesTest.java │ │ ├── QueueRuleTest.java │ │ ├── RawPayloadPolicyTest.java │ │ ├── TaskPolicyTest.java │ │ ├── UtilsTest.java │ │ └── WorkspacePolicyTest.java │ └── resources/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── policyengine/ │ ├── policy1.json │ ├── policy2.json │ ├── policy3.json │ ├── policy4.json │ ├── policy5.json │ └── policy6.json ├── pom.xml ├── repository/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── repository/ │ │ ├── FetchRequest.java │ │ ├── FetchResult.java │ │ ├── GitCliRepositoryProvider.java │ │ ├── GitClient.java │ │ ├── GitClientConfiguration.java │ │ ├── LastModifiedSnapshot.java │ │ ├── MavenRepositoryProvider.java │ │ ├── Repository.java │ │ ├── RepositoryAccessJournal.java │ │ ├── RepositoryCache.java │ │ ├── RepositoryException.java │ │ ├── RepositoryProvider.java │ │ ├── RepositoryProviders.java │ │ └── Snapshot.java │ └── test/ │ ├── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── repository/ │ │ ├── GitClientFetch2Test.java │ │ ├── GitClientFetchTest.java │ │ ├── GitClientRealTest.java │ │ ├── GitClientSpeedTest.java │ │ ├── GitUriTest.java │ │ └── GitUtils.java │ └── resources/ │ ├── branch-1/ │ │ └── branch-1.txt │ ├── logback.xml │ ├── master/ │ │ └── master.txt │ ├── tag-1/ │ │ └── tag-1.txt │ ├── test4/ │ │ ├── concord.yml │ │ └── new_concord.yml │ └── test5/ │ ├── 0_concord.yml │ ├── 1_concord.yml │ └── 2_concord.yml ├── runtime/ │ ├── common/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── runtime/ │ │ │ └── common/ │ │ │ ├── FormService.java │ │ │ ├── ObjectTruncater.java │ │ │ ├── ProcessHeartbeat.java │ │ │ ├── SensitiveDataMasker.java │ │ │ ├── SerializationUtils.java │ │ │ ├── StateManager.java │ │ │ ├── cfg/ │ │ │ │ ├── ApiConfiguration.java │ │ │ │ ├── CommonProcessConfiguration.java │ │ │ │ ├── DependencyManagerConfiguration.java │ │ │ │ ├── DockerConfiguration.java │ │ │ │ ├── LoggingConfiguration.java │ │ │ │ ├── RunnerConfiguration.java │ │ │ │ └── SecurityManagerConfiguration.java │ │ │ ├── injector/ │ │ │ │ ├── InjectorUtils.java │ │ │ │ ├── InstanceId.java │ │ │ │ └── TaskHolder.java │ │ │ └── logger/ │ │ │ ├── LogSegmentDeserializer.java │ │ │ ├── LogSegmentHeader.java │ │ │ ├── LogSegmentSerializer.java │ │ │ └── LogSegmentStatus.java │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── runtime/ │ │ └── common/ │ │ ├── ObjectTruncaterTest.java │ │ └── SensitiveDataMaskerTest.java │ ├── loader/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── process/ │ │ └── loader/ │ │ ├── DelegatingProjectLoader.java │ │ ├── ImportsNormalizer.java │ │ ├── ProjectLoader.java │ │ ├── ProjectLoaderUtils.java │ │ ├── StandardRuntimeTypes.java │ │ └── UnsupportedRuntimeTypeException.java │ ├── model/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── runtime/ │ │ └── model/ │ │ ├── AllowNulls.java │ │ ├── Configuration.java │ │ ├── EffectiveConfiguration.java │ │ ├── EffectiveProcessDefinitionProvider.java │ │ ├── ExpressionStep.java │ │ ├── FlowDefinition.java │ │ ├── Form.java │ │ ├── FormField.java │ │ ├── Location.java │ │ ├── Options.java │ │ ├── ProcessDefinition.java │ │ ├── Profile.java │ │ ├── SourceMap.java │ │ ├── Step.java │ │ ├── TaskCallStep.java │ │ └── Trigger.java │ ├── v1/ │ │ ├── README.md │ │ ├── impl/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── walmartlabs/ │ │ │ │ │ └── concord/ │ │ │ │ │ └── runner/ │ │ │ │ │ ├── ApiClientFactoryImpl.java │ │ │ │ │ ├── ApiClientFactoryProvider.java │ │ │ │ │ ├── CheckpointManager.java │ │ │ │ │ ├── ConcordSecurityManager.java │ │ │ │ │ ├── ContextUtils.java │ │ │ │ │ ├── DefaultVariablesConverter.java │ │ │ │ │ ├── DependencyManagerConfigurationProvider.java │ │ │ │ │ ├── DependencyManagerImpl.java │ │ │ │ │ ├── ExecutorServiceProvider.java │ │ │ │ │ ├── LockServiceImpl.java │ │ │ │ │ ├── Main.java │ │ │ │ │ ├── ObjectStorageImpl.java │ │ │ │ │ ├── OutVariablesParser.java │ │ │ │ │ ├── PolicyEngineHolder.java │ │ │ │ │ ├── ProcessApiClient.java │ │ │ │ │ ├── SecretServiceImpl.java │ │ │ │ │ ├── SerializationUtils.java │ │ │ │ │ ├── TaskCallInterceptor.java │ │ │ │ │ ├── TaskClasses.java │ │ │ │ │ ├── VariablesSnapshotListener.java │ │ │ │ │ └── engine/ │ │ │ │ │ ├── ApiConfigurationImpl.java │ │ │ │ │ ├── CheckpointTask.java │ │ │ │ │ ├── ConcordExecutionContextFactory.java │ │ │ │ │ ├── ConcordFormService.java │ │ │ │ │ ├── DefaultElementEventProcessor.java │ │ │ │ │ ├── DefaultEventReportingService.java │ │ │ │ │ ├── DockerServiceImpl.java │ │ │ │ │ ├── ElementEventProcessor.java │ │ │ │ │ ├── EngineFactory.java │ │ │ │ │ ├── EventConfiguration.java │ │ │ │ │ ├── EventReportingService.java │ │ │ │ │ ├── FileEventStorage.java │ │ │ │ │ ├── FileFormStorage.java │ │ │ │ │ ├── FilePersistenceManager.java │ │ │ │ │ ├── FormUtilsTask.java │ │ │ │ │ ├── LogTagMetadataProvider.java │ │ │ │ │ ├── NopElementEventProcessor.java │ │ │ │ │ ├── PolicyPreprocessor.java │ │ │ │ │ ├── ProcessElementInterceptor.java │ │ │ │ │ ├── ProcessErrorProcessor.java │ │ │ │ │ ├── ProcessMetadataProcessor.java │ │ │ │ │ ├── ProcessOutVariables.java │ │ │ │ │ ├── ProcessOutVariablesListener.java │ │ │ │ │ ├── ProtectedVarContext.java │ │ │ │ │ ├── ResourceResolverImpl.java │ │ │ │ │ ├── TaskEventInterceptor.java │ │ │ │ │ ├── TaskInterceptor.java │ │ │ │ │ ├── TaskRegistry.java │ │ │ │ │ └── el/ │ │ │ │ │ ├── InjectVariableELResolver.java │ │ │ │ │ ├── ResolverUtils.java │ │ │ │ │ └── TaskResolver.java │ │ │ │ └── resources/ │ │ │ │ └── logback.xml │ │ │ └── test/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── runner/ │ │ │ │ └── engine/ │ │ │ │ ├── ConcordExecutionContextTest.java │ │ │ │ ├── EventReportingServiceTest.java │ │ │ │ ├── PolicyPreprocessorTest.java │ │ │ │ └── el/ │ │ │ │ ├── AbstractElResolverTest.java │ │ │ │ ├── InjectVariableELResolverTest.java │ │ │ │ └── TaskResolverTest.java │ │ │ └── resources/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── runner/ │ │ │ └── policy.json │ │ ├── model/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ ├── project/ │ │ │ │ │ ├── ImportsNormalizer.java │ │ │ │ │ ├── InternalConstants.java │ │ │ │ │ ├── NoopImportsNormalizer.java │ │ │ │ │ ├── ProjectLoader.java │ │ │ │ │ ├── model/ │ │ │ │ │ │ ├── Profile.java │ │ │ │ │ │ ├── ProjectDefinition.java │ │ │ │ │ │ ├── ProjectDefinitionUtils.java │ │ │ │ │ │ ├── Resources.java │ │ │ │ │ │ └── Trigger.java │ │ │ │ │ └── yaml/ │ │ │ │ │ ├── Atom.java │ │ │ │ │ ├── Grammar.java │ │ │ │ │ ├── KV.java │ │ │ │ │ ├── ListInput.java │ │ │ │ │ ├── YamlConverterException.java │ │ │ │ │ ├── YamlDeserializers.java │ │ │ │ │ ├── YamlFormConverter.java │ │ │ │ │ ├── YamlImportConverter.java │ │ │ │ │ ├── YamlParser.java │ │ │ │ │ ├── YamlParserException.java │ │ │ │ │ ├── YamlProcessConverter.java │ │ │ │ │ ├── YamlProjectConverter.java │ │ │ │ │ ├── YamlResourcesConverter.java │ │ │ │ │ ├── YamlTriggersConverter.java │ │ │ │ │ ├── converter/ │ │ │ │ │ │ ├── Chunk.java │ │ │ │ │ │ ├── ConverterContext.java │ │ │ │ │ │ ├── DockerOptionsConverter.java │ │ │ │ │ │ ├── StepConverter.java │ │ │ │ │ │ ├── YamlCallConverter.java │ │ │ │ │ │ ├── YamlCheckpointConverter.java │ │ │ │ │ │ ├── YamlDockerStepConverter.java │ │ │ │ │ │ ├── YamlEventConverter.java │ │ │ │ │ │ ├── YamlExitConverter.java │ │ │ │ │ │ ├── YamlExpressionStepConverter.java │ │ │ │ │ │ ├── YamlFormCallConverter.java │ │ │ │ │ │ ├── YamlGroupConverter.java │ │ │ │ │ │ ├── YamlIfExprConverter.java │ │ │ │ │ │ ├── YamlReturnConverter.java │ │ │ │ │ │ ├── YamlScriptConverter.java │ │ │ │ │ │ ├── YamlSetVariablesStepConverter.java │ │ │ │ │ │ ├── YamlSwitchExprConverter.java │ │ │ │ │ │ ├── YamlTaskShortStepConverter.java │ │ │ │ │ │ └── YamlTaskStepConverter.java │ │ │ │ │ ├── model/ │ │ │ │ │ │ ├── YamlCall.java │ │ │ │ │ │ ├── YamlCheckpoint.java │ │ │ │ │ │ ├── YamlDefinition.java │ │ │ │ │ │ ├── YamlDefinitionFile.java │ │ │ │ │ │ ├── YamlDockerStep.java │ │ │ │ │ │ ├── YamlEvent.java │ │ │ │ │ │ ├── YamlExit.java │ │ │ │ │ │ ├── YamlExpressionStep.java │ │ │ │ │ │ ├── YamlFormCall.java │ │ │ │ │ │ ├── YamlFormDefinition.java │ │ │ │ │ │ ├── YamlFormField.java │ │ │ │ │ │ ├── YamlGroup.java │ │ │ │ │ │ ├── YamlIfExpr.java │ │ │ │ │ │ ├── YamlImport.java │ │ │ │ │ │ ├── YamlProcessDefinition.java │ │ │ │ │ │ ├── YamlProfile.java │ │ │ │ │ │ ├── YamlProfileFile.java │ │ │ │ │ │ ├── YamlProject.java │ │ │ │ │ │ ├── YamlReturn.java │ │ │ │ │ │ ├── YamlScript.java │ │ │ │ │ │ ├── YamlSetVariablesStep.java │ │ │ │ │ │ ├── YamlStep.java │ │ │ │ │ │ ├── YamlSwitchExpr.java │ │ │ │ │ │ ├── YamlTaskShortStep.java │ │ │ │ │ │ ├── YamlTaskStep.java │ │ │ │ │ │ └── YamlTrigger.java │ │ │ │ │ └── validator/ │ │ │ │ │ ├── StepValidator.java │ │ │ │ │ ├── Validator.java │ │ │ │ │ ├── ValidatorContext.java │ │ │ │ │ └── YamlCheckpointValidator.java │ │ │ │ └── runtime/ │ │ │ │ └── v1/ │ │ │ │ ├── ProjectLoaderV1.java │ │ │ │ └── wrapper/ │ │ │ │ ├── ConfigurationV1.java │ │ │ │ ├── EffectiveProcessDefinitionProviderV1.java │ │ │ │ ├── FlowDefinitionV1.java │ │ │ │ ├── ProcessDefinitionV1.java │ │ │ │ ├── ProfileV1.java │ │ │ │ ├── SourceMapV1.java │ │ │ │ └── TriggerV1.java │ │ │ └── test/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ ├── project/ │ │ │ │ │ ├── BrokenTest.java │ │ │ │ │ ├── ProjectLoaderTest.java │ │ │ │ │ └── yaml/ │ │ │ │ │ ├── AbstractYamlParserTest.java │ │ │ │ │ ├── DiagramPrint.java │ │ │ │ │ ├── YamlParserTest.java │ │ │ │ │ └── validator/ │ │ │ │ │ └── YamlValidationTest.java │ │ │ │ └── runtime/ │ │ │ │ └── model/ │ │ │ │ └── ImportTest.java │ │ │ └── resources/ │ │ │ ├── 000.yml │ │ │ ├── 001.yml │ │ │ ├── 002.yml │ │ │ ├── 003.yml │ │ │ ├── 004.yml │ │ │ ├── 005.yml │ │ │ ├── 006.yml │ │ │ ├── 007.yml │ │ │ ├── 008.yml │ │ │ ├── 009.yml │ │ │ ├── 010.yml │ │ │ ├── 011.yml │ │ │ ├── 012.yml │ │ │ ├── 013.yml │ │ │ ├── 014.yml │ │ │ ├── 015.yml │ │ │ ├── 016.yml │ │ │ ├── 017.yml │ │ │ ├── 018.yml │ │ │ ├── 019.yml │ │ │ ├── 020.yml │ │ │ ├── 021.yml │ │ │ ├── 021_2.yml │ │ │ ├── 022.yml │ │ │ ├── 022_2.yml │ │ │ ├── 023.yml │ │ │ ├── 024.yml │ │ │ ├── 025.yml │ │ │ ├── 026.yml │ │ │ ├── 027.yml │ │ │ ├── 028.yml │ │ │ ├── 029.yml │ │ │ ├── 030.yml │ │ │ ├── 031.yml │ │ │ ├── 032.yml │ │ │ ├── 033.yml │ │ │ ├── 034.yml │ │ │ ├── 035.yml │ │ │ ├── 036.yml │ │ │ ├── 037.yml │ │ │ ├── 040.yml │ │ │ ├── 041.yml │ │ │ ├── 042.yml │ │ │ ├── 043.yml │ │ │ ├── 044.yml │ │ │ ├── 045.yml │ │ │ ├── 046.yml │ │ │ ├── 047.yml │ │ │ ├── 048.yml │ │ │ ├── 049.yml │ │ │ ├── 050.yml │ │ │ ├── 051.yml │ │ │ ├── 052.yml │ │ │ ├── 053.yml │ │ │ ├── 054.yml │ │ │ ├── 055.yml │ │ │ ├── 056.yml │ │ │ ├── 057.yml │ │ │ ├── 058.yml │ │ │ ├── 059.yml │ │ │ ├── 060.yml │ │ │ ├── 061.yml │ │ │ ├── 062.yml │ │ │ ├── 063.yml │ │ │ ├── 064.yml │ │ │ ├── 065.yml │ │ │ ├── 066.yml │ │ │ ├── 067.yml │ │ │ ├── 068.yml │ │ │ ├── 069.yml │ │ │ ├── 070.yml │ │ │ ├── 071.yml │ │ │ ├── 072.yml │ │ │ ├── 073.yml │ │ │ ├── 074.yml │ │ │ ├── 075.yml │ │ │ ├── 076.yml │ │ │ ├── 077.yml │ │ │ ├── 100.yml │ │ │ ├── 101.yml │ │ │ ├── 102.yml │ │ │ ├── 103.yml │ │ │ ├── 104.yml │ │ │ ├── 105.yml │ │ │ ├── 106.yml │ │ │ ├── 107.yml │ │ │ ├── 108.yml │ │ │ ├── 109.yml │ │ │ ├── 110.yml │ │ │ ├── 111.yml │ │ │ ├── 112.yml │ │ │ ├── 113.yml │ │ │ ├── brokenFlows/ │ │ │ │ ├── concord.yml │ │ │ │ └── flows/ │ │ │ │ └── test.yml │ │ │ ├── brokenMain/ │ │ │ │ └── concord.yml │ │ │ ├── brokenProfiles/ │ │ │ │ └── profiles/ │ │ │ │ └── test.yml │ │ │ ├── complex/ │ │ │ │ └── .concord.yml │ │ │ ├── duplicateConfiguration/ │ │ │ │ └── .concord.yml │ │ │ ├── duplicateConfigurationVariable/ │ │ │ │ └── .concord.yml │ │ │ ├── emptyField/ │ │ │ │ └── .concord.yml │ │ │ ├── externalscript/ │ │ │ │ └── test.js │ │ │ ├── junk.yml │ │ │ ├── logback-test.xml │ │ │ ├── multiProjectFile/ │ │ │ │ ├── concord/ │ │ │ │ │ ├── 0.yml │ │ │ │ │ └── 1.yml │ │ │ │ └── concord.yml │ │ │ ├── old.yml │ │ │ ├── simple/ │ │ │ │ └── .concord.yml │ │ │ └── validator/ │ │ │ └── 002.yml │ │ └── pom.xml │ └── v2/ │ ├── model/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── runtime/ │ │ │ └── v2/ │ │ │ ├── ConcordJsonSchemaGenerator.java │ │ │ ├── Constants.java │ │ │ ├── ImportsNormalizer.java │ │ │ ├── NoopImportsNormalizer.java │ │ │ ├── ProcessDefinitionUtils.java │ │ │ ├── ProjectLoaderV2.java │ │ │ ├── ProjectSerializerV2.java │ │ │ ├── exception/ │ │ │ │ ├── InvalidFieldDefinitionException.java │ │ │ │ ├── InvalidValueException.java │ │ │ │ ├── InvalidValueTypeException.java │ │ │ │ ├── MandatoryFieldNotFoundException.java │ │ │ │ ├── OneOfMandatoryFieldsNotFoundException.java │ │ │ │ ├── UnknownOptionException.java │ │ │ │ ├── UnsupportedException.java │ │ │ │ ├── YamlParserException.java │ │ │ │ └── YamlProcessingException.java │ │ │ ├── model/ │ │ │ │ ├── AbstractStep.java │ │ │ │ ├── Checkpoint.java │ │ │ │ ├── EventConfiguration.java │ │ │ │ ├── ExclusiveMode.java │ │ │ │ ├── ExitStep.java │ │ │ │ ├── Expression.java │ │ │ │ ├── ExpressionOptions.java │ │ │ │ ├── Flow.java │ │ │ │ ├── FlowCall.java │ │ │ │ ├── FlowCallOptions.java │ │ │ │ ├── Form.java │ │ │ │ ├── FormCall.java │ │ │ │ ├── FormCallOptions.java │ │ │ │ ├── FormField.java │ │ │ │ ├── GithubTriggerExclusiveMode.java │ │ │ │ ├── GroupOfSteps.java │ │ │ │ ├── GroupOfStepsOptions.java │ │ │ │ ├── IfStep.java │ │ │ │ ├── Location.java │ │ │ │ ├── Loop.java │ │ │ │ ├── ParallelBlock.java │ │ │ │ ├── ParallelBlockOptions.java │ │ │ │ ├── ProcessDefinition.java │ │ │ │ ├── ProcessDefinitionConfiguration.java │ │ │ │ ├── Profile.java │ │ │ │ ├── Resources.java │ │ │ │ ├── Retry.java │ │ │ │ ├── ReturnStep.java │ │ │ │ ├── ScriptCall.java │ │ │ │ ├── ScriptCallOptions.java │ │ │ │ ├── SetVariablesStep.java │ │ │ │ ├── SourceMap.java │ │ │ │ ├── Step.java │ │ │ │ ├── SuspendStep.java │ │ │ │ ├── SwitchStep.java │ │ │ │ ├── TaskCall.java │ │ │ │ ├── TaskCallOptions.java │ │ │ │ ├── TaskCallValidation.java │ │ │ │ ├── Trigger.java │ │ │ │ ├── ValidationConfiguration.java │ │ │ │ └── WithItems.java │ │ │ ├── parser/ │ │ │ │ ├── Atom.java │ │ │ │ ├── CheckpointGrammar.java │ │ │ │ ├── ConditionalExpressionsGrammar.java │ │ │ │ ├── ConfigurationGrammar.java │ │ │ │ ├── ExitGrammar.java │ │ │ │ ├── ExpressionGrammar.java │ │ │ │ ├── FlowCallGrammar.java │ │ │ │ ├── FlowsGrammar.java │ │ │ │ ├── FormFieldParser.java │ │ │ │ ├── FormsGrammar.java │ │ │ │ ├── GrammarLookup.java │ │ │ │ ├── GrammarMisc.java │ │ │ │ ├── GrammarOptions.java │ │ │ │ ├── GrammarV2.java │ │ │ │ ├── GroupOfStepsGrammar.java │ │ │ │ ├── ImportsGrammar.java │ │ │ │ ├── KV.java │ │ │ │ ├── ListInput.java │ │ │ │ ├── LogGrammar.java │ │ │ │ ├── LoopGrammar.java │ │ │ │ ├── ParallelGrammar.java │ │ │ │ ├── ProcessDefinitionGrammar.java │ │ │ │ ├── ProfilesGrammar.java │ │ │ │ ├── PublicFlowsGrammar.java │ │ │ │ ├── ResourcesGrammar.java │ │ │ │ ├── RetryGrammar.java │ │ │ │ ├── ReturnGrammar.java │ │ │ │ ├── ScriptGrammar.java │ │ │ │ ├── SetVariablesGrammar.java │ │ │ │ ├── SimpleOptions.java │ │ │ │ ├── StepOptions.java │ │ │ │ ├── SuspendGrammar.java │ │ │ │ ├── TaskGrammar.java │ │ │ │ ├── ThreadLocalFileName.java │ │ │ │ ├── ThrowGrammar.java │ │ │ │ ├── TriggersGrammar.java │ │ │ │ ├── UnknownOption.java │ │ │ │ ├── YamlDeserializersV2.java │ │ │ │ ├── YamlList.java │ │ │ │ ├── YamlObject.java │ │ │ │ ├── YamlObjectConverter.java │ │ │ │ ├── YamlParserV2.java │ │ │ │ ├── YamlValue.java │ │ │ │ └── YamlValueType.java │ │ │ ├── schema/ │ │ │ │ ├── BlockStepMixIn.java │ │ │ │ ├── CheckpointStepMixIn.java │ │ │ │ ├── ExitStepMixIn.java │ │ │ │ ├── ExpressionFullMixIn.java │ │ │ │ ├── ExpressionShortMixIn.java │ │ │ │ ├── FlowCallStepMixIn.java │ │ │ │ ├── FlowsMixIn.java │ │ │ │ ├── FormCallStepMixIn.java │ │ │ │ ├── FormFieldMixIn.java │ │ │ │ ├── FormFieldsMixIn.java │ │ │ │ ├── GroupOfStepsMixIn.java │ │ │ │ ├── IfStepMixIn.java │ │ │ │ ├── ImportMixIn.java │ │ │ │ ├── ImportsMixIn.java │ │ │ │ ├── LogStepMixIn.java │ │ │ │ ├── LogYamlStepMixIn.java │ │ │ │ ├── LoopMixIn.java │ │ │ │ ├── NamedStep.java │ │ │ │ ├── ParallelStepMixIn.java │ │ │ │ ├── ProcessDefinitionConfigurationMixIn.java │ │ │ │ ├── ProcessDefinitionMixIn.java │ │ │ │ ├── RetryMixIn.java │ │ │ │ ├── ReturnStepMixIn.java │ │ │ │ ├── ScriptCallMixIn.java │ │ │ │ ├── SetStepMixIn.java │ │ │ │ ├── StepMixIn.java │ │ │ │ ├── SuspendStepMixIn.java │ │ │ │ ├── SwitchStepMixIn.java │ │ │ │ ├── TaskCallMixIn.java │ │ │ │ ├── ThrowStepMixIn.java │ │ │ │ ├── TriggerMixIn.java │ │ │ │ ├── TryStepMixIn.java │ │ │ │ └── package-info.java │ │ │ ├── serializer/ │ │ │ │ ├── CheckpointStepSerializer.java │ │ │ │ ├── DurationSerializer.java │ │ │ │ ├── ExitStepSerializer.java │ │ │ │ ├── ExpressionStepSerializer.java │ │ │ │ ├── FlowCallStepSerializer.java │ │ │ │ ├── FlowSerializer.java │ │ │ │ ├── FormCallStepSerializer.java │ │ │ │ ├── FormDefinitionSerializer.java │ │ │ │ ├── FormFieldSerializer.java │ │ │ │ ├── GroupOfStepsSerializer.java │ │ │ │ ├── IfStepSerializer.java │ │ │ │ ├── LoopOptionsSerializer.java │ │ │ │ ├── ParallelBlockSerializer.java │ │ │ │ ├── ProcessDefinitionSerializer.java │ │ │ │ ├── RetryOptionsSerializer.java │ │ │ │ ├── ReturnStepSerializer.java │ │ │ │ ├── ScriptCallStepSerializer.java │ │ │ │ ├── SerializerUtils.java │ │ │ │ ├── SetVariablesStepSerializer.java │ │ │ │ ├── SimpleOptionsSerializer.java │ │ │ │ ├── SuspendStepSerializer.java │ │ │ │ ├── SwitchStepSerializer.java │ │ │ │ ├── TaskCallStepSerializer.java │ │ │ │ ├── TriggerSerializer.java │ │ │ │ └── WithItemsSerializer.java │ │ │ └── wrapper/ │ │ │ ├── ConfigurationV2.java │ │ │ ├── EffectiveProcessDefinitionProviderV2.java │ │ │ ├── FlowDefinitionV2.java │ │ │ ├── ProcessDefinitionV2.java │ │ │ ├── ProfileV2.java │ │ │ └── TriggerV2.java │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── project/ │ │ │ └── runtime/ │ │ │ └── v2/ │ │ │ ├── ConcordJsonSchemaGeneratorTest.java │ │ │ ├── ProjectLoaderV2Test.java │ │ │ ├── ProjectSerializerV2Test.java │ │ │ └── parser/ │ │ │ ├── AbstractParserTest.java │ │ │ ├── YamlErrorParserTest.java │ │ │ └── YamlOkParserTest.java │ │ └── resources/ │ │ ├── 000.1.yml │ │ ├── 000.2.yml │ │ ├── 000.yml │ │ ├── 001.yml │ │ ├── 002.1.yml │ │ ├── 002.yml │ │ ├── 003.yml │ │ ├── 004.yml │ │ ├── 005.yml │ │ ├── 006.yml │ │ ├── 007.yml │ │ ├── 008.yml │ │ ├── 009.yml │ │ ├── 010.yml │ │ ├── 011.yml │ │ ├── 012.yml │ │ ├── 013.yml │ │ ├── 014.1.yml │ │ ├── 014.yml │ │ ├── 015.yml │ │ ├── 016.yml │ │ ├── 017.yml │ │ ├── 018.yml │ │ ├── 019.yml │ │ ├── 020.yml │ │ ├── 021.yml │ │ ├── args-order.concord.yml │ │ ├── errors/ │ │ │ ├── checkpoint/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 004.yml │ │ │ │ └── 005.yml │ │ │ ├── configuration/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 004.yml │ │ │ │ ├── 005.yml │ │ │ │ ├── 005_1.yml │ │ │ │ ├── 006.yml │ │ │ │ ├── 007.yml │ │ │ │ ├── 008.yml │ │ │ │ ├── 009.yml │ │ │ │ ├── 010.yml │ │ │ │ ├── 011.yml │ │ │ │ ├── 011_1.yml │ │ │ │ ├── 012.yml │ │ │ │ ├── 013.yml │ │ │ │ ├── 014.yml │ │ │ │ ├── 015.yml │ │ │ │ ├── 016.yml │ │ │ │ ├── 017.yml │ │ │ │ ├── 018.yml │ │ │ │ ├── 019.yml │ │ │ │ ├── 020.yml │ │ │ │ ├── 021.yml │ │ │ │ ├── 022.yml │ │ │ │ ├── 023.yml │ │ │ │ └── 024.yml │ │ │ ├── expression/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 005.yml │ │ │ │ ├── 006.yml │ │ │ │ ├── 007.yml │ │ │ │ ├── 008.yml │ │ │ │ ├── 009.yml │ │ │ │ └── 010.yml │ │ │ ├── flowCall/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 005.yml │ │ │ │ ├── 006.yml │ │ │ │ ├── 007.yml │ │ │ │ ├── 008.yml │ │ │ │ ├── 009.yml │ │ │ │ ├── 010.yml │ │ │ │ ├── 011.yml │ │ │ │ ├── 012.yml │ │ │ │ ├── 013.yml │ │ │ │ ├── 014.yml │ │ │ │ ├── 015.yml │ │ │ │ ├── 016.yml │ │ │ │ ├── 017.yml │ │ │ │ └── 018.yml │ │ │ ├── flows/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ └── 002.yml │ │ │ ├── formCall/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 004.yml │ │ │ │ ├── 005.yml │ │ │ │ ├── 006.yml │ │ │ │ ├── 007.yml │ │ │ │ └── 008.yml │ │ │ ├── forms/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 004.yml │ │ │ │ ├── 005.yml │ │ │ │ ├── 006.yml │ │ │ │ ├── 007.yml │ │ │ │ ├── 008.yml │ │ │ │ ├── 009.yml │ │ │ │ ├── 010.yml │ │ │ │ ├── 011.yml │ │ │ │ ├── 012.yml │ │ │ │ └── 013.yml │ │ │ ├── group/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 004.yml │ │ │ │ ├── 005.yml │ │ │ │ ├── 006.yml │ │ │ │ ├── 007.yml │ │ │ │ └── 008.yml │ │ │ ├── if/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 004.yml │ │ │ │ ├── 005.yml │ │ │ │ ├── 006.yml │ │ │ │ └── 007.yml │ │ │ ├── imports/ │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 004.yml │ │ │ │ ├── 005.yml │ │ │ │ ├── 006.yml │ │ │ │ ├── 007.yml │ │ │ │ ├── 008.yml │ │ │ │ ├── 009.yml │ │ │ │ ├── 010.yml │ │ │ │ ├── 011.yml │ │ │ │ ├── 012.yml │ │ │ │ ├── 013.yml │ │ │ │ ├── 014.yml │ │ │ │ ├── 015.yml │ │ │ │ └── 016.yml │ │ │ ├── parallel/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 004.yml │ │ │ │ └── 005.yml │ │ │ ├── profiles/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ └── 003.yml │ │ │ ├── publicFlows/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ └── 002.yml │ │ │ ├── resources/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ └── 002.yml │ │ │ ├── scripts/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ └── 003.yml │ │ │ ├── setVariables/ │ │ │ │ ├── 000.yml │ │ │ │ └── 001.yml │ │ │ ├── switch/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ └── 003.yml │ │ │ ├── tasks/ │ │ │ │ ├── 000.yml │ │ │ │ ├── 001.yml │ │ │ │ ├── 002.yml │ │ │ │ ├── 003.yml │ │ │ │ ├── 005.yml │ │ │ │ ├── 006.yml │ │ │ │ ├── 007.yml │ │ │ │ ├── 008.yml │ │ │ │ ├── 009.yml │ │ │ │ ├── 010.yml │ │ │ │ ├── 011.yml │ │ │ │ ├── 012.yml │ │ │ │ ├── 013.yml │ │ │ │ ├── 014.yml │ │ │ │ ├── 015.yml │ │ │ │ ├── 016.yml │ │ │ │ ├── 017.yml │ │ │ │ ├── 018.yml │ │ │ │ ├── 019.yml │ │ │ │ ├── 020.yml │ │ │ │ └── 021.yml │ │ │ └── triggers/ │ │ │ ├── 001.yml │ │ │ ├── 002.yml │ │ │ ├── 003.yml │ │ │ ├── 004.yml │ │ │ ├── 005.yml │ │ │ ├── 006.yml │ │ │ ├── 007.yml │ │ │ ├── 008.yml │ │ │ ├── 009.yml │ │ │ ├── 010.yml │ │ │ ├── 011.yml │ │ │ ├── 012.yml │ │ │ ├── 013.yml │ │ │ ├── 014.yml │ │ │ ├── 015.yml │ │ │ ├── 016.yml │ │ │ ├── 017.yml │ │ │ ├── 018.yml │ │ │ ├── 019.yml │ │ │ ├── 020.yml │ │ │ ├── 021.yml │ │ │ ├── 022.yml │ │ │ ├── 023.yml │ │ │ ├── 024.yml │ │ │ ├── 025.yml │ │ │ ├── 026.yml │ │ │ ├── 027.yml │ │ │ ├── 028.yml │ │ │ ├── 029.yml │ │ │ ├── 030.yml │ │ │ ├── 031.yml │ │ │ ├── 031_1.yml │ │ │ ├── 032.yml │ │ │ ├── 033.yml │ │ │ ├── 034.yml │ │ │ ├── 035.yml │ │ │ └── 037.yml │ │ ├── multiProjectFile/ │ │ │ ├── concord/ │ │ │ │ ├── 1.concord.yml │ │ │ │ ├── 2.concord.yml │ │ │ │ └── 3.concord.yml │ │ │ └── concord.yml │ │ ├── schema/ │ │ │ └── concord.yml │ │ ├── serializer/ │ │ │ ├── expressionStep.yml │ │ │ ├── expressionStepOutExpr.yml │ │ │ ├── flowCallStep.yml │ │ │ ├── flowCallStepOutExpr.yml │ │ │ ├── flowCallStepOutMapping.yml │ │ │ ├── formCallStep.yml │ │ │ ├── groupOfSteps.yml │ │ │ ├── ifStep.yml │ │ │ ├── parallelBlock.yml │ │ │ ├── parallelBlockOutExpr.yml │ │ │ ├── processDefinition.yml │ │ │ ├── returnStep.yml │ │ │ ├── scriptStep.yml │ │ │ ├── setVariablesStep.yml │ │ │ ├── suspendStep.yml │ │ │ ├── switchStep.yml │ │ │ ├── taskCallStep.yml │ │ │ ├── taskCallStepOutExpr.yml │ │ │ └── taskCallStepParallel.yml │ │ └── validationConfig.yml │ ├── pom.xml │ ├── runner/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── runtime/ │ │ │ │ └── v2/ │ │ │ │ └── runner/ │ │ │ │ ├── DefaultDependencyManager.java │ │ │ │ ├── DefaultDockerService.java │ │ │ │ ├── DefaultEventReportingService.java │ │ │ │ ├── DefaultFileService.java │ │ │ │ ├── DefaultLockService.java │ │ │ │ ├── DefaultPersistenceService.java │ │ │ │ ├── DefaultProcessConfigurationProvider.java │ │ │ │ ├── DefaultResourceResolver.java │ │ │ │ ├── DefaultRuntime.java │ │ │ │ ├── DefaultSecretService.java │ │ │ │ ├── DefaultSynchronizationService.java │ │ │ │ ├── DefaultTaskVariablesProvider.java │ │ │ │ ├── DefaultTaskVariablesService.java │ │ │ │ ├── DependencyManagerConfigurationProvider.java │ │ │ │ ├── DockerProcessBuilder.java │ │ │ │ ├── EventReportingService.java │ │ │ │ ├── FormServiceProvider.java │ │ │ │ ├── InjectorFactory.java │ │ │ │ ├── Main.java │ │ │ │ ├── MapBackedDefaultTaskVariablesService.java │ │ │ │ ├── MetadataProcessor.java │ │ │ │ ├── OutVariablesProcessor.java │ │ │ │ ├── PersistenceService.java │ │ │ │ ├── PolicyEngineProvider.java │ │ │ │ ├── ProcessSnapshot.java │ │ │ │ ├── ProcessStatusCallback.java │ │ │ │ ├── ResourceResolver.java │ │ │ │ ├── Runner.java │ │ │ │ ├── SensitiveDataHolder.java │ │ │ │ ├── SensitiveDataPersistenceService.java │ │ │ │ ├── StackTraceCollector.java │ │ │ │ ├── StateBackwardCompatibility.java │ │ │ │ ├── SynchronizationService.java │ │ │ │ ├── SynchronizationServiceListener.java │ │ │ │ ├── TaskResultService.java │ │ │ │ ├── checkpoints/ │ │ │ │ │ ├── CheckpointService.java │ │ │ │ │ ├── CheckpointUploader.java │ │ │ │ │ ├── DefaultCheckpointService.java │ │ │ │ │ ├── DefaultCheckpointUploader.java │ │ │ │ │ └── StateArchive.java │ │ │ │ ├── compiler/ │ │ │ │ │ ├── CheckpointCompiler.java │ │ │ │ │ ├── CompilerContext.java │ │ │ │ │ ├── CompilerUtils.java │ │ │ │ │ ├── DefaultCompiler.java │ │ │ │ │ ├── ExitCompiler.java │ │ │ │ │ ├── ExpressionCompiler.java │ │ │ │ │ ├── FlowCallCompiler.java │ │ │ │ │ ├── FormCallCompiler.java │ │ │ │ │ ├── GroupOfStepsCompiler.java │ │ │ │ │ ├── IfCompiler.java │ │ │ │ │ ├── ParallelStepCompiler.java │ │ │ │ │ ├── ReturnCompiler.java │ │ │ │ │ ├── ScriptCallCompiler.java │ │ │ │ │ ├── SetVariablesCompiler.java │ │ │ │ │ ├── StepCompiler.java │ │ │ │ │ ├── SuspendCompiler.java │ │ │ │ │ ├── SwitchCompiler.java │ │ │ │ │ └── TaskCallCompiler.java │ │ │ │ ├── context/ │ │ │ │ │ ├── ContextFactory.java │ │ │ │ │ ├── ContextImpl.java │ │ │ │ │ ├── ContextVariables.java │ │ │ │ │ ├── ContextVariablesWithOverrides.java │ │ │ │ │ ├── DefaultContextFactory.java │ │ │ │ │ ├── ResumeEventImpl.java │ │ │ │ │ └── TaskContext.java │ │ │ │ ├── el/ │ │ │ │ │ ├── DefaultExpressionEvaluator.java │ │ │ │ │ ├── EvalContextFactoryImpl.java │ │ │ │ │ ├── FunctionHolder.java │ │ │ │ │ ├── LazyEvalContext.java │ │ │ │ │ ├── LazyEvalList.java │ │ │ │ │ ├── LazyEvalMap.java │ │ │ │ │ ├── LazyExpressionEvaluator.java │ │ │ │ │ ├── MethodNotFoundException.java │ │ │ │ │ ├── ThreadLocalEvalContext.java │ │ │ │ │ ├── functions/ │ │ │ │ │ │ ├── AllVariablesFunction.java │ │ │ │ │ │ ├── CurrentFlowNameFunction.java │ │ │ │ │ │ ├── EvalAsMapFunction.java │ │ │ │ │ │ ├── HasFlowFunction.java │ │ │ │ │ │ ├── HasNonNullVariableFunction.java │ │ │ │ │ │ ├── HasVariableFunction.java │ │ │ │ │ │ ├── IsDebugFunction.java │ │ │ │ │ │ ├── IsDryRunFunction.java │ │ │ │ │ │ ├── MarkAsSensitiveFunction.java │ │ │ │ │ │ ├── OrDefaultFunction.java │ │ │ │ │ │ ├── ThrowFunction.java │ │ │ │ │ │ └── UuidFunction.java │ │ │ │ │ └── resolvers/ │ │ │ │ │ ├── BeanELResolver.java │ │ │ │ │ ├── CompositeBeanELResolver.java │ │ │ │ │ ├── DefaultInvocationContext.java │ │ │ │ │ ├── MapELResolver.java │ │ │ │ │ ├── MethodAccessorResolver.java │ │ │ │ │ ├── SensitiveDataProcessor.java │ │ │ │ │ ├── TaskMethodResolver.java │ │ │ │ │ ├── TaskResolver.java │ │ │ │ │ └── VariableResolver.java │ │ │ │ ├── guice/ │ │ │ │ │ ├── BaseRunnerModule.java │ │ │ │ │ ├── CurrentClasspathModule.java │ │ │ │ │ ├── DefaultRunnerModule.java │ │ │ │ │ ├── ExpressionSupportModule.java │ │ │ │ │ ├── ObjectMapperProvider.java │ │ │ │ │ └── ProcessDependenciesModule.java │ │ │ │ ├── logging/ │ │ │ │ │ ├── ConcordLogEncoder.java │ │ │ │ │ ├── CustomLayout.java │ │ │ │ │ ├── DefaultLoggingClient.java │ │ │ │ │ ├── LogContext.java │ │ │ │ │ ├── LogContextThreadGroup.java │ │ │ │ │ ├── LogLevelFilter.java │ │ │ │ │ ├── LogUtils.java │ │ │ │ │ ├── LoggerProvider.java │ │ │ │ │ ├── LoggingClient.java │ │ │ │ │ ├── LoggingConfigurator.java │ │ │ │ │ ├── RunnerLogger.java │ │ │ │ │ ├── SegmentStatusMarker.java │ │ │ │ │ ├── SegmentedLogger.java │ │ │ │ │ └── SimpleLogger.java │ │ │ │ ├── remote/ │ │ │ │ │ ├── ApiClientProvider.java │ │ │ │ │ ├── DefaultProcessStatusCallback.java │ │ │ │ │ ├── EventRecordingExecutionListener.java │ │ │ │ │ └── TaskCallEventRecordingListener.java │ │ │ │ ├── script/ │ │ │ │ │ ├── DefaultScriptEvaluator.java │ │ │ │ │ ├── SanitizedMap.java │ │ │ │ │ ├── ScriptContext.java │ │ │ │ │ ├── ScriptEngineBindings.java │ │ │ │ │ ├── ScriptEngineProperties.java │ │ │ │ │ ├── ScriptEvaluator.java │ │ │ │ │ ├── ScriptResult.java │ │ │ │ │ ├── ScriptVariables.java │ │ │ │ │ └── VariablesSanitizer.java │ │ │ │ ├── sdk/ │ │ │ │ │ ├── ApiClientFactoryImpl.java │ │ │ │ │ ├── ApiConfigurationImpl.java │ │ │ │ │ └── ApiConfigurationV1Impl.java │ │ │ │ ├── tasks/ │ │ │ │ │ ├── ContextProvider.java │ │ │ │ │ ├── TaskCallEvent.java │ │ │ │ │ ├── TaskCallInterceptor.java │ │ │ │ │ ├── TaskCallListener.java │ │ │ │ │ ├── TaskCallPolicyChecker.java │ │ │ │ │ ├── TaskException.java │ │ │ │ │ ├── TaskProviders.java │ │ │ │ │ ├── TaskResultListener.java │ │ │ │ │ ├── TaskSchemaLookupResult.java │ │ │ │ │ ├── TaskSchemaRegistry.java │ │ │ │ │ ├── TaskSchemaValidationException.java │ │ │ │ │ ├── TaskSchemaValidationResult.java │ │ │ │ │ ├── TaskSchemaValidator.java │ │ │ │ │ ├── TaskV2Provider.java │ │ │ │ │ └── V2.java │ │ │ │ └── vm/ │ │ │ │ ├── BlockCommand.java │ │ │ │ ├── CheckpointCommand.java │ │ │ │ ├── CloseLogSegmentCommand.java │ │ │ │ ├── CopyVariablesCommand.java │ │ │ │ ├── ElementEventProducer.java │ │ │ │ ├── ErrorWrapper.java │ │ │ │ ├── ExitCommand.java │ │ │ │ ├── ExposeLastErrorCommand.java │ │ │ │ ├── ExpressionCommand.java │ │ │ │ ├── FlowCallCommand.java │ │ │ │ ├── ForkCommand.java │ │ │ │ ├── FormCallCommand.java │ │ │ │ ├── IfCommand.java │ │ │ │ ├── JoinCommand.java │ │ │ │ ├── LogSegmentScopeCommand.java │ │ │ │ ├── LogSegmentUtils.java │ │ │ │ ├── LoopItemSanitizer.java │ │ │ │ ├── LoopWrapper.java │ │ │ │ ├── OutputUtils.java │ │ │ │ ├── ParallelCommand.java │ │ │ │ ├── ParallelExecutionException.java │ │ │ │ ├── ResumeLogSegmentsCommand.java │ │ │ │ ├── RetryWrapper.java │ │ │ │ ├── ReturnCommand.java │ │ │ │ ├── SaveLastErrorCommand.java │ │ │ │ ├── SaveOutVariablesOnErrorCommand.java │ │ │ │ ├── ScriptCallCommand.java │ │ │ │ ├── SetVariablesCommand.java │ │ │ │ ├── StepCommand.java │ │ │ │ ├── StepOptionsUtils.java │ │ │ │ ├── SuspendCommand.java │ │ │ │ ├── SuspendStepCommand.java │ │ │ │ ├── SwitchCommand.java │ │ │ │ ├── TaskCallCommand.java │ │ │ │ ├── TaskCallUtils.java │ │ │ │ ├── TaskResumeCommand.java │ │ │ │ ├── TaskSchemaValidation.java │ │ │ │ ├── TaskSuspendCommand.java │ │ │ │ ├── UpdateLocalsCommand.java │ │ │ │ ├── VMUtils.java │ │ │ │ ├── WithItemsWrapper.java │ │ │ │ └── WrappedException.java │ │ │ └── resources/ │ │ │ ├── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── runtime/ │ │ │ │ └── v2/ │ │ │ │ └── runner/ │ │ │ │ └── dockerPasswd │ │ │ └── logback.xml │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── runtime/ │ │ │ └── v2/ │ │ │ └── runner/ │ │ │ ├── EventReportingServiceTest.java │ │ │ ├── checkpoints/ │ │ │ │ └── DefaultCheckpointServiceTest.java │ │ │ ├── context/ │ │ │ │ └── ResumeEventImplTest.java │ │ │ ├── el/ │ │ │ │ ├── DummyContext.java │ │ │ │ ├── ExpressionEvaluatorTest.java │ │ │ │ ├── HasNoNullVariableFunctionTest.java │ │ │ │ ├── HasVariableFunctionTest.java │ │ │ │ ├── ImmutablesTest.java │ │ │ │ ├── MethodNotFoundExceptionTest.java │ │ │ │ ├── OrDefaultFunctionTest.java │ │ │ │ └── SingleFrameContext.java │ │ │ ├── guice/ │ │ │ │ └── ModuleTest.java │ │ │ ├── remote/ │ │ │ │ └── TaskCallEventRecordingListenerTest.java │ │ │ ├── tasks/ │ │ │ │ ├── TaskCallInterceptorTest.java │ │ │ │ ├── TaskSchemaRegistryTest.java │ │ │ │ ├── TaskSchemaValidatorTest.java │ │ │ │ └── other/ │ │ │ │ └── OtherSchemaTask.java │ │ │ └── vm/ │ │ │ ├── JoinCommandTest.java │ │ │ ├── SaveLastErrorCommandTest.java │ │ │ ├── TaskSchemaValidationTest.java │ │ │ └── VMUtilsTest.java │ │ └── resources/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── runtime/ │ │ └── v2/ │ │ └── runner/ │ │ └── tasks/ │ │ ├── full.schema.json │ │ ├── invalidJson.schema.json │ │ ├── invalidSection.schema.json │ │ ├── noSection.schema.json │ │ ├── other/ │ │ │ └── shared.schema.json │ │ ├── rootNotObject.schema.json │ │ └── shared.schema.json │ ├── runner-test/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── runtime/ │ │ │ └── v2/ │ │ │ └── runner/ │ │ │ ├── TestCheckpointUploader.java │ │ │ └── TestRuntimeV2.java │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── runtime/ │ │ │ └── v2/ │ │ │ └── runner/ │ │ │ ├── LogExceptionsTest.java │ │ │ ├── LogSegmentsTest.java │ │ │ ├── MainTest.java │ │ │ ├── functions/ │ │ │ │ └── TestFunction.java │ │ │ └── tasks/ │ │ │ ├── ReentrantTaskExample.java │ │ │ └── Tasks.java │ │ └── resources/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── runtime/ │ │ └── v2/ │ │ └── runner/ │ │ ├── argsFromArgs/ │ │ │ └── concord.yml │ │ ├── base64Sensitive/ │ │ │ └── concord.yaml │ │ ├── call/ │ │ │ └── concord.yml │ │ ├── callOut/ │ │ │ └── concord.yml │ │ ├── callOutWithItems/ │ │ │ └── concord.yml │ │ ├── callWithErrorBlock/ │ │ │ └── concord.yml │ │ ├── checkpointExpr/ │ │ │ └── concord.yml │ │ ├── checkpointRestore/ │ │ │ └── concord.yml │ │ ├── checkpointRestore2/ │ │ │ └── concord.yml │ │ ├── checkpoints/ │ │ │ └── concord.yml │ │ ├── currentFlowName/ │ │ │ └── concord.yml │ │ ├── defaultVariables/ │ │ │ └── concord.yml │ │ ├── doNotTouchFlowNameVariable/ │ │ │ └── concord.yaml │ │ ├── doubleValues/ │ │ │ └── concord.yaml │ │ ├── dryRunReadyAsExpression/ │ │ │ └── concord.yml │ │ ├── entrySetSerialization/ │ │ │ └── concord.yaml │ │ ├── errorBlockScoping/ │ │ │ └── concord.yml │ │ ├── evalAsMap/ │ │ │ └── concord.yml │ │ ├── exceptionFromExpression/ │ │ │ └── concord.yml │ │ ├── exit/ │ │ │ └── concord.yml │ │ ├── exitWithMeta/ │ │ │ └── concord.yml │ │ ├── exprOutExpr/ │ │ │ └── concord.yml │ │ ├── faultyExpression/ │ │ │ └── concord.yml │ │ ├── faultyFlow/ │ │ │ └── concord.yml │ │ ├── faultyTask/ │ │ │ └── concord.yml │ │ ├── faultyTask4/ │ │ │ └── concord.yml │ │ ├── faultyTaskOut/ │ │ │ └── concord.yml │ │ ├── flowCallOutCompat/ │ │ │ └── concord.yaml │ │ ├── flowCallOutExpression/ │ │ │ └── concord.yml │ │ ├── form/ │ │ │ └── concord.yml │ │ ├── hasFlow/ │ │ │ └── concord.yml │ │ ├── hasNonNullVariable/ │ │ │ └── concord.yml │ │ ├── hello/ │ │ │ └── concord.yml │ │ ├── ifExpression/ │ │ │ └── concord.yml │ │ ├── ifExpressionAsNull/ │ │ │ └── concord.yml │ │ ├── ifExpressionAsString/ │ │ │ └── concord.yml │ │ ├── incVariable/ │ │ │ └── concord.yaml │ │ ├── initiator/ │ │ │ └── concord.yml │ │ ├── injectorTest/ │ │ │ └── concord.yml │ │ ├── invalidExpression/ │ │ │ └── concord.yml │ │ ├── isDebug/ │ │ │ └── concord.yml │ │ ├── lazyEvalMapInArgs/ │ │ │ └── concord.yaml │ │ ├── logExceptionTests/ │ │ │ ├── failResultFromTask/ │ │ │ │ └── concord.yaml │ │ │ ├── fromExpression/ │ │ │ │ └── concord.yaml │ │ │ ├── fromParallel/ │ │ │ │ └── concord.yaml │ │ │ ├── fromParallelParallel/ │ │ │ │ └── concord.yaml │ │ │ ├── fromScript/ │ │ │ │ └── concord.yaml │ │ │ ├── fromTask/ │ │ │ │ └── concord.yaml │ │ │ ├── invalidArgs/ │ │ │ │ └── concord.yaml │ │ │ ├── methodNotFound/ │ │ │ │ └── concord.yaml │ │ │ ├── userDefinedExceptionFromExpression/ │ │ │ │ └── concord.yaml │ │ │ ├── userDefinedExceptionFromTask/ │ │ │ │ └── concord.yaml │ │ │ ├── userDefinedExceptionFromTaskParallel/ │ │ │ │ └── concord.yaml │ │ │ └── variableNotFound/ │ │ │ └── concord.yaml │ │ ├── logSegments/ │ │ │ ├── call/ │ │ │ │ └── concord.yaml │ │ │ ├── callWithError/ │ │ │ │ └── concord.yaml │ │ │ ├── callWithErrorThrowError/ │ │ │ │ └── concord.yaml │ │ │ ├── callWithName/ │ │ │ │ └── concord.yaml │ │ │ ├── callWithNameLoop/ │ │ │ │ └── concord.yaml │ │ │ ├── callWithRetry/ │ │ │ │ └── concord.yaml │ │ │ ├── checkpointInvalid/ │ │ │ │ └── concord.yaml │ │ │ ├── expr/ │ │ │ │ └── concord.yaml │ │ │ ├── exprInvalid/ │ │ │ │ └── concord.yaml │ │ │ ├── exprWithName/ │ │ │ │ └── concord.yaml │ │ │ ├── script/ │ │ │ │ └── concord.yml │ │ │ ├── scriptInvalid/ │ │ │ │ └── concord.yml │ │ │ ├── scriptInvalidBody/ │ │ │ │ └── concord.yml │ │ │ ├── task/ │ │ │ │ └── concord.yaml │ │ │ ├── taskError/ │ │ │ │ └── concord.yaml │ │ │ ├── taskErrorWithError/ │ │ │ │ └── concord.yaml │ │ │ ├── taskErrorWithLoop/ │ │ │ │ └── concord.yaml │ │ │ ├── taskErrorWithReturn/ │ │ │ │ └── concord.yaml │ │ │ ├── taskLoopInvalid/ │ │ │ │ └── concord.yaml │ │ │ ├── taskLoopParallel/ │ │ │ │ └── concord.yaml │ │ │ ├── taskLoopSerial/ │ │ │ │ └── concord.yaml │ │ │ ├── taskLoopWithError/ │ │ │ │ └── concord.yaml │ │ │ ├── taskOutInvalid/ │ │ │ │ └── concord.yaml │ │ │ ├── taskUndefined/ │ │ │ │ └── concord.yaml │ │ │ ├── taskWithInvalidName/ │ │ │ │ └── concord.yaml │ │ │ ├── taskWithReentrantSuspend/ │ │ │ │ └── concord.yaml │ │ │ ├── taskWithRetry/ │ │ │ │ └── concord.yaml │ │ │ ├── taskWithRetryInvalid/ │ │ │ │ └── concord.yaml │ │ │ ├── taskWithSensitiveDataInName/ │ │ │ │ └── concord.yaml │ │ │ ├── taskWithSuspend/ │ │ │ │ └── concord.yaml │ │ │ └── throw/ │ │ │ └── concord.yaml │ │ ├── logging/ │ │ │ └── concord.yml │ │ ├── loopBlock/ │ │ │ └── concord.yml │ │ ├── loopSerializationError/ │ │ │ └── concord.yml │ │ ├── loopSet/ │ │ │ └── concord.yml │ │ ├── multipleWithItems/ │ │ │ └── concord.yml │ │ ├── nestedSet/ │ │ │ └── concord.yml │ │ ├── nonSerializableLocal/ │ │ │ └── concord.yml │ │ ├── npeInExpression/ │ │ │ └── concord.yml │ │ ├── orDefault/ │ │ │ └── concord.yml │ │ ├── parallelEmptyCall/ │ │ │ └── concord.yml │ │ ├── parallelForm/ │ │ │ └── concord.yml │ │ ├── parallelIn/ │ │ │ └── concord.yml │ │ ├── parallelLoopExit/ │ │ │ └── concord.yml │ │ ├── parallelLoopItemIndex/ │ │ │ └── concord.yml │ │ ├── parallelLoopTask/ │ │ │ └── concord.yml │ │ ├── parallelOut/ │ │ │ └── concord.yml │ │ ├── parallelOutExpr/ │ │ │ └── concord.yml │ │ ├── parallelWithError/ │ │ │ └── concord.yml │ │ ├── parallelWithItemsTask/ │ │ │ └── concord.yml │ │ ├── prefixedFunctions/ │ │ │ └── concord.yml │ │ ├── reentrantTask/ │ │ │ └── concord.yml │ │ ├── reentrantTaskSchemaValidation/ │ │ │ └── concord.yml │ │ ├── reentrantTaskWithError/ │ │ │ └── concord.yml │ │ ├── retry/ │ │ │ └── concord.yml │ │ ├── retryInput/ │ │ │ └── concord.yml │ │ ├── return/ │ │ │ └── concord.yml │ │ ├── scriptAttached/ │ │ │ ├── .concord.yml │ │ │ └── scripts/ │ │ │ └── myscript.js │ │ ├── scriptError/ │ │ │ └── concord.yml │ │ ├── scriptEsVersion/ │ │ │ ├── concord.yml │ │ │ └── index.js │ │ ├── scriptEsVersionInvalid/ │ │ │ └── concord.yml │ │ ├── scriptInline/ │ │ │ └── concord.yml │ │ ├── scriptOut/ │ │ │ └── concord.yml │ │ ├── scriptOutExpr/ │ │ │ └── concord.yml │ │ ├── scriptUnboundedInputMapOk/ │ │ │ └── concord.yml │ │ ├── scriptVariablesSanitize/ │ │ │ └── concord.yml │ │ ├── sensitiveData/ │ │ │ └── concord.yaml │ │ ├── sensitiveFunction/ │ │ │ └── concord.yml │ │ ├── serialEmptyCall/ │ │ │ └── concord.yml │ │ ├── serialLoopExit/ │ │ │ └── concord.yml │ │ ├── setVariableOverride/ │ │ │ └── concord.yaml │ │ ├── setVariables/ │ │ │ └── concord.yml │ │ ├── stackTrace/ │ │ │ └── concord.yml │ │ ├── stackTrace2/ │ │ │ └── concord.yml │ │ ├── stackTrace3/ │ │ │ └── concord.yml │ │ ├── stackTrace4/ │ │ │ └── concord.yml │ │ ├── stackTrace5/ │ │ │ └── concord.yml │ │ ├── stackTrace6/ │ │ │ └── concord.yml │ │ ├── stackTrace7/ │ │ │ └── concord.yml │ │ ├── suspend/ │ │ │ └── concord.yml │ │ ├── switchExpressionCaseExpression/ │ │ │ └── concord.yml │ │ ├── switchExpressionDefault/ │ │ │ └── concord.yml │ │ ├── switchExpressionFull/ │ │ │ └── concord.yml │ │ ├── systemOutRedirect/ │ │ │ └── concord.yml │ │ ├── taskIgnoreErrors/ │ │ │ └── concord.yml │ │ ├── taskIgnoreErrors2/ │ │ │ └── concord.yml │ │ ├── taskInputInterpolate/ │ │ │ └── concord.yml │ │ ├── taskOut/ │ │ │ └── concord.yml │ │ ├── taskOutWithItems/ │ │ │ └── concord.yml │ │ ├── taskResultPolicy/ │ │ │ ├── .concord/ │ │ │ │ └── policy.json │ │ │ └── concord.yml │ │ ├── tasks/ │ │ │ └── reentrantTask.schema.json │ │ ├── threadLocals/ │ │ │ └── concord.yml │ │ ├── throwExpression/ │ │ │ └── concord.yml │ │ ├── tryError/ │ │ │ └── concord.yml │ │ ├── unknownMethod/ │ │ │ └── concord.yml │ │ ├── unknownTask/ │ │ │ └── concord.yml │ │ ├── unresolvedVarInLoop/ │ │ │ └── concord.yml │ │ ├── unresolvedVarInRetry/ │ │ │ └── concord.yml │ │ ├── unresolvedVarInStepName/ │ │ │ └── concord.yml │ │ ├── uuid/ │ │ │ └── concord.yml │ │ ├── varScoping/ │ │ │ └── concord.yml │ │ ├── variablesAfterResume/ │ │ │ └── concord.yaml │ │ ├── withItemsBlock/ │ │ │ └── concord.yml │ │ └── withItemsSet/ │ │ └── concord.yml │ ├── sdk/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── runtime/ │ │ └── v2/ │ │ └── sdk/ │ │ ├── AllowNulls.java │ │ ├── ApiConfiguration.java │ │ ├── Compiler.java │ │ ├── Constants.java │ │ ├── Context.java │ │ ├── ContextUtils.java │ │ ├── CustomBeanMethodResolver.java │ │ ├── CustomTaskMethodResolver.java │ │ ├── DependencyManager.java │ │ ├── DockerContainerSpec.java │ │ ├── DockerService.java │ │ ├── DryRunReady.java │ │ ├── ELFunction.java │ │ ├── EvalContext.java │ │ ├── EvalContextFactory.java │ │ ├── Execution.java │ │ ├── ExpressionEvaluator.java │ │ ├── FileService.java │ │ ├── Invocation.java │ │ ├── InvocationContext.java │ │ ├── LockService.java │ │ ├── MapBackedVariables.java │ │ ├── MethodInvoker.java │ │ ├── NoopVariables.java │ │ ├── ProcessConfiguration.java │ │ ├── ProcessInfo.java │ │ ├── ProjectInfo.java │ │ ├── ReentrantTask.java │ │ ├── ResumeEvent.java │ │ ├── SecretNotFoundException.java │ │ ├── SecretService.java │ │ ├── SensitiveData.java │ │ ├── SensitiveDataHolder.java │ │ ├── Task.java │ │ ├── TaskProvider.java │ │ ├── TaskResult.java │ │ ├── UserDefinedException.java │ │ ├── Variables.java │ │ └── WorkingDirectory.java │ └── vm/ │ ├── README.md │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── svm/ │ ├── Command.java │ ├── EvalResult.java │ ├── ExecutionListener.java │ ├── ExecutionListenerHolder.java │ ├── Frame.java │ ├── FrameId.java │ ├── FrameType.java │ ├── InMemoryState.java │ ├── ParallelExecutionException.java │ ├── PopFrameCommand.java │ ├── Runtime.java │ ├── RuntimeFactory.java │ ├── StackTraceItem.java │ ├── State.java │ ├── StateBackwardCompatibility.java │ ├── ThreadError.java │ ├── ThreadId.java │ ├── ThreadStatus.java │ └── VM.java ├── sdk/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── sdk/ │ │ ├── ApiConfiguration.java │ │ ├── ClientException.java │ │ ├── Constants.java │ │ ├── Context.java │ │ ├── ContextUtils.java │ │ ├── DependencyManager.java │ │ ├── DockerContainerSpec.java │ │ ├── DockerService.java │ │ ├── EventService.java │ │ ├── EventType.java │ │ ├── HasKey.java │ │ ├── InjectVariable.java │ │ ├── LockService.java │ │ ├── LogTags.java │ │ ├── MapUtils.java │ │ ├── MockContext.java │ │ ├── ObjectStorage.java │ │ ├── ProjectInfo.java │ │ ├── RepositoryInfo.java │ │ ├── Secret.java │ │ ├── SecretNotFoundException.java │ │ ├── SecretService.java │ │ ├── Task.java │ │ └── UserDefinedException.java │ └── test/ │ └── java/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── sdk/ │ └── ContextUtilsTest.java ├── server/ │ ├── README.md │ ├── db/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── db/ │ │ │ │ ├── AbstractDao.java │ │ │ │ ├── DataSourceUtils.java │ │ │ │ ├── DatabaseChangeLogProvider.java │ │ │ │ ├── DatabaseConfiguration.java │ │ │ │ ├── DatabaseModule.java │ │ │ │ ├── JsonStorageDB.java │ │ │ │ ├── LiquibaseLogService.java │ │ │ │ ├── LiquibaseLogger.java │ │ │ │ ├── MainDB.java │ │ │ │ ├── MainDBChangeLogProvider.java │ │ │ │ ├── PgIntRange.java │ │ │ │ ├── PgUtils.java │ │ │ │ └── ResultSetInputStream.java │ │ │ └── resources/ │ │ │ ├── META-INF/ │ │ │ │ └── services/ │ │ │ │ ├── liquibase.lockservice.LockService │ │ │ │ └── liquibase.logging.LogService │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── server/ │ │ │ └── db/ │ │ │ ├── liquibase.xml │ │ │ ├── v0.0.1.xml │ │ │ ├── v0.12.0.xml │ │ │ ├── v0.13.0.xml │ │ │ ├── v0.14.0.xml │ │ │ ├── v0.17.0.xml │ │ │ ├── v0.18.0.xml │ │ │ ├── v0.2.0.xml │ │ │ ├── v0.20.0.xml │ │ │ ├── v0.21.0.xml │ │ │ ├── v0.22.0.xml │ │ │ ├── v0.23.0.xml │ │ │ ├── v0.24.0.xml │ │ │ ├── v0.27.0.xml │ │ │ ├── v0.31.0.xml │ │ │ ├── v0.38.0.xml │ │ │ ├── v0.39.0.xml │ │ │ ├── v0.41.0.xml │ │ │ ├── v0.44.0.xml │ │ │ ├── v0.45.0.xml │ │ │ ├── v0.46.0.xml │ │ │ ├── v0.47.0.xml │ │ │ ├── v0.48.0.xml │ │ │ ├── v0.52.0.xml │ │ │ ├── v0.56.0.xml │ │ │ ├── v0.58.0.xml │ │ │ ├── v0.59.0.xml │ │ │ ├── v0.60.0.xml │ │ │ ├── v0.61.0.xml │ │ │ ├── v0.64.0.xml │ │ │ ├── v0.65.0.xml │ │ │ ├── v0.66.0.xml │ │ │ ├── v0.67.0.xml │ │ │ ├── v0.69.0.xml │ │ │ ├── v0.70.0.xml │ │ │ ├── v0.71.0.xml │ │ │ ├── v0.74.0.xml │ │ │ ├── v0.77.0.xml │ │ │ ├── v0.79.0.xml │ │ │ ├── v0.80.0.xml │ │ │ ├── v0.81.0.xml │ │ │ ├── v0.83.0.xml │ │ │ ├── v0.84.0.xml │ │ │ ├── v0.85.0.xml │ │ │ ├── v0.86.0.xml │ │ │ ├── v0.87.0.xml │ │ │ ├── v0.88.0.xml │ │ │ ├── v0.89.0.xml │ │ │ ├── v0.90.0.xml │ │ │ ├── v0.92.0.xml │ │ │ ├── v0.93.0.xml │ │ │ ├── v0.95.0.xml │ │ │ ├── v0.97.0.xml │ │ │ ├── v0.99.0.xml │ │ │ ├── v1.0.0.xml │ │ │ ├── v1.10.0.xml │ │ │ ├── v1.102.0.xml │ │ │ ├── v1.103.0.xml │ │ │ ├── v1.104.0.xml │ │ │ ├── v1.11.0.xml │ │ │ ├── v1.12.0.xml │ │ │ ├── v1.13.0.xml │ │ │ ├── v1.18.0.xml │ │ │ ├── v1.21.0.xml │ │ │ ├── v1.22.0.xml │ │ │ ├── v1.24.0.xml │ │ │ ├── v1.27.0.xml │ │ │ ├── v1.28.0.xml │ │ │ ├── v1.32.0.xml │ │ │ ├── v1.33.0.xml │ │ │ ├── v1.34.0.xml │ │ │ ├── v1.34.1.xml │ │ │ ├── v1.34.2.xml │ │ │ ├── v1.34.3.xml │ │ │ ├── v1.35.0.xml │ │ │ ├── v1.38.0.xml │ │ │ ├── v1.40.0.xml │ │ │ ├── v1.41.0.xml │ │ │ ├── v1.43.0.xml │ │ │ ├── v1.45.0.xml │ │ │ ├── v1.48.0.xml │ │ │ ├── v1.49.0.xml │ │ │ ├── v1.5.0.xml │ │ │ ├── v1.56.0.xml │ │ │ ├── v1.57.0.xml │ │ │ ├── v1.58.0.xml │ │ │ ├── v1.60.0.xml │ │ │ ├── v1.66.0.xml │ │ │ ├── v1.69.0.xml │ │ │ ├── v1.7.0.xml │ │ │ ├── v1.75.0.xml │ │ │ ├── v1.76.0.xml │ │ │ ├── v1.78.0.xml │ │ │ ├── v1.79.0.xml │ │ │ ├── v1.8.0.xml │ │ │ ├── v1.81.0.xml │ │ │ ├── v1.83.0.xml │ │ │ ├── v1.85.0.xml │ │ │ ├── v1.86.0.xml │ │ │ ├── v1.88.0.xml │ │ │ ├── v1.90.0.xml │ │ │ ├── v1.91.0.xml │ │ │ ├── v1.94.0.xml │ │ │ ├── v1.95.0.xml │ │ │ ├── v1.96.0.xml │ │ │ ├── v1.98.1.xml │ │ │ ├── v1.99.0.xml │ │ │ ├── v2.10.0.xml │ │ │ ├── v2.12.0.xml │ │ │ ├── v2.14.0.xml │ │ │ ├── v2.21.0.xml │ │ │ ├── v2.22.0.xml │ │ │ ├── v2.23.0.xml │ │ │ ├── v2.31.0.xml │ │ │ ├── v2.35.0.xml │ │ │ ├── v2.8.0.xml │ │ │ └── v2.9.0.xml │ │ └── test/ │ │ ├── filtered-resources/ │ │ │ └── db.properties │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── db/ │ │ │ ├── MigrationTest.java │ │ │ └── PgIntRangeTest.java │ │ └── resources/ │ │ └── logback-test.xml │ ├── dist/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── assembly/ │ │ │ ├── default.conf │ │ │ ├── dist.xml │ │ │ └── start.sh │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── server/ │ │ │ └── dist/ │ │ │ └── Main.java │ │ └── resources/ │ │ └── concord-server.conf │ ├── impl/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── filtered-resources/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── server/ │ │ │ │ └── version.properties │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── server/ │ │ │ │ ├── AgentWorkerUtils.java │ │ │ │ ├── ApiEntity.java │ │ │ │ ├── ApiEntityValidationException.java │ │ │ │ ├── ApiServerModule.java │ │ │ │ ├── CommandType.java │ │ │ │ ├── ConcordObjectMapper.java │ │ │ │ ├── ConcordServer.java │ │ │ │ ├── ConcordServerModule.java │ │ │ │ ├── DependencyManagerConfigurationProvider.java │ │ │ │ ├── GenericOperationResult.java │ │ │ │ ├── GenericRequestErrorHandler.java │ │ │ │ ├── HttpUtils.java │ │ │ │ ├── Listeners.java │ │ │ │ ├── Locks.java │ │ │ │ ├── MigrateDB.java │ │ │ │ ├── MultipartUtils.java │ │ │ │ ├── OffsetDateTimeParam.java │ │ │ │ ├── OperationResult.java │ │ │ │ ├── PeriodicTask.java │ │ │ │ ├── PingResponse.java │ │ │ │ ├── RequestContext.java │ │ │ │ ├── RequestUtils.java │ │ │ │ ├── SecureRandomProvider.java │ │ │ │ ├── ServerResource.java │ │ │ │ ├── Utils.java │ │ │ │ ├── UuidGenerator.java │ │ │ │ ├── Version.java │ │ │ │ ├── VersionResponse.java │ │ │ │ ├── WrappedValue.java │ │ │ │ ├── agent/ │ │ │ │ │ ├── AgentCommand.java │ │ │ │ │ ├── AgentCommandWatchdog.java │ │ │ │ │ ├── AgentCommandsDao.java │ │ │ │ │ ├── AgentManager.java │ │ │ │ │ ├── AgentModule.java │ │ │ │ │ ├── AgentResource.java │ │ │ │ │ ├── AgentWorkerEntry.java │ │ │ │ │ ├── Commands.java │ │ │ │ │ ├── dispatcher/ │ │ │ │ │ │ └── Dispatcher.java │ │ │ │ │ └── websocket/ │ │ │ │ │ ├── ConcordWebSocketServlet.java │ │ │ │ │ ├── WebSocketChannel.java │ │ │ │ │ ├── WebSocketCreator.java │ │ │ │ │ ├── WebSocketListener.java │ │ │ │ │ ├── WebSocketMetricsModule.java │ │ │ │ │ └── WebSocketModule.java │ │ │ │ ├── audit/ │ │ │ │ │ ├── ActionSource.java │ │ │ │ │ ├── AuditAction.java │ │ │ │ │ ├── AuditDao.java │ │ │ │ │ ├── AuditLog.java │ │ │ │ │ ├── AuditLogCleaner.java │ │ │ │ │ ├── AuditLogEntry.java │ │ │ │ │ ├── AuditLogFilter.java │ │ │ │ │ ├── AuditLogModule.java │ │ │ │ │ ├── AuditLogResource.java │ │ │ │ │ └── AuditObject.java │ │ │ │ ├── boot/ │ │ │ │ │ ├── BackgroundTasks.java │ │ │ │ │ ├── ConcordSecurityManager.java │ │ │ │ │ ├── ContextHandlerConfigurator.java │ │ │ │ │ ├── CustomErrorHandler.java │ │ │ │ │ ├── FilterChainConfigurator.java │ │ │ │ │ ├── FormRequestErrorHandler.java │ │ │ │ │ ├── HttpServer.java │ │ │ │ │ ├── RequestErrorHandler.java │ │ │ │ │ ├── ShiroListener.java │ │ │ │ │ ├── filters/ │ │ │ │ │ │ ├── AuthenticationHandler.java │ │ │ │ │ │ ├── CORSFilter.java │ │ │ │ │ │ ├── ConcordAuthenticatingFilter.java │ │ │ │ │ │ ├── ConcordFilterChainConfigurator.java │ │ │ │ │ │ ├── NoCacheFilter.java │ │ │ │ │ │ ├── QoSFilter.java │ │ │ │ │ │ ├── RequestContextFilter.java │ │ │ │ │ │ ├── ShiroFilterHolder.java │ │ │ │ │ │ └── UnauthenticatedToken.java │ │ │ │ │ ├── resteasy/ │ │ │ │ │ │ ├── ConcordApiDescriptor.java │ │ │ │ │ │ ├── ExceptionMapperSupport.java │ │ │ │ │ │ ├── GuiceResourceFactory.java │ │ │ │ │ │ ├── ObjectMapperContextResolver.java │ │ │ │ │ │ ├── ResteasyBootstrapListener.java │ │ │ │ │ │ ├── ResteasyModule.java │ │ │ │ │ │ ├── UnexpectedExceptionMapper.java │ │ │ │ │ │ └── WebappExceptionMapper.java │ │ │ │ │ ├── servlets/ │ │ │ │ │ │ └── FormServletHolder.java │ │ │ │ │ ├── statics/ │ │ │ │ │ │ └── StaticResourcesConfigurator.java │ │ │ │ │ └── validation/ │ │ │ │ │ ├── ConstraintViolationExceptionMapper.java │ │ │ │ │ ├── DefaultGetterPropertySelectionStrategy.java │ │ │ │ │ ├── ValidationErrorsExceptionMapper.java │ │ │ │ │ ├── ValidationExceptionMapperSupport.java │ │ │ │ │ ├── ValidationInterceptor.java │ │ │ │ │ └── ValidationModule.java │ │ │ │ ├── cfg/ │ │ │ │ │ ├── AgentConfiguration.java │ │ │ │ │ ├── ApiKeyConfiguration.java │ │ │ │ │ ├── AuditConfiguration.java │ │ │ │ │ ├── ConcordSecretStoreConfiguration.java │ │ │ │ │ ├── ConfigurationModule.java │ │ │ │ │ ├── ConsoleConfiguration.java │ │ │ │ │ ├── CustomFormConfiguration.java │ │ │ │ │ ├── DatabaseConfigurationModule.java │ │ │ │ │ ├── DependenciesConfiguration.java │ │ │ │ │ ├── EmailNotifierConfiguration.java │ │ │ │ │ ├── EnqueueWorkersConfiguration.java │ │ │ │ │ ├── ExternalEventsConfiguration.java │ │ │ │ │ ├── GitConfiguration.java │ │ │ │ │ ├── GithubConfiguration.java │ │ │ │ │ ├── ImportConfiguration.java │ │ │ │ │ ├── JsonStorageDBConfiguration.java │ │ │ │ │ ├── LdapConfiguration.java │ │ │ │ │ ├── LdapGroupSyncConfiguration.java │ │ │ │ │ ├── LockingConfiguration.java │ │ │ │ │ ├── MainDBConfiguration.java │ │ │ │ │ ├── PolicyCacheConfiguration.java │ │ │ │ │ ├── ProcessConfiguration.java │ │ │ │ │ ├── ProcessQueueConfiguration.java │ │ │ │ │ ├── ProcessWaitWatchdogConfiguration.java │ │ │ │ │ ├── ProcessWatchdogConfiguration.java │ │ │ │ │ ├── QosConfiguration.java │ │ │ │ │ ├── RememberMeConfiguration.java │ │ │ │ │ ├── RepositoryConfiguration.java │ │ │ │ │ ├── SecretStoreConfiguration.java │ │ │ │ │ ├── ServerConfiguration.java │ │ │ │ │ ├── TemplatesConfiguration.java │ │ │ │ │ ├── TriggersConfiguration.java │ │ │ │ │ ├── Utils.java │ │ │ │ │ └── WorkerMetricsConfiguration.java │ │ │ │ ├── console/ │ │ │ │ │ ├── ConsoleModule.java │ │ │ │ │ ├── ConsoleService.java │ │ │ │ │ ├── CustomFormService.java │ │ │ │ │ ├── CustomFormServiceV1.java │ │ │ │ │ ├── CustomFormServiceV2.java │ │ │ │ │ ├── FormSessionResponse.java │ │ │ │ │ ├── ProcessCardAccessEntry.java │ │ │ │ │ ├── ProcessCardEntry.java │ │ │ │ │ ├── ProcessCardManager.java │ │ │ │ │ ├── ProcessCardOperationResponse.java │ │ │ │ │ ├── ProcessCardRequest.java │ │ │ │ │ ├── ProcessCardResource.java │ │ │ │ │ ├── RepositoryTestRequest.java │ │ │ │ │ ├── ResponseTemplates.java │ │ │ │ │ ├── UserActivityResourceV2.java │ │ │ │ │ ├── UserActivityResponse.java │ │ │ │ │ ├── UserInfoResponse.java │ │ │ │ │ └── UserResponse.java │ │ │ │ ├── events/ │ │ │ │ │ ├── DefaultEventFilter.java │ │ │ │ │ ├── Event.java │ │ │ │ │ ├── EventInitiatorSupplier.java │ │ │ │ │ ├── EventModule.java │ │ │ │ │ ├── ExpressionUtils.java │ │ │ │ │ ├── ExternalEventResource.java │ │ │ │ │ ├── GithubEventResource.java │ │ │ │ │ ├── TriggerEventInitiatorResolver.java │ │ │ │ │ ├── TriggerProcessExecutor.java │ │ │ │ │ ├── externalevent/ │ │ │ │ │ │ ├── ExternalEventTriggerProcessor.java │ │ │ │ │ │ ├── ExternalEventTriggerV1Processor.java │ │ │ │ │ │ └── ExternalEventTriggerV2Processor.java │ │ │ │ │ └── github/ │ │ │ │ │ ├── Constants.java │ │ │ │ │ ├── GithubRepoInfo.java │ │ │ │ │ ├── GithubTriggerProcessor.java │ │ │ │ │ ├── GithubUtils.java │ │ │ │ │ └── Payload.java │ │ │ │ ├── message/ │ │ │ │ │ ├── MessageChannel.java │ │ │ │ │ └── MessageChannelManager.java │ │ │ │ ├── metrics/ │ │ │ │ │ ├── FailedTaskError.java │ │ │ │ │ ├── FailedTaskMetrics.java │ │ │ │ │ ├── JettySessionMetricsModule.java │ │ │ │ │ ├── JettyStatisticsModule.java │ │ │ │ │ ├── MetricInterceptor.java │ │ │ │ │ ├── MetricModule.java │ │ │ │ │ ├── MetricRegistryProvider.java │ │ │ │ │ ├── MetricTypeListener.java │ │ │ │ │ ├── MetricUtils.java │ │ │ │ │ ├── MetricsRegistrator.java │ │ │ │ │ ├── MetricsServletHolder.java │ │ │ │ │ ├── NoSyntheticMethodMatcher.java │ │ │ │ │ └── WorkerMetrics.java │ │ │ │ ├── org/ │ │ │ │ │ ├── CreateOrganizationResponse.java │ │ │ │ │ ├── EntityOwner.java │ │ │ │ │ ├── OrganizationDao.java │ │ │ │ │ ├── OrganizationEntry.java │ │ │ │ │ ├── OrganizationManager.java │ │ │ │ │ ├── OrganizationModule.java │ │ │ │ │ ├── OrganizationOperationResult.java │ │ │ │ │ ├── OrganizationResource.java │ │ │ │ │ ├── OrganizationVisibility.java │ │ │ │ │ ├── ProjectProcessResource.java │ │ │ │ │ ├── ResourceAccessEntry.java │ │ │ │ │ ├── ResourceAccessLevel.java │ │ │ │ │ ├── ResourceAccessUtils.java │ │ │ │ │ ├── inventory/ │ │ │ │ │ │ ├── CreateInventoryQueryResponse.java │ │ │ │ │ │ ├── CreateInventoryResponse.java │ │ │ │ │ │ ├── DeleteInventoryDataResponse.java │ │ │ │ │ │ ├── DeleteInventoryQueryResponse.java │ │ │ │ │ │ ├── InventoryDataDao.java │ │ │ │ │ │ ├── InventoryDataItem.java │ │ │ │ │ │ ├── InventoryDataResource.java │ │ │ │ │ │ ├── InventoryEntry.java │ │ │ │ │ │ ├── InventoryModule.java │ │ │ │ │ │ ├── InventoryOwner.java │ │ │ │ │ │ ├── InventoryQueryEntry.java │ │ │ │ │ │ ├── InventoryQueryResource.java │ │ │ │ │ │ ├── InventoryResource.java │ │ │ │ │ │ └── JsonBuilder.java │ │ │ │ │ ├── jsonstore/ │ │ │ │ │ │ ├── JsonStoreAccessManager.java │ │ │ │ │ │ ├── JsonStoreCapacity.java │ │ │ │ │ │ ├── JsonStoreDao.java │ │ │ │ │ │ ├── JsonStoreDataDao.java │ │ │ │ │ │ ├── JsonStoreDataEntry.java │ │ │ │ │ │ ├── JsonStoreDataManager.java │ │ │ │ │ │ ├── JsonStoreDataResource.java │ │ │ │ │ │ ├── JsonStoreEntry.java │ │ │ │ │ │ ├── JsonStoreManager.java │ │ │ │ │ │ ├── JsonStoreModule.java │ │ │ │ │ │ ├── JsonStoreQueryDao.java │ │ │ │ │ │ ├── JsonStoreQueryEntry.java │ │ │ │ │ │ ├── JsonStoreQueryExecDao.java │ │ │ │ │ │ ├── JsonStoreQueryManager.java │ │ │ │ │ │ ├── JsonStoreQueryRequest.java │ │ │ │ │ │ ├── JsonStoreQueryResource.java │ │ │ │ │ │ ├── JsonStoreRequest.java │ │ │ │ │ │ ├── JsonStoreResource.java │ │ │ │ │ │ └── JsonStoreVisibility.java │ │ │ │ │ ├── policy/ │ │ │ │ │ │ ├── PolicyCheckResource.java │ │ │ │ │ │ ├── PolicyDao.java │ │ │ │ │ │ ├── PolicyEntry.java │ │ │ │ │ │ ├── PolicyLinkEntry.java │ │ │ │ │ │ ├── PolicyModule.java │ │ │ │ │ │ ├── PolicyOperationResponse.java │ │ │ │ │ │ └── PolicyResource.java │ │ │ │ │ ├── project/ │ │ │ │ │ │ ├── DiffUtils.java │ │ │ │ │ │ ├── EncryptValueResponse.java │ │ │ │ │ │ ├── EncryptedProjectValueManager.java │ │ │ │ │ │ ├── KvDao.java │ │ │ │ │ │ ├── KvEntry.java │ │ │ │ │ │ ├── KvManager.java │ │ │ │ │ │ ├── ProjectAccessManager.java │ │ │ │ │ │ ├── ProjectDao.java │ │ │ │ │ │ ├── ProjectEntry.java │ │ │ │ │ │ ├── ProjectKvCapacity.java │ │ │ │ │ │ ├── ProjectManager.java │ │ │ │ │ │ ├── ProjectModule.java │ │ │ │ │ │ ├── ProjectOperationResponse.java │ │ │ │ │ │ ├── ProjectOperationResult.java │ │ │ │ │ │ ├── ProjectRepositoryManager.java │ │ │ │ │ │ ├── ProjectResource.java │ │ │ │ │ │ ├── ProjectResourceV2.java │ │ │ │ │ │ ├── ProjectValidator.java │ │ │ │ │ │ ├── ProjectVisibility.java │ │ │ │ │ │ ├── RepositoryDao.java │ │ │ │ │ │ ├── RepositoryEntry.java │ │ │ │ │ │ ├── RepositoryResource.java │ │ │ │ │ │ ├── RepositoryResourceV2.java │ │ │ │ │ │ ├── RepositoryValidationException.java │ │ │ │ │ │ └── RepositoryValidationExceptionMapper.java │ │ │ │ │ ├── secret/ │ │ │ │ │ │ ├── GetDataRequest.java │ │ │ │ │ │ ├── KeyPairUtils.java │ │ │ │ │ │ ├── PasswordChecker.java │ │ │ │ │ │ ├── PasswordGenerator.java │ │ │ │ │ │ ├── PublicKeyResponse.java │ │ │ │ │ │ ├── SecretDao.java │ │ │ │ │ │ ├── SecretEntryV2.java │ │ │ │ │ │ ├── SecretException.java │ │ │ │ │ │ ├── SecretExceptionMapper.java │ │ │ │ │ │ ├── SecretManager.java │ │ │ │ │ │ ├── SecretModule.java │ │ │ │ │ │ ├── SecretOperationResponse.java │ │ │ │ │ │ ├── SecretResource.java │ │ │ │ │ │ ├── SecretResourceUtils.java │ │ │ │ │ │ ├── SecretResourceV2.java │ │ │ │ │ │ ├── SecretStoreEntry.java │ │ │ │ │ │ ├── SecretStoreResource.java │ │ │ │ │ │ ├── SecretType.java │ │ │ │ │ │ ├── SecretUpdateParams.java │ │ │ │ │ │ ├── SecretUpdateRequest.java │ │ │ │ │ │ ├── SecretVisibility.java │ │ │ │ │ │ ├── provider/ │ │ │ │ │ │ │ └── SecretStoreProvider.java │ │ │ │ │ │ └── store/ │ │ │ │ │ │ ├── SecretStore.java │ │ │ │ │ │ └── concord/ │ │ │ │ │ │ └── ConcordSecretStore.java │ │ │ │ │ ├── team/ │ │ │ │ │ │ ├── AddTeamLdapGroupsResponse.java │ │ │ │ │ │ ├── AddTeamUsersResponse.java │ │ │ │ │ │ ├── CreateTeamResponse.java │ │ │ │ │ │ ├── RemoveTeamUsersResponse.java │ │ │ │ │ │ ├── TeamDao.java │ │ │ │ │ │ ├── TeamEntry.java │ │ │ │ │ │ ├── TeamLdapGroupEntry.java │ │ │ │ │ │ ├── TeamManager.java │ │ │ │ │ │ ├── TeamMemberType.java │ │ │ │ │ │ ├── TeamModule.java │ │ │ │ │ │ ├── TeamResource.java │ │ │ │ │ │ ├── TeamRole.java │ │ │ │ │ │ └── TeamUserEntry.java │ │ │ │ │ └── triggers/ │ │ │ │ │ ├── CronTriggerProcessor.java │ │ │ │ │ ├── CronUtils.java │ │ │ │ │ ├── GithubTriggerEnricher.java │ │ │ │ │ ├── TriggerEntry.java │ │ │ │ │ ├── TriggerInternalIdCalculator.java │ │ │ │ │ ├── TriggerManager.java │ │ │ │ │ ├── TriggerResource.java │ │ │ │ │ ├── TriggerRunAs.java │ │ │ │ │ ├── TriggerScheduleDao.java │ │ │ │ │ ├── TriggerScheduler.java │ │ │ │ │ ├── TriggerSchedulerEntry.java │ │ │ │ │ ├── TriggerUtils.java │ │ │ │ │ ├── TriggerV2Resource.java │ │ │ │ │ ├── TriggersDao.java │ │ │ │ │ └── TriggersModule.java │ │ │ │ ├── package-info.java │ │ │ │ ├── policy/ │ │ │ │ │ ├── EntityAction.java │ │ │ │ │ ├── EntityType.java │ │ │ │ │ ├── PolicyCache.java │ │ │ │ │ ├── PolicyException.java │ │ │ │ │ ├── PolicyManager.java │ │ │ │ │ ├── PolicyModule.java │ │ │ │ │ └── PolicyUtils.java │ │ │ │ ├── process/ │ │ │ │ │ ├── ErrorMessage.java │ │ │ │ │ ├── ImportManagerProvider.java │ │ │ │ │ ├── ImportsNormalizerFactory.java │ │ │ │ │ ├── LogSegment.java │ │ │ │ │ ├── LogSegmentOperationResponse.java │ │ │ │ │ ├── LogSegmentRequest.java │ │ │ │ │ ├── LogSegmentUpdateRequest.java │ │ │ │ │ ├── OutVariablesUtils.java │ │ │ │ │ ├── Payload.java │ │ │ │ │ ├── PayloadBuilder.java │ │ │ │ │ ├── PayloadManager.java │ │ │ │ │ ├── PayloadUtils.java │ │ │ │ │ ├── ProcessAccessManager.java │ │ │ │ │ ├── ProcessCleaner.java │ │ │ │ │ ├── ProcessDataInclude.java │ │ │ │ │ ├── ProcessEntry.java │ │ │ │ │ ├── ProcessException.java │ │ │ │ │ ├── ProcessExceptionMapper.java │ │ │ │ │ ├── ProcessHeartbeatResource.java │ │ │ │ │ ├── ProcessKind.java │ │ │ │ │ ├── ProcessKvResource.java │ │ │ │ │ ├── ProcessLogResourceV2.java │ │ │ │ │ ├── ProcessManager.java │ │ │ │ │ ├── ProcessModule.java │ │ │ │ │ ├── ProcessResource.java │ │ │ │ │ ├── ProcessResourceV2.java │ │ │ │ │ ├── ProcessSecurityContext.java │ │ │ │ │ ├── ResumeProcessResponse.java │ │ │ │ │ ├── SessionTokenCreator.java │ │ │ │ │ ├── StartProcessResponse.java │ │ │ │ │ ├── TotalRuntimeCalculator.java │ │ │ │ │ ├── TriggeredByEntry.java │ │ │ │ │ ├── checkpoint/ │ │ │ │ │ │ ├── GenericCheckpointResponse.java │ │ │ │ │ │ ├── ProcessCheckpointResource.java │ │ │ │ │ │ ├── ProcessCheckpointV2Resource.java │ │ │ │ │ │ └── RestoreCheckpointRequest.java │ │ │ │ │ ├── event/ │ │ │ │ │ │ ├── EventPhase.java │ │ │ │ │ │ ├── NewProcessEvent.java │ │ │ │ │ │ ├── ProcessEventDao.java │ │ │ │ │ │ ├── ProcessEventEntry.java │ │ │ │ │ │ ├── ProcessEventFilter.java │ │ │ │ │ │ ├── ProcessEventManager.java │ │ │ │ │ │ ├── ProcessEventRequest.java │ │ │ │ │ │ └── ProcessEventResource.java │ │ │ │ │ ├── form/ │ │ │ │ │ │ ├── ExternalFileFormValidatorLocale.java │ │ │ │ │ │ ├── ExternalFileFormValidatorLocaleV2.java │ │ │ │ │ │ ├── FormAccessManager.java │ │ │ │ │ │ ├── FormInstanceEntry.java │ │ │ │ │ │ ├── FormListEntry.java │ │ │ │ │ │ ├── FormManager.java │ │ │ │ │ │ ├── FormModule.java │ │ │ │ │ │ ├── FormResource.java │ │ │ │ │ │ ├── FormResourceV1.java │ │ │ │ │ │ ├── FormResourceV2.java │ │ │ │ │ │ ├── FormServiceV1.java │ │ │ │ │ │ ├── FormServiceV2.java │ │ │ │ │ │ ├── FormSubmitResponse.java │ │ │ │ │ │ ├── FormSubmitResult.java │ │ │ │ │ │ └── FormUtils.java │ │ │ │ │ ├── keys/ │ │ │ │ │ │ ├── AttachmentKey.java │ │ │ │ │ │ ├── HeaderKey.java │ │ │ │ │ │ ├── Key.java │ │ │ │ │ │ └── KeyIndex.java │ │ │ │ │ ├── locks/ │ │ │ │ │ │ ├── LockEntry.java │ │ │ │ │ │ ├── LockResult.java │ │ │ │ │ │ ├── ProcessLocksDao.java │ │ │ │ │ │ ├── ProcessLocksResource.java │ │ │ │ │ │ └── ProcessLocksWatchdog.java │ │ │ │ │ ├── logs/ │ │ │ │ │ │ ├── ProcessLogAccessManager.java │ │ │ │ │ │ ├── ProcessLogManager.java │ │ │ │ │ │ └── ProcessLogsDao.java │ │ │ │ │ ├── pipelines/ │ │ │ │ │ │ ├── EnqueueProcessPipeline.java │ │ │ │ │ │ ├── ForkPipeline.java │ │ │ │ │ │ ├── NewProcessPipeline.java │ │ │ │ │ │ ├── ResumePipeline.java │ │ │ │ │ │ └── processors/ │ │ │ │ │ │ ├── AssertOutVariablesProcessor.java │ │ │ │ │ │ ├── AssertWorkspaceArchiveProcessor.java │ │ │ │ │ │ ├── AttachmentStoringProcessor.java │ │ │ │ │ │ ├── AuthorizationProcessor.java │ │ │ │ │ │ ├── Chain.java │ │ │ │ │ │ ├── ChangeUserProcessor.java │ │ │ │ │ │ ├── CleanupProcessor.java │ │ │ │ │ │ ├── ClearStartAtProcessor.java │ │ │ │ │ │ ├── ConfigurationProcessor.java │ │ │ │ │ │ ├── ConfigurationStoringProcessor.java │ │ │ │ │ │ ├── CurrentUserInfoProcessor.java │ │ │ │ │ │ ├── CustomEnqueueProcessors.java │ │ │ │ │ │ ├── DependenciesProcessor.java │ │ │ │ │ │ ├── EffectiveProcessDefinitionProcessor.java │ │ │ │ │ │ ├── EnqueueingProcessor.java │ │ │ │ │ │ ├── EntryPointProcessor.java │ │ │ │ │ │ ├── ExceptionProcessor.java │ │ │ │ │ │ ├── ExclusiveGroupProcessor.java │ │ │ │ │ │ ├── FailProcessor.java │ │ │ │ │ │ ├── FinalizerProcessor.java │ │ │ │ │ │ ├── ForkCleanupProcessor.java │ │ │ │ │ │ ├── ForkHandlersProcessor.java │ │ │ │ │ │ ├── ForkPolicyProcessor.java │ │ │ │ │ │ ├── ForkRepositoryInfoProcessor.java │ │ │ │ │ │ ├── ForkRuntimeProcessor.java │ │ │ │ │ │ ├── FormFilesStoringProcessor.java │ │ │ │ │ │ ├── InitialQueueEntryProcessor.java │ │ │ │ │ │ ├── InitiatorUserInfoProcessor.java │ │ │ │ │ │ ├── InvalidProcessStateException.java │ │ │ │ │ │ ├── InvalidProcessStateExceptionMapper.java │ │ │ │ │ │ ├── LoggingMDCProcessor.java │ │ │ │ │ │ ├── NewQueueEntryProcessor.java │ │ │ │ │ │ ├── OutVariablesSettingProcessor.java │ │ │ │ │ │ ├── PayloadProcessor.java │ │ │ │ │ │ ├── PayloadRestoreProcessor.java │ │ │ │ │ │ ├── PayloadStoreProcessor.java │ │ │ │ │ │ ├── Pipeline.java │ │ │ │ │ │ ├── PolicyExportProcessor.java │ │ │ │ │ │ ├── PolicyProcessor.java │ │ │ │ │ │ ├── ProcessDefinitionProcessor.java │ │ │ │ │ │ ├── ProcessHandlersProcessor.java │ │ │ │ │ │ ├── RawPayloadPolicyProcessor.java │ │ │ │ │ │ ├── RepositoryInfoUpdateProcessor.java │ │ │ │ │ │ ├── RepositoryProcessor.java │ │ │ │ │ │ ├── RequestParametersProcessor.java │ │ │ │ │ │ ├── RestoredPayloadValidationProcessor.java │ │ │ │ │ │ ├── ResumeConfigurationProcessor.java │ │ │ │ │ │ ├── ResumeEventsProcessor.java │ │ │ │ │ │ ├── ResumeMarkerStoringProcessor.java │ │ │ │ │ │ ├── ResumeProcessor.java │ │ │ │ │ │ ├── ResumingHooksProcessor.java │ │ │ │ │ │ ├── ResumingProcessor.java │ │ │ │ │ │ ├── RunAsCurrentProcessUserProcessor.java │ │ │ │ │ │ ├── SecuritySubjectProcessor.java │ │ │ │ │ │ ├── SessionTokenProcessor.java │ │ │ │ │ │ ├── StateImportingProcessor.java │ │ │ │ │ │ ├── TagsExtractingProcessor.java │ │ │ │ │ │ ├── TemplateFilesProcessor.java │ │ │ │ │ │ ├── TemplateScriptProcessor.java │ │ │ │ │ │ ├── UserInfoProcessor.java │ │ │ │ │ │ ├── WorkspaceArchiveProcessor.java │ │ │ │ │ │ ├── cfg/ │ │ │ │ │ │ │ └── ProcessConfigurationUtils.java │ │ │ │ │ │ ├── policy/ │ │ │ │ │ │ │ ├── ContainerPolicyApplier.java │ │ │ │ │ │ │ ├── FilePolicyApplier.java │ │ │ │ │ │ │ ├── PolicyApplier.java │ │ │ │ │ │ │ ├── ProcessRuntimePolicyApplier.java │ │ │ │ │ │ │ ├── ProcessTimeoutPolicyApplier.java │ │ │ │ │ │ │ └── WorkspacePolicyApplier.java │ │ │ │ │ │ └── signing/ │ │ │ │ │ │ └── Signing.java │ │ │ │ │ ├── queue/ │ │ │ │ │ │ ├── EnqueuedBatchTask.java │ │ │ │ │ │ ├── EnqueuedTask.java │ │ │ │ │ │ ├── EnqueuedTaskProvider.java │ │ │ │ │ │ ├── ExternalProcessListenerHandler.java │ │ │ │ │ │ ├── FilterUtils.java │ │ │ │ │ │ ├── MetadataUtils.java │ │ │ │ │ │ ├── ProcessFilter.java │ │ │ │ │ │ ├── ProcessInitiatorEntry.java │ │ │ │ │ │ ├── ProcessKeyCache.java │ │ │ │ │ │ ├── ProcessKeyCacheGaugeModule.java │ │ │ │ │ │ ├── ProcessQueueDao.java │ │ │ │ │ │ ├── ProcessQueueEntry.java │ │ │ │ │ │ ├── ProcessQueueGaugeModule.java │ │ │ │ │ │ ├── ProcessQueueManager.java │ │ │ │ │ │ ├── ProcessQueueWatchdog.java │ │ │ │ │ │ ├── ProcessRequirementsEntry.java │ │ │ │ │ │ ├── ProcessStatusListener.java │ │ │ │ │ │ └── dispatcher/ │ │ │ │ │ │ ├── ConcurrentProcessFilter.java │ │ │ │ │ │ ├── ConcurrentProcessFilterDao.java │ │ │ │ │ │ ├── Dispatcher.java │ │ │ │ │ │ ├── ExclusiveProcessFilter.java │ │ │ │ │ │ ├── ExclusiveProcessFilterDao.java │ │ │ │ │ │ ├── Filter.java │ │ │ │ │ │ └── WaitProcessFinishFilter.java │ │ │ │ │ ├── state/ │ │ │ │ │ │ ├── ProcessCheckpointDao.java │ │ │ │ │ │ ├── ProcessCheckpointManager.java │ │ │ │ │ │ └── ProcessStateManager.java │ │ │ │ │ └── waits/ │ │ │ │ │ ├── AbstractWaitCondition.java │ │ │ │ │ ├── ProcessCompletionCondition.java │ │ │ │ │ ├── ProcessLockCondition.java │ │ │ │ │ ├── ProcessSleepCondition.java │ │ │ │ │ ├── ProcessWaitDao.java │ │ │ │ │ ├── ProcessWaitHandler.java │ │ │ │ │ ├── ProcessWaitManager.java │ │ │ │ │ ├── ProcessWaitWatchdog.java │ │ │ │ │ ├── WaitConditionUpdater.java │ │ │ │ │ ├── WaitProcessFinishHandler.java │ │ │ │ │ ├── WaitProcessLockHandler.java │ │ │ │ │ ├── WaitProcessSleepHandler.java │ │ │ │ │ ├── WaitProcessStatusListener.java │ │ │ │ │ └── WaitType.java │ │ │ │ ├── repository/ │ │ │ │ │ ├── ClasspathRepositoryProvider.java │ │ │ │ │ ├── InvalidRepositoryPathException.java │ │ │ │ │ ├── RepositoryCacheCleanupTask.java │ │ │ │ │ ├── RepositoryManager.java │ │ │ │ │ ├── RepositoryModule.java │ │ │ │ │ ├── RepositoryRefresher.java │ │ │ │ │ ├── RepositoryValidationResponse.java │ │ │ │ │ ├── ServerAuthTokenProvider.java │ │ │ │ │ └── listeners/ │ │ │ │ │ ├── ProcessDefinitionRefreshListener.java │ │ │ │ │ ├── RepositoryRefreshListener.java │ │ │ │ │ └── TriggerRefreshListener.java │ │ │ │ ├── role/ │ │ │ │ │ ├── RoleDao.java │ │ │ │ │ ├── RoleModule.java │ │ │ │ │ ├── RoleOperationResponse.java │ │ │ │ │ └── RoleResource.java │ │ │ │ ├── sdk/ │ │ │ │ │ └── process/ │ │ │ │ │ └── CustomEnqueueProcessor.java │ │ │ │ ├── security/ │ │ │ │ │ ├── BasicAuthenticationHandler.java │ │ │ │ │ ├── GithubAuthenticatingFilter.java │ │ │ │ │ ├── LocalRequestFilter.java │ │ │ │ │ ├── Permission.java │ │ │ │ │ ├── Roles.java │ │ │ │ │ ├── SecurityModule.java │ │ │ │ │ ├── SecurityUtils.java │ │ │ │ │ ├── SessionTokenAuthenticationHandler.java │ │ │ │ │ ├── UnauthenticatedExceptionMapper.java │ │ │ │ │ ├── UnauthorizedException.java │ │ │ │ │ ├── UnauthorizedExceptionMapper.java │ │ │ │ │ ├── UserPrincipal.java │ │ │ │ │ ├── UserSecurityContext.java │ │ │ │ │ ├── apikey/ │ │ │ │ │ │ ├── ApiKey.java │ │ │ │ │ │ ├── ApiKeyAuthenticationHandler.java │ │ │ │ │ │ ├── ApiKeyCleaner.java │ │ │ │ │ │ ├── ApiKeyDao.java │ │ │ │ │ │ ├── ApiKeyEntry.java │ │ │ │ │ │ ├── ApiKeyExpirationNotifier.java │ │ │ │ │ │ ├── ApiKeyManager.java │ │ │ │ │ │ ├── ApiKeyModule.java │ │ │ │ │ │ ├── ApiKeyRealm.java │ │ │ │ │ │ ├── ApiKeyResource.java │ │ │ │ │ │ ├── ApiKeyResourceV2.java │ │ │ │ │ │ ├── ApiKeyUtils.java │ │ │ │ │ │ ├── CreateApiKeyRequest.java │ │ │ │ │ │ ├── CreateApiKeyResponse.java │ │ │ │ │ │ ├── EmailNotifier.java │ │ │ │ │ │ └── loader/ │ │ │ │ │ │ ├── ApiKeyEntry.java │ │ │ │ │ │ ├── ApiKeyLoader.java │ │ │ │ │ │ └── ApiKeyLoaderDao.java │ │ │ │ │ ├── github/ │ │ │ │ │ │ ├── GithubKey.java │ │ │ │ │ │ └── GithubRealm.java │ │ │ │ │ ├── internal/ │ │ │ │ │ │ ├── InternalRealm.java │ │ │ │ │ │ └── LocalUserInfoProvider.java │ │ │ │ │ ├── ldap/ │ │ │ │ │ │ ├── CachingLdapManager.java │ │ │ │ │ │ ├── ConcordDnsSrvLdapContextFactory.java │ │ │ │ │ │ ├── ConcordLdapContextFactory.java │ │ │ │ │ │ ├── LdapContextFactoryProvider.java │ │ │ │ │ │ ├── LdapGroupDao.java │ │ │ │ │ │ ├── LdapGroupManager.java │ │ │ │ │ │ ├── LdapGroupSearchResult.java │ │ │ │ │ │ ├── LdapManager.java │ │ │ │ │ │ ├── LdapManagerImpl.java │ │ │ │ │ │ ├── LdapManagerProvider.java │ │ │ │ │ │ ├── LdapPrincipal.java │ │ │ │ │ │ ├── LdapRealm.java │ │ │ │ │ │ ├── LdapUserInfoProvider.java │ │ │ │ │ │ ├── LdapUtils.java │ │ │ │ │ │ ├── SyncUserLdapGroupRequest.java │ │ │ │ │ │ ├── TrustingSslSocketFactory.java │ │ │ │ │ │ ├── UserLdapGroupResource.java │ │ │ │ │ │ └── UserLdapGroupSynchronizer.java │ │ │ │ │ ├── rememberme/ │ │ │ │ │ │ └── ConcordRememberMeManager.java │ │ │ │ │ └── sessionkey/ │ │ │ │ │ ├── SessionKey.java │ │ │ │ │ ├── SessionKeyPrincipal.java │ │ │ │ │ └── SessionKeyRealm.java │ │ │ │ ├── task/ │ │ │ │ │ ├── SchedulerDao.java │ │ │ │ │ ├── TaskScheduler.java │ │ │ │ │ └── TaskSchedulerModule.java │ │ │ │ ├── template/ │ │ │ │ │ ├── TemplateAliasDao.java │ │ │ │ │ ├── TemplateAliasEntry.java │ │ │ │ │ ├── TemplateAliasResource.java │ │ │ │ │ ├── TemplateAliasResponse.java │ │ │ │ │ └── TemplateModule.java │ │ │ │ └── user/ │ │ │ │ ├── AbstractUserInfoProvider.java │ │ │ │ ├── CreateUserRequest.java │ │ │ │ ├── CreateUserResponse.java │ │ │ │ ├── DeleteUserResponse.java │ │ │ │ ├── RoleEntry.java │ │ │ │ ├── UpdateUserRolesRequest.java │ │ │ │ ├── User.java │ │ │ │ ├── UserDao.java │ │ │ │ ├── UserEntry.java │ │ │ │ ├── UserInfoProvider.java │ │ │ │ ├── UserManager.java │ │ │ │ ├── UserModule.java │ │ │ │ ├── UserResource.java │ │ │ │ ├── UserResourceV2.java │ │ │ │ ├── UserTeam.java │ │ │ │ └── UserType.java │ │ │ └── resources/ │ │ │ ├── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── server/ │ │ │ │ ├── console/ │ │ │ │ │ ├── badRequest.html │ │ │ │ │ ├── footer.html │ │ │ │ │ ├── formNotFound.html │ │ │ │ │ ├── genericError.html │ │ │ │ │ ├── header.html │ │ │ │ │ ├── inProgress.html │ │ │ │ │ ├── processError.html │ │ │ │ │ └── processFinished.html │ │ │ │ ├── email/ │ │ │ │ │ └── api-key-expiration.mustache │ │ │ │ ├── org/ │ │ │ │ │ └── triggers/ │ │ │ │ │ └── concord.yml │ │ │ │ └── selfcheck/ │ │ │ │ └── concord.yml │ │ │ ├── logback.xml │ │ │ ├── openapi-server-config.yaml │ │ │ └── security.json │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── server/ │ │ │ ├── AbstractDaoTest.java │ │ │ ├── ConcordObjectMapperTest.java │ │ │ ├── ListenersTest.java │ │ │ ├── OffsetDateTimeTest.java │ │ │ ├── TestObjectMapper.java │ │ │ ├── events/ │ │ │ │ ├── ExpressionUtilsTest.java │ │ │ │ ├── GithubEventInitiatorSupplierTest.java │ │ │ │ ├── GithubUtilsTest.java │ │ │ │ └── github/ │ │ │ │ └── PayloadTest.java │ │ │ ├── org/ │ │ │ │ ├── jsonstore/ │ │ │ │ │ └── JsonStorageQueryExecDaoTest.java │ │ │ │ ├── project/ │ │ │ │ │ └── DiffUtilsTest.java │ │ │ │ ├── secret/ │ │ │ │ │ └── PasswordCheckerTest.java │ │ │ │ └── triggers/ │ │ │ │ ├── CronUtilsTest.java │ │ │ │ └── TriggerInternalIdCalculatorTest.java │ │ │ ├── policy/ │ │ │ │ └── PolicyCacheTest.java │ │ │ ├── process/ │ │ │ │ ├── ImmutablesTest.java │ │ │ │ ├── pipelines/ │ │ │ │ │ └── processors/ │ │ │ │ │ ├── ConfigurationProcessorTest.java │ │ │ │ │ └── TemplateScriptProcessTest.java │ │ │ │ ├── queue/ │ │ │ │ │ ├── FilterUtilsTest.java │ │ │ │ │ ├── ProcessKeyCacheTest.java │ │ │ │ │ └── dispatcher/ │ │ │ │ │ └── DispatcherTest.java │ │ │ │ ├── state/ │ │ │ │ │ └── ProcessStateManagerTest.java │ │ │ │ └── waits/ │ │ │ │ ├── WaitConditionSerializeTest.java │ │ │ │ └── WaitProcessFinishHandlerTest.java │ │ │ ├── repository/ │ │ │ │ └── ServerAuthTokenProviderTest.java │ │ │ ├── security/ │ │ │ │ └── secret/ │ │ │ │ └── SecretDaoTest.java │ │ │ ├── tasks/ │ │ │ │ └── SchedulerDaoTest.java │ │ │ ├── template/ │ │ │ │ ├── ConfigurationUtilsTest.java │ │ │ │ ├── ProjectDaoTest.java │ │ │ │ └── kv/ │ │ │ │ └── KvDaoTest.java │ │ │ └── user/ │ │ │ └── UserDaoTest.java │ │ └── resources/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── server/ │ │ ├── instance/ │ │ │ └── example.yml │ │ ├── org/ │ │ │ └── jsonstore/ │ │ │ └── queries.txt │ │ └── process/ │ │ └── attachmentTest/ │ │ ├── dir1/ │ │ │ └── test1.txt │ │ ├── dir2/ │ │ │ └── test2.txt │ │ └── test0.txt │ ├── liquibase-ext/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── server/ │ │ └── liquibase/ │ │ └── ext/ │ │ ├── ApiTokenCreator.java │ │ ├── NoopLockService.java │ │ └── migration/ │ │ └── SecretsHashMigrationTask.java │ ├── plugins/ │ │ ├── README.md │ │ ├── ansible/ │ │ │ ├── client2/ │ │ │ │ └── pom.xml │ │ │ ├── db/ │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── walmartlabs/ │ │ │ │ │ └── concord/ │ │ │ │ │ └── server/ │ │ │ │ │ └── plugins/ │ │ │ │ │ └── ansible/ │ │ │ │ │ └── db/ │ │ │ │ │ ├── AnsibleDBChangeLogProvider.java │ │ │ │ │ └── DatabaseModule.java │ │ │ │ └── resources/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── server/ │ │ │ │ └── plugins/ │ │ │ │ └── ansible/ │ │ │ │ └── db/ │ │ │ │ └── liquibase.xml │ │ │ ├── impl/ │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── walmartlabs/ │ │ │ │ │ └── concord/ │ │ │ │ │ └── server/ │ │ │ │ │ └── plugins/ │ │ │ │ │ └── ansible/ │ │ │ │ │ ├── AbstractAnsibleEvent.java │ │ │ │ │ ├── AbstractEventProcessor.java │ │ │ │ │ ├── AnsibleEvent.java │ │ │ │ │ ├── AnsibleEventsConfiguration.java │ │ │ │ │ ├── AnsibleHostProcessor.java │ │ │ │ │ ├── AnsibleModule.java │ │ │ │ │ ├── Constants.java │ │ │ │ │ ├── DbUtils.java │ │ │ │ │ ├── EventDao.java │ │ │ │ │ ├── EventFetcher.java │ │ │ │ │ ├── EventMarkerDao.java │ │ │ │ │ ├── EventProcessor.java │ │ │ │ │ ├── PlayInfoProcessor.java │ │ │ │ │ ├── PlaybookInfoEvent.java │ │ │ │ │ ├── PlaybookInfoProcessor.java │ │ │ │ │ ├── PlaybookResultEvent.java │ │ │ │ │ ├── PlaybookResultProcessor.java │ │ │ │ │ ├── ProcessAnsibleResource.java │ │ │ │ │ ├── ProcessEventEntry.java │ │ │ │ │ ├── TaskInfoProcessor.java │ │ │ │ │ └── queue/ │ │ │ │ │ ├── AnsibleConfigurationConstants.java │ │ │ │ │ ├── InventoryProcessor.java │ │ │ │ │ └── PrivateKeyProcessor.java │ │ │ │ └── resources/ │ │ │ │ └── openapi-ansible-config.yaml │ │ │ └── pom.xml │ │ ├── kafka-event-sink/ │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── server/ │ │ │ └── plugins/ │ │ │ └── eventsink/ │ │ │ └── kafka/ │ │ │ ├── KafkaConnector.java │ │ │ ├── KafkaEventSink.java │ │ │ ├── KafkaEventSinkConfiguration.java │ │ │ └── KafkaModule.java │ │ ├── noderoster/ │ │ │ ├── client2/ │ │ │ │ └── pom.xml │ │ │ ├── db/ │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── walmartlabs/ │ │ │ │ │ └── concord/ │ │ │ │ │ └── server/ │ │ │ │ │ └── plugins/ │ │ │ │ │ └── noderoster/ │ │ │ │ │ └── db/ │ │ │ │ │ ├── DatabaseModule.java │ │ │ │ │ ├── NodeRosterDB.java │ │ │ │ │ └── NodeRosterDBChangeLogProvider.java │ │ │ │ └── resources/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── server/ │ │ │ │ └── plugins/ │ │ │ │ └── noderoster/ │ │ │ │ └── db/ │ │ │ │ └── liquibase.xml │ │ │ ├── impl/ │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── walmartlabs/ │ │ │ │ │ └── concord/ │ │ │ │ │ └── server/ │ │ │ │ │ └── plugins/ │ │ │ │ │ └── noderoster/ │ │ │ │ │ ├── ArtifactEntry.java │ │ │ │ │ ├── ArtifactsResource.java │ │ │ │ │ ├── FactsResource.java │ │ │ │ │ ├── HostEntry.java │ │ │ │ │ ├── HostFilter.java │ │ │ │ │ ├── HostManager.java │ │ │ │ │ ├── HostNormalizer.java │ │ │ │ │ ├── HostsDataInclude.java │ │ │ │ │ ├── HostsResource.java │ │ │ │ │ ├── NodeRosterModule.java │ │ │ │ │ ├── ProcessEntry.java │ │ │ │ │ ├── ProcessesResource.java │ │ │ │ │ ├── cfg/ │ │ │ │ │ │ ├── NodeRosterDBConfiguration.java │ │ │ │ │ │ └── NodeRosterEventsConfiguration.java │ │ │ │ │ ├── dao/ │ │ │ │ │ │ ├── ArtifactsDao.java │ │ │ │ │ │ └── HostsDao.java │ │ │ │ │ └── processor/ │ │ │ │ │ ├── AbstractEventProcessor.java │ │ │ │ │ ├── AnsibleEvent.java │ │ │ │ │ ├── AnsibleEventsProcessor.java │ │ │ │ │ ├── EventData.java │ │ │ │ │ ├── EventMarkerDao.java │ │ │ │ │ ├── HostArtifactsProcessor.java │ │ │ │ │ ├── HostFactsProcessor.java │ │ │ │ │ ├── Partitioner.java │ │ │ │ │ ├── ProcessHostsProcessor.java │ │ │ │ │ └── Processor.java │ │ │ │ └── resources/ │ │ │ │ └── openapi-noderoster-config.yaml │ │ │ └── pom.xml │ │ ├── oidc/ │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── walmartlabs/ │ │ │ │ └── concord/ │ │ │ │ └── server/ │ │ │ │ └── plugins/ │ │ │ │ └── oidc/ │ │ │ │ ├── OidcAuthFilter.java │ │ │ │ ├── OidcAuthenticationHandler.java │ │ │ │ ├── OidcCallbackFilter.java │ │ │ │ ├── OidcFilterChainConfigurator.java │ │ │ │ ├── OidcLogoutFilter.java │ │ │ │ ├── OidcPluginModule.java │ │ │ │ ├── OidcRealm.java │ │ │ │ ├── OidcService.java │ │ │ │ ├── OidcToken.java │ │ │ │ ├── PluginConfiguration.java │ │ │ │ ├── UserProfile.java │ │ │ │ └── UserProfileConverter.java │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── server/ │ │ │ └── plugins/ │ │ │ └── oidc/ │ │ │ └── OidcRealmTest.java │ │ ├── oneops/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── server/ │ │ │ └── plugins/ │ │ │ └── oneops/ │ │ │ ├── Constants.java │ │ │ ├── OneOpsConfiguration.java │ │ │ ├── OneOpsEventResource.java │ │ │ ├── OneOpsTriggerProcessor.java │ │ │ ├── OneOpsTriggerV1Processor.java │ │ │ └── OneOpsTriggerV2Processor.java │ │ ├── pfed-sso/ │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── server/ │ │ │ └── plugins/ │ │ │ └── pfedsso/ │ │ │ ├── AbstractHttpFilter.java │ │ │ ├── JwkHelper.java │ │ │ ├── JwtAuthenticator.java │ │ │ ├── PluginModule.java │ │ │ ├── RedirectHelper.java │ │ │ ├── SsoAuthFilter.java │ │ │ ├── SsoCallbackFilter.java │ │ │ ├── SsoClient.java │ │ │ ├── SsoConfiguration.java │ │ │ ├── SsoCookies.java │ │ │ ├── SsoFilterChainConfigurator.java │ │ │ ├── SsoHandler.java │ │ │ ├── SsoLogoutFilter.java │ │ │ ├── SsoRealm.java │ │ │ ├── SsoToken.java │ │ │ ├── SsoUserInfoProvider.java │ │ │ ├── encryption/ │ │ │ │ ├── AbstractEncryptionConfiguration.java │ │ │ │ ├── ECEncryptionConfiguration.java │ │ │ │ ├── EncryptionConfiguration.java │ │ │ │ ├── EncryptionConfigurationFactory.java │ │ │ │ ├── RSAEncryptionConfiguration.java │ │ │ │ └── SecretEncryptionConfiguration.java │ │ │ └── signature/ │ │ │ ├── ECSignatureConfiguration.java │ │ │ ├── RSASignatureConfiguration.java │ │ │ ├── SecretSignatureConfiguration.java │ │ │ ├── SignatureConfiguration.java │ │ │ └── SignatureConfigurationFactory.java │ │ ├── pom.xml │ │ └── webapp/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── ca/ │ │ │ └── ibodrov/ │ │ │ └── concord/ │ │ │ └── webapp/ │ │ │ ├── ExcludedPrefixes.java │ │ │ ├── StaticResource.java │ │ │ ├── WebappFilter.java │ │ │ └── WebappPluginModule.java │ │ └── test/ │ │ └── java/ │ │ └── ca/ │ │ └── ibodrov/ │ │ └── concord/ │ │ └── webapp/ │ │ └── ExcludedPrefixesTest.java │ ├── pom.xml │ ├── queue-client/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── walmartlabs/ │ │ │ └── concord/ │ │ │ └── server/ │ │ │ └── queueclient/ │ │ │ ├── MessageSerializer.java │ │ │ ├── QueueClient.java │ │ │ ├── QueueClientConfiguration.java │ │ │ └── message/ │ │ │ ├── CommandRequest.java │ │ │ ├── CommandResponse.java │ │ │ ├── Message.java │ │ │ ├── MessageType.java │ │ │ ├── ProcessRequest.java │ │ │ └── ProcessResponse.java │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── server/ │ │ └── queueclient/ │ │ └── MessageSerializerTest.java │ └── sdk/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── walmartlabs/ │ │ └── concord/ │ │ └── server/ │ │ └── sdk/ │ │ ├── AllowNulls.java │ │ ├── BackgroundTask.java │ │ ├── ConcordApplicationException.java │ │ ├── PartialProcessKey.java │ │ ├── ProcessKey.java │ │ ├── ProcessKeyCache.java │ │ ├── ProcessStatus.java │ │ ├── Range.java │ │ ├── ScheduledTask.java │ │ ├── audit/ │ │ │ ├── AuditEvent.java │ │ │ └── AuditLogListener.java │ │ ├── events/ │ │ │ ├── ProcessEvent.java │ │ │ └── ProcessEventListener.java │ │ ├── log/ │ │ │ ├── ProcessLogEntry.java │ │ │ └── ProcessLogListener.java │ │ ├── metrics/ │ │ │ ├── GaugeProvider.java │ │ │ ├── InjectCounter.java │ │ │ ├── InjectMeter.java │ │ │ └── WithTimer.java │ │ ├── rest/ │ │ │ ├── ApiDescriptor.java │ │ │ ├── Component.java │ │ │ └── Resource.java │ │ ├── security/ │ │ │ └── AuthenticationException.java │ │ └── validation/ │ │ ├── Validate.java │ │ ├── ValidationErrorXO.java │ │ └── ValidationErrorsException.java │ └── test/ │ └── java/ │ └── com/ │ └── walmartlabs/ │ └── concord/ │ └── server/ │ └── sdk/ │ ├── ProcessKeyTest.java │ └── SerializationTest.java ├── targetplatform/ │ └── pom.xml └── vagrant/ ├── README.md ├── Vagrantfile ├── agent.conf ├── playbook.yml ├── server.conf └── user.ldif ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codex ================================================ ================================================ FILE: .gitattributes ================================================ *.min.js binary *.min.css binary ================================================ FILE: .github/settings.xml ================================================ gha ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: push: branches: [ 'master' ] pull_request: branches: [ 'master' ] concurrency: group: ${{ github.ref }}-build cancel-in-progress: true jobs: check-labels: runs-on: ubuntu-latest outputs: skip: ${{ steps.check.outputs.skip }} steps: - name: Check PR labels id: check run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then labels=$(jq -r '.pull_request.labels | map(.name) | join(",")' "$GITHUB_EVENT_PATH") if [[ "$labels" == *"[no ci]"* ]]; then echo "skip=true" >> $GITHUB_OUTPUT else echo "skip=false" >> $GITHUB_OUTPUT fi else echo "skip=false" >> $GITHUB_OUTPUT echo "skip_tests=false" >> $GITHUB_OUTPUT fi build: needs: check-labels if: needs.check-labels.outputs.skip == 'false' env: WORK: ${{ github.workspace }}/tmp MAVEN_REPO_LOCAL: ${{ github.workspace }}/tmp/.m2/repository strategy: matrix: profile: [ 'jdk17', 'jdk17-aarch64' ] include: - jdk_version: '17' - profile: 'jdk17' runs_on: ubuntu-24.04 - profile: 'jdk17-aarch64' runs_on: ubuntu-24.04-arm fail-fast: false runs-on: ${{ matrix.runs_on }} steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 with: driver: docker - name: Login to DockerHub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 if: github.event.pull_request.head.repo.full_name == 'walmartlabs/concord' with: username: ${{ secrets.OSS_DOCKERHUB_USERNAME }} password: ${{ secrets.OSS_DOCKERHUB_PASSWORD }} - name: Set up JDK uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: '${{ matrix.jdk_version }}' distribution: 'temurin' - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Restore Maven cache id: restore-maven-cache uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ${{ env.MAVEN_REPO_LOCAL }} key: ${{ runner.os }}-maven-${{ matrix.profile }}-${{ hashFiles('**/pom.xml', '.github/settings.xml') }} restore-keys: | ${{ runner.os }}-maven-${{ matrix.profile }}- ${{ runner.os }}-maven- - name: Build and test with Maven env: SKIP_DOCKER_TESTS: "true" run: | mkdir -p "${WORK}" "${MAVEN_REPO_LOCAL}" chmod 1777 "${WORK}" ./mvnw -s .github/settings.xml -B clean install \ -Dmaven.repo.local="${MAVEN_REPO_LOCAL}" \ -Djava.io.tmpdir="${WORK}" \ -Pgha -Pdocker -Pit -P${{ matrix.profile }} - name: Remove local Concord artifacts from Maven cache if: success() run: rm -rf "${MAVEN_REPO_LOCAL}/com/walmartlabs/concord" - name: Save Maven cache if: success() && steps.restore-maven-cache.outputs.cache-hit != 'true' uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ${{ env.MAVEN_REPO_LOCAL }} key: ${{ steps.restore-maven-cache.outputs.cache-primary-key }} ================================================ FILE: .github/workflows/docker-multiarch.yml ================================================ name: docker-multiarch on: workflow_dispatch: inputs: ref: description: Release tag to build, e.g. 2.39.0 required: true type: string docker_tag: description: Docker tag to apply to the published images required: true type: string docker_namespace: description: Docker Hub namespace to publish into required: true default: walmartlabs type: string pull_request: branches: [ 'master' ] permissions: contents: read concurrency: group: docker-multiarch-${{ github.event.pull_request.number || github.event.inputs.docker_tag || github.event.inputs.ref || github.ref }} cancel-in-progress: true jobs: check-labels: runs-on: ubuntu-latest outputs: skip: ${{ steps.check.outputs.skip }} steps: - name: Check PR labels id: check run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then labels=$(jq -r '.pull_request.labels | map(.name) | join(",")' "$GITHUB_EVENT_PATH") if [[ "$labels" == *"[no ci]"* ]]; then echo "skip=true" >> $GITHUB_OUTPUT else echo "skip=false" >> $GITHUB_OUTPUT fi else echo "skip=false" >> $GITHUB_OUTPUT fi build: needs: check-labels if: needs.check-labels.outputs.skip == 'false' runs-on: ubuntu-24.04 timeout-minutes: 120 env: WORK: ${{ github.workspace }}/tmp MAVEN_REPO_LOCAL: ${{ github.workspace }}/tmp/.m2/repository IMAGE_PLATFORMS: linux/amd64,linux/arm64 LOCAL_REGISTRY: 127.0.0.1:5000 steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.ref || github.event.pull_request.head.sha }} - name: Resolve build target run: | if [[ "${{ github.event_name }}" == 'workflow_dispatch' ]]; then build_ref='${{ github.event.inputs.ref }}' build_tag='${{ github.event.inputs.docker_tag }}' docker_namespace='${{ github.event.inputs.docker_namespace }}' maven_also_make='' release_tag="${build_ref#refs/tags/}" if ! [[ "${release_tag}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "Workflow dispatch expects a release tag like '2.39.0', got '${build_ref}'." >&2 exit 1 fi git fetch --depth=1 origin "refs/tags/${release_tag}:refs/tags/${release_tag}" tag_commit="$(git rev-parse "refs/tags/${release_tag}^{commit}")" head_commit="$(git rev-parse HEAD)" if [[ "${tag_commit}" != "${head_commit}" ]]; then echo "Workflow dispatch ref '${build_ref}' resolved to ${head_commit}, but tag '${release_tag}' points to ${tag_commit}." >&2 exit 1 fi else build_ref='${{ github.event.pull_request.head.sha }}' build_tag="pr-${{ github.event.pull_request.number }}-$(git rev-parse --short=12 HEAD)" docker_namespace="${LOCAL_REGISTRY}/walmartlabs" maven_also_make='-am' fi if ! [[ "${build_tag}" =~ ^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$ ]]; then echo "Tag '${build_tag}' is not a valid Docker tag." >&2 exit 1 fi if [[ -z "${docker_namespace}" ]]; then echo "Docker namespace must not be empty." >&2 exit 1 fi if [[ "${{ github.event_name }}" == 'workflow_dispatch' ]] && [[ "${docker_namespace}" == */* ]]; then echo "Workflow dispatch expects a Docker Hub namespace only, e.g. 'walmartlabs'." >&2 exit 1 fi echo "BUILD_TAG=${build_tag}" >> "$GITHUB_ENV" echo "BUILD_REF=${build_ref}" >> "$GITHUB_ENV" echo "DOCKER_NAMESPACE=${docker_namespace}" >> "$GITHUB_ENV" echo "MAVEN_ALSO_MAKE=${maven_also_make}" >> "$GITHUB_ENV" echo "Using BUILD_REF=${build_ref}" echo "Using BUILD_TAG=${build_tag}" echo "Using DOCKER_NAMESPACE=${docker_namespace}" echo "Using MAVEN_ALSO_MAKE=${maven_also_make}" git rev-parse HEAD - name: Restore Maven cache id: restore-maven-cache uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ${{ env.MAVEN_REPO_LOCAL }} key: ${{ runner.os }}-maven-docker-multiarch-${{ hashFiles('**/pom.xml', '.github/settings.xml') }} restore-keys: | ${{ runner.os }}-maven-docker-multiarch- ${{ runner.os }}-maven- - name: Set up JDK uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: '17' distribution: 'temurin' - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm64 - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 with: driver: docker-container driver-opts: | network=host - name: Start local registry if: github.event_name != 'workflow_dispatch' run: | docker run -d -p 5000:5000 --name registry registry:2 for i in $(seq 1 30); do if curl -fsS "http://${LOCAL_REGISTRY}/v2/" >/dev/null; then exit 0 fi sleep 1 done docker logs registry exit 1 - name: Login to DockerHub if: github.event_name == 'workflow_dispatch' uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: username: ${{ secrets.OSS_DOCKERHUB_USERNAME }} password: ${{ secrets.OSS_DOCKERHUB_PASSWORD }} - name: Prepare Docker image contexts run: | mkdir -p "${WORK}" "${MAVEN_REPO_LOCAL}" chmod 1777 "${WORK}" maven_reactor_args=() if [[ -n "${MAVEN_ALSO_MAKE}" ]]; then maven_reactor_args+=("${MAVEN_ALSO_MAKE}") fi ./mvnw -s .github/settings.xml -B clean package \ -Dmaven.repo.local="${MAVEN_REPO_LOCAL}" \ -Djava.io.tmpdir="${WORK}" \ -DskipTests \ -Pgha \ -pl docker-images/base,docker-images/ansible,docker-images/agent,docker-images/server,docker-images/agent-operator \ "${maven_reactor_args[@]}" - name: Build Docker images working-directory: docker-images run: | platform_args=() IFS=',' read -ra platforms <<< "${IMAGE_PLATFORMS}" for platform in "${platforms[@]}"; do platform="${platform//[[:space:]]/}" if [[ -n "${platform}" ]]; then platform_args+=(--set "*.platform=${platform}") fi done docker buildx bake \ --push \ "${platform_args[@]}" \ --var "DOCKER_NAMESPACE=${DOCKER_NAMESPACE}" \ --var "DOCKER_TAG=${BUILD_TAG}" \ --var "JDK_VERSION=17" - name: Verify multi-arch manifests run: | images=( concord-base concord-ansible concord-agent concord-server concord-agent-operator ) for image in "${images[@]}"; do ref="${DOCKER_NAMESPACE}/${image}:${BUILD_TAG}" echo "Inspecting ${ref}" output=$(docker buildx imagetools inspect "${ref}") printf '%s\n' "${output}" printf '%s\n' "${output}" | grep -q 'linux/amd64' printf '%s\n' "${output}" | grep -q 'linux/arm64' done - name: Remove local Concord artifacts from Maven cache if: success() run: rm -rf "${MAVEN_REPO_LOCAL}/com/walmartlabs/concord" - name: Save Maven cache if: success() && steps.restore-maven-cache.outputs.cache-hit != 'true' uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ${{ env.MAVEN_REPO_LOCAL }} key: ${{ steps.restore-maven-cache.outputs.cache-primary-key }} ================================================ FILE: .gitignore ================================================ *.iml *.ipr *.iws *.retry .*.swo .*.swp .arcconfig .classpath .DS_Store .idea .pmd .pmdruleset.xml .project .settings .vagrant .vscode buildNumber.properties console2/build flow-typed/ jmeter.log nb-configuration.xml node_modules/ nohup.out npm-debug.log pom.xml.bak pom.xml.next pom.xml.versionsBackup release.properties target/ yarn.lock .mvn/wrapper/maven-wrapper.jar .java-version .attach_pid* dependency-reduced-pom.xml ================================================ FILE: .insights.yml ================================================ team: - id: 20906 product: - id: C0AF273F-7470-4A8B-B76E-190F00FFF920 languages: - JavaScript - JVM cloud: - autoRestart: false ================================================ FILE: .looper/render-settings.sh ================================================ #!/usr/bin/env bash set -euo pipefail out="${1:-/dev/stdout}" required_vars=( MAVEN_MIRROR NEXUS_URL MAVEN_SITE_URL NODE_DOWNLOAD_URL NPM_INSTALL_CMD AGENT_IMAGE ANSIBLE_IMAGE CONSOLE_IMAGE DB_IMAGE DIND_IMAGE OLDAP_IMAGE S3MOCK_IMAGE SELENIUM_IMAGE SERVER_IMAGE SOCAT_IMAGE ) for name in "${required_vars[@]}"; do if [ -z "${!name:-}" ]; then echo "Missing required environment variable: ${name}" >&2 exit 1 fi done xml_escape() { printf '%s' "$1" | sed \ -e 's/&/\&/g' \ -e 's/"/\"/g' \ -e "s/'/\'/g" \ -e 's//\>/g' } cat >"${out}" < walmart-gec external:* $(xml_escape "${MAVEN_MIRROR}") looper false \${env.SCM_CONNECTION} central http://central true true central http://central true true walmart false $(xml_escape "${NODE_DOWNLOAD_URL}") $(xml_escape "${NPM_INSTALL_CMD}") walmart-gec $(xml_escape "${NEXUS_URL}") \${public.serverId} \${public.nexusUrl}/content/repositories/devtools \${public.serverId} \${public.nexusUrl}/content/repositories/devtools-snapshots mvn-site $(xml_escape "${MAVEN_SITE_URL}") $(xml_escape "${AGENT_IMAGE}") $(xml_escape "${ANSIBLE_IMAGE}") $(xml_escape "${CONSOLE_IMAGE}") $(xml_escape "${DB_IMAGE}") $(xml_escape "${DIND_IMAGE}") $(xml_escape "${OLDAP_IMAGE}") $(xml_escape "${S3MOCK_IMAGE}") $(xml_escape "${SELENIUM_IMAGE}") $(xml_escape "${SERVER_IMAGE}") $(xml_escape "${SOCAT_IMAGE}") EOF ================================================ FILE: .looper/settings.xml ================================================ walmart-gec external:* ${env.MAVEN_MIRROR} looper false ${env.SCM_CONNECTION} central http://central true true central http://central true true walmart false ${env.NODE_DOWNLOAD_URL} ${env.NPM_INSTALL_CMD} walmart-gec ${env.NEXUS_URL} ${public.serverId} ${public.nexusUrl}/content/repositories/devtools ${public.serverId} ${public.nexusUrl}/content/repositories/devtools-snapshots mvn-site ${env.MAVEN_SITE_URL} ${env.AGENT_IMAGE} ${env.ANSIBLE_IMAGE} ${env.CONSOLE_IMAGE} ${env.DB_IMAGE} ${env.DIND_IMAGE} ${env.OLDAP_IMAGE} ${env.S3MOCK_IMAGE} ${env.SELENIUM_IMAGE} ${env.SERVER_IMAGE} ${env.SOCAT_IMAGE} ================================================ FILE: .looper.yml ================================================ inherit: job:///walmart-concord ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. wrapperVersion=3.3.2 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip ================================================ FILE: .sentinelpolicy ================================================ codescanaccess=j3clark,v0v001r,msprin1 maxmedium=1 ================================================ FILE: AGENTS.md ================================================ # AGENTS Start here: - [it/README.md](it/README.md) - [README.md](README.md) - [NOTES.md](NOTES.md) Working approach: - Read the nearest module `README.md` before editing code in that area. - Keep `console2/`, backend modules, and runtime/agent changes separated unless the task is explicitly cross-cutting. - When investigating `it/console` UI test failures, inspect screenshots from `it/console/target/screenshots/` and prefer native vision/image tools when available; use OCR or terminal renderers only as fallback. ================================================ FILE: CHANGELOG.md ================================================ # Change Log ## [2.40.0] - 2026-04-28 ### Added - runtime-v2: initial support for JSON schema validation for task in/out parameters ([#1263](https://github.com/walmartlabs/concord/pull/1263)); - concord-cli: basic support for suspend/resume and forms ([#1295](https://github.com/walmartlabs/concord/pull/1295)); - project: build multi-arch Docker images ([#1299](https://github.com/walmartlabs/concord/pull/1299)). ### Changed - concord-console2: remove redux-saga dependency ([#1290](https://github.com/walmartlabs/concord/pull/1290)); - project: respect PR labels in docker-multiarch flow ([#1304](https://github.com/walmartlabs/concord/pull/1304)); - concord-console2: remove redux dependency and dead code ([#1306](https://github.com/walmartlabs/concord/pull/1306)); - project: update dependencies ([#1310](https://github.com/walmartlabs/concord/pull/1310)); - concord-console2: migrate to react-router v7 ([#1307](https://github.com/walmartlabs/concord/pull/1307)); - concord-server: assert permissions for process details ([#1309](https://github.com/walmartlabs/concord/pull/1309)). ## [2.39.0] - 2026-04-09 ### Added - concord-server, queue-client: include requirements in ProcessResponse ([#1287](https://github.com/walmartlabs/concord/pull/1287)); - concord-server: add EffectiveYamlPolicy to control rendering and persisting of effective.concord.yml ([#1301](https://github.com/walmartlabs/concord/pull/1301)). ### Changed - project: change label for arm64 GHA runners, fix more flaky tests ([#1268](https://github.com/walmartlabs/concord/pull/1268)). - project: update groovy test dependencies to 5.X ([#1280](https://github.com/walmartlabs/concord/pull/1280)); - project: upgrade concord-maven-plugin to 0.0.37 ([#1283](https://github.com/walmartlabs/concord/pull/1283)); - concord-server: add GitHub user mapping for user when found via fallback lookup ([#1285](https://github.com/walmartlabs/concord/pull/1285)); - console2: upgrade to Vite 8 ([#1286](https://github.com/walmartlabs/concord/pull/1286)); - project: add AGENTS.md, update READMEs and notes ([#1288](https://github.com/walmartlabs/concord/pull/1288)); - project: enable useNativeGit in git-commit-id-plugin ([#1289](https://github.com/walmartlabs/concord/pull/1289)); - project: update Node version ([#1291](https://github.com/walmartlabs/concord/pull/1291)); - project: upgrade and pin versions of GHA actions ([#1292](https://github.com/walmartlabs/concord/pull/1292)); - concord-server-it: attempt to fix CronIT flakiness ([#1293](https://github.com/walmartlabs/concord/pull/1293)); - project: update dependencies ([#1294](https://github.com/walmartlabs/concord/pull/1294)); - concord-console2: pass test-ids directly instead of using wrappers ([#1296](https://github.com/walmartlabs/concord/pull/1296)); - project: improve mvnd (and Maven 4) support ([#1297](https://github.com/walmartlabs/concord/pull/1297)); - console2: update dependencies ([#1302](https://github.com/walmartlabs/concord/pull/1302)). ## [2.38.0] - 2026-03-11 ### Changed - server: make git allowedSchemes configurable and support non-string values in test server config ([#12282](https://github.com/walmartlabs/concord/pull/1282)); - common: use abstract MappingAuthConfig for thenCallRealMethod() support on mocks ([#1278](https://github.com/walmartlabs/concord/pull/1278)). ## [2.37.0] - 2026-03-05 ### Added - cli: add `--target-dir` option to specify custom payload target directory ([#1275](https://github.com/walmartlabs/concord/pull/1275)); - cli: allow tasks to interact with remote during local runs ([#1269](https://github.com/walmartlabs/concord/pull/1269)); - repository, server, agent: configurable limit for git cli output ([#1266](https://github.com/walmartlabs/concord/pull/1266)). ### Changed - runtime-v2: fix JoinCommand not waiting for threads in UNWINDING state ([#1277](https://github.com/walmartlabs/concord/pull/1277)); - runtime-v2: fix JoinCommand collecting failed threads from unrelated parallel blocks ([#1276](https://github.com/walmartlabs/concord/pull/1276)); - console2: fix copyToClipboard is not a function ([#1274](https://github.com/walmartlabs/concord/pull/1274)); - server: validate restored payload IDs on process restart ([#1273](https://github.com/walmartlabs/concord/pull/1273)); - runtime-v2: mask sensitive data in log segment names ([#1272](https://github.com/walmartlabs/concord/pull/1272)); - examples: fix/update runtime-v2 example dependencies ([#1271](https://github.com/walmartlabs/concord/pull/1271)); - project: build with postgres 14 image ([#1148](https://github.com/walmartlabs/concord/pull/1148)); - console2: reduce calls to trigger API ([#1251](https://github.com/walmartlabs/concord/pull/1251)). ## [2.36.0] - 2026-02-04 ### Added - concord-server: add ldap.trustAllCertificates, disable by default ([#1261](https://github.com/walmartlabs/concord/pull/1261)); - concord-cli: add support for .gitignore ([#1264](https://github.com/walmartlabs/concord/pull/1264)). ### Changed - runtime-v2: add 'name' attribute to script call in schema ([#1248](https://github.com/walmartlabs/concord/pull/1248)); - common, server: skip rate limit metrics for null auth id ([#1249](https://github.com/walmartlabs/concord/pull/1249)); - concord-console2: replace some saga usage with hooks ([#1252](https://github.com/walmartlabs/concord/pull/1252)); - project: update dependency versions ([#1253](https://github.com/walmartlabs/concord/pull/1254)); - concord-noderoster: do not build the tarball anymore ([#1257](https://github.com/walmartlabs/concord/pull/1257)); - concord-console-it: add basic tests for team UI ([#1258](https://github.com/walmartlabs/concord/pull/1258)); - project: concord-maven-plugin version up ([#1259](https://github.com/walmartlabs/concord/pull/1259)); - concord-agent: send segmented logs in order logged ([#1260](https://github.com/walmartlabs/concord/pull/1260)); - concord-server: assert user enabled before process restart or handler ([#1262](https://github.com/walmartlabs/concord/pull/1262)); - project: update testcontainers-concord version ([#1265](https://github.com/walmartlabs/concord/pull/1265)). ## [2.35.0] - 2025-12-29 ### Changed - oidc: handle session invalidation errors ([#1239](https://github.com/walmartlabs/concord/pull/1239)); - concord-server: sanitize escaped unicode nul characters from events ([#1241](https://github.com/walmartlabs/concord/pull/1241)); - oidc: serialization and mapping fix ([#1245](https://github.com/walmartlabs/concord/pull/1245)); - server, agent: github app installation clone support ([#1242](https://github.com/walmartlabs/concord/pull/1242)); - concord-server: option to look up GH webhook event sender by email ([#1243](https://github.com/walmartlabs/concord/pull/1243)); - noderoster: configurable host cache size and eviction duration ([#1246](https://github.com/walmartlabs/concord/pull/1246)); - concord-server: inject ProcessKeyCache interface instead of implementation to utilize singleton scope ([#1247](https://github.com/walmartlabs/concord/pull/1247)); - concord-db, concord-server: cache external app user mapping in database ([#1244](https://github.com/walmartlabs/concord/pull/1244)). ## [2.34.0] - 2025-10-29 ### Added - runtime-v2: allow plugins to supply custom EL functions ([#1225](https://github.com/walmartlabs/concord/pull/1225)); - runtime-v2: allow paths in SensitiveData keys ([#1228](https://github.com/walmartlabs/concord/pull/1228)); - runtime-v2: add EL function to mark strings as sensitive ([#1230](https://github.com/walmartlabs/concord/pull/1230)). ### Changed - concord-console2: migrate off CRA to Vite ([#1231](https://github.com/walmartlabs/concord/pull/1231)); - concord-server-it: fix testFailedHosts for modern Ansible versions ([#1232](https://github.com/walmartlabs/concord/pull/1232)); - concord-server: retrieve user ldap groups for form access assertion ([#1233](https://github.com/walmartlabs/concord/pull/1233)); - project: update dependencies ([#1234](https://github.com/walmartlabs/concord/pull/1234)); - concord-console2: update dependencies, README ([#1235](https://github.com/walmartlabs/concord/pull/1235)); - concord-server-it: improve test ([#1236](https://github.com/walmartlabs/concord/pull/1236)); - concord-console2: update Node and Vite versions ([#1237](https://github.com/walmartlabs/concord/pull/1237)). ## [2.33.3] - 2025-10-14 ### Changed - repository: fix uri scheme restriction for SSH repo uris ([#1226](https://github.com/walmartlabs/concord/pull/1226)); ## [2.33.2] - 2025-10-09 ### Changed - concord-server: fix API key creation for the current user ([#1222](https://github.com/walmartlabs/concord/pull/1222)); ## [2.33.1] - 2025-10-06 ### Changed - pfed-sso: upgrade nimbus-jose-jwt version ([#1219](https://github.com/walmartlabs/concord/pull/1219)); - server: fix usernameSignature generation ([#1221](https://github.com/walmartlabs/concord/pull/1221)); ## [2.33.0] - 2025-09-22 ### Added - ansible-tasks: support inventories specified in configFile ([#1216](https://github.com/walmartlabs/concord/pull/1216)). ### Changed - project: use JSch fork ([#1210](https://github.com/walmartlabs/concord/pull/1210)); - slack-tasks: update readme with required oauth scope info ([#1213](https://github.com/walmartlabs/concord/pull/1213)); - it: do not archive deps.dir into payloads ([#1214](https://github.com/walmartlabs/concord/pull/1214)); - ansible-tasks: remove ini4j dependency ([#1215](https://github.com/walmartlabs/concord/pull/1215)); - concord-server: do not create UserPrincipal for API keys without userId ([#1218](https://github.com/walmartlabs/concord/pull/1218)). ### Breaking - oidc: remove pac4j dependency. Will cause (de)serialization issues for SUSPENDED processes with OIDC initiators ([#1217](https://github.com/walmartlabs/concord/pull/1217)). ## [2.32.0] - 2025-08-20 ### Added - concord-cli: support for remote secrets, configurable secrets providers ([#1143](https://github.com/walmartlabs/concord/pull/1143)); - concord-agent: configurable list of runners ([#1182](https://github.com/walmartlabs/concord/pull/1182)); - concord-cli: add self-update command ([#1198](https://github.com/walmartlabs/concord/pull/1198)). ### Changed - concord-client2: add Concord-specific media type for validation errors ([#1199](https://github.com/walmartlabs/concord/pull/1199)); - docker-images: use Debian 12 ([#1201](https://github.com/walmartlabs/concord/pull/1201)); - concord-cli: do not call System.exit in RemoteSecretsProvider ([#1202](https://github.com/walmartlabs/concord/pull/1202)); - concord-cli: ignore unknown properties in the CLI config file ([#1203](https://github.com/walmartlabs/concord/pull/1203)); - resource-task: Use the shared IOUtils assertInPath method for resource lookups ([#1204](https://github.com/walmartlabs/concord/pull/1204)); - project: enable trimHeaderLine in license-maven-plugin ([#1205](https://github.com/walmartlabs/concord/pull/1205)); - noderoster: optimize event list query ([#1206](https://github.com/walmartlabs/concord/pull/1206)); - concord-common: deprecate IOUtils ([#1208](https://github.com/walmartlabs/concord/pull/1208)). ## [2.31.0] - 2025-08-04 ### Added - concord-tasks: add createApiKey action ([#1194](https://github.com/walmartlabs/concord/pull/1194)); - concord-server: option to specify API key value ([#1195](https://github.com/walmartlabs/concord/pull/1195)); - concord-tasks: add createOrUpdateApiKey action ([#1196](https://github.com/walmartlabs/concord/pull/1196)). ### Changed - concord-runtime-model: more flexible interfaces, simplify code ([#1184](https://github.com/walmartlabs/concord/pull/1184)); - project: fix typo ([#1191](https://github.com/walmartlabs/concord/pull/1191)); - runtime-v2: additional error logging when cloning state fails ([#1192](https://github.com/walmartlabs/concord/pull/1192)); - project: update dependencies ([#1193](https://github.com/walmartlabs/concord/pull/1193)). ## [2.30.0] - 2025-07-12 ### Added - http-task: support sending int values in multipart requests ([#1174](https://github.com/walmartlabs/concord/pull/1174)); - concord-server: config option to disable template script processing ([#1176](https://github.com/walmartlabs/concord/pull/1176)); - common, runtime-v1: add method to assert given path string resolves in expected parent ([#1179](https://github.com/walmartlabs/concord/pull/1179)). ### Changed - concord-cli: produce an executable binary again ([#1171](https://github.com/walmartlabs/concord/pull/1171)); - concord-server: allow get user for concordSystemReader role ([#1173](https://github.com/walmartlabs/concord/pull/1173)); - runtime-v2: return copies in ObjectMapperProvider ([#1175](https://github.com/walmartlabs/concord/pull/1175)); - concord-server: do not log session token ([#1177](https://github.com/walmartlabs/concord/pull/1177)); - concord-server: kill process when resume fails with no state ([#1178](https://github.com/walmartlabs/concord/pull/1178)); - runtime-v2: validate form name constraints as documented ([#1180](https://github.com/walmartlabs/concord/pull/1180)); - runtime-v1: validate form name constraints as documented ([#1181](https://github.com/walmartlabs/concord/pull/1181)); - runtime-v2: remove unused imports, dead code ([#1185](https://github.com/walmartlabs/concord/pull/1185)); - concord-console2: sort variables in task call details ([#1186](https://github.com/walmartlabs/concord/pull/1186)); - http-task: allow send long multipart with debug enabled ([#1188](https://github.com/walmartlabs/concord/pull/1188)); - concord-server: fix tx in process card dao ([#1189](https://github.com/walmartlabs/concord/pull/1189)). ## [2.29.0] - 2025-06-13 ### Added - concord-server: refactor WebSocketChannelManager, allow message sources in plugins ([#1056](https://github.com/walmartlabs/concord/pull/1056)); - concord-agent: one-shot mode ([#1150](https://github.com/walmartlabs/concord/pull/1150)); - slack-tasks: allow send blocks instead of simple text ([#1161](https://github.com/walmartlabs/concord/pull/1161)); - concord-server, concord-console2: add console.cfgFile config parameter ([#1166](https://github.com/walmartlabs/concord/pull/1166)); - runtime-v2: support expressions for output variables in call step ([#1170](https://github.com/walmartlabs/concord/pull/1170)). ### Changed - concord-server, concord-console2: use webapp plugin ([#1154](https://github.com/walmartlabs/concord/pull/1154)); - runtime-v2: add extra check to testThrowParallelWithPayload ([#1157](https://github.com/walmartlabs/concord/pull/1157)); - concord-server-it: reduce agent and server poll delays to speed up tests ([#1159](https://github.com/walmartlabs/concord/pull/1159)); - concord-runtime-v1/v2-it: reduce agent and server poll delays to speed up tests ([#1163](https://github.com/walmartlabs/concord/pull/1163)); - runtime-v2: improve handling of @SensitiveData on bridge methods ([#1164](https://github.com/walmartlabs/concord/pull/1164)); - webapp: fix prefix matching ([#1167](https://github.com/walmartlabs/concord/pull/1167)); - oidc: fix role mapping validation ([#1168](https://github.com/walmartlabs/concord/pull/1168)); - concord-server-db: use our own implementation of session locks ([#1169](https://github.com/walmartlabs/concord/pull/1169)). ## [2.28.0] - 2025-06-04 ### Added - concord-server: add more repository-level GitHub events ([#1149](https://github.com/walmartlabs/concord/pull/1149)); - runtime-v2: allow marking nested values as sensitive data ([#1151](https://github.com/walmartlabs/concord/pull/1151)); - misc-tasks, runtime-v2: support masking when base64 encoding/decoding sensitive data ([#1155](https://github.com/walmartlabs/concord/pull/1155)). ### Changed - runtime-v1, runtime-v2: refactor loader structure ([#1147](https://github.com/walmartlabs/concord/pull/1147)); - examples: update description ([#1153](https://github.com/walmartlabs/concord/pull/1153)); - concord-server-it: make dependency resolver timeout same as in runtime-v2 ITs ([#1156](https://github.com/walmartlabs/concord/pull/1156)); - docker-images: update JDK versions ([#1158](https://github.com/walmartlabs/concord/pull/1158)); - project: use central-publishing-maven-plugin ([#1160](https://github.com/walmartlabs/concord/pull/1160)). ## [2.27.0] - 2025-05-22 ### Added - concord-server: default Git credentials hostname whitelist ([#1139](https://github.com/walmartlabs/concord/pull/1139)); - concord-server: log listening address, starting tasks ([#1141](https://github.com/walmartlabs/concord/pull/1141)); ### Changed - oidc: validate team and role mappings ([#1144](https://github.com/walmartlabs/concord/pull/1144)); - concord-server-db: use session locks in Liquibase ([#1145](https://github.com/walmartlabs/concord/pull/1145)); - concord-server: handle SIGTERM ([#1146](https://github.com/walmartlabs/concord/pull/1146)). ## [2.26.0] - 2025-05-05 ### Added - resource-task: add fromYamlString method interface ([#1125](https://github.com/walmartlabs/concord/pull/1125)); - concord-cli: use jansi, add colors to run output ([#1138](https://github.com/walmartlabs/concord/pull/1138)). ### Changed - concord-agent, concord-agent-operator: better shutdown hook, handle SIGTERM ([#1130](https://github.com/walmartlabs/concord/pull/1130)); - project: enable full annotation processing ([#1131](https://github.com/walmartlabs/concord/pull/1131)); - project: fix concord-runner-v2 test discovery ([#1133](https://github.com/walmartlabs/concord/pull/1133)); - project: update testcontainers-concord version ([#1135](https://github.com/walmartlabs/concord/pull/1135)); - concord-server: use ProjectLoader interface, bind explicitly ([#1136](https://github.com/walmartlabs/concord/pull/1136)); - concord-server: split ProcessSecurityContext ([#1137](https://github.com/walmartlabs/concord/pull/1137)). ## [2.25.1] - 2025-04-26 ### Changed - runtime-v2: set lowercase loop mode value in schema ([#1122](https://github.com/walmartlabs/concord/pull/1122)); - http-tasks: refactor tests, support for parallel tests ([#1123](https://github.com/walmartlabs/concord/pull/1123)); - project: update bouncycastle dependency versions ([#1127](https://github.com/walmartlabs/concord/pull/1127)); - concord-server: handle invalid regex in dispatcher requirements ([#1128](https://github.com/walmartlabs/concord/pull/1128)); - project: downgrade nimbus-jose-jwt ([#1129](https://github.com/walmartlabs/concord/pull/1129)). ## [2.25.0] - 2025-03-23 ### Changed - concord-agent: replace simple immutables with records ([#1091](https://github.com/walmartlabs/concord/pull/1091)); - concord-server, concord-server-db: generate UUIDs on the server, use UUID v7 ([#1106](https://github.com/walmartlabs/concord/pull/1106)); - concord-server: upgrade Shiro to 2.x ([#1107](https://github.com/walmartlabs/concord/pull/1107)); - concord-server, concord-client2: uncomment valid code ([#1108](https://github.com/walmartlabs/concord/pull/1108)); - concord-server: remove more @Named usage ([#1110](https://github.com/walmartlabs/concord/pull/1110)); - concord-server: remove deprecated SecretEntry ([#1111](https://github.com/walmartlabs/concord/pull/1111)); - concord-server: remove deprecated /logs/* endpoint ([#1113](https://github.com/walmartlabs/concord/pull/1113)); - project: update dependencies ([#1114](https://github.com/walmartlabs/concord/pull/1114)); - project: update README ([#1115](https://github.com/walmartlabs/concord/pull/1115)); - docker-images: update JDK versions ([#1116](https://github.com/walmartlabs/concord/pull/1116)); - project: update dependencies ([#1118](https://github.com/walmartlabs/concord/pull/1118)); - project: remove unused dependencies ([#1119](https://github.com/walmartlabs/concord/pull/1119)). ### Breaking - project: remove client1 ([#1013](https://github.com/walmartlabs/concord/pull/1013)); - concord-server: remove deprecated features ([#1112](https://github.com/walmartlabs/concord/pull/1112)). ## [2.24.0] - 2025-03-15 ### Added - plugins: new env and collections tasks ([#1092](https://github.com/walmartlabs/concord/pull/1092)); - slack-tasks: modern bot token support ([#1093](https://github.com/walmartlabs/concord/pull/1093)); - plugins: allow suspend/resume process from concord task ([#1094](https://github.com/walmartlabs/concord/pull/1094)); - concord-server: default message for unexpected errors ([#1096](https://github.com/walmartlabs/concord/pull/1096)); - concord-server, concord-console2: add support for processExecMode ([#1102](https://github.com/walmartlabs/concord/pull/1102)). ### Changed - runtime-v2: restrict regex github trigger attributes to string value ([#1097](https://github.com/walmartlabs/concord/pull/1097)); - mock-tasks: correctly handle mocks in `set variables` step ([#1098](https://github.com/walmartlabs/concord/pull/1098)); - concord-console2: handle non-existent root parent ([#1100](https://github.com/walmartlabs/concord/pull/1100)); - concord-server: disable deprecated process start endpoints ([#1101](https://github.com/walmartlabs/concord/pull/1101)); - concord-plugin: set dry-run mode for subprocesses by default if the parent process was started in dry-run mode ([#1104](https://github.com/walmartlabs/concord/pull/1104)); - project: update Maven plugin versions ([#1105](https://github.com/walmartlabs/concord/pull/1105)). ## [2.23.0] - 2025-02-26 ### Added - concord-server: allow handle configuration in custom enqueue processor ([#1088](https://github.com/walmartlabs/concord/pull/1088)); - concord-agent: allow skipping repository state ([#1089](https://github.com/walmartlabs/concord/pull/1089)). ### Changed - runtime-v2: hide stack trace and log location for ParallelExecutionException ([#1081](https://github.com/walmartlabs/concord/pull/1081)); - concord-server: close MultipartInput explicitly ([#1084](https://github.com/walmartlabs/concord/pull/1084); - console2: configure children process columns ([#1085](https://github.com/walmartlabs/concord/pull/1085)); - runtimev2: error message for process arguments evaluation ([#1086](https://github.com/walmartlabs/concord/pull/1086)); - concord-tasks: send archive as a file instead of byte array ([#1087](https://github.com/walmartlabs/concord/pull/1087)); - concord-server: handle request without cookies ([#1090](https://github.com/walmartlabs/concord/pull/1090)). ## [2.22.0] - 2025-02-14 ### Added - concord-server: order ui_process_card by a new order_id field ([#1075](https://github.com/walmartlabs/concord/pull/1075)); - misc-tasks: add base64 task, shortcut for current ISO timestamp ([#1078](https://github.com/walmartlabs/concord/pull/1078)); - http-tasks: use default variables ([#1080](https://github.com/walmartlabs/concord/pull/1080)). ### Changed - concord-server: simplify UserInfo, UserInfoProcessor ([#1061](https://github.com/walmartlabs/concord/pull/1061)); - project: update to latest Jetty 12.x ([#1068](https://github.com/walmartlabs/concord/pull/1068)); - concord-agent, runtime-v2: miscellaneous improvements ([#1070](https://github.com/walmartlabs/concord/pull/1070)); - concord-agent-operator: use informers API ([#1072](https://github.com/walmartlabs/concord/pull/1072)); - concord-agent-operator: attempt to improve error logging ([#1073](https://github.com/walmartlabs/concord/pull/1073)); - runtime-v2: do not log stack trace for EL MethodNotFound exception and unify the error messages ([#1076](https://github.com/walmartlabs/concord/pull/1076)); - project: do not log into Docker in check runs started in forks ([#1079](https://github.com/walmartlabs/concord/pull/1079)); - concord-server: proper close queue client ([#1082](http://github.com/walmartlabs/concord/pull/1082)). ## [2.21.0] - 2025-01-15 ### Added - concord-repository: support for `mvn://` scheme ([#1063](https://github.com/walmartlabs/concord/pull/1063)). ### Changed - concord-server: fix header validation in SessionTokenAuthenticationHandler ([#1044](https://github.com/walmartlabs/concord/pull/1044)); - concord-console2: add support for redirectTo to the login page ([#1046](https://github.com/walmartlabs/concord/pull/1046)); - runtime-v2: allow throw payload with exception ([#1049](https://github.com/walmartlabs/concord/pull/1049)); - runtime-v2: use SensitiveDataHolder for task parameter masking ([#1050](https://github.com/walmartlabs/concord/pull/1050)); - runtime-v2: add exceptions to ParallelExecutionException ([#1051](https://github.com/walmartlabs/concord/pull/1051)); - runtime-v2: mask workDir value in logs by default ([#1052](https://github.com/walmartlabs/concord/pull/1052)); - runtime-v2: save out variables for failed process ([#1053](https://github.com/walmartlabs/concord/pull/1053)); - concord-server: allow tokens without users, remove user from default agent token ([#1054](https://github.com/walmartlabs/concord/pull/1054)); - concord-targetplatform: update dependencies ([#1057](https://github.com/walmartlabs/concord/pull/1057)); - concord-server: replace synchronized with locks ([#1060](https://github.com/walmartlabs/concord/pull/1060)); - runtime-v2: fix for exception stack trace logging ([#1062](https://github.com/walmartlabs/concord/pull/1062)); - concord-agent-operator: fix ConfigMap creation, update example CRDs ([#1065](https://github.com/walmartlabs/concord/pull/1065)); - concord-server: fix PolicyCache reloading loop ([#1066](https://github.com/walmartlabs/concord/pull/1066)); - concord-agent-operator: simple exit on watcher error ([#1067](https://github.com/walmartlabs/concord/pull/1067)). ## [2.20.0] - 2024-11-20 ### Added - mock-tasks: allow flows to be executed instead of tasks ([#1042](https://github.com/walmartlabs/concord/pull/1042)). ### Changed - plugins: fix dependency scopes ([#1017](https://github.com/walmartlabs/concord/pull/1017)); - concord-cli: fix dependencies when generating effective yaml ([#1018](https://github.com/walmartlabs/concord/pull/1018)); - runtime-v2: remove extraneous error logging ([#1021](https://github.com/walmartlabs/concord/pull/1021)); - runtime-v2: script meta options should not override the script step name ([#1022](https://github.com/walmartlabs/concord/pull/1022)); - runtime-v2: fix error messages ([#1023](https://github.com/walmartlabs/concord/pull/1023)); - mock-tasks: allow matching mocks by meta attributes or step name ([#1024](https://github.com/walmartlabs/concord/pull/1024)); - concord-console2: fix zero content-length parsing in makeError ([#1025](https://github.com/walmartlabs/concord/pull/1025)); - concord-server: split ConcordAuthenticationHandler into separate handlers ([#1026](https://github.com/walmartlabs/concord/pull/1026)); - mocks-task: record task events for mocked tasks ([#1027](https://github.com/walmartlabs/concord/pull/1027)); - concord-server: fix session token handling ([#1032](https://github.com/walmartlabs/concord/pull/1032)); - concord-server: cleanup control chars from JSONB ([#1034](https://github.com/walmartlabs/concord/pull/1034)); - concord-server: add authorization check to User lookup endpoint in API ([#1035](https://github.com/walmartlabs/concord/pull/1035)); - it: tighten up polling intervals ([#1036](https://github.com/walmartlabs/concord/pull/1036)); - project: use concord-maven-plugin ([#1038](https://github.com/walmartlabs/concord/pull/1038)); - concord-server: fix process card createOrUpdate ([#1039](https://github.com/walmartlabs/concord/pull/1039)); - runtime-v2: allow mark sensitive data for task.execute result ([#1040](https://github.com/walmartlabs/concord/pull/1040)). ## [2.19.0] - 2024-11-05 ## Added - runtime-v2: introduce extraDependencies ([#1014](https://github.com/walmartlabs/concord/pull/1014)); - runtime-v2: initial support for dry-run mode ([#1007](https://github.com/walmartlabs/concord/pull/1007)); - concord-console2: add a full-screen page for process cards ([#1009](https://github.com/walmartlabs/concord/pull/1009)); - mock-tasks: support for method mocks in tasks ([#1010](https://github.com/walmartlabs/concord/pull/1010)); - mock-tasks: support for task call verify ([#1012](https://github.com/walmartlabs/concord/pull/1012)). ## Changed - concord-server: remove resteasy-guice dependency ([#997](https://github.com/walmartlabs/concord/pull/997)); - project: update HikariCP version ([#1000](https://github.com/walmartlabs/concord/pull/1000)); - runtime-v2: flush events on process error ([#1001](https://github.com/walmartlabs/concord/pull/1001)); - concord-server: fix AuthenticationHandler result handling ([#1003](https://github.com/walmartlabs/concord/pull/1003)); - server: fix ConcordKey validation regex to 128 character limit ([#1004](https://github.com/walmartlabs/concord/pull/1004)); - project: update docker-login action in build flow ([#1005](https://github.com/walmartlabs/concord/pull/1005)); - runtime-v2: log task method name in event ([#1006](https://github.com/walmartlabs/concord/pull/1006)); - runtime-v2: move tests to separate module ([#1008](https://github.com/walmartlabs/concord/pull/1008)); - runtime-v2: use shorter delay while polling status of threads ([#1011](https://github.com/walmartlabs/concord/pull/1011)). ### Breaking - runtime-v2: store flow location in process definition. Note, this changes the type of `context.execution().processDefinition().flows()` object available in runtime-v2 processes. ([#995](https://github.com/walmartlabs/concord/pull/995)); - runtime-v2: remove ProjectLoadListener interface ([#1015](https://github.com/walmartlabs/concord/pull/1015)). ## [2.18.0] - 2024-10-13 ### Added - concord-console2: status filter for log segments ([#980](https://github.com/walmartlabs/concord/pull/980)); - runtime-v2: interface for steps that generate element events ([#987](https://github.com/walmartlabs/concord/pull/987)). ### Changed - project: respect PR labels ([#961](https://github.com/walmartlabs/concord/pull/961)); - it: re-enable OldAgentIT ([#962](https://github.com/walmartlabs/concord/pull/962)); - runtime-v2: add github exclusive trigger to schema ([#977](https://github.com/walmartlabs/concord/pull/977)); - concord-server: bind EventEnrichers explicitly ([#978](https://github.com/walmartlabs/concord/pull/978)); - oidc, concord-console2: improve error handling ([#979](https://github.com/walmartlabs/concord/pull/979)); - runtime-v2: fix the issue when old agents can't parse process configuration with new attributes ([#981](https://github.com/walmartlabs/concord/pull/981)). - agent-operator: save cookies received from API ([#984](https://github.com/walmartlabs/concord/pull/984)); - project: fixes for build-time warnings ([#985](https://github.com/walmartlabs/concord/pull/985)); - concord-server: explicitly bind more classes ([#986](https://github.com/walmartlabs/concord/pull/986)); - concord-server: more fixes for non auto-wiring environments ([#988](https://github.com/walmartlabs/concord/pull/988)); - project: update frontend-maven-plugin ([#992](https://github.com/walmartlabs/concord/pull/992)); - runtime-v2: fix manual trigger exclusive schema ([#993](https://github.com/walmartlabs/concord/pull/993)); - concord-server: fix updateWaitConditions when wait condition without processes ([#994](https://github.com/walmartlabs/concord/pull/994)). ### Breaking - project: fork ollie-config and make it a submodule. Server plugins must be updated to use the new package `com.walmartlabs.concord.config` instead of `com.walmartlabs.ollie.config` ([#989](https://github.com/walmartlabs/concord/pull/989)). ## [2.17.0] - 2024-09-18 ### Added - runtime-v2: option for event batching for runner events ([#949](https://github.com/walmartlabs/concord/pull/949)); - runtime-v1: option for event batching for runner events ([#950](https://github.com/walmartlabs/concord/pull/950)); - console2, server: simple user info page ([#952](https://github.com/walmartlabs/concord/pull/952)). ### Changed - project: update Maven wrapper ([#967](https://github.com/walmartlabs/concord/pull/967)); - oidc: redirect back to auth in failed callbacks ([#969](https://github.com/walmartlabs/concord/pull/969)); - project: miscellaneous fixes for build-time warnings, add missing @deprecated annotations, remove redundant dependencies ([#970](https://github.com/walmartlabs/concord/pull/970)); - agent-operator: create agent pod client only for Running pods ([#973](https://github.com/walmartlabs/concord/pull/973)); - concord-server: remove GithubTriggerProcessor interface ([#974](https://github.com/walmartlabs/concord/pull/974)); - docker: configure safe.directory for git 2.35+ ([#976](https://github.com/walmartlabs/concord/pull/976)). ### Changed ## [2.16.0] - 2024-09-05 ### Added - runtime-v2: option to update meta only on termination or suspend ([#948](https://github.com/walmartlabs/concord/pull/948)); - policy-engine: allow rewriting with multiple values in `dependencyRewrite` policies ([#952](https://github.com/walmartlabs/concord/pull/952)); - concord-server: allow non-standard runtimes ([#954](https://github.com/walmartlabs/concord/pull/954)); - oidc: support "from" when logging out ([#958](https://github.com/walmartlabs/concord/pull/958)). ### Changed - runtime-v1: update bpm library to fix saving variables before suspend ([#955](https://github.com/walmartlabs/concord/pull/955)); - concord-server: fix DB change set 1580200-a when `superuserAvailable` is set to `false` ([#957](https://github.com/walmartlabs/concord/pull/957)); - concord-server: skip pull\_request process start when useEventCommitId is enabled and event is from a different repo ([#959](https://github.com/walmartlabs/concord/pull/959)); - docker-images: update ansible galaxy community.docker version ([#960](https://github.com/walmartlabs/concord/pull/960)); - cli: fix duplicate step logs ([#963](https://github.com/walmartlabs/concord/pull/963)). ## [2.15.0] - 2024-08-07 ### Added - agent: configure host/ip for maintenance-mode endpoint ([#945](https://github.com/walmartlabs/concord/pull/945)); - concord-task: new method to wait and check that processes have finished ([#943](https://github.com/walmartlabs/concord/pull/943)); - agent-operator: use maintenance mode before terminating agent ([#946](https://github.com/walmartlabs/concord/pull/946)); ### Changed - pfed-sso: fix to not return null for not permanently disabled users ([#947](https://github.com/walmartlabs/concord/pull/947)). - agent-operator: consider pods already marked for deletion during downscaling ([#951](https://github.com/walmartlabs/concord/pull/951)). ## [2.14.0] - 2024-07-13 ### Added - concord-server: calculate total process RUNNING time ([#933](https://github.com/walmartlabs/concord/pull/933)); - concord-server: expose websocket channels ([#935](https://github.com/walmartlabs/concord/pull/935)); - resource-tasks: add versions of writeAs* methods that accept destination ([#937](https://github.com/walmartlabs/concord/pull/937)); - runtime-v1: option to update meta only on termination or suspend ([#938](https://github.com/walmartlabs/concord/pull/938)); - project: add JDK 21 profiles ([#941](https://github.com/walmartlabs/concord/pull/941)). ### Changed - project: update Groovy to 2.5.23 ([#940](https://github.com/walmartlabs/concord/pull/940)); - dependency-manager: resolve only unique dependencies ([#936](https://github.com/walmartlabs/concord/pull/936)); - concord-server: move com.walmartlabs.concord.server.ansible.* into ansible plugin ([#502](https://github.com/walmartlabs/concord/pull/502)); - concord-server: migrate to PROCESS_META and PROCESS_TRIGGER_INFO tables ([#669](https://github.com/walmartlabs/concord/pull/669)); - runtime-v2: use draft-07 of JSON Schema for better tool compatibility ([#939](https://github.com/walmartlabs/concord/pull/939)); - project: update dependency versions in the parent pom ([#942](https://github.com/walmartlabs/concord/pull/942)). ## [2.13.0] - 2024-06-19 ### Added - runtime-v2: "suspend" status support for log segments ([#927](https://github.com/walmartlabs/concord/pull/927)); - mocks: examples, support for storing the input, add `throwError` ([#928](https://github.com/walmartlabs/concord/pull/928)); - runtime-v2: add more events to execution listeners ([#931](https://github.com/walmartlabs/concord/pull/931)). ### Changed - runtime-v2: rename MultiException and limit stack trace depth ([#930](https://github.com/walmartlabs/concord/pull/930)). ## [2.12.0] - 2024-06-12 ### Added - dependency-manager: allow `LATEST` to pull latest version from remote repositories ([#913](https://github.com/walmartlabs/concord/pull/913)); - runtime-v2: additional log segment statuses for error and suspended states ([#918](https://github.com/walmartlabs/concord/pull/918)); - runtime-v2: initial support for thread local variables ([#920](https://github.com/walmartlabs/concord/pull/920)); - project: add mock task to parent pom ([#925](https://github.com/walmartlabs/concord/pull/925)); - runtime-v2: initial support for finalizers ([#926](https://github.com/walmartlabs/concord/pull/926)). ### Changed - docker: fix image build on aarch64 hosts ([#717](https://github.com/walmartlabs/concord/pull/717)); - concord-console2: upgrade node version ([#890](https://github.com/walmartlabs/concord/pull/890)); - runtime-v2: fix log segment assigment during parallel execution ([#905](https://github.com/walmartlabs/concord/pull/905)); - project: agent-operator module re-organization ([#906](https://github.com/walmartlabs/concord/pull/906)); - concord-server: fix process metadata values after resume ([#907](https://github.com/walmartlabs/concord/pull/907)); - concord-server: process wait conditions in batch mode ([#910](https://github.com/walmartlabs/concord/pull/910)); - project: update Liquibase to 4.8.0 ([#912](https://github.com/walmartlabs/concord/pull/912)); - project: update resteasy to latest 4.x ([#914](https://github.com/walmartlabs/concord/pull/914)); - server: exception mapper for InvalidProcessStateException ([#916](https://github.com/walmartlabs/concord/pull/916)); - runtime-v2: allow double (floating point) values in YAML ([#917](https://github.com/walmartlabs/concord/pull/917)); - project: build both x86 and aarch64 versions ([#921](https://github.com/walmartlabs/concord/pull/921)); - concord-server: fix UserDao list method ([#924](https://github.com/walmartlabs/concord/pull/924)). ## [2.11.1] - 2024-05-12 ### Changed - concord-server: reduce Shiro usage ([#889](https://github.com/walmartlabs/concord/pull/889)); - runtime-v2: fix sensitive data masking in maps ([#897](https://github.com/walmartlabs/concord/pull/893)); - concord-server, tasks: disable repos on deleted ref, only refresh repos matching event branch ([#894](https://github.com/walmartlabs/concord/pull/894)); - concord-server: fix Jetty metrics ([#899](https://github.com/walmartlabs/concord/pull/899)); - concord-server: add some missing GHA event types (repository, status, workflow_job, workflow_run) ([#900](https://github.com/walmartlabs/concord/pull/900)); - dependency-manager: make it a singleton ([#901](https://github.com/walmartlabs/concord/pull/901)); - concord-server: fix initialization of wait conditions after process restart ([#903](https://github.com/walmartlabs/concord/pull/903)); - runtime-v2: fix itemIndex in parallel loops ([#904](https://github.com/walmartlabs/concord/pull/904)). ## [2.11.0] - 2024-04-30 ### Added - agent-operator: scaling strategies and configurable requirements ([#893](https://github.com/walmartlabs/concord/pull/893)). ### Changed - ansible-tasks: be more helpful when commands are missing. Check if `ansible-playbook` or `virtualenv` exist before running. ([#887](https://github.com/walmartlabs/concord/pull/887)); - project: upgrade dependencies - Jackson to 2.17.0, Jetty to 12.0.7, Wiremock to 3.5.2 and others. ([#861](https://github.com/walmartlabs/concord/pull/861)); - concord-server: allow plugins to supply their own top-level API endpoints ([#891](https://github.com/walmartlabs/concord/pull/891)); - concord-server: minor improvements to the remember me cookie logic ([#892](https://github.com/walmartlabs/concord/pull/892)); - concord-server: tone down websocket errors ([#895](https://github.com/walmartlabs/concord/pull/895)); - concord-server: do not invalidate sessions in onFailedLogin ([#896](https://github.com/walmartlabs/concord/pull/896)). ## [2.10.1] - 2024-04-04 ### Changed - concord-server: fix json serialization of UserActivityResponse ([#885](https://github.com/walmartlabs/concord/pull/885)). ## [2.10.0] - 2024-04-01 ### Added - plugins: add new mock-tasks plugin ([#754](https://github.com/walmartlabs/concord/pull/754)); - runtime-v2: logYaml step ([#816](https://github.com/walmartlabs/concord/pull/816)); - concord-server, concord-console2: add "process cards" ([#808](https://github.com/walmartlabs/concord/pull/808)); - concord-agent: kill runner child PIDs ([#880](https://github.com/walmartlabs/concord/pull/880)). ### Changed - server: fix trigger id calculation for complex args: heterogeneous lists, list of maps ([#882](https://github.com/walmartlabs/concord/pull/882)); - concord-server: skip validation of disabled repos during project creation ([#883](https://github.com/walmartlabs/concord/pull/883)); - concord-server: process wait conditions synchronously ([#884](https://github.com/walmartlabs/concord/pull/884)). ## [2.9.0] - 2024-02-28 ### Added - concord-server: option to permanently disable a user ([#875](https://github.com/walmartlabs/concord/pull/875)); - tasks: asserts ([#876](https://github.com/walmartlabs/concord/pull/876)); - concord-agent, dependency-manager: support for Maven offline mode ([#869](https://github.com/walmartlabs/concord/pull/869)); - concord-server: skip repository refresh when repo is disabled ([#872](https://github.com/walmartlabs/concord/pull/872)); - runtime-v2: threadId to task details ([#874](https://github.com/walmartlabs/concord/pull/874)); - concord-console2: add more details to trigger list ([#878](https://github.com/walmartlabs/concord/pull/878)); ### Changed - concord-console: adjust polling frequency based on client activity ([#634](https://github.com/walmartlabs/concord/pull/634)); - runtime-v1: fix for resume from same step (bpm version up) ([#879](https://github.com/walmartlabs/concord/pull/879)); - cli: api client provider for cli (just to load tasks) ([#877](https://github.com/walmartlabs/concord/pull/877)); - ansible: add module_defaults callback, remove deprecated gather_subset in config ([#873](https://github.com/walmartlabs/concord/pull/873)); - runtime-v2: ignore empty string as sensitive data ([#871](https://github.com/walmartlabs/concord/pull/871)); - project: fix maven compiler source version in parent pom ([#870](https://github.com/walmartlabs/concord/pull/870)). ## [2.8.0] - 2024-01-15 ### Added - concord-console2: kv capacity ([#795](https://github.com/walmartlabs/concord/pull/795)); - concord-server, concord-console2: ability to restart runtime-v2 processes ([#850](https://github.com/walmartlabs/concord/pull/850)). ### Changed - concord-server: invalidate session on failed login ([#859](https://github.com/walmartlabs/concord/pull/859)); - runtime-v2: error location for loop, call, parallel, retry commands (v2) ([#865](https://github.com/walmartlabs/concord/pull/865)); - runtime-v2: fix incorrect variable merging for set variables step ([#862](https://github.com/walmartlabs/concord/pull/862)). ## [2.7.0] - 2024-01-08 ### Added - concord-cli: Add option for default task variables ([#848](https://github.com/walmartlabs/concord/pull/848)). ### Changed - runtime-v2: resume event to json serialization fix ([#860](https://github.com/walmartlabs/concord/pull/860)); - project: drop siesta-server dependency ([#826](https://github.com/walmartlabs/concord/pull/826)); - resource-task: writeYaml: do not split YAML into multiple lines ([#854](https://github.com/walmartlabs/concord/pull/854)); - concord-server: logout any session on login failure ([#858](https://github.com/walmartlabs/concord/pull/858)). ### Breaking - project: drop siesta-api dependency ([#857](https://github.com/walmartlabs/concord/pull/857)). ## [2.6.0] - 2023-12-28 ### Added - concord-server: expose fetch with version ([#853](https://github.com/walmartlabs/concord/pull/853)); - server: allow regexp in meta filters ([#852](https://github.com/walmartlabs/concord/pull/852)). ### Changed - project: switch to concord-client2 ([#821](https://github.com/walmartlabs/concord/pull/821)); - concord-server: remove more @Named ([#839](https://github.com/walmartlabs/concord/pull/839)); - client2: allow serialize collections ([#846](https://github.com/walmartlabs/concord/pull/846)); - runtime-v2: skip annotations for varargs ([#845](https://github.com/walmartlabs/concord/pull/845)); - concord-repository: fetch with quiet option ([#851](https://github.com/walmartlabs/concord/pull/851)). ## [2.5.0] - 2023-12-10 ### Added - concord-server: support @Priority annotation when binding Jetty components ([#841](https://github.com/walmartlabs/concord/pull/841)); ### Changed - runtime-v2: allow "true|false" string in if expression ([#844](https://github.com/walmartlabs/concord/pull/844)); - docker-images: Upgrade default Ansible installation to 2.14 ([#843](https://github.com/walmartlabs/concord/pull/843)); - ansible-plugin: callback compatibility for Ansible 2.14 ([#842](https://github.com/walmartlabs/concord/pull/842)); - concord-server: resume process now returns BAD_REQUEST if no event found ([#838](https://github.com/walmartlabs/concord/pull/838)). ### Breaking - docker-images: drop CentOS-based images, use Debian by default ([#843](https://github.com/walmartlabs/concord/pull/843). ## [2.4.0] - 2023-11-26 ### Added - concord-server: add `EXTRA_CLASSPATH` to start script ([#836](https://github.com/walmartlabs/concord/pull/836)); ### Changed - concord-agent-operator: use JDK 17 base image ([#836](https://github.com/walmartlabs/concord/pull/836)); - concord-common: shared ObjectMapperProvider ([#836](https://github.com/walmartlabs/concord/pull/836)). ## [2.3.0] - 2023-11-21 ### Added - testing-concord-server: add getter for the server instance ([#832](https://github.com/walmartlabs/concord/pull/832)); - testing-concord-server: add agent wrapper, simple test ([835](https://github.com/walmartlabs/concord/pull/835)). ### Changed - project: attach source jars only on release ([#832](https://github.com/walmartlabs/concord/pull/832)); - concord-server: auto-wire modules in concord-server/dist instead of impl ([#834](https://github.com/walmartlabs/concord/pull/834)). ## [2.2.0] - 2023-11-13 ### Added - pfed-sso: enable bearer token authentication ([#811](https://github.com/walmartlabs/concord/pull/811)). ### Changed - runtime-v2: fix exit from parallel loop #830 ([#830](https://github.com/walmartlabs/concord/pull/830)); - console2: calculate process duration from process last running timestamp ([#794](https://github.com/walmartlabs/concord/pull/794)); - console2: do not drop secrets form values on error/password check fail ([#798](https://github.com/walmartlabs/concord/pull/798)); - project: attach javadoc jars only on release ([#823](https://github.com/walmartlabs/concord/pull/823)); - project: upgrade to source level 17 ([#824](https://github.com/walmartlabs/concord/pull/824)); - project: remove more @Named usage ([#828](https://github.com/walmartlabs/concord/pull/828)). ## [2.1.0] - 2023-10-10 ### Added - new concord-client-v2 ([#810](https://github.com/walmartlabs/concord/pull/810)); - runtime-v2: hasFlow function ([#813](https://github.com/walmartlabs/concord/pull/813)); - runtime-v2: uuid function ([#812](https://github.com/walmartlabs/concord/pull/812)); - runtime-v2: allow listen to project load events at runtime ([#785](https://github.com/walmartlabs/concord/pull/785)); - console2: allow changing JSON store org ([#790](https://github.com/walmartlabs/concord/pull/790)). ### Changed - runtime-v2: automatically convert non serializable map.entry to serializable in exp ([#815](https://github.com/walmartlabs/concord/pull/815)); - server: return 404 when repository is not found ([#806](https://github.com/walmartlabs/concord/pull/806)); - runtime-v2: fix global vars update after resume ([#809](https://github.com/walmartlabs/concord/pull/809)); - console2: handle procesess with commitId, but without repoUrl ([#807](https://github.com/walmartlabs/concord/pull/807)); - runtime-v2: fix initialize of array expression ([#800](https://github.com/walmartlabs/concord/pull/800)); - server: only admins can access policies ([#792](https://github.com/walmartlabs/concord/pull/792)); - cli: active profiles fix ([#789](https://github.com/walmartlabs/concord/pull/789)). ## [2.0.0] - 2023-08-16 # Breaking - project: drop support for JDK 8 and JDK 11. Make JDK 17 the new default version. ## [1.103.0] - 2023-07-16 ### Added - runtime-v2: hide sensitive data in MapELResolver ([#781](https://github.com/walmartlabs/concord/pull/781)); - tasks-v2: use debug flag from process configuration ([#780](https://github.com/walmartlabs/concord/pull/780)); - concord-console2: show process duration on toolbar ([#779](https://github.com/walmartlabs/concord/pull/779)); - concord-console2: allow customizing columns in the main process table ([#777](https://github.com/walmartlabs/concord/pull/777)); - console2: added `last updated at` and `age` to the secret page ([#775](https://github.com/walmartlabs/concord/pull/775)); - runtime-v2: hasNonNullVariable function ([#774](https://github.com/walmartlabs/concord/pull/774)); - runtime-v2: log call stack on error ([#761](https://github.com/walmartlabs/concord/pull/761)); - concord-server: Allow restriction of secrets to multiple projects ([#688](https://github.com/walmartlabs/concord/pull/688)). ### Changed - server: fix DB cleanup job ([#784](https://github.com/walmartlabs/concord/pull/784)); - runtime-v2: hide stacktrace for UserDefinedException ([#782](https://github.com/walmartlabs/concord/pull/782)); - console2: enable save button on repository submit error ([#771](https://github.com/walmartlabs/concord/pull/771)); - runtime-v2: handle NPE in expressions ([#776](https://github.com/walmartlabs/concord/pull/776)); - concord-ansible-plugin: fix handling of play and task names longer than 1024 chars ([#772](https://github.com/walmartlabs/concord/pull/772)); - console2, server: redirect to requested URL after oidc/sso auth ([#764](https://github.com/walmartlabs/concord/pull/764)); - console2: do not remove project after rename ([#770](https://github.com/walmartlabs/concord/pull/770)); - runtime-v2: fix timezone text case in DSL schema ([#769](https://github.com/walmartlabs/concord/pull/769)); - docker-images: fix build for Debian 12 based images ([#767](https://github.com/walmartlabs/concord/pull/767)); - runtime-v2: serialization fix ([#758](https://github.com/walmartlabs/concord/pull/758)); - concord-cli: add no-default-cfg option ([#763](https://github.com/walmartlabs/concord/pull/763)); - concord-cli: reduce noise in dependency resolution errors ([#757](https://github.com/walmartlabs/concord/pull/757)); - console2: do not remove project after rename; ([#770](https://github.com/walmartlabs/concord/pull/770)). ## [1.102.0] - 2023-05-22 ### Added - concord-server: allow any GH event attribute in `exclusive.groupBy` ([#753](https://github.com/walmartlabs/concord/pull/753)); - concord-server, concord-policy: ability to restrict `runtime` type for project processes created after set date (e.g. to forbid usage of older runtimes in new projects) ([#745](https://github.com/walmartlabs/concord/pull/745)). ### Changed - concord-server, concord-console2: handle empty process lists in wait condition ([#756](https://github.com/walmartlabs/concord/pull/756)); - concord-task: ignore suspend if no processes provided ([#755](https://github.com/walmartlabs/concord/pull/755)); - concord-server: refresh repository triggers synchronously ([#734](https://github.com/walmartlabs/concord/pull/734)); - runtime-v2, cli: hide parallel block stacktraces for UserDefinedExceptions ([#751](https://github.com/walmartlabs/concord/pull/751)); - runtime-v2: hide stacktraces in propertyNotFound exceptions, improve error messages ([#752](https://github.com/walmartlabs/concord/pull/752)); - runtime-v2: allow expressions in `parallelism` values ([#746](https://github.com/walmartlabs/concord/pull/746)); - server: `created_at` DB field to projects table ([#744](https://github.com/walmartlabs/concord/pull/744)); - runtime-v2: allow increment variables in expressions ([#740](https://github.com/walmartlabs/concord/pull/740)). ## [1.101.0] - 2023-03-29 ### Added - server: update process policy on process resume ([#731](https://github.com/walmartlabs/concord/pull/731)). ### Changed - concord-server: allow auth plugins handle authorization token ([#737](https://github.com/walmartlabs/concord/pull/737)); - concord-server: remove more Named usage ([#729](https://github.com/walmartlabs/concord/pull/729)) - concord-server: truncate `createdAt` nanoseconds when creating new process keys ([#736](https://github.com/walmartlabs/concord/pull/736); - concord-console: fix rendering of multiple string values in forms ([#735](https://github.com/walmartlabs/concord/pull/735)). ## [1.100.0] - 2023-03-09 ### Added - runtime-v2: mask sensitive data in logs ([#719](https://github.com/walmartlabs/concord/pull/719)); - cli: process/project info from variables ([#727](https://github.com/walmartlabs/concord/pull/727)); - runtime-v2: support for "session state" process attachments ([#722](https://github.com/walmartlabs/concord/pull/722)). ### Changed - concord-console: fix v2 log segment spinner after interrupted process ([#728](https://github.com/walmartlabs/concord/pull/728)); - cli: log errors from dependency resolver only in verbose mode ([#723](https://github.com/walmartlabs/concord/pull/723)); - cli: log flow step name (if provided) ([#724](https://github.com/walmartlabs/concord/pull/724)); - agent: log artifact resolve errors only in debug mode ([#725](https://github.com/walmartlabs/concord/pull/725)); - runtime-v2: predictable order of process arguments ([#721](https://github.com/walmartlabs/concord/pull/721)); - concord-server: remove more @Named usage ([#650](https://github.com/walmartlabs/concord/pull/650)). ## [1.99.0] - 2023-02-24 ### Added - concord-server: implement removal of disabled user accounts. Old accounts can now be automatically removed (the feature is disabled by default) ([#716](https://github.com/walmartlabs/concord/pull/716)); - runtime-v2: allow custom JS lang levels in scripts ([#709](https://github.com/walmartlabs/concord/pull/709)); - runtime-v2: function for throw exception ([#712](https://github.com/walmartlabs/concord/pull/712)); - concord-server: added 'last updated at' field for kv records ([#701](https://github.com/walmartlabs/concord/pull/701)); - policy-engine, server: initial support for the KV store policies ([#702](https://github.com/walmartlabs/concord/pull/702)); - concord-server: pass external trigger event ID via process arguments ([#715](https://github.com/walmartlabs/concord/pull/715)). ### Changed - server: use clean directory for each refresh listener ([#707](https://github.com/walmartlabs/concord/pull/707)); - concord-server: refactor ConcordLdapContextFactory implementation ([#695](https://github.com/walmartlabs/concord/pull/695)); - runtime-v2: improve serialization of `loop` items ([#714](https://github.com/walmartlabs/concord/pull/714)); - concord-agent, queue-client: make more delays configurable ([#705](https://github.com/walmartlabs/concord/pull/705)); - console2: single vertical scroll for process log page ([#696](https://github.com/walmartlabs/concord/pull/696)); - console2: allow expand all log segments ([#698](https://github.com/walmartlabs/concord/pull/698)); - dependency-manager: log exception ([#700](https://github.com/walmartlabs/concord/pull/700)); - cli: check that state is serializable in checkpoint service ([#703](https://github.com/walmartlabs/concord/pull/703)); - runtime-v2: allow expression for form call values and runAs ([#704](https://github.com/walmartlabs/concord/pull/704)); - runtime-v2: fix argument passing in forks ([#708](https://github.com/walmartlabs/concord/pull/708)). ## [1.98.2] - 2023-02-08 ### Changed - concord-server: clean nulls from trigger conditions, args, cfg ([#713](https://github.com/walmartlabs/concord/pull/713)); ## [1.98.1] - 2022-12-22 ### Changed - concord-server: @Inject refactoring (part 1) ([#658](https://github.com/walmartlabs/concord/pull/658)); - concord-server, oidc: OIDC team/role mapping. Maps OpenID properties (e.g. `groups`) to Concord teams and roles ([#682](https://github.com/walmartlabs/concord/pull/682)); - concord-server: `process_queue` table split (part 1) ([#668](https://github.com/walmartlabs/concord/pull/668)); - runtime-v2: do not create log segments for expressions by default. Logs produced by expression blocks without `name` will no longer be displayed as a separate log "segment"; ([#689](https://github.com/walmartlabs/concord/pull/689)); - concord-console: new compact view for the Log tab ([#690](https://github.com/walmartlabs/concord/pull/690)); - concord-server-db: a migration task to update secrets using the updated hashing algorithm ([#691](https://github.com/walmartlabs/concord/pull/691)); - concord-task: fix concurrency issue when collecting output of processes ([#693](https://github.com/walmartlabs/concord/pull/693)); - concord-server-db: pass secret salt as a base64 value ([#694][https://github.com/walmartlabs/concord/pull/689]). ## [1.98.0] - 2022-12-07 ### Added - runtime-v2: provide checkpoint name after restore ([#677](https://github.com/walmartlabs/concord/pull/677)); - policy: new policy to restrict raw payload ([#679](https://github.com/walmartlabs/concord/pull/679)); - concord-cli: provide default process configuration ([#649](https://github.com/walmartlabs/concord/pull/649)); - policy: policy to restrict runtime of process ([#671](https://github.com/walmartlabs/concord/pull/671)); - resource-task: add printJson() method ([#676](https://github.com/walmartlabs/concord/pull/676)); - server: cleanup agent commands ([#674](https://github.com/walmartlabs/concord/pull/674)); - policy-engine: `cron` trigger policy ([#686](https://github.com/walmartlabs/concord/pull/686)). ### Changed - runtime-v2: fix parallel loop execution when no out variable defined ([#659](https://github.com/walmartlabs/concord/pull/659)); - console2: repository list now with paging ([#643](https://github.com/walmartlabs/concord/pull/643)); - server: api for list project repositories with limit/offset ([#643](https://github.com/walmartlabs/concord/pull/643)); - runtime-v2: "throw" with a string value shouldn't produce a stacktrace ([#673](https://github.com/walmartlabs/concord/pull/673)); - concord-server: deprecate `process_queue.commit_msg` ([#670](https://github.com/walmartlabs/concord/pull/670)); - runtime-v2: move expression evaluator into sdk ([#667](https://github.com/walmartlabs/concord/pull/667)); - cli: log checkpoint instead of throwing Exception ([#665](https://github.com/walmartlabs/concord/pull/665)); - http-task: allow any value as json body ([#675](https://github.com/walmartlabs/concord/pull/675)); - docker-images: change the default shell to bash in Debian-based images ([#644](https://github.com/walmartlabs/concord/pull/675)); - runtime-v2: fix `entryPoint` calculation in effective YAML ([#685](https://github.com/walmartlabs/concord/pull/685)). ## [1.97.0] - 2022-10-11 ### Added - github: queryParams condition ([#663](https://github.com/walmartlabs/concord/pull/663)); - dependency-manager: allow exclusion artifacts from transitive dependencies ([#657](https://github.com/walmartlabs/concord/pull/657)). ### Changed - concord-cli: load deps from active profiles ([#654](https://github.com/walmartlabs/concord/pull/654)); - runtime-v2: fix parallel execution of ruby scripts ([#651](https://github.com/walmartlabs/concord/pull/651)); - concord-server: termintate process wait watchdog loop on batches less than fetch limit ([#656](https://github.com/walmartlabs/concord/pull/656)); - runtime-v2: fix serialization error of flow call command ([#655](https://github.com/walmartlabs/concord/pull/655)); - concord-cli: ensure absolute target dir ([#652](https://github.com/walmartlabs/concord/pull/652)); - runtime-v2: allow access to current argument when argument is evaluated ([#664](https://github.com/walmartlabs/concord/pull/664)). ## [1.96.1] - 2022-09-06 ### Added ### Changed - project: initial JDK 17 support ([#625](https://github.com/walmartlabs/concord/pull/625)); - concord-console: fix for change visibility and renaming of secrets from UI ([#642](https://github.com/walmartlabs/concord/pull/642)); - runtime-v2: runtime-v2: fix NPE in flow call step ([#645](https://github.com/walmartlabs/concord/pull/645)); - concord-server: remove log call for github event in repository refresh flow ([#633](https://github.com/walmartlabs/concord/pull/633)); ## [1.96.0] - 2022-08-10 ### Added - concord-cli: option to show version ([#615](https://github.com/walmartlabs/concord/pull/615)); - concord-server: implement endpoints for adding LDAP groups to roles ([#606](https://github.com/walmartlabs/concord/pull/606)); - concord-ansile, concord-console: add sort options to the Ansible host stats ([#610](https://github.com/walmartlabs/concord/pull/610); - docker-images: support for debian os based docker images ([#611](https://github.com/walmartlabs/concord/pull/611)). ### Changed - concord-server: fix out vars processing and restrictions ([#609](https://github.com/walmartlabs/concord/pull/609); - concord-cli: fixed broken JS support ([#612](https://github.com/walmartlabs/concord/pull/612)); - concord-repository: use regular repositories in tests ([#616](https://github.com/walmartlabs/concord/pull/616)); - concord-server, runtime-v2: fix file upload in forms ([#623](https://github.com/walmartlabs/concord/pull/623)); - agent-operator: support for apiextensions.k8s.io/v1 crd to support k8s 1.22+ ([#624](https://github.com/walmartlabs/concord/pull/624)); - concord-server: limit the number of acceptor threads to `core count / 4` (min 1) ([#627](https://github.com/walmartlabs/concord/pull/627)); - project: update to Groovy 2.5.17 to support JDK 17 ([#639](https://github.com/walmartlabs/concord/pull/639)). ## [1.95.0] - 2022-04-16 ### Added - concord-server: add API for updating secrets ([#590](https://github.com/walmartlabs/concord/pull/590)); - http-tasks: add proxy authentication parameters ([#597](https://github.com/walmartlabs/concord/pull/597)); - ansible-tasks: implement stats file for flows with multiple playbook runs ([#596](https://github.com/walmartlabs/concord/pull/596)); - runtime-v1, v2: add correlationId to checkpoint events ([#581](https://github.com/walmartlabs/concord/pull/581)); - resource-tasks, runtime-v2: support for properties files ([#593](https://github.com/walmartlabs/concord/pull/593)). ### Changed - project: improve jdk16 compatibility ([#592](https://github.com/walmartlabs/concord/pull/592)); - concord-server: introduce exclusive wait conditions ([#595](https://github.com/walmartlabs/concord/pull/595)); - project: improve mvnd support ([#567](https://github.com/walmartlabs/concord/pull/567)); - runner: exit JVM on OOM error ([#594](https://github.com/walmartlabs/concord/pull/594)); - runtime-v2: fix `currentFlowName()` in error blocks ([#591](https://github.com/walmartlabs/concord/pull/591)); - runtime-v2: serialize ignoreErrors only if it is true ([#588](https://github.com/walmartlabs/concord/pull/588)); - resource-task: convert relative paths to absolute ([#589](https://github.com/walmartlabs/concord/pull/589)); - runtime-v2: redirect script output to logger ([#587](https://github.com/walmartlabs/concord/pull/587)); - agent: fix log segments parser ([#586](https://github.com/walmartlabs/concord/pull/586)); - resource-task: fix java 8 date/time serialization ([#584](https://github.com/walmartlabs/concord/pull/584)); - it: explicitly specify initialBranch for git tests ([#582](https://github.com/walmartlabs/concord/pull/582)). ## [1.93.3] - 2022-03-11 ### Changed - agent: fix log segments parse ([#586](https://github.com/walmartlabs/concord/pull/586)). ## [1.94.0] - 2022-03-07 ### Added - concord-server: add `orgUpdate` permission ([#552](https://github.com/walmartlabs/concord/pull/552)); - runtime-v2: add `orDefault` function ([#557](https://github.com/walmartlabs/concord/pull/557)); - runtime-v2: add `isDebug` function ([#558](https://github.com/walmartlabs/concord/pull/558)). - agent: option to ignore artifact descriptor repositories ([#561](https://github.com/walmartlabs/concord/pull/561)); - runtime-v2: project document support for suspendTimeout ([#562](https://github.com/walmartlabs/concord/pull/562)); - concord-server: add process-wait-watchdog metrics ([#566](https://github.com/walmartlabs/concord/pull/566)); - runtime-v2: implement `loop` syntax - improved version of `(parallel)withItems` ([#578](https://github.com/walmartlabs/concord/pull/578)). ### Changed - concord-agent: use a single temporary directory for API clients ([#544](https://github.com/walmartlabs/concord/pull/544)); - runtime-v1/v2: remove temporary Docker files ([#545](https://github.com/walmartlabs/concord/pull/545)); - runtime-v2: support for runAs (running as another user) in cron ([#547](https://github.com/walmartlabs/concord/pull/547)); - project: update Guava version, remove unneeded usage ([#550](https://github.com/walmartlabs/concord/pull/550)); - runtime-v1/v2: use Nashorn compat mode for GraalVM ([#551](https://github.com/walmartlabs/concord/pull/551)); - project: upgrade dependencies ([#554](https://github.com/walmartlabs/concord/pull/554)); - runtime-v2: remove `configuration.activeProfiles` from the JSON schema (specifying `activeProfiles` in YAML documents was never supported) ([#556](https://github.com/walmartlabs/concord/pull/556)); - runtime-v2: allow arrays for GH trigger conditions ([#563](https://github.com/walmartlabs/concord/pull/563)); - ansible: clear host filters after switching to the next stat tab ([#575](https://github.com/walmartlabs/concord/pull/575)); - ansible: filter host groups by playbookId ([#574](https://github.com/walmartlabs/concord/pull/574)); - runtime-v2: allow null values in configuration.arguments ([#571](https://github.com/walmartlabs/concord/pull/571)); - concord-server: implement a better way to kill processes ([#572](https://github.com/walmartlabs/concord/pull/572)); - runtime-v2: fix currentFlowName after restoring from a checkpoint ([#580](https://github.com/walmartlabs/concord/pull/580)). ## [1.93.2] - 2022-02-17 ### Changed - runtime-v1: do not override process arguments with default variables ([#569](https://github.com/walmartlabs/concord/pull/569)). ## [1.93.1] - 2022-02-11 ### Added - concord-server: add orgUpdate permission ([#552](https://github.com/walmartlabs/concord/pull/552)). ### Changed - runtime-v2: fix segment status parse ([#549](https://github.com/walmartlabs/concord/pull/549)); - graalvm: use nashorn compat mode ([#551](https://github.com/walmartlabs/concord/pull/551)); - agent: allow ignore artifact descriptor repositories ([#561](https://github.com/walmartlabs/concord/pull/561)). ## [1.93.0] - 2022-01-24 ### Added - runtime-v2: function to retrieve the current flow name ([#514](https://github.com/walmartlabs/concord/pull/514)); - runtime-v2: add `evalAsMap` flow function ([#520](https://github.com/walmartlabs/concord/pull/520)); - concord-agent: add dependencyResolveTimeout configuration parameter ([#522](https://github.com/walmartlabs/concord/pull/522)); - concord-parent: support for Apple M1 silicon ([#527](https://github.com/walmartlabs/concord/pull/527)); - concord-console: add icon for suspended status ([#530](https://github.com/walmartlabs/concord/pull/530)). ### Changed - runtime-v2: disable GraalVM runtime compilation warning, fix logging ([#517](https://github.com/walmartlabs/concord/pull/517)); - runtime-v2: use stdout instead of files for logging ([#518](https://github.com/walmartlabs/concord/pull/518)); - concord-server, runtime-v2: fix for empty exclusive group values ([#519](https://github.com/walmartlabs/concord/pull/519); - concord-server, lock-tasks: avoid creating a wait condition on each lock aquisition attempt ([#521](https://github.com/walmartlabs/concord/pull/521)); - concord-agent: log artifact download errors into process log ([#523](https://github.com/walmartlabs/concord/pull/523)); - project: migrate to JUnit 5 ([#528](https://github.com/walmartlabs/concord/pull/528)); - runtime-v1: remove variables from default vars with the same names as task names to avoid conflicts ([#529](https://github.com/walmartlabs/concord/pull/529)); - concord-server: remove `/api/service/process_portal` ([#531](https://github.com/walmartlabs/concord/pull/531)); - runtime-v2: log errors when `ignoreErrors` specified ([#532](https://github.com/walmartlabs/concord/pull/532)); - concord-server: better checkpoint data validation ([#533](https://github.com/walmartlabs/concord/pull/533)); - runtime-v2: update JSON schema generation for `ignoreErrors` params ([#536](https://github.com/walmartlabs/concord/pull/536)); - project: add missing `serialVersionUUID` fields to model classes ([#537](https://github.com/walmartlabs/concord/pull/537)); - concord-console, runtime-v2: do not automatically open the system log segment ([#539](https://github.com/walmartlabs/concord/pull/539)); - runtime-v2: support for `out` parameters of `script` steps ([#540](https://github.com/walmartlabs/concord/pull/540)). ## [1.92.0] - 2021-12-07 ### Added - runtime-v2: `ignoreErrors` mode support for tasks ([#484](https://github.com/walmartlabs/concord/pull/484)); - concord-server: record audit log when processes are cancelled ([#495](https://github.com/walmartlabs/concord/pull/495)); - file-tasks: new methods - `move`, `relativize` ([#498](https://github.com/walmartlabs/concord/pull/498)); - crypto-v2: allow users to specify `dest` directory when calling `exportAsFile` ([#499](https://github.com/walmartlabs/concord/pull/499)); - concord-server: allow users to override timeout for error handlers using new process configuration property `handlerProcessTimeout` ([#501](https://github.com/walmartlabs/concord/pull/501)); - http-tasks: log invalid JSON bodies and parse errors ([#505](https://github.com/walmartlabs/concord/pull/505)). ### Changed - docker: fixed the behavior of `stdout` parameter ([#472](https://github.com/walmartlabs/concord/pull/472)); - concord-task: fix `suspendForCompletion` action in the v2 version of the task ([#488](https://github.com/walmartlabs/concord/pull/488)); - concord-task: return fork IDs in the `ids` variable even for single-fork calls (v2 only) ([#488](https://github.com/walmartlabs/concord/pull/488)); - runtime-v2: fix exception when `MetadataProcessor` called after an `exit` step ([#491](https://github.com/walmartlabs/concord/pull/491)); - concord-server: fix concurrent process status update ([#493](https://github.com/walmartlabs/concord/pull/493)); - runtime-v2: fixed an issue with variable propagation in the presense of an `error` block in flow calls ([#496](https://github.com/walmartlabs/concord/pull/496)); - runtime-v1, runtime-v2: tone down the metadata processor's logs ([#500](https://github.com/walmartlabs/concord/pull/500)); - smtp-tasks: simplify default variables, allow `host` and `port` parameters without nesting into `smtpParams` ([#503](https://github.com/walmartlabs/concord/pull/503)). ### Breaking - concord-task: return process IDs as `String` instead of `UUID` ([#488](https://github.com/walmartlabs/concord/pull/488)). - runtime-v2, docker: replace the `logOutput` parameter with a number of new fine-grained controls - `redirectErrorStream`, `logOut`, `logErr`, `saveOut` and `saveErr` ([#489](https://github.com/walmartlabs/concord/pull/489)); - concord-server, concord-console: remove the old deprecated Ansible UI and related API endpoints ([#497](https://github.com/walmartlabs/concord/pull/497)); - concord-server: remove support for `process.defaultConfiguration` configuration parameter. This effectively removes concord-server's support of default process variable files (#504). ## [1.91.0] - 2021-11-05 ### Added - concord-server, concord-console: allow users to disable triggers in repositories ([#476](https://github.com/walmartlabs/concord/pull/476)); - runtime-v2: add feature to record metadata in tasks call ([#479](https://github.com/walmartlabs/concord/pull/476)). ### Changed - concord-agent: do not retry logs on 4xx-5xx response ([#475](https://github.com/walmartlabs/concord/pull/475)). - concord-server: throw error on null teams list in bulk access update ([#477](https://github.com/walmartlabs/concord/pull/477)); - concord-repository: fix `checkAlreadyFetched` behavior when checking out an older commit using the current branch ([#480](https://github.com/walmartlabs/concord/pull/480)). ## [1.90.0] - 2021-09-16 ### Added - runtime-v2: log effective process dependencies in debug mode ([#467](https://github.com/walmartlabs/concord/pull/467)). ### Changed - project: switch to AdoptOpenJDK, initial support for JDK 11 and 16. The default Docker images use JDK 8 by default ([#434](https://github.com/walmartlabs/concord/pull/434)); - concord-server-db: fix checksum expectations for API key related changesets ([#463](https://github.com/walmartlabs/concord/pull/463)); - concord-server: remove an old, unused task from the DB ([#464](https://github.com/walmartlabs/concord/pull/464)); - runtime-v2: improved parser error message ([#465](https://github.com/walmartlabs/concord/pull/465)); - concord-server: fix a potential race condition in the process dispatcher ([#466](https://github.com/walmartlabs/concord/pull/466)); - concord-server: do not create http sessions for api-key auth ([#471](https://github.com/walmartlabs/concord/pull/471)); - concord-server: disable servlet sessions for session token authentication ([#473](https://github.com/walmartlabs/concord/pull/473)); - runtime-v1, runtime-v2: ability to disable events recording ([#474](https://github.com/walmartlabs/concord/pull/474)). ## [1.89.2] - 2021-09-14 ### Changed - concord-server: do not create http sessions for api-key auth ([#471](https://github.com/walmartlabs/concord/pull/471)); - concord-server: disable http sessions for session token auth ([#473](https://github.com/walmartlabs/concord/pull/473)). ## [1.89.1] - 2021-09-03 ### Changed - concord-server-db: fix checksum expectations for API key related changesets ([#463](https://github.com/walmartlabs/concord/pull/463)); - concord-server: remove unused task from DB ([#464](https://github.com/walmartlabs/concord/pull/464)). ## [1.89.0] - 2021-08-22 ### Added - concord-server: ability to load user API keys from a local file ([#457](https://github.com/walmartlabs/concord/pull/457)). ### Changed - runtime-v2: sanitize script variables, make sure values are `Serializable` ([#458](https://github.com/walmartlabs/concord/pull/458)); - concord-server: do not mark processes that have been in the `NEW` status for a long time as failed to start ([#459](https://github.com/walmartlabs/concord/pull/459)); - pfed-sso: redirect to login on failure ([#462](https://github.com/walmartlabs/concord/pull/462)). ## [1.88.1] - 2021-08-06 ### Changed - runtime-v2: allow step names to be placed anywhere in the step's definition (i.e. not necessarily as the first element) ([#452](https://github.com/walmartlabs/concord/pull/452)); - runtime-v2: fix evaluation of `retry` expressions. Now expressions are allowed to return any `Number` (e.g. `Integer`, `Long`, etc) ([#454](https://github.com/walmartlabs/concord/pull/454)); - runtime-v2: fix input parameter override on `retry`; ([#455](https://github.com/walmartlabs/concord/pull/455)). ## [1.88.0] - 2021-07-29 ### Added - concord-server: a new API endpoint to force sync LDAP groups of a specific user ([#442](https://github.com/walmartlabs/concord/pull/442)); - runtime-v1, runtime-v2: add support for `*.yaml` Concord files in addition to `*.yml` ([#443](https://github.com/walmartlabs/concord/pull/443)); - runtime-v2: allow expressions to be used as the `in` block value in `task`, `flow` and `script` steps ([#447](https://github.com/walmartlabs/concord/pull/447)). ### Changed - concord-server: ignore synthetic methods annotated with `WithTimer` ([#444](https://github.com/walmartlabs/concord/pull/444)); - concord-targetplatform: update jackson-databind version to address [CVE](https://github.com/advisories/GHSA-288c-cq4h-88gq) ([#449](https://github.com/walmartlabs/concord/pull/449)); - concord-server: roll back changes introduced in [#390](https://github.com/walmartlabs/concord/pull/390) ([#450](https://github.com/walmartlabs/concord/pull/450)); - ansible: fix retry file persistance (`saveRetry` task parameter) ([#451](https://github.com/walmartlabs/concord/pull/451)). ## [1.87.0] - 2021-07-12 ### Added - concord-task: `start`, `startExternal` and `fork` actions now return process IDs ([#427](https://github.com/walmartlabs/concord/pull/427)); - concord-server: add more metrics for wait conditions ([#438](https://github.com/walmartlabs/concord/pull/438)). ### Changed - concord-cli, concord-server, concord-agent: ability to run Concord on Java 16 ([#429](https://github.com/walmartlabs/concord/pull/429)); - concord-server: fix insertion of wait conditions for forked processes ([#430](https://github.com/walmartlabs/concord/pull/430)); - concord-console: fix checkpoint color for failed processes ([#432](https://github.com/walmartlabs/concord/pull/432)); - concord-console: fix the editor component initialization. Affects the JSON store query and the project configuration editors ([#433](https://github.com/walmartlabs/concord/pull/433)); - concord-server: clean up repository cache using a separate thread ([#436](https://github.com/walmartlabs/concord/pull/436)); - runtime-v2: add grammar for the `files` condition in GitHub triggers ([#437](https://github.com/walmartlabs/concord/pull/437)); - concord-server: use optimistic locking for wait conditions ([#439](https://github.com/walmartlabs/concord/pull/439)); - concord-server: send `PROCESS_STATUS` events to listeners (fix for the regression in 1.85.0+) ([#440](https://github.com/walmartlabs/concord/pull/440)); - concord-server: cancel listeners on exception ([#441](https://github.com/walmartlabs/concord/pull/441)). ## [1.86.3] - 2021-06-16 ### Changed - concord-server: update metrics version, use lock free timers ([#425](https://github.com/walmartlabs/concord/pull/425)). ## [1.86.2] - 2021-06-02 ### Added - http-tasks: ability to provide a custom trust stores ([#399](https://github.com/walmartlabs/concord/pull/399)); - concord-server: new configuration parameter `db.changeLogParameters.defaultAgentToken`. Sets the default API token for the Agent ([#410](https://github.com/walmartlabs/concord/pull/410)); - concord-server: add DNS Service Record feature ldap (dynamic loading of LDAP server address) ([#412](https://github.com/walmartlabs/concord/pull/412)); - smtp-tasks: read default parameters (`defaultProcessCfg` policy) in the runtime-v2 version of the task ([#419](https://github.com/walmartlabs/concord/pull/419)); - resource-tasks: parse object from JSON string ([#420](https://github.com/walmartlabs/concord/pull/420)). ### Changed - dependency-manager: update the `maven-resolver` version ([#405](https://github.com/walmartlabs/concord/pull/405)); - concord-server: the `iam-sso` plugin has been removed ([#407](https://github.com/walmartlabs/concord/pull/407)); - concord-repository: remove dependency on`jgit` ([#414](https://github.com/walmartlabs/concord/pull/414)); - concord-task: `startExternal` action should ignore the `suspend` parameter ([#416](https://github.com/walmartlabs/concord/pull/416)); - concord-console: update dependencies ([#418](https://github.com/walmartlabs/concord/pull/418)). ### Breaking - concord-server, concord-agent: the agent's default API token has been removed. The server now automatically generates a new API token on the first start ([#410](https://github.com/walmartlabs/concord/pull/410) and ([#413](https://github.com/walmartlabs/concord/pull/413)). ## [1.85.0] - 2021-04-27 ### Added - ansible: option to limit the logging verbosity based on the inventory size ([#384](https://github.com/walmartlabs/concord/pull/384)); - runtime-v2: support `name` attribute for script calls ([#402](https://github.com/walmartlabs/concord/pull/402)). ### Changed - concord-server: restoring from a checkpoint now generates a new `CHECKPOINT_RESTORE` event instead of a `PROCESS_STATUS` event with custom payload ([#389](https://github.com/walmartlabs/concord/pull/389)); - concord-server: use process events to calculate the process queue stats ([#390](https://github.com/walmartlabs/concord/pull/390)); - runtime-v2: rollback the state cleanup code added in [#358](https://github.com/walmartlabs/concord/pull/358). Fixed in ([#391](https://github.com/walmartlabs/concord/pull/391)); - concord-server: fix validation of form fields with expressions in allowed values ([#392](https://github.com/walmartlabs/concord/pull/392)); - concord-server: set SameSite=Lax for the session cookie ([#394](https://github.com/walmartlabs/concord/pull/394)); - concord-server: only pass enabled repositories to refresh task ([395](https://github.com/walmartlabs/concord/pull/395)); - concord-server: do not mark processes as `FAILED` after resuming from an invalid status ([#396](https://github.com/walmartlabs/concord/pull/396)). - concord-server: use optimistic locking when updating wait conditions ([#397](https://github.com/walmartlabs/concord/pull/397)); - docker-compose: fix agent and dind communication ([#400](https://github.com/walmartlabs/concord/pull/400)); - runtime-v2: log exception as a single error line ([#401](https://github.com/walmartlabs/concord/pull/401)); - concord-repository: specify path to the `.gitmodules` file when reading submodule urls ([#403](https://github.com/walmartlabs/concord/pull/403)); - runtime-v2: subsequent execution of reentrant tasks now uses the same log segment as for the first call ([#406](https://github.com/walmartlabs/concord/pull/406)). ## [1.84.0] - 2021-04-08 ### Added - concord-server: support multiple wait conditions per process ([#368](https://github.com/walmartlabs/concord/pull/368)); - concord-server: cleanup job for `wait_conditions`. Automatically remove `wait_conditions` of expunged processes ([#380](https://github.com/walmartlabs/concord/pull/380)); - concord-cli: option to generate the effective Concord YAML file ([#382](https://github.com/walmartlabs/concord/pull/382)); - runtime-v2: add example of Ansible's `register` statement usage ([#387](https://github.com/walmartlabs/concord/pull/387)). ### Changed - pfed-sso: now provides a `LdapPrincipal` for compatibility with legacy authentication providers ([#376](https://github.com/walmartlabs/concord/pull/376)); - concord-imports: hide sensitive data in `toString` methods ([#378](https://github.com/walmartlabs/concord/pull/378)); - runtime-v2: fix the duration format when generating an effective yaml ([#383](https://github.com/walmartlabs/concord/pull/383)). - concord-repository: automatically create `repositoryInfo` directory if not exists ([#386](https://github.com/walmartlabs/concord/pull/386)); - pref-sso: do not redirect if refresh token exists ([#388](https://github.com/walmartlabs/concord/pull/388)). ## [1.83.0] - 2021-03-25 ### Added - concord-server: log handler process IDs (`onTimeout`, `onCancel`, etc) in the parent process log ([#350](https://github.com/walmartlabs/concord/pull/350)); - concord-console: on the repository list page, add commit ID and repository path links; ([#351](https://github.com/walmartlabs/concord/pull/351)); - concord-console, runtime-v1, runtime-v2: ability to log process IDs as links in the UI ([#356](https://github.com/walmartlabs/concord/pull/356)); - concord-repository, concord-server, concord-agent: option to skip fetching if local commit ID equals remote commit ID ([#359](https://github.com/walmartlabs/concord/pull/359)). - concord-agent: option to redirect process logs to stdout ([#362](https://github.com/walmartlabs/concord/pull/362)); - concord-server, runtime-v2: it is now possible to resume a process waiting for multiple external events ([#370](https://github.com/walmartlabs/concord/pull/370)); - concord-server, concord-console: new process status `WAITING`. Before this change, the `SUSPENDED` status was used for both processes suspended on an event (e.g. on a form) and processes waiting for "external" conditions (e.g. concurrent execution limits, waiting for another process or lock, etc). This PR creates a clear separation in statuses for such cases ([#371](https://github.com/walmartlabs/concord/pull/371) and [#379](https://github.com/walmartlabs/concord/pull/379)). ### Changed - runtime-v2: implicitly pass variables into scripts ([#349](https://github.com/walmartlabs/concord/pull/349)); - concord-repository: disable git auto-maintenance ([#353](https://github.com/walmartlabs/concord/pull/353)); - runtime-v2: fix error handling in reentrable tasks ([#354](https://github.com/walmartlabs/concord/pull/354)). - concord-server, concord-console: fix the rendering of form dropdown fields with single allowed values ([#357](https://github.com/walmartlabs/concord/pull/357)) and ([372](https://github.com/walmartlabs/concord/pull/372)); - runtime-v2: cleanup `workDir` files after loading the state ([#358](https://github.com/walmartlabs/concord/pull/358)); - concord-client: skip empty bodies in error responses, use the HTTP status message instead ([#363](https://github.com/walmartlabs/concord/pull/363)); - runtime-v2: fixed the `exclusive` mode grammar in triggers (cron, generic, oneops) ([#364](https://github.com/walmartlabs/concord/pull/364)); - concord-server: set `initiator` for `onCancel`, `onFailure` and `onTimeout` handlers ([#365](https://github.com/walmartlabs/concord/pull/365)); - concord-console: update node.js to the current LTS (14.16.0) ([#366](https://github.com/walmartlabs/concord/pull/366)); - sleep-task: use UUIDs as event names to support parallel execution ([#369](https://github.com/walmartlabs/concord/pull/369)); - concord-agent: disable preforks by default, use stable `workDir` ([#374](https://github.com/walmartlabs/concord/pull/374)). ## [1.82.0] - 2021-03-08 ### Added - pfed-sso: add README ([#339](https://github.com/walmartlabs/concord/pull/339)); - runtime-v2: support for nested variables in `hasVariable` function ([#343](https://github.com/walmartlabs/concord/pull/343)); - examples: runtime-v2 demo ([#344](https://github.com/walmartlabs/concord/pull/344)); - http-tasks: support for external keystore files ([#345](https://github.com/walmartlabs/concord/pull/345)). ### Changed - runtime-v2: fix the detection of script languages based on the script's file name ([#339](https://github.com/walmartlabs/concord/pull/339)); - ansible: append values to `PYTHONPATH`, allow users to use their own `PYTHONPATH` in addition to the provided one ([#341](https://github.com/walmartlabs/concord/pull/341)); - concord-server: fixed batching of processes with custom `branch` or `commitId`. Previously the batching mechanism might determine the effective branch and/or commitId incorrectly ([#347](https://github.com/walmartlabs/concord/pull/347)); - concord-server: fix repository refresh filtering on GitHub event ([#348](https://github.com/walmartlabs/concord/pull/348)). ## [1.81.0] - 2021-03-01 ### Added - pfed-sso: add a configuration parameter for token signature validation ([#325](https://github.com/walmartlabs/concord/pull/325)); - runtime-v1, runtime-v2: optionally expose docker daemon ([#332](https://github.com/walmartlabs/concord/pull/332)); - concord-server, concord-console: add log segment duration ([#335](https://github.com/walmartlabs/concord/pull/335)). ### Changed - concord-server, pfed-sso: make sso login independent of ldap ([#327](https://github.com/walmartlabs/concord/pull/327)); - concord-console: add a redirect on SSO token/session expiration ([#327](https://github.com/walmartlabs/concord/pull/327)); - dependency-manager: remove dots from the `resolveFile` log, prevent URLs from being mangled in the UI ([#330](https://github.com/walmartlabs/concord/pull/330)); - concord-console: show runtime-v1 recorded in-vars ([#331](https://github.com/walmartlabs/concord/pull/331)); - concord-repository: fix fetching when both commitId and branch specified ([#329](https://github.com/walmartlabs/concord/pull/329)). - runtime-v2: fix Docker passwd generation ([#332](https://github.com/walmartlabs/concord/pull/332)); - concord-server, pfed-sso: get user information from the DB ([#333](https://github.com/walmartlabs/concord/pull/333)); - project: remove unused dependencies ([#334](https://github.com/walmartlabs/concord/pull/334)); - concord-server: allow overriding of default configuration values using environment variables ([#336](https://github.com/walmartlabs/concord/pull/336)); - concord-common: skip outside paths when unzipping files ([#337](https://github.com/walmartlabs/concord/pull/337)); - concord-server: sort log segments by IDs in addition to their timestamps to make the order more stable (e.g. for `parallel` situations) ([#338](https://github.com/walmartlabs/concord/pull/338)). ## [1.80.0] - 2021-02-14 ### Added - concord-server: new authentication plugin `pfed-sso` ([#318](https://github.com/walmartlabs/concord/pull/323)). ### Changed - docker-images: restrict `cryptography` dependency to 3.3.1 ([#323](https://github.com/walmartlabs/concord/pull/323)); - concord-server: use `java.time.Duration` for all intervals in the server configuration file ([#322](https://github.com/walmartlabs/concord/pull/322)); - concord-server: fix the batch cancellation of processes without an agent ([#317](https://github.com/walmartlabs/concord/pull/317)); - concord-server: if both `orgId` and `orgName` are given, the `/api/v2/process` filter ignores `orgName` ([#319](https://github.com/walmartlabs/concord/pull/319)). ## [1.79.0] - 2021-02-04 ### Added - concord-server: support for `configuration.suspendTimeout`. Allows users to specify the maximum amount of time the process can be in the `SUSPENDED` state; ([#315](https://github.com/walmartlabs/concord/pull/315)); - runtime-v2: add EL resolver for plain accessor methods, e.g. `MyBean#property()`. Allows seamless usage of classes generated by [Immutables](https://immutables.github.io/) ([#316](https://github.com/walmartlabs/concord/pull/316)); - concord-client: report the base API URL in error messages ([#312](https://github.com/walmartlabs/concord/pull/312)); - concord-tasks: add upsertQuery methods to the JSON store task ([#311](https://github.com/walmartlabs/concord/pull/311)). ### Changed - concord-repository: fixed the Git CLI command for retrieving commit info (extraneous `\n` in commit description) ([#313](https://github.com/walmartlabs/concord/pull/313)); - concord-console: make `projectId` and `projectName` optional in the `SecretEntry` definition (minor API usage fix) ([#306](https://github.com/walmartlabs/concord/pull/306)); - concord-client, runtime-v1, runtime-v2: make `SecretService` throw `SecretNotFoundException` when the requested secret is not found ([#309](https://github.com/walmartlabs/concord/pull/309)); - concord-console: fixed linking of organizations and secrets ([#308](https://github.com/walmartlabs/concord/pull/308)) - concord-server: fixed linking of projects and secrets ([#307](https://github.com/walmartlabs/concord/pull/307)) - concord-console: the project dropdown list on the new secret page and on the secret settings page was replaced with a search field ([#305](https://github.com/walmartlabs/concord/pull/305)). ## [1.78.0] - 2021-01-22 ### Added - concord-server: new `exclusive.mode` mode `cancelOld`. When a new process starts using the `cancelOld` mode, all currently running processes within the same `exclusive.group` are automatically cancelled ([#300](https://github.com/walmartlabs/concord/pull/300)); - concord-cli: notification when copying a large working directory into `target/` ([#302](https://github.com/walmartlabs/concord/pull/302)); - concord-server: new `github` trigger parameter - `exclusive.groupBy`. Currently accepts only `branch` value. Provides a way to cancel new or already running processes that were triggered by a `push` into the same Git branch ([#301](https://github.com/walmartlabs/concord/pull/301)); - concord-server: when starting a process using a Git repository, save the branch name in `process_queue` (in addition to `commit_id`) ([#296](https://github.com/walmartlabs/concord/pull/296)). ### Changed - concord-repository: allow passing branch, tag and/or commit ID as a single request parameter ([#304](https://github.com/walmartlabs/concord/pull/304)); - ansible: use [Apache Kerby](https://directory.apache.org/kerby/) instead of `sun.security.*` ([#303](https://github.com/walmartlabs/concord/pull/303); - concord-server: remove SQL parser, use restricted views for querying JSON stores ([#297](https://github.com/walmartlabs/concord/pull/297)); - concord-console: the secret dropdown list on the repository page is replaced with a search field ([#299](https://github.com/walmartlabs/concord/pull/299)). ## [1.77.0] - 2021-01-07 ### Added - dependency-manager: support for pre-emptive basic authentication when talking to Maven repositories ([#293](https://github.com/walmartlabs/concord/pull/293)); - runtime-v2: support for the `name` attribute in `try` and `block` elements ([#289](https://github.com/walmartlabs/concord/pull/289)); - runtime-v2: automatically generate and publish the JSON schema for `runtime: concord-v2` syntax ([#283](https://github.com/walmartlabs/concord/pull/283)); - concord-console, runtime-v2: show file name in the segment info popup ([#288](https://github.com/walmartlabs/concord/pull/288)). ### Changed - runtime-v2: use JDK's `AbstractMap.SimpleImmutableEntry` to iterate over `Map` elements in the `withItems` implementation instead of a custom class. ([#285](https://github.com/walmartlabs/concord/pull/285)). ## [1.76.1] - 2020-12-22 ### Changed - runtime-v2: fixed a checkpoint serialization issue when classes from `dependencies` are used as flow variables. ## [1.76.0] - 2020-12-22 ### Added - concord-console: custom columns in the process list can now be rendered as links; - runtime-v2: new annotation `@SensitiveData` can be used to prevent task method arguments from being recorded in process events; - concord-server, concord-console: audit log for external events, the process status page can now display external events that triggered the process; - policy-engine: new policy type `dependencyRewrite`. Can be used to override process dependencies (e.g. to force a specific version). ### Changed - runtime-v2: improved serialization of `lastError` objects that contain circular references (e.g. Guice exceptions); - concord-server, concord-console: use `main` as the default Git branch; - concord-agent, runtime-v1: make sure temporary directories are removed; - policy-engine: rule matching code refactoring, use common map matcher, add more tests; - concord-server: assert type of the active profiles collections, trim values; - runtime-v1, runtime-v2: update the list of "retryable" errors for `docker pull` operations to support recent versions of docker-client. ## [1.75.0] - 2020-12-05 ### Added - runtime-v2: add grammar for `repositoryInfo` conditions in GitHub triggers; - concord-server: new GitHub trigger condition `repository.enabled`. Allows filtering by the repository's enabled/disabled state; - runtime-v2, concord-console: record `post` events for failed tasks, highlight failed tasks on the events tab. ### Changed - concord-console: reload the repository list after removing a repository; - concord-console: add scrolling to the last error popup, trigger information popup and the task call details popup; - concord-repository: preserve the logger's MDC when logging Git client operations; - concord-imports: additional logging when processing `imports`; - concord-repository: the git client was reworked to better support partial fetching of repositories; - runtime-v2: unwrap runtime exceptions produced by expressions; - concord-client: tidy up the error logging - don't log a separate `WARN` message when the server responds with an error; - runtime-v2: save original stacktraces when throwing a `MultiException` (e.g. in `parallel` situations); - concord-console: now users should be correctly redirected to their destination page after login. ## [1.74.0] - 2020-11-24 ### Added - concord-server: allow "any of [list]" conditions when matching process `requirements`; - concord-console, runtime-v2: a button to launch the form wizard from the process log tab; - concord-console, runtime-v2: link to download individual log segments; - concord-console, runtime-v2: individual form links. ### Changed - concord-server, runtime-v2: correctly remove submitted forms; - runtime-v2: `set` now supports references to partially evaluated values (e.g. variables defined in the same `set` block), including nested keys; - concord-server: add transaction-aware versions of `ProjectManager#get` and `ProjectAccessManager#assertAccess` methods; - runtime-v2: now only resuming threads receive the resume event's payload. This fixes the issue with multiple `parallel` forms missing submitted data; - concord-agent, runtime-v2: make the runner responsible for log segment creation; - runtime-v2: fixed an issue when submitting a form can cause other unsubmitted forms to dissappear; - runtime-v2: fixed compilation of block steps when `error` is used. ### Breaking - runtime-v2: `set` with nested keys now replaces the top-level reference. If `set` is called in a flow, the changes to nested data won't be visible in the caller flow unless `out` is used; - policy-engine, concord-server: rename `entity` policy's types for consistency. ## [1.73.0] - 2020-11-15 ### Added - concord-console: `entryPoint` can now be used as a column in the process list. ### Changed - runtime-v2: fixed an issue when a parent process checkpoint was incorrectly applied to the process' forks; - concord-tasks: 'kill' shouldn't error on empty instanceId lists; - concord-tasks: fix common parameters not being inherited by `forks` in the runtime-v2 version of the `concord` task; - runtime-v1, runtime-v2: avoid reading partially written `instanceId` files; - concord-server: allow null values when merging policies; - runtime-v2: fixed the merging of multiple Concord YAML files (e.g. `configuration.events` blocks and others). ## [1.72.0] - 2020-11-09 ### Added - concord-console: the process list now support boolean values in custom columns and filters; - runtime-v2: save `lastError` in process metadata (feature parity with the runtime v1); - policy-engine, concord-server: the `entity` policy now supports repository objects; - concord-console, concord-repository: support for OAuth (personal) tokens for Git authentication. ### Changed - concord-console: load add log segments on every refresh (temporary workaround); - runtime-v2: fix the loop frame being added to the call stack on every step of `withItems`; - runtime-v2: fixed an issue preventing form `values` from being visible in `data.js` (custom forms); - project: update Maven Wrapper to 0.5.6; - runtime-v2: fixed evaluation of literals values for empty collections (e.g. `[]` used with `set` or in expressions); - concord-server: improved error messages when processing `imports`. ## [1.71.0] - 2020-11-03 ### Added - oneops: record incoming events in the audit log; - runtime-v2: initial support for `parallelWithItems`; - runtime-v2: support for expressions in `out` blocks of task calls, flow calls, `parallel` and `expr` blocks; - runtime-v2: save the current thread ID in `TaskResult`; - agent-operator: add `requirements` filter; - concord-console: the login form now provides a link to the API key login form. ### Changed - concord-tasks: fix the missing injection annotations in the v2 version of the `jsonStore` task (makes it useable in v2 again); - concord-server: `ProcessKeyCache` no longer caches misses; - concord-server: fixed handling of `any` conditions in the `github` trigger's `files` filter. ### Breaking - runtime-v2: new version the `TaskResult` structure, now itcan be used to tell the runtime to suspend the process. ## [1.70.0] - 2020-10-23 ### Added - concord-server: expose task failures in metrics (including error messages); - concord-server: new GitHub trigger `condition` - `files.any`. Contains all `added`, `deleted` or `modified` files in a `push` event. ### Changed - runtime-v2: add support for expressions in `name` blocks for all types of steps; - concord-server: fixed some edge cases when converting timezones in process status history; - runtime-v2: automatically convert values of out variables in scripts to their Java counterparts - Maps, Lists, etc; - runtime-v2: fixed a NPE when tasks return `null` instead of `TaskResult`; - concord-cli: print out the error's stacktrace if `verbose` is enabled. ## [1.69.0] - 2020-10-15 ### Added - runtime-v2: steps can now specify `slf4j` logging level using the `meta.logLevel` property. ### Changed - concord-server: timestamp values in process status history are now returned with a correct time zone; - concord-server: throw an exception if the resuming process wasn't actually `SUSPENDED`; - runtime-v2: `SLF4JPrintStreams` messages should no longer appear in log segments; - concord-server: task scheduler improvements - save the last error into the DB, simplify polling, improved task heartbeat/stall checks; - concord-server: escape expressions (`${...}`) in the input data when resuming processes using `/api/v1/process/{id}/resume/{eventName}` endpoint; - ansible: fixed an issue preventing Kerberos authentication from working in nested Docker containers (i.e. when `dockerImage` is used); - concord-server: allow non-admins to search audit logs by `eventId` (`/api/v1/audit?eventId=...`); - concord-console: fix widths of columns on the secret list page; - runtime-v2: `if` steps now treat `null` values as `false`; - concord-server: allow teams with no members (i.e. teams with LDAP groups only). ## [1.68.0] - 2020-10-05 ### Added - concord-server: a PayloadBuilder method to add files to the process' `workDir`; - file-tasks: initial version. ### Changed - slack-tasks: read default variables from context instead of `@InjectVariables`. This fixes an issue of calling `slack` using expressions; - runtime-v2: fixed handling of process state snapshots. Now the runtime correcly resumes from state snapshots and forks. ## [1.67.0] - 2020-10-01 ### Added - variables-tasks: runtime-v2 support for the `vars` task; - runtime-v2: allow profiles to override flows, forms; - concord-server: tx aware OrganizationManager's `createOrUpdate` and `createOrGet` methods, useful for server-side plugins; - concord-server: tx aware SecretManager's `createBinaryData` method; - concord-console, runtime-v2: the process events list now includes links to the source code of steps; - resource-tasks: `prettyPrintYaml` method to output data as formatted YAML; - concord-server, concord-console: option to download "the effective YAML" - a single YAML document with all Concord resources for a given process. ### Changed - concord-server: fixed the handling of the `values` field when `readonly` form fields are used; - runtime-v2: fixed a typo in code that prevented default form field values from working correctly; - concord-tasks: `repositoryRefresh` task was converted to v2; - concord-agent: tone down logging when the main thread is interrupted; - ansible: allow any Java `Collection` types (list, set, etc) as inventory host lists; - concord-agent: improved error handling in workers; - concord-server: fixed the error message template when validating forms; - concord-server: fixed a potential race when registering internal metrics. Fixes a `ConcurrentModificationException` when Server is running in an embedded environment, e.g. testcontainer-concord's LOCAL mode. ### Breaking - runtime-v2: `ProcessDefinition#configuration()` is a separate type now; - runtime-v2: move `ProjectInfo` classes to the SDK. ## [1.66.0] - 2020-09-17 ### Added - concord-server: optional `startAt` filter in the `/api/v2/process/requirements` endpoint; - concord-cli: support for creating new secrets using SecretManager's `create*` methods; - http-tasks: `followRedirects` now works for `POST` requests too; - concord-server: access changes for projects, secrets and JSON stores are now recorded in the audit log (e.g. assigning a team to a project); - concord-server: configurable out variables mode for projects. Now project owners can restrict who can specify `out` variables in the process request; - slack: full support for runtime-v2. All v1 actions should now be available in the v2 version too; - runtime-v2, concord-console: record and display task results (as in `configuration.events.recordTaskOutVars`). ### Changed - concord-server, concord-console: don't send `WWW-Authenticate` for unauthorized UI requests to prevent the basic auth popup from showing; - concord-server: `dateTime` form fields are now converted into UTC on submit; - agent-operator: when checking the queue's status, filter out processes with `startAt` in the future; - docker-images: python 2 to 3 migration, ansible 2.8 by default; - ansible: python 2 to 3 compatibility fixes; - concord-server: fixed the checksum of the `170000` changeset; - concord-server: fixed an issue of the `runtime` parameter not being passed correctly to the process' forks. ## [1.65.0] - 2020-09-09 ### Added - concord-console: hyperlinks to individual log segments so users can share/access a segment instead of scrolling; - concord-server: support for added/removed/modified `files` in GitHub push trigger conditions; - runtime-v2: support nested variables in `configuration.events.inVarsBlacklist`/`outVarsBlacklist`; - concord-agent: option to keep the workDir in a configured directory after the process ends; - concord-console: allow changing owners of secrets; ### Changed - concord-console: keep the "system" segment open by default so users can see overall progress; - concord-server: `USERS.IS_ADMIN` column is deprecated and its usage has been removed from the code; - runtime-v2: working process checkpoints support. ## [1.64.0] - 2020-09-02 ### Added - runtime-v2: support for expressions in checkpoint names. ### Changed - runtime-v2: improved error message when trying to assign a non-serializable value into a `TaskResult`; - concord-console: increase the number of visible log segments to 100 (was 30); - concord-server: change the wording of the maximum number of dependencies error message; - runtime-v2: merge EventConfiguration when loading multiple resources. Allows users to specify the `events` configuration in any Concord YAML file available for the process; - concord-server: fixed the maximum number of dependencies error message; - runtime-v2: create a single merged `event` configuration from `event` sections of all loaded Concord YAML files. ### Breaking - runtime-v2: rename `SecretParams#name` to `secretName`. Affects the secret creation methods in `SecretService`); - runtime-v2: assume `retry.delay` is in seconds (like in v1). ## [1.63.0] - 2020-08-30 ### Added - concord-imports: support for `dir` imports (external directories), disabled by default; - crypto-tasks: an action to create new secrets (runtime-v2 only); - concord-server: JSON store data can now be updated by either `POST` or `PUT` method; - runtime-v2: support for nested `set`; - runtime-v2: support for `out` variables in `try` and `block` steps; - runtime-v2: support for naming of `log`, `throw` and `expr` steps; - runtime-v2: additional `Variables#assert*` methods. ### Changed - runtime-v2: fixed a potential NPE in the `retry` code when handling exceptions w/o message; - docker-images: remove `dumb-init` as Docker ships its own init implementation; - runtime-v2: when using `withItems`, `out` variables are now collected into lists; - concord-server: remove user's LDAP groups from the DB when the account is disabled; - runtime-v1: fixed an issue when GitHub triggers without `version` were treated as v1 triggers; - concord-console: new look for the runtime v2 log viewer; - concord-console: fix layout of the JSON store query editor; - iam-sso: mark the SSO cookie as "secure"; - ansible: fixed an issue when `ignore_error` values were saved "as is" in Ansible events (e.g. unevaluated expressions); - concord-agent: do not print out the process' `sessionToken` in logs; - concord-console: fix manual refreshing of the process list; - concord-server: allow nested metadata filters in `GET /api/v2/process` and `GET /api/v2/process/count` endpoints; - concord-server: runtime-v2 compatibility for processes started via the browser link (aka "the process portal"). ### Breaking - concord-server: the process list entries (`GET /api/v2/process` endpoint) no longer include `imports`. ## [1.62.0] - 2020-08-19 ### Added - concord-server: a way to deploy the DB schema without `SUPERUSER` role and/or `CREATE EXTENSION` privileges; - concord-console: a way to open a process log at a specific checkpoint by using `#/process/{id}/log#{eventCorrelationId}`; - runtime-v2: the v2 SDK now provides the `DependencyManager` interface. ### Changed - concord-client: `ClientUtils#getHeaders` now correctly assumes case-insensitivity of header names; - concord-server: `OrganizationManager#create` and `#update` replaced with a single `createOrUpdate` method; - ansible: when using `auth.privateKey.path` don't remove the file after the play's end. ### Breaking - runtime-v2: `Task#execute` and `ReentrantTask#resume` now return a new common result type - `com.walmartlabs.concord.runtime.v2.sdk.TaskResult`. ## [1.61.0] - 2020-08-13 ### Added - concord-agent: `CONCORD_JAVA_OPTS` environment variable can now be used to specify additional JVM options. ### Changed - concord-server: allow `null` values in process event data to improve backward compatibility with older plugins; - runtime-v2: warn about `@Singleton` tasks; - runtime-v1, runtime-v2: `throw` with a string value doesn't produce a stacktrace anymore; - project: JDK 11 compatibility. ### Breaking - runtime-v2: `ReentrantTask` now accepts `ResumeEvent` instead of `Map`; - runtime-v2: `@DefaultVariables` annotation was replaced with `Context#defaultVariables()` method; - runtime-v2: the SDK module no longer shares interfaces/types with the v1 SDK: - `ApiConfiguration` - `DockerContainerSpec` ## [1.60.1] - 2020-08-05 ### Changed - concord-agent: fixed an issue preventing the Agent from working in the `LOCAL` mode of [testcontainers-concord](https://github.com/concord-workflow/testcontainers-concord). ## [1.60.0] - 2020-08-05 ### Added - runtime-v2: support for `name` attributes in task, flow call and expresion steps; - concord-server: store process `dependencies` in the `process_queue.dependencies` column; - runtime-v2: `LockService` implementation; - concord-server: expose the current number of processes with "wait conditions" as a new metric - `process_queue_enqueued_wait`. ### Changed - concord-agent: use process session token to append logs, download state, update status, etc. Previously, agents used an API token from the configuration file for such operations; - concord-console: the refresh action of the process list page was rewritten to use React Hooks; - concord-console: fixed scrolling on the repository list page in the presence of a modal dialog. ## [1.59.0] - 2020-07-30 ### Changed - concord-console: improved error handling, standartize refresh indicators/buttons; - concord-server: fixed the parsing of `process.maxStateAge` configuration parameter. ### Breaking - concord-server, concord-server-sdk: the `ProcessKey` interface was replaced with a concrete type, moved from the server's `impl` module; - concord-server: remove the support for GitHub triggers v1. The v1 triggers are deprecared since 1.32.0+. ## [1.58.1] - 2020-07-28 ### Changed - concord-server: count child processes towards the concurrency limit; - agent-operator: now correctly calculates the pod's configuration hash, fixes an issue when pods where incorrectly scheduled for replacement due to the pool's size changes; - concord-server: perform all process key lookups via `ProcessKeyCache`; - concord-console: disable autocomplete for password fields on the new secret page and when encrypting a string. ### Breaking - concord-agent, concord-server, concord-cli: the configuration parameters that previously used integers for duration values (e.g. `repositoryCache.maxAge`) now use literal duration values. ## [1.58.0] - 2020-07-24 ### Added - concord-console: host status filter to the Ansible host list page; - runtime-v2: support for the `debug` mode. ### Changed - runtime-v2: improved validation of the `script` step; - concord-tasks: log the child process' URL; - concord-cli: by default do not remove the `target` directory after the process finishes; - concord-cli: trim whitespace when reading single-value (string) secrets; - concord-server: fixed an issue preventing JSON store creation requests from being correctly validated (i.e. disallow invalid store names). ### Breaking - concord-server: `java.util.Date` and `java.sql.Timestamp` used in internal and external Java APIs are replaced with `java.time.OffsetDateTime`; - concord-server-db: `timestamp` columns are migrated to `timestamptz`. The migration procedure requires the DB user to have `SUPERUSER` privileges for the duration of the migration. ## [1.57.0] - 2020-07-22 ### Added - concord-server, concord-repository: an option to ignore remote fetch if the local commit ID is the same as the remote commit ID; - concord-server: audit logs for API key creation and removal; - policy-engine, concord-server: new policy `state`. Allows control of various aspects of process state (size, filenames, etc); - runtime-v1: new option configuration option `runner.enableSisuIndex`. Enables usage of the Sisu bindings index; - concord-server: creation of organizations now requires a role with the `createOrg` permission. ### Changed - concord-server: fixed an issue preventing the validation of the request data from working correctly in JSON Store endpoints; - concord-repository: skip fetch if the local copy points to the same commit; - concord-repository: correctly release the repository's lock; - concord-server: fixed a bug in the "process portal" page template which was preventing the page from reloading; - agent-operator: the API client now saves session cookies to avoid the need to authenticate on every request; - concord-server: allow users with the admin role to create new LDAP-based accounts even if the user creation is disabled for the realm; - concord-task: fixed an issue preventing subsequent calls to the task to fail when the suspend/resume feature is used; - runtime-v2: now requires `sisu-maven-plugin` to be used in all plugins. ## [1.56.0] - 2020-07-09 ### Added - concord-cli: initial support for `profiles`; - concord-cli: option to remove the target directory before the process starts; - concord-cli: provide feedback while processing `imports`; - concord-cli: the `run` command now supports profiles; - dependency-manager: support for proxy servers (http/https); - runtime-v2: support for `meta` attributes in `log` steps. ### Changed - concord-server: fixed an issue with process wait conditions not being cleared in time; - concord-server: fixed a potential NPE in the repository cache cleanup code; - project: JDK11+ compatibility; - concord-server: force all usernames to lower case to avoid issues with AD/LDAP authentication and environments with multiple auth/z providers. ## [1.55.0] - 2020-07-01 ### Added - runtime-v2: warn when a variable name "shadows" a task name; - concord-server: lift restrictions on the format of usernames. Allow `@` and other special characters; - concord-server: a short project cache for GitHub triggers v1 to avoid multiple DB queries when filtering out triggers; - concord-tasks: runtime v2 compatibility; - concord-server, agent-operator: fetch only process requirements, configurable fetch depth; - concord-server: new policy to control size of process attachments. ### Changed - agent-operator: new CR template property `%%preStopHook%%` for injecting the preStop hook script's content; - concord-cli: `run` action now copies all files into the `target` directory in the current directory; - concord-client: the error message when the requested secret type doesn't match the result now includes the name of the secret; - policy-engine: no longer dependens on `commons-lang3`; - concord-console, concord-server: `ENQUEUED` filter now includes processes with `start_at < now`; - concord-server: stop all background and scheduled tasks when the maintenance mode is enabled; - concord-repository: move cache directories before removing to reduce the time spent while locked; - concord-server: fix process status update in the queue batching task; - concord-server: optimize the session key validation by reducing the amount of data pulled from the DB. ## [1.54.0] - 2020-06-24 ### Added - concord-console: an indicator for partially loaded log segments; - runtime-v2: initial support for parallel block's in/out; - runtime-v2: initial support for in/out variables in `parallel` blocks; - runtime-v2: initial support for "reentrant tasks" (tasks that can be suspended and resumed); - agent-operator: add the preStop script to the `dind` container; - concord-server: additional metrics for github events - the number of processes started/triggers fetched/etc per event; - concord-console: new loading indicator for log segments; - runtime-v2: add `projectInfo` to `context`; - concord-cli: option to run individual Concord YAML files. ### Changed - concord-server: when updating/moving a project assert name when no ID given; - concord-server: fixed the calculation of the `enqueued-workers-available` metric; - concord-console: updated Json Store capacity indicator; - concord-console: auto refresh the child process list; - concord-console: show open segments correctly when the system segment's visibility changes. ## [1.53.1] - 2020-06-16 ### Changed - concord-server: fixed an issue preventing `useInitiator` from being correctly handled by GitHub triggers. ## [1.53.0] - 2020-06-13 ### Added - concord-server: option to ignore "empty" `push` notifications from GitHub; - concord-server: optional batching mechanism for the `NEW` to `ENQUEUED` transition; - runtime-v2, concord-console: show `meta.segmentName` as the log segment's name; - concord-console: show GitHub event details in the "triggered by" popup; - docker-tasks: runtime v2 compatibility; - runtime-v2: option to redirect `System.out` and `err` to segmented logs; - runtime-v2: support for `error` blocks in `call` steps; - runtime-v2: `Context` can now be injected into tasks. ### Changed - concord-repository: fixed an issue preventing `imports` from being excluded from the process state; - agent-operator: use `preStop` hooks instead of directly removing pods; - concord-server: OneOps specific endpoints extracted as a server plugin; - concord-console: re-load repository data when the process start popup opens; - ansible: improved error messages in lookup plugins for Concord secrets; - concord-console: the team list page and the "find a team" dropdown are rewritten to use React Hooks instead of `react-redux`; - runtime-v1: trim data when recording in/out variables; - concord-console: the trigger list moved to the repository page into a separate tab; - concord-console: the login form was rewritten to use React Hooks instead of `react-redux`; - runtime-v2: `call` can now have multiple `out` variables; - concord-server: the endpoint for creation of Inventory queries now supports both `text/plain` and `application/json` content types; - concord-server: disable key validation when uploading existing SSH key pairs. The library (JSch) used to validate keys doesn't support keys larger than 2048 bits. ### Breaking - runtime-v2: the `Task` interface now accepts input `Variables` instead of the context. ## [1.52.0] - 2020-06-03 ### Added - runtime-v2: `throw` step support; - runtime-v2: record pre/post events for task called using expressions; - runtime-v2, concord-console: show the YAML file name on the events tab. ### Changed - concord-server: fix the adding of team members by user IDs; - agent-operator: the autoscaler now scales up more rapidly and scaled down more gradually; - slack: reduce chattiness in the process log by moving some of the log statements to `debug`; - runtime-v2: replace custom service injector with Guice-based injector; - runtime-v2: more details when recording step events; - runtime-v2: fix logging for steps without "proper" segments - scripts, expressions, etc; - runtime-v2: start the heartbeat as soon as possible; - runtime-v2: support for the "short form" of task calling has been removed; - concord-server: re-initialize the process initiator when creating a fork. This fixes an issue when a process fork is created using an API key that belongs to a user other than the parent process' current user. ### Breaking - runtime-v2: support `@InjectVariable` annotations has been removed. ## [1.51.0] - 2020-05-27 ### Added - runtime-v2: implement process heartbeat; - concord-console: custom columns in the process list can now use `requirements` as the `source`; - runtime-v2: support for `error` blocks in `task` steps; - dynamic-tasks: runtime v2 compatibility; - concord-console: new "Duration" column in the process list. Shows the amount of time spent `RUNNING` a process; - noderoster-tasks: runtime v2 compatibility. ### Changed - concord-console: `ENQUEUED` processes that are scheduled for future (using `startAt`) can now be displayed separately from regular `ENQUEUED` processes and vice versa; - ansible: escape special characters when the command script is created; - concord-server: fixed an issue preventing the LDAP group synchronizer from working correctly if there are previously disabled user accounts; - concord-console: use IDs when adding users to teams to avoid issues with duplicate usernames or the need for `userType` parameter. Allow `cron` and other system users to be added to teams using the UI. ### Breaking - runtime-v2: make all variables local. Variables must be explictly passed between flows using `in` and `out` parameters; - docker: rebase images on top of `centos:8`. ## [1.50.1] - 2020-05-19 ### Changed - concord-server: fix the "Unconnected sockets not implemented" issue when connection timeout is used for LDAP connections. ## [1.50.0] - 2020-05-18 ### Added - concord-server: configurable `connectTimeout` and `readTimeout` for LDAP calls; - concord-server: log duration of GIT operations; - runtime-v2: support for return and exit steps; - ansible: new parameters to enable or disable various features: `enableEvents`, `enableStats`, `enableOutsVars`; - runtime-v2: support for expressions in call step; - runtime-v2: grammar support for cron trigger's `timezone`; - concord-cli: support for `crypto.exportAsFile`; - runtime-v2: `template` support; - runtime-v2: support for out variables; - repository: support for "non detached" checkouts; - runtime-v2: support for setting variables using `set`. ### Changed - iam-sso: re-enable disabled user accounts on successful login; - concord-server: fix handling of `Bearer` tokens; - runtime-v2: in/out params in task events are now truncated to limit the amount of data saved; - runtime-v1: process metadata updates are now automatically retried in case of errors; - runtime-v2: add `concord/concord.yml`to the list of default `resources`; - runtime-v2: replace Jackson's `JsonLocation` with a custom type; - runtime-v2: log step execution errors with the step's location; - runtime-v2: the default expression evaluator now implements different evaluation rules for process arguments, task inputs and `set` steps. ## [1.49.0] - 2020-05-06 ### Added - concord-cli: support for `imports`; - concord-cli: allow specifying an `entryPoint`; - concord-cli: support for `crypto.exportAsString`; - concord-cli: initial support for `crypto.decryptString`; - concord-cli: initial support for "default variables". The built-in variables provide some useful defaults for the Ansible plugin; - runtime-v2: support for process metadata; - runtime-v2: support for "processTimeout", "exclusive", "events" and "requirements" elements; - runtime-v2: allow process suspension via `Context#suspend` method; - concord-server: show a custom 404 page when a form is not found; - misc-tasks, datetime-tasks: runtime v2 compatibility; - lock-tasks: runtime v2 compatibility; - ansible: runtime v2 compatibility; - locale-tasks: runtime v2 compatibility; - smtp-tasks: runtime v2 compatibility; - concord-server, concord-console, concord-agent, runtime-v2: initial implementation of "segmented" process logs. Each task now gets a separate segment of the process log and can be shown on the UI individually; - log-tasks: add `level` parameter to the v2 version of the task; - concord-agent: configurable default JVM parameters; - concord-console: show `startAt` to the process toolbar; - concord-server: make the embedded Jetty aware of proxy headers such as `X-Forwarded-For`; - concord-server: audit log now includes API key IDs. ### Changed - runtime-v1: fixed an issue preventing process variables from being passed into an `onFailure` handler if the parent process fails with an unhandled exception; - concord-agent: log dependency check results using `WARN` level; - runtime-v2: fix github trigger's exclusive attribute definition; - ansible: improved detection of the `setup` task type; - project: replace ollie dependency with ollie-config; - concord-server: fix the "show only user's organizations" toggle; - concord-console: don't do the initial search when the Node Roster page opens; - concord-server, concord-console: fixed the behaviour of the "show only user's organizations" toggle on the organizations list page. ## [1.48.1] - 2020-04-22 ### Changed - concord-server: fixed an issue when the `requestInfo` variable isn't provided in some cases (regression); - concord-server: fixed an issue when the `projectInfo` variable is overwritten with an incorrect value when the process is resumed (regression). ## [1.48.0] - 2020-04-21 ### Added - kv-tasks: runtime v2 compatibility; - resource-tasks: runtime v2 compatibility; - crypto-tasks: runtime v2 compatibility; - runtime-v2: optional support for "segmented logs" where each task call gets its own log file; - log-tasks: runtime v2 compatiblity; - throw-tasks: runtime v2 compatibility; - runtime-v2: support for `resources`; - runtime-v2: support for `script` steps. ### Changed - concord-server: public organizations are now visible for everyone regardless of membership; - concord-server: organization owners can now see their organizations even if they don't belong to any team in it; - concord-server: fixed the even type filter when querying repository events; - policy-engine, concord-server: remove deprecated and unused `queue` policies - `process`, `processPerOrg` and `processPerProject`; - runtime-v2: improved "method not found" error messages when evaluating expressions; - runtime-v2, concord-server: improved detection of the `runtime` parameter. Now it can be specified in the `configuration` section or in the request parameters; - runtime-v2: the expression evaluator now correctly supports partial evaluation of nested data; - concord-server: some endpoints that were previously automatically creating users no longer do so. E.g. when specifying an owner of a JSON store, the user record must exist beforehand; - concord-project-loader: allow `runtime` to be specified externally, e.g. in process request parameters; - ansible: improved validation of `inventory` and `vaultPassword` parameters; - concord-server: make the "Copying the repository's data" message shorter, don't print out the repository's metadata; - concord-server, concord-agent, runtime-v1, v2: major process/runner configuration refactoring. The process' session token is no longer saved as a file in the working directory, but passed as a process configuration field; - repository: removed delay between `fetch` retries. ## [1.47.1] - 2020-04-10 ### Changed - concord-agent: fix "preforking" by removing the process' session key from runner configuration files; - concord-server: removed old LDAP user search endpoint `/api/service/console/search/users` and all associated code. ## [1.47.0] - 2020-04-08 ### Added - runtime-v1, runtime-v2, concord-server: support for `publicFlows` - a top-level element with a list of public flows. Only public flows are allowed to be used as `entryPoint` values; - concord-server: additional audit logging for user management - account creation/update, enabling or disabling of the account; - iam-sso: option to convert user domain names via `sso.domainMapping` configuration parameter; - http-tasks: initial support for runtime v2. ### Changed - concord-server: improved validation of API key names, improved handling of duplicates; - runtime-v2: now the runtime evaluates only top-level variables in task, form and flow call parameters to avoid undesirable "interpolation" of nested values; - concord-server: when executing `ldap.principalSearchFilter` pass username and domain values separately in additional arguments; - concord-agent: the start script no longer depends on `uuid` executable. ## [1.46.0] - 2020-04-02 ### Added - runtime-v2: support for `if` and `switch` steps; - runtime-v2: support for lists in `githubOrg` and `githubRepo` conditions in `github` triggers; - runtime-v2: ability to save all task call results in the process state for later use. Useful for implementing policies that restrict flow execution based on results of previously called tasks; - concord-server: automatically re-enable AD/LDAP user accounts that were previously disabled by the AD/LDAP synchronization process; - runtime-v2: `DockerService` support; - runtime-v2: support for task policies. ### Changed - concord-server, concord-agent: externalize default git operation timeout duration. Increase default value to ten minutes; - concord-server: treat empty project name as null when updating secret. This fixes an issue preventing users from being able to "unlink" secrets from projects. ## [1.45.0] - 2020-03-30 ### Added - runtime-v2: initial support for checkpoints; - runtime-v2: support for multiple `TaskProvider` instances; - runtime-v2: initial implementation of `SecretService` and `FileService`; - concord-server: option to generate the default admin API token on start; - runtime-v2: support for triggers; - runtime-v2: process event recording; - http-tasks: response headers are now saved into the result variable; - concord-console: alternate shading for actionable tables and lists; - concord-server, concord-agent: configurable repository locks. The maximum number of concurrent Git operations can now be configured in the Server and Agent configuration files; - runtime-v2: initial support for task defaults (default task variables). ### Changed - concord-console: some of user selection fields (e.g. a project owner field) now perform search using the DB data, without accessing external AD/LDAP servers; - concord-agent: fixed a potential race condition when the maintenance mode is activated; - dependency-manager: disable Aether caching, allows `snapshotPolicy.updatePolicy` to work correctly; - concord-console: make the scroll up button always visible; - concord-tasks: `requirements` are now correctly passed into the API request; - vagrant: fixed the database container startup. ## [1.44.0] - 2020-03-12 ### Added - concord-server: configurable CORS origin; - concord-console: ability to filter processes by a repository name; - new server plugin: `oidc`. Allows user authentication using an OpenID Connect provider; - concord-tasks: `meta` field can now be specified when starting or forking processes using the `concord` task; - concord-cli: initial support for running v2 flows; - runtime-v2: support for `imports`; - runtime-v2: support for running v1 tasks. ### Changed - ansible: no longer requires `ujson` Python module. The module is still included into the `concord-ansible` image for backward compatibility with older version of the Ansible plugin; - ansible: when recording events make sure that "unsafe strings" (`AnsibleUnsafeText`) are truncated just like regular strings. This prevents large amount of data from getting into the event's payload; - ansible: fix `concord_data_secret` lookup when used with Python 3; - http-tasks: show `Authorization` header value in `debug` mode; - http-tasks: validate the `auth.basic.token` value; - concord-console: limit the number of rows returned when running JSON store queries; - concord-console: fixed the secret move popup's message; - concord-console: fixed vertical alignment in the repository events table. ## [1.43.0] - 2020-03-05 ### Added - concord-tasks: ability to pass `requirements` into the called process; - concord-server: `v2` syntax for generic and OneOps triggers; - concord-server, concord-console: ability to move project and secrets across organizations; - smtp-tasks: disable debug output by default, add `debug` option; - concord-console: show the process status in the window's favicon. ### Changed - concord-server: fixed the raw payload mode check. Now if the project's raw payload mode is set to "Only team members", the server correctly looks for team members with `READER` access level or higher; - http-tasks: honor the `ignoreErrors` value when handling "unauhorized" (401) responses; - ansible: the sensitive data filter is now opt-in by default and can be enabled with the new `enableLogFiltering` parameter; - concord-server: fixed handling of GitHub `team` events. Now it correctly calculates the appropriate `githubRepo` values; - concord-server: if `startAt` value is in the past, log the current server time in the error message; - concord-console: make `lastUpdatedAt` available as one of the built-in columns again. ## [1.42.0] - 2020-02-26 ### Added - concord-console: the repository edit page was redesigned to include the new "Events" tab. Currently, the "Events" tab allows users to see incoming GitHub events which can be used to debug trigger conditions or monitor the repository traffic; - concord-server: new `saveAs` parameter in the process resume endpoint. Allows saving the received JSON body as the specified process `configuration` value; - misc-tasks: new `datetime.currentWithZone` methods to get the current date/time for a specific time zone; - concord-sdk: new `Context#interpolate(Object, Map)` method to interpolate values using the specified `Map` as variables. ### Changed - concord-server: process "wait conditions" (locks, waiting for other processes, etc) are now processed in batches; - concord-server: the process queue dispatcher now able to handle multiple processes per projects at the time; - ansible, noderoster: trim the data (host names, host groups and task names) before inserting it into the DB; - concord-console: the `lastUpdatedAt` column was removed from all default process lists. It still can be used in custom column configurations; - concord-tasks: fixed a bug causing duplicate entries in the `jobIds` variable when multiple forks start; - concord-server: better validation of JSON Store query results. The server expects single column results with valid JSON objects as values and will report so if the query results don't pass the validation; - smtp-tasks: better validation of input parameters; - concord-tasks: make sure the API response's body is closed, prevent leaks. ## [1.41.0] - 2020-02-13 ### Added - kv-tasks: allow calling from `script` environment; - project-model: support for expressions in retry parameters; - concord-server: support for non-repository GitHub events (e.g. `team`, `organization`, etc); - concord-console: date/time filters for the audit log. ### Changed - ansible: improved Python 3 compatibility; - concord-server: fixed the `repoId`/`repoName` filter in the `/api/v2/process` endpoint; - concord-server: fixed a bug in the process queue dispatcher when a process with `requirements` that cannot be satisfied could block other processes in the same project from being dispatched to workers; - slack-task: allow sending messages with JSON and support updates; - slack-task: make the `action` parameter of the `slackChannel` task case-insensitive; - concord-task: multiple process forks are now started in parallel. ## [1.40.0] - 2020-02-06 ### Added - concord-task: option to disable `debug` logging, including the process arguments; - concord-server: support for filtering on repository ID or name in `/api/v2/process` and `/api/v2/process/count` endpoints; - concord-console: initial version of the Node Roster UI; - concord-console: new "Audit Log" tab on the organization, project, team, secret and JSON store pages; - policies, concord-server: ability to set default configuration for processes using the new `defaultProcessCfg` policy; - slack: new parameter `replyBroadcast`. If `true` a reply to a thread is also posted to the channel. ### Changed - concord-console: support for calling processes with arguments using manual triggers; - concord-server: improvements in audit logging of team changes. Now the audit events contain the change's delta; - concord-server: only JSON objects (Java Maps) are now allowed as JSON Store items. ## [1.39.3] - 2020-01-29 ### Added - concord-agent: make the maintenance mode port configurable. ### Changed - k8s: the example CRDs were updated to include the pod's name into the agent's `capabilities`; - concord-server: the process queue dispatcher now sends responses outside of the global lock; - concord-console: fixed "flickering" when switching between playbooks in the new Ansible UI; - ansible: callback plugins updated to support both Python 2 and 3; - concord-server: the `/api/v1/process/{id}/log` endpoint now performs additional permissions check. Now only initiators, project `WRITERS`, admins and "global readers" can access process logs. Disabled by default. ## [1.38.2] - 2020-01-23 ### Changed - concord-server: fixed negative maxSize in JSON store capacity response; - concord-server: fixed the existence check when creating or updating an inventory. ## [1.38.1] - 2020-01-22 ### Changed - concord-server: fixed a potential NPE when handling non-repository GitHub events (issues, teams, etc); - concord-server: revert changes in `/api/v1/org/${orgName}/inventory/${inventoryName}/data` endpoints. ## [1.38.0] - 2020-01-21 ### Added - concord-server: configurable max request size for the embedded Jetty server; - new JSON Store API, UI and a flow task; - concord-server: provide `event.commitId` variable for `pull_request` and `push` events in `github` triggers; - node-roster: initial version; - concord-server, concord-console: disable checkpoint restoration for suspended processes; - concord-console: pagination to the process wait conditions page; - dependency-manager: support for authentication and release/snapshot policies; - ansible: allow mix and match of inline inventories and file paths; - concord-console: customizable pages for external resources presented as iframes. ### Changed - concord-server: the Inventory API is deprecated in favor of the JSON Store API; - concord-console: fixed pagination of the child process list; - concord-server: the system trigger (responsible for refreshing repositories) now correctly triggers only on "push" events; - concord-server: when refreshing trigger definitions in the DB, the server now correctly detects changes and updates/replaces only the changed triggers; - concord-console: require an additional confirmation when removing a repository; - concord-server: show the `available_worker` metric even if all workers of a specific "flavor" are gone; - concord-server: escape expressions (`${}`) in external event data. All string values in the `event` variable (which is provided for processes triggered by external events) will have their `${` escaped as `\${`; - concord-cli: fix the build, the standalone JAR is now runnable again; - ansible: fixed processing of events that are created by the task called in an expression. ## [1.37.1] - 2020-01-06 ### Changed - variables-task: fixed an interpolation issue in the `set` implementation. Now it correctly works for nested values that are referencing variables from the "outside" scope. ## [1.37.0] - 2020-01-02 ### Added - docker: option to use the container's user instead of forcing the default Concord UID; - concord-console: project configuration editor; - slack: new action `addReaction`; - concord-server, plugins: new server plugin `kafka-event-sink`. Sends process events into a Kafka topic; - concord-server-sdk: new interfaces `ProcessEventListener`, `AuditLogListener` and `ProcessLogListener`. Allows server plugins to listen for process-level events, process logs and audit events. ### Changed - slack: make `authToken` and `apiToken` parameters interchangeable; - kv-tasks: disallow `null` or empty keys; - concord-server: fixed an RBAC issue when loading extended process event data. Now the `/api/v1/process/PROCESS_ID/event?includeAll=true` correctly checks for org/project permissions and ownership; - project-model-v1: merge `dependencies` lists from all loaded Concord YAML files; - docker: expose the host's `DOCKER_HOST` to the containers running in the `docker` task; - concord-console: fixed the dropdown behavior in the operation confirmation popup; - variables-tasks: fixed an interpolation issue when multiple dependant values are `set` simultaneously; - http: fixed a potential NPE on empty responses; - concord-server, concord-console, ansible: hosts statuses are now limited to `ok`, `failed` and `unreachable`. Fixes the host status calculation and reduces the number of recorded host events; - concord-server, concord-console, ansible: fixed handling of retries in the new Ansible UI; - concord-server, concord-console: fixed the failed hosts/tasks request for processes with multiple playbook executions; - concord-server: fixed a division by zero error when calculating Ansible play progress; - ansible: correctly handle Jinja2 expressions in host groups when recording events; - concord-server: sanitize `\u0000` in strings when inserting JSONB data (e.g. process events). ### Breaking - concord-agent: support for the container-per-process execution mode is removed. It will be brought back in the future as a a separate type of Agent; - concord-agent: `runner` configuration section is renamed to `runnerV1` to better support alternative runtimes. ## [1.36.1] - 2019-12-11 ### Changed - ansible: improved handling of `currentRetryCount` attributes in playbook events. This fixes an issue that was preventing Ansible events from being correctly processed by the server. ## [1.36.0] - 2019-12-09 ### Added - concord-console: the about page now shows the date when the environment was last updated (optional, requires update of `cfg.js`); - concord-console: support for multiple profiles in the repository run dialog; - server: optional caching of LDAP query results; - http: default `User-Agent` value, contains the version of the plugin; - imports: ability to `exclude` files when importing external resources; - concord-console: additional process menu links can now be specified using the `cfg.js` file; - ansible, concord-console: new view for Ansible runtime statistics; - concord-server: optional rate limit for the process start endpoints; - slack: new option to `ignoreErrors`; - concord-server: cache for policies; - concord-server, concord-console: the "new project" and the "new secret" buttons can now be disabled on the organization level using an `entity` policy; - concord-server, concord-agent: configurable LRU cache for GIT repositories; - concord-console: pagination for the project list; - docker, ansible: automatic retries when pulling images; - concord-server: configurable hard limit for the process log size; - ansible: new parameter `enablePolicy` to apply Concord policies to plays; - concord-server: new endpoints `/api/v1/agent/all/workers` and `/api/v1/agent/all/workersCount` to retrieve the list of currently available agent workers and the number of available workers grouped by a property in agent `capabilities`; ### Changed - concord-server, concord-console: when validating repositories return errors and warnings separately. Downgrade a missing `entryPoint` reference in triggers to a warning; - concord-console: project and secret visibility is now private by default; - concord-console: fixed missing support for `activeProfiles` for manual triggers; - concord-console: updated look of the process status toolbar; - concord-server: enabled support for `onFailure`, `onCancel` and other process handlers for forks; - slack: better handling of invalid response codes, increase delay between retries; - concord-service: replace `WatchService` with simple polling to better support the reloading of the default process configuration in Docker environment; - runtime-v1: move the processing of `imports` to the `ProjectLoader`; - concord-console: better handling of undefined process metadata values; - runtime-v1: fixed an issue with nested `retry` blocks; - concord-server: handle processes stuck in the `PREPARING` status; - concord-server: fixed an issue preventing the `github` trigger's `useEventCommitId` from working correctly; - concord-server: upgraded to Ollie 0.0.33; - concord-server: additional permission checks when downloading process attachments. Now only process initiators, project owners or admins can download attachments; - ansible: disable Concord policies by default; - dependency-manager: better handling of partially downloaded artifacts; - concord-console: show the last error icon on the process status page; - concord-server: forks now re-use the parent process' `imports`. ### Breaking - concord-server: the `/api/v1/process/{id}/kv/{key}/string` endpoint now returns `Content-Type: text/plain` instead of `application/json`. This fixes an issue with non JSON strings; - concord-server: make the v2 the default version for `github` triggers. The existing projects must update their `github` trigger definitions or set the default version in the server configuration file; - concord-console: the image is removed. The Console files are now served by the Server itself; - concord-runner: remove deprecated process definition attributes: - `__attr_localPath`; - concord-sdk: remove deprecated interfaces and annotations: - `com.walmartlabs.concord.common.InjectVariable`; - `com.walmartlabs.concord.common.Task`. ## [1.35.1] - 2019-10-17 ### Changed - concord-server: additional configuration settings for the handling of suspended processes; - concord-server: improve the asynchronous processing of external events; - concord-server: fix a potential issue when GitHub triggers a process without a valid user account. ## [1.35.0] - 2019-10-16 ### Added - concord-console: the repository links that end with `.git` are now correctly displayed on the process status page; - concord-server: new `agent-workers-available` metric, shows how many Agent worker slots are available; - docker: support for capturing the command's `stderr` in addition to `stdout`; - http-tasks: support for `multipart/form-data` requests; - concord-agent: support for running processes with custom JVM parameters. ### Changed - concord-runner: sort dependencies before loading to ensure consistent class loading; - concord-server, concord-console: fill-in the process status page's "Triggered By" for processes triggered by `cron`; - concord-agent: fix system log messages that may appear out of order in the process log. ## [1.34.3] - 2019-10-07 ### Changed - concord-server: ignore individual errors when refreshing multiple repositories at once; - concord-server-db: add missing index on `PROCESS_CHECKPOINTS (INSTANCE_ID)`; - concord-server: some optimizations for the Ansible event processing; - concord-server: optimize `GET /api/v2/process` endpoint, equality filter on metadata fields is now much faster; - concord-server: additional metrics for LDAP and Ansible event processing. ## [1.34.2] - 2019-09-27 ### Changed - concord-runner: update BPM to 0.58.2, enables interpolation of expressions in form field labels; - concord-server: fixed an issue preventing `useInitiator: false` from working correctly for GitHub triggers. ## [1.34.1] - 2019-09-22 ### Changed - concord-server: fixed the system trigger definition. Now it correctly fires up on changes in all registered repositories. ## [1.34.0] - 2019-09-19 ### Added - concord-server: new endpoint `/api/v2/process/count`. Returns the number of processes for the specified filters; - dependency-manager: automatic retries, improved error reporting; - concord-server: the `/api/v2/process` endpoint now supports different types of metadata filters (`eq`, `startsWith`, etc); - concord-server-sdk: now provides metrics annotations (e.g. `@WithTimer`). ### Changed - concord-agent: dependency resolution logs are now correctly sent back to the server; - concord-server: fix `STARTING` statuses not being registered in the process history; - concord-server: the `ping` endpoint now checks for the DB connection; - concord-server: the repository refresh process is updated to use GitHub triggers v2. ### Breaking - concord-server: the `/api/2/process` endpoint now requires a project ID or a project name to be specified when metadata filters are used; - concord-server: the entity policy's attribute `trigger.name` is renamed to `trigger.eventSource`. ## [1.33.0] - 2019-09-07 ### Added - concord-runner: additional logging when the process heartbeat is restored; - concord-server: configurable key size for generated key pairs; - concord-server: expose Jetty Sessions metrics; - concord-task: new method `getOutVars` to retrieve out variables of already running or finished processes; - concord-task: support for `outVars` for `action: fork` when `suspend` is enabled; - concord-server: new `exclusive` syntax for triggers and regular processes. ### Changed - concord-server: optimize the agent command dispatching; - concord-console: fixed overflowing in the Ansible event list popup; - concord-server: fixed an issue with incorrect process status transition of forked processes; - concord-server: fixed an issue preventing the process wait condition history from being correctly filled in. ## [1.32.0] - 2019-09-04 ### Added - concord-server: the process resume event name is now exposed as `eventName` variable. It can be used to detect when the process is restored from a checkpoint or resumed after suspension; - concord-server: regexes are now supported in the process' `requirements`; - concord-tasks: `suspend` support for `action: fork`; - concord-tasks: new method `suspendForCompletion` - suspends the parent process until the specified processes are done; - http-tasks: option to disable automatic redirect follow with `followRedirects: false`; - concord-server, project-model: initial version of the new streamlined GitHub trigger implementation (opt-in). ### Changed - concord-console: fixed a date-formatting bug preventing `date` and `dateTime` process form fields from working correctly; - ansible: fixed an issue preventing the host status callback from working correctly when the host is unreachable; - ansible: callback plugins now send a custom `User-Agent` header; - concord-server: optimize the process dispatching, perform filtering on the server's side; - concord-server: parallel processing of external event triggers; - concord-console: hide form actions on the process status page if the process is stopped; - concord-agent: move some of the system log messages to the `debug` level; - concord-server: additional logging when the process is enqueued. ## [1.31.1] - 2019-08-27 ### Changed - concord-console: new icon for the `NEW` process status; - concord-console: fix the process status page refresh. ## [1.31.0] - 2019-08-27 ### Added - concord-runner: additional diagnostic when the process state contains non serializable variables; - concord-runner, concord-sdk: expose the `DependencyManager` to plugins. Allows plugins to download external artifacts using a persistent cache directory; - concord-console: add pagination and server-side filtering to the organization list page; - concord-console: display trigger information on the process status page. ### Changed - concord-server: allow overriding of `requirements` with `profiles`; - inventory: the `query` method now automatically retries in case of network or intermittent backend errors; - concord-server: fixed an issue preventing processes that were scheduled for future (using `startAt`) from resuming correctly; - docker-tasks: now `stdout` output is captured without Docker system messages (i.e. without the image download messages); - concord-console: relax validation rules for `git+ssh` repository URLs. Allows usage of non-GitHub GIT URLs; - concord-server: fixed an issue preventing the "payload archive" endpoint (the one that accepts ZIP archives as `application/octet-stream`) from working. ## [1.30.0] - 2019-08-18 ### Added - concord-server: expose "the number of enqueued processes that must be executed now" as a separate metric (i.e. without `startAt` or with `startAt` in the past); - concord-server: expose `repoBranch` and `repoPath` in the `projectInfo` variable; - concord-console: show the process' timeout of the status page; - project-model: support for `retry` for flow `call` blocks. ### Changed - http-tasks: skip certificate validation for all certificates (not only for self-signed); - concord-server: process start requests are now handled asynchronously. The process queue entry is created as soon as possible with the `NEW` status and processes in a separate thread pool; - ansible: fixed passing of `--ssh-extra-args` parameters; - concord-agent, queue-client: replace `maxWebSocketInactivity` parameter with `websocketPingInterval` and `websocketMaxNoActivityPeriod`. Use the latter to detect dead connections. ## [1.29.0] - 2019-08-06 ### Added - concord-server: process events now can be filtered by their sequence ID in the `/api/v1/process/{id}/events` endpoint. ### Changed - concord-server: allow project `OWNERS` to download state archives of the project's processes; - concord-server: record `github` events before starting any processes; - http-tasks: use `UTF-8` for string and JSON requests by default; - concord-server, concord-ansible: use the event's timestamp instead of the DB's timestamp when recording events (if available); - concord-console: fix the project payload settings being applied when the settings page opens; - concord-console: use the same process toolbar on all tabs on the process page. ## [1.28.1] - 2019-08-01 ### Changed - concord-server: use MDC to log process ID in the pipeline processors; - http-tasks: improved input parameter validation, additional validation for JSON responses; - concord-console, concord-runner: fixed displayed duration for Ansible and process events; - concord-server: allow retrieval of public keys of project-scoped key pairs; - ansible: improved calculation of host statuses during playbook execution. ### Breaking - concord-server: `sync=true` option is removed. Processes can no longer be started in the "synchronous" mode, users should poll for the process status updates instead. ## [1.28.0] - 2019-07-27 ### Added - concord-server: save external `github` events into the audit log; - concord-server: save the process' trigger information in the process queue. The process endpoints now return the new `triggeredBy` field; - concord-console: display the process' `startAt` on the process status page; - concord-server: support for multiple `user` entries in the form's `runAs` block; - sleep-tasks, concord-server: a way to suspend a process until the specified date/time. ### Changed - concord-server, concord-console: the `acceptsRawPayload` property in projects is replaced with `rawPayloadMode`. The old property is deprecated. New projects are created with `rawPayloadMode: DISABLED` by default; - concord-tasks: fixed an issue when a subprocess is started using an API key specified by the user with `suspend: true`; - ansible: the `limits` parameter now accepts list and array values; - concord-server: some optimizations for the process event processing (including Ansible). Reduces the contention on the process queue table; - ansible: fixed an issue with events not being sent to the server in some cases; - concord-server: make organization names optional when using secrets in `imports`; - concord-console: fixed an issue with the profile selection in the manual trigger popup; - concord-console: hide system files in the process attachments list; - concord-server: fixed an issue preventing `imports` from working correctly in `onFailure` and other handlers; - concord-console: add process tags to the process status page; - concord-server: process `tags` can now be specified using a comma-separated startup argument, e.g. `curl -F tags=x,y,z`; - concord-server: store repository info (commit ID, commit message, etc) early in the pipeline to preserve the data in case or process startup errors (e.g. bad syntax). ## [1.27.0] - 2019-07-20 ### Added - http-tasks: support for `application/x-www-form-urlencoded` (`request: form` type); - concord-server, concord-console: pagination for the secret list; - concord-server: support for "exclusive" triggers and "exclusive" execution groups; - concord-console: a form to change the secret's project. ### Changed - bpm: updated to `0.58.1`, resolves an issue with incorrect `context#getCurrentFlowName()` value in some cases; - project-model: fixed a bug preventing `withItems` and `retry` from working correctly when used together; - concord-server: process statup errors now correctly shown when the "browser link" is used; - concord-server, concord-console: fix the Ansible event status calculation; - ansible: fixed an issue with using arrays as the `tags` parameter values. ## [1.26.0] - 2019-07-10 ### Added - concord-console: add the UI's version to the About page. ### Changed - concord-console: fixed the issue with duplicate results in the "find user" field; - concord-server: fixed potential NPE when searching users in AD/LDAP (e.g. by using the Console's "find user" field); - concord-console: fixed the bug preventing the clear button on the process list's filter popup from working. ### Breaking - concord-server: the inventory subsystem now only accepts JSON objects as top-level entries; - concord-server: the "Landing Page" support is removed. ## [1.25.0] - 2019-07-02 ### Added - concord-server: support for triggers in `entity` policies. ### Changed - concord-server: GitHub trigger's `useInitiator` is now correctly runs the process using the commit user's security context. This fixes the issue with child processes not inheriting the initiator; - concord-server: fixed the handling of `queue.concurrent` policies: enqueued processes now track each running process instead of a single one; - concord-console: fixed the issue when a repository refresh error persists even after the refresh dialog is closed. ## [1.24.2] - 2019-06-27 ### Changed - concord-server: use GitHub event's `ldap_dn` to determine the event's initiator. ## [1.24.1] - 2019-06-27 ### Changed - concord-server: fixed the login when format of usernames provided by users didn't match the data returned by the AD/LDAP server; - concord-server: fixed the initial loading of default process configuration. ## [1.24.0] - 2019-06-25 ### Added - concord-server: automatic reload of `defaultConfiguration` file without restart; - http-tasks: support for query parameters; - concord-server: option to disable automatic user creation for the LDAP realm; - concord-console: display error details for `FAILED` processes; - concord-server, concord-console: support for AD/LDAP domains, custom username validators; - concord-server: API endpoints for role and permission management; - concord-console: add "copy to clipboard" buttons to entity IDs; - concord-server, concord-console: initial implementation of "manual" triggers; - project-model: `error` blocks support for `script` steps. ### Changed - http-tasks: make `method: GET` default; - concord-server: drop the `process_checkpoint` to `process_queue` FK, use a cleaning job instead. Enabled usage of partitioning for the `process_checkpoints` table; - docker-tasks: remove dependency on `io.takari.bpm/bpm-engine-api`; - concord-runner, docker-images: use `file.encoding=UTF-8` by default. Fixes the issue with Unicode passwords; - concord-server: correctly pass the parent's `requirements` when forking a process. ## [1.23.0] - 2019-06-17 ### Added - smtp: support for attachments. ### Changed - concord-server: fix symlink handling when importing the process state; - concord-tasks: fix the kill action. Now it is correctly accepts the `instanceId` parameter. ## [1.22.0] - 2019-06-16 ### Added - concord-cli: initial release; - project-model: support for configurable resource paths such as `./profiles`, `./flows`, etc; - project-model: new trigger type - `manual`. Can be used to configure process entry point available through the UI; - ansible: initial support for installing additional pip packages using Python's virtualenv; - ansible: support for multiple vault IDs/passwords; - concord-runner: configurable time interval without a heartbeat before the process fails; - concord-runner: the current flow name is now available via `${context.getCurrentFlowName()}` method; - concord-server: return a list of form `fields` in the generated `data.js` for custom forms. The list is in the original order of the form definition. ### Changed - concord-server: fixed a potential NPE when handling process metadata; - ansible: keep both the head and the tail when trimming long string values in events; - project-model: `withItems` now supports iteration over Java Map elements; - docker-tasks: make `cmd` optional; - concord-server, concord-agents: external `imports` are now processed without copying into the process state; - ansible: send process events asynchronously; - concord-client, concord-runner: use a custom `User-Agent` header; - concord-server: fixed a NPE when handling optional `file` form fields. ### Breaking - project-model: external `imports` are now a top-level element. ## [1.21.0] - 2019-05-23 ### Added - concord-server: additional metrics for the process key cache; - concord-server: configurable delay when polling for agent commands; - concord-console: show the "so far" duration of active statuses on the process status history page; - concord-runner: new options to enable or disable recording of task `in` and `out` variables. Including the option to blacklist specific variable names; - concord-console: display task call details in the process log viewer; - project-model: support for expressions in form calls. ## Changed - docker: the task can now be called using the regular `task` syntax (in addition to the previous `docker` form); - concord-server: keep the original values of `readonly` form fields on submit; - concord-server: throttle the AD/LDAP user group caching; - concord-console: fixed the log toolbar's flickering issue. ## [1.20.1] - 2019-05-16 ### Changed - concord-server: fix the `imports` processing - `configuration` objects from the imported resources are now loaded correctly; - concord-server: another fix for the issue with symlinks in GIT repositories. ## [1.20.0] - 2019-05-16 ### Added - http-tasks: new `debug` parameter. Enables additional logging of request and response data; - concord-tasks: log the job's URLs when starting new processes; - concord-console: the process log viewer's options are now persisted using the browser's Local Storage. ### Changed - project-model: fixed an issue with nested objects used in `withItems`; - concord-server: fixed an issue with circular symlinks in GIT repositories; - concord-console: fixed an issue preventing the UI from working correctly in MS Edge (missing `URLSearchParams` polyfill); - concord-agent: ignore subsequent attempts to enable the maintenance mode; - concord-server: `SUSPENDED` processes are now ignored when calculating the concurrent processes limit; - concord-console: fixed an issue preventing checkpoints from being rendered correctly in some cases. ## [1.19.1] - 2019-05-12 ### Changed - concord-console: better handling of log tag parsing errors; - concord-runner, concord-console: use a less common log tag name; - k8s/agent-operator: simultaneously handle pool size and configuration changes. ## [1.19.0] - 2019-05-12 ### Added - concord-console: option to split process logs by task calls; - concord-tasks: support for file attachments when starting new processes; - concord-sdk, crypto-tasks: expose `encryptString` method to flows; - slack: support for creating and replying to threads; - concord-agent, concord-runner: support for a configurable list of volumes to mount into Docker containers created by plugins. - concord-tasks: support for file attachments when starting a process ### Changed - concord-tasks: improved validation of input parameters; - ansible: fixed handling of `auth` parameters. The deprecated `user` and `privateKey` parameters are working now again; - concord-console: the URL parsing in the log viewer is updated to better handle URLs in quotes. ## [1.18.1] - 2019-05-07 ### Changed - concord-server: fixed the handling of processes with wait conditions. ## [1.18.0] - 2019-05-05 ### Added - ansible: support for multiple `inventory` and `inventoryFile` entries; - log-tasks: `logDebug`, `logWarn` and `logError` tasks; - concord-server: initial support for RBAC permissions; - ansible: initial Kerberos authentication support. ### Changed - project: initial support for server-side plugins. Ansible-related endpoints are moved into `server/plugins/ansible`; - concord-console: stop the form wizard when the user navigates away; - docker: include the Taurus PIP module in the default Ansible image; - concord-server: improve error messages in case of authentication failure due to an invalid input or an internal error. ## [1.17.0] - 2019-04-24 ### Added - docker: install Ansible's `k8s` dependencies; - k8s/agent-operator: initial version; - concord-server: return `requirements` when fetching a list of processes using `/api/v2/process`; - concord-server: SSO support for custom forms; - concord-console: make the log viewer URLs clicable; - ansible: support for non-root paths when fetching external roles; - http-tasks: new parameter `requestTimeout`. ### Changed - http-tasks: make `response` parameter optional; - concord-server: fixed the SSO https->http redirect; - concord-console: performance optimizations for the log viewer; - concord-runner: allow 2 letter checkpoint names. ## [1.16.1] - 2019-04-18 ### Changed - concord-console: use the configured project metadata to render the checkpoints page; - concord-runner: allow whitespaces in checkpoint names. ## [1.16.0] - 2019-04-17 ### Added - concord-server: new configuration parameter `ldap.excludeAttributes` - provides a way to exclude specific LDAP attributes from being returned in the user's `attributes`; - concord-server, concord-console: JWT-based SSO service support; - ansible: existing JSON and YAML extra vars files can now be used with the new `extraVarsFiles` parameter; - resource-tasks: new method `prettyPrintJson` - returns formatted JSON as a string; - concord-server, concord-console: ability to disable a process to prevent restoring it from a checkpoint after completion. ### Changed - concord-server: filter out all non string LDAP attributes; - concord-server: support for multivalue LDAP attributes when fetching user details from AD/LDAP; - concord-console: fixed the page limit dropdown on the checkpoint view page; - concord-common: do not escape backslashes when creating a ZIP achive; - project-model, concord-runner: support expressions in checkpoint names. ## [1.15.0] - 2019-04-03 ### Added - concord-agent, concord-runner: support for the configurable `logLevel`. ### Changed - concord-server: fix the `${initiator}` and `${currentUser}` data fetching when dealing with multiple account types. ## [1.14.0] - 2019-03-31 ### Added - policy-engine, concord-server: support for entity owner-based policies; - concord-tasks: new action `startExternal`. Can be used to start a new process on an external Concord instance. ### Changed - concord-client, concord-tasks: fixed a bug preventing `baseUrl` and `apiKey` parameters from being correctly applied; - http-tasks: correctly handle empty (204) responses; - concord-server: fixed a potential NPE when setting a new owner for projects without owner. ## [1.13.1] - 2019-03-27 ### Changed - http-task: make `ignoreErrors` work with connection timeouts; - concord-server: fixed an authentication issue with passwords that end with a `:`; ## [1.13.0] - 2019-03-26 ### Added - concord-sdk: additional `MapUtils` methods to retrieve Map, List and Integer values; - concord-console: show user display names on the team member list page; - concord-server: new user attribute - `displayName`. Automatically stored for AD/LDAP users. For local users it can be set using the API; - concord-server, concord-console: project owners can now be updated using the API or the Console; - concord-server, consord-console: organization owners can now be set using the API or the Console; - concord-server, concord-runner, concord-tasks: (optionally) suspend the parent process while waiting for a child process (only for `start`); - project-model: support for arrays in `withItems`; - concord-server: an API method to remove an organization (admin only). ### Changed - concord-agent: fix JVM "pre-forking". Now the process poll is correctly shared between all workers; - concord-server: more detailed error messages in case of invalid encrypted strings; - concord-console: fixed a bug when team members could not be deleted. ## [1.12.0] - 2019-03-13 ### Added - policy-engine, concord-server: support for organization-level max concurrent process limits; - concord-console: process metadata filtering for the checkpoint list page; - concord-server, concord-console: teams can now be associated with LDAP groups; - concord-server: user accounts can now be disabled via an API call; - concord-server: new automatically-provided process variable - `requestInfo`. Contains the request's parameters, headers and the user's IP address. ### Changed - concord-agent: fixed the issue preventing the process startup errors from being logged correctly; - concord-console: fixed a login issue with non-ASCII usernames or passwords. ## [1.11.0] - 2019-03-04 ### Added - concord-server, ansible: track `retry` count per host; - concord-agent: failover support for the websocket connections; - ansible: print a "No changes will be made" warning if `check` or `syntaxCheck` modes are used. ### Changed - concord-console: fixed the handling of processes with checkpoints but without history; - concord-server: when a repository refresh fails show the error cause instead of a wrapped exception; - project-model: fixed the behaviour of nested and/or sequential task calls with `retry`; - ansible: the task now correcly records both pre- and post-action events; - concord-console: fixed the log timestamp pattern, now Ansible log timestamps are correctly converted into the local time; - ansible: `{%raw%}` strings are working again. ## [1.10.0] - 2019-02-27 ### Added - concord-server: support for GitHub webhooks limited to a specific repository; - forms: support for `date` and `dateTime` field types; - concord-server, concord-console: ability to temporarily disable repositories. ### Changed - concord-server: return `400 Bad Request` when trying to `decryptString` in a process without the project; - bpm: updated to `0.54.0` to support expressions in `script` names. ## [1.9.0] - 2019-02-20 ### Added - concord-server: option to sign the `initiator` and `currentUser` usernames. Signatures can be validated using the configured public key. ### Changed - concord-agent: when resolving dependencies, use `latest` as the indicator of an automatically selected version; - concord-server: remove keywhiz support. The Concord-Keywhiz integration could be implemented as a separate plugin; - concord-server: removed support for archiving process state and checkpoints into an S3 endpoint. The recommended way is to use table partitioning or a custom (external) data retention solution; - concord-console: fixed the process list filtering. ## [1.8.1] - 2019-02-18 ### Changed - concord-agent: fix the dependency resolution for plugins with non-default versions; ## [1.8.0] - 2019-02-17 ### Added - concord-server: option to retrieve a single item in an inventory; - concord-sdk: initial implementation of `LockService`; - concord-server: support for `activeProfiles` in `cron` triggers; - concord-server: new endpoint `GET /api/v2/process`. Returns a list of processes with optional filtering (including metadata) and pagination; - concord-sdk: new utility methods to work with the process variables; - concord-server: initial support for in-process locks. ### Changed - concord-server: force logout users on any authentication error. Fixes the issue with "remember me" users with passwords changed between the server's restarts; - repository: clean up and reset locally cached repositories on checkout; - concord-server: skip invalid host names when processing Ansible events; - concord-runner: more graceful handling of errors while saving "out" variables; - concord-server: check for permissions when retrieving process details via `GET /api/v2/process/{id}`. - concord-server, concord-agent: load the dependency version list from the server. ## [1.7.2] - 2019-02-11 ### Changed - concord-server: forks should keep the original `_main.json` minus the `arguments`. This fixes the issue with forks and `onCancel/onFailure` handlers which are using external dependencies; - concord-console: fixed the row selection bug in the process list component. ## [1.7.1] - 2019-02-09 ### Changed - concord-server: fix authorization of `cron` triggers. ## [1.7.0] - 2019-02-07 ### Added - throw-tasks: now capable of throwing exceptions with custom payload: maps, lists, etc; - concord-console: new "Wait Conditions" tab on the process status page; - concord-server: new process configuration option `exclusiveExec`: restricts the process execution to one process at the time per project; - http-tasks: proxy support via `proxy` parameter; - concord-server: option to restrict the external events endpoint `/api/v1/events/{eventName}` to users with specific roles. ### Changed - ansible: Concord polices now receive interpolated variable values; - concord-console: display empty checkpoint group for suspended processes; - concord-runner: save and restore the last known variables for forked processes. This allows forks and onCancel/onError/etc handlers to access the parent process' variables; - concord-server: use roles instead of user flags. E.g. `concordAdmin` role instead of `USERS.IS_ADMIN`. ## [1.6.0] - 2019-01-31 ### Added - concord-console: new flow selection dropdown in the process start popup. ### Changed - concord-server: do not copy the parent process' forms when forking a process; - repository: fix the GIT clone bug for repositories without a `master` branch; - concord-console: fix the checkpoint grouping issue, preventing checkpoints from being correctly rendered; - project-model: improve stacktraces in case of YAML parsing errors; - concord-runner: fix the timestamp format in the `processLog` logger. Plugins such as Ansible should use the correct timestamp format now. ## [1.5.0] - 2019-01-30 ### Added - concord-server: new form attribute `submittedBy` - automatically created for each form after it submitted, contains the form user information. Can be enabled with `saveSubmittedBy` form call option; - concord-console: add option to convert log timestamps into local time; - concord-server, concord-runner: use UTC in log timestamps; - concord-agent: additional logging while downloading the process' repository data and state; - concord-server, concord-console: "Remember Me" cookie support; - concord-console: list of checkpoints on the process status page; - concord-runner: new method `context#form`, allows dynamic creation of forms in tasks, scripts and expressions; - concord-console: profile selection when starting a process; - concord-tasks: new task `concordSecrets`. ### Changed - concord-server, concord-agent: disable `git.httpLowSpeedLimit` as it was causing major performance issues when cloning large repositories; - concord-server: reduce the default max session age to 30 minutes. ## [1.4.0] - 2019-01-23 ### Added - concord-server: return the build's commit id in the server version response; - vars plugin: `get` method can now return nested variables or fallback to a specified value; - concord-server: new endpoint `/api/v2/process/{id}`, allows customization of the data included in the response; - concord-server: new API endpoint `/api/v2/process/{id}/checkpoint`, allows restoring checkpoints with a `GET` request; - concord-console: bring back the status column to the child process list; ### Changed - concord-server, project-model: allow expressions in the form's `runAs` parameter; - concord-server: close websocket channels when the maintenance mode is enabled; - ansible: fixed an issue that caused the sensitive data masking plugin (`concord_protectdata.py`) to fail on non-ASCII strings. ## [1.3.0] - 2019-01-15 ### Added - concord-console: list of attachments added to the process status page; - slack: when sending a message, the task now returns a `result` object. ### Changed - concord-console: flow events moved to a separate tab on the process status page; - concord-server: copy the parent process' repository info to forked processes. This fixes an issue preventing process forks from working correctly. ## [1.2.2] - 2019-01-14 ### Changed - concord-server: store custom form files in the process state regardless of whether they are form a repository or not. This fixes an issue preventing custom forms from working correctly. ## [1.2.1] - 2019-01-13 ### Changed - concord-agent, concord-runner: log additional performance metrics when running in `debug` mode; - dependency-manager: pre-sort dependency URIs to ensure stable dependency resolution order. This improves chances of getting a pre-forked JVM instead of spinning a new one; - concord-console: fix potential data race when loading process checkpoints; - concord-server: last modified date of process state files is now correctly calculated when importing the state. ## [1.2.0] - 2019-01-09 ### Added - http-tasks: support for `PATCH` method. ### Changed - concord-server: fixed a bug preventing the Ansible events from being processed correctly; - concord-console: fixed the parsing of GIT URLs on the repository list page; - concord-server: fixed an issue preventing git submodules using the default (token-based) auth from working; - concord-server: reject flow attachments if "Allow payload archives" is disabled in the project's settings; - concord-console: disable "New Project" button if the user is not a member of the organization. ## [1.1.0] - 2019-01-04 ### Added - concord-console: new "Checkpoint View" for projects; - concord-console: allow addition of new elements for `type*` and `type+` form fields. ### Changed - concord-console: the process start popup now correctly displays the branch of the selected repository; - concord-server: allow project-scoped secrets to be used when cloning the project's repositories; - concord-agent: improved error logging when cloning repositories; - concord-runner: fixed an issue preventing the runner from terminating correctly on `java.lang.Error`; - concord-server: set the default session timeout to 10 hrs (was unlimited); - concord-server: accept GitHub events without branch information (e.g. archive events). ## [1.0.0] - 2018-12-26 ### Added - concord-server, concord-console: custom process list filters based on process metadata; - concord-server: new `/api/v2/trigger` endpoint. Currently only the list method is provided; - concord-console: externalize extra links in the system menu, allow for environment-specific overrides; - concord-console: ability to specify the entry point when starting a process; - ansible: new `syntaxCheck` option to run `ansible-playbook --syntax-check`; - ansible: new `check` option to run `ansible-playbook --check`. ### Changed - concord-console: fixed an issue with error messages persisting after navigating out of the page; - concord-server: fixed the logic of the Ansible event processor. Now it should correctly handle very long-running processes; - concord-console: fixed a bug preventing the Ansible host filter from working correctly; - concord-runner: fix the handling of process arguments when restoring a process from a checkpoint; - concord-server, concord-agent: perform `git clone` on the Agent, keep only the changes in the DB; - bpm: updated to 0.51.0, fixed the resolution order of tasks and variables. Now flow variables can shadow the registered tasks; - concord-console: prevent table overflow on process detail table; - concord-console: move the Ansible stats table above the flow event table; - concord-server: fixed the host status calculation in the Ansible event processor. Now `SKIPPED` is correctly overwritten by other statuses in multi-step plays. ## [0.99.1] - 2018-12-06 ### Changed - ansible: use `/tmp/${USER}/ansible` as the default `remote_tmp`; - concord-server: fixed a bug in `/api/service/process_portal` preventing the endpoint from working. ## [0.99.0] - 2018-12-05 ### Added - project-model: support for programmatically-defined form fields; - concord-server: new `useEventCommitId` parameter of `github` triggers; - concord-server: `repoBranchOrTag` and `repoCommitId` parameters to start a process with the override of the configured branch/tag/commitId values; - concord-console: pagination for the Ansible host list on the process status page; - http-tasks: new parameter `ignoreErrors` - instead of throwing exceptions on unauthorized requests, the task returns the result object with the error; - slack: new `slackChannel` task for creating and archiving channels and groups; - concord-server: a metric gauge for the number of currently connected websocket clients; - misc-tasks: new `datetime` task; - concord-server: pagination support for the child process list page; - concord-server: support for policy inheritance; - concord-server: `offset` and `limit` to the process checkpoint list endpoint; - concord-server: support for exposing form and nested values as process metadata; - concord-server: support for default metadata values. ### Changed - docker: set the minimal Ansible version to 2.6.10; - concord-project-model: forbid empty flow and form definitions; - concord-server: use a single local clone per GIT URL; - concord-server: fixed an issue, causing `onFailure` to fire up multiple times in clustered environments. - concord-server: `cron` triggers are now using the DB's time to calculate the schedule; - concord-console: improved repository validation error messages; - concord-console: dropdown menus with optional values now correctly render the empty "value"; - concord-console: fixed a bug preventing the child process list from working correctly; - concord-server, concord-console: Ansible events are now pre-processed and stored on the backend, making the Process Status page more responsive when working with large Ansible plays. - concord-server: evaluate parsed expression value in custom form field's `allow` attribute - concord-server: change the (potential) partitioning key in `PROCESS_EVENTS` from `EVENT_DATE` to `INSTANCE_CREATED_AT`. ## [0.98.1] - 2018-11-23 ### Changed - concord-queue-client: fixed potential OOM when handling connection errors. ## [0.98.0] - 2018-11-18 ### Added - concord-server: add `meta` to the process checkpoint list endpoint. - concord-console: pagination support for the process list. ### Changed - concord-console: fixed a session handling bug. Now the session is correctly restored on UI reload. ## [0.97.0] - 2018-11-13 ### Added - http-task: `connectTimeout` and `socketTimeout` parameters; - concord-server: GitHub triggers can now use `payload` field with the original event's data; - concord-server: new API endpoint to retrieve a list of processes including their status history and checkpoints; - concord-console: a warning if a password stored as a secret is too weak; - concord-agent: an endpoint to get the current status of the maintenance mode. ### Changed - concord-server: removed GitHub webhook registration and repository cache; - concord-server: fixed a bug preventing relative file upload paths from working with `/api/v1/process` endpoint; - concord-server: the session cookie (`JSESSIONID`) is now marked as `HttpOnly`; - concord-common: ensure that `CONCORD_TMP_DIR` environment variable is defined; - concord-server: fixed a bug causing incorrect matching of Concord repositories with incoming GitHub events. ## [0.96.0] - 2018-11-07 ### Added - concord-console: new tab on the process status page - "Child Processes"; - project-model, concord-server: support for `readonly`, `placeholder` and `search` form field attributes; - project-model: `withItems` now correctly handles `out` variables of tasks; - concord-server: support for GitHub events other than `push` or PR-related events; - concord-server: GitHub webhook support for unknown (not registered in Concord) repositories. ### Changed - concord-server: fixed a bug preventing process checkpoints from being correctly archived; - docker: updated Ansible to 2.6.7. ### Breaking - concord-tasks: IN parameter `jobs` renamed to `forks` to avoid collision with OUT parameter `jobs`. ## [0.95.0] - 2018-11-03 ### Added - concord-server, concord-runner: store `lastError` in `out` variables in the process' metadata. - concord-server: `afterCreatedAt` parameter to the process list API endpoint. ### Changed - concord-server: allow admins to access any form; - project-model: checkpoint names must be unique across all loaded flow definitions; - project-model: fixed a bug preventing nested `withItems` from working correctly; - bpm: updated to 0.49.0. Now all context types implement `eval` and `interpolate` methods. ### Breaking - concord-client: `ProcessApi#metadata` renamed to `#updateMetadata`. ## [0.94.0] - 2018-10-29 ### Added - concord-server: optional "default filter" for all GitHub triggers; - concord-server: make optional the unknown GitHub webhook removal. ### Changed - concord-runner: fixed a bug preventing dynamic task registration from working correctly. ## [0.93.1] - 2018-10-28 ### Changed - concord-server: fixed a bug preventing the checkpoint archiver from working correctly. ## [0.93.0] - 2018-10-28 ### Added - concord-server: add `payload` to `github` trigger events; - concord-server: GitHub webhook URLs can now supply additional parameters via query parameters; - concord-server: configurable period values for cleanup tasks; - concord-sdk: support for "protected" variables that can be set only by allowed tasks; - ansible, policy-engine: support for restricting of allowed URLs in `maven_artifact`, `uri` and `docker_container`; - policy-engine: support for value expressions; - concord-console: new process history tab on the status page. ### Changed - concord-server: allow GitHub events without explicit webhook registration (e.g. organization-level hooks); - concord-console: the process filtering list is performed on the server now; - concord-server: `task_locks` are replaced with the task schedule table; - concord-server: merge the existing process variables with template variables; - bpm: updated to 0.48.0, fixed `context#getVariableNames` issue. ### Breaking - concord-server: `github.enabled` configuration parameters renamed to `github.webhookRegistrationEnabled`; ## [0.92.0] - 2018-10-21 ### Added - concord-server: optional rate limit for process start requests; - concord-server: ability to assign a policy to a user; - ansible: additional validation for `groupVars`. ### Changed - concord-server: return `429` then requests are rate-limited or rejected by the queue policy; - concord-server: fixed an issue preventing organization data from being correctly updated via REST; - project-model: using `withItems` with `null` now skips the block's execution instead of throwing an error. ## [0.91.0] - 2018-10-14 ### Added - concord-server: support for `timezone` in `cron` triggers; - concord-console: ability to cancel multiple processes; - concord-server: the secret decryption error now contains the secret's name; - concord-server, concord-console: refresh GitHub webhooks when a repository is refreshed; - concord-server: timeout options for GIT's HTTPS and SSH transports; - concord-server: a policy rule for setting the maximum allowed `processTimeout` value. ### Changed - concord-server: the "default variables" file replaced with the "default configuration" file. Instead of containing the `arguments` section, it now contains the while `configuration` object. ### Breaking - concord-server: `process.defaultVariables` configuration file parameter renamed to `process.defaultConfiguration`; - concord-sdk: `Constants.Request.USE_INITIATOR` renamed to `Constants.Trigger.USE_INITIATOR`. ## [0.90.0] - 2018-10-09 ### Added - concord-server: assign an save a unique "request ID" to link audit logs and the process queue data; - concord-server: ability to restrict the max number of forks using policies; - concord-server: ability to restrict the max number of processes in the queue for a given status using policies; - docker: `envFile` parameter to define environment variables using a file; - concord-console: show repository names in the project process list; - concord-server: ability to overwrite process configuration using policies. ### Changed - concord-server: fixed an issue preventing process timeouts from correctly working with multiple running processes; - concord-tasks: override the API endpoint with `baseUrl`. ## [0.89.3] - 2018-10-02 ### Changed - concord-console: fixed an issue preventing the process start redirect from working. ## [0.89.2] - 2018-10-01 ### Changed - concord-server: fixed variabled of the `spec` field in `cron` triggers; - concord-server: fixed an issue causing `cron` triggers to use the default flow. ## [0.89.1] - 2018-09-30 ### Changed - ansible: fix a bug preventing the task callback from recording task start events. ## [0.89.0] - 2018-09-30 ### Added - concord-server: user-defined process timeouts support; - concord-server: "initiator passthrough" support for OneOps and GitHub events; - concord-server: added support for GitHub PR events; - concord-agent, concord-runner: initial support for "container per process" execution model. ### Changed - ansible: use ANSI colors by default; - concord-server: fixed a bug preventing checkpoints from working with `.concord.yml` files. ## [0.88.0] - 2018-09-19 ### Added - dependency-manager: load plugin versions from a file. Allows omitting the version qualifier for the registered plugins. ### Changed - concord-server: fixed a bug preventing repositories from being automatically refreshed on GitHub push events; - concord-console: fixed project process list filtering. ## [0.87.0] - 2018-09-16 ### Added - concord-console: prevent loading of too much data on the process status page, show a warning instead; - concord-server: support for email form fields (`inputType: "email"`); - concord-server: expose Jetty statistics; - concord-console: add filtering to the organization list; - concord-console: UI for managing access to projects and secrets; - concord-server, concord-console: initial support for process-, organization- and project-level metadata. ### Changed - concord-server: fixed a bug preventing GIT repositories with large number of tags from working; - concord-server: apply RBAC filters when listing secrets. ## [0.86.0] - 2018-09-05 ### Added - concord-server: new API method to list process checkpoints; - concord-agent: option to ignore SSL certificate errors for API calls; - concord-console: add filtering to the secret list and the team list pages; - concord-server, concord-console: option to use a service account to retrieve GIT repositories instead of user keys; - concord-server: store project keys (used in `encrypt/decryptString` methods) in the DB. ### Changed - concord-console: fix handling of process statup errors; - concord-server: in LDAP auth try `userPrincipalName` first; - concord-server: require `READER` access level to refresh triggers (instead of `WRITER`); - concord-server: process-level variables are now correctly override the system-wide defaults; - concord-server: fixed audit logging for AD/LDAP authentication; - concord-server: improved audit logging for projects and repositories; - concord-server: store `initiatorId` instead of `initiator` username in the process queue table; - project-model: fix handling of `null` values in `set` step. ## [0.85.0] - 2018-08-15 ### Added - concord-server: additional metrics, process queue gauges; - resource-tasks: new method to read YAML files. Optional support for string interpolation in JSON and YAML files; - ansible: initial support for external roles. ### Changed - runner: upgrade the BPM engine version to `0.47.1` to fix a bug preventing correct handling of IN variable evaluation errors. ## [0.84.1] - 2018-08-12 ### Changed - concord-server: fix missing `@Named` annotations for JOOQ configuration. Causes the wrong datasource to be injected. ## [0.84.0] - 2018-08-12 ### Added - concord-server: `${processInfo.activeProfiles}` variable; - concord-runner: new utility task `forms` to create links to process forms and the form wizard; - concord-server, concord-agent, project-model, concord-runner: initial support for process checkpoints; - concord-server, policy-engine: support for "max concurrent processes" policy. ### Changed - docker: increased the default API proxy timeout to 180 seconds; - concord-console: fix the custom view link on the process form page; - concord-console: fixed retieval of "Last 10 processes" on the Activity page; - concord-server: fixed a bug causing `onFailure` handler processes to fail due to missing session keys and `projectInfo` variables. ### Breaking - concord-server: form API endpoints now accept form names instead of IDs. E.g. `/api/v1/process/PROCESS_ID/form/FORM_NAME`. ## [0.83.1] - 2018-08-06 ### Changed - concord-server: revert inventory RBAC changes, allow publicly writable inventories; - concord-console: fix the process status filter on the project processes page; - concord-console: limit `Activity` data to the current user's organizations. ## [0.83.0] - 2018-08-05 ### Added - concord-server: more metrics; - concord-console: new process list filters - `Status` and `Initiator`; - concord-console: new default page `Activity`; - concord-console: allow updating of project visibility and description; - concord-server: improved support for multi-select fields in custom forms; - ansible: option to save Ansible host statistics as a flow variable; - http-tasks: support for `DELETE` method; - project-model: support for `activeProfiles` in trigger definitions; - concord-server: option to disable triggers (specific or all); - concord-server: the Prometheus metrics endpoint now proxied by the console's nginx. ### Changed - concord-server: fix permissions check when killing a process; - concord-console: fix duplicate form events; - concord-console: replace automatic redirect to a custom form with a link; - concord-console: escape HTML in process logs. ## [0.82.0] - 2018-07-29 ### Added - concord-server: additional metrics (JVM, memory, JDBC, etc). ## [0.81.0] - 2018-07-24 ### Added - concord-server: delay before processes are moved into the archive; - concord-server: support for custom form validation messages; - concord-server: option to disable GIT's "shallow clone"; - concord-server: initial integration with Prometheus; - concord-server: option to disable the process state archiving task. ### Changed - concord-server: make the process state archiving task to pick up next items as soon it's done with the current work. ## [0.80.0] - 2018-07-22 ### Added - project-model: support for multiple Concord project files in `concord/*.yml`; - concord-server: support for archiving of process state into S3-compatible object stores; - concord-server: optionally encrypt sensitive data in the process state; - project-model: allow expressions to be used in flow calls; - plugins: new built-in task `throw`; - concord-console: ability to filter Ansible hosts by their inventory group; - concord-server: RBAC checks for process kill and status update operations. ### Changed - concord-server: return `404` when the secret doesn't exist; - concord-server: fix handling of empty inventory query params. - concord-server: fixed the repository webhook registration when project or repository is created or updated. ## [0.79.2] - 2018-07-16 ### Changed - concord-agent: fixed the configuration variable names; - concord-agent: increase default timeout values; - concord-server: correctly handle table aliases in inventory queries; - concord-server: escape special characters in commit messages `processInfo.repoCommitMessage`. ## [0.79.1] - 2018-07-16 ### Changed - concord-server: fixed an issue preventing `initiator.attributes.mail` from populating. ## [0.79.0] - 2018-07-15 ### Added - project-model: additional YAML validations to prevent duplicate keys such as `configuration` or `arguments`; - concord-server: an option to perform DB migrations separately; - concord-server: periodic audit log clean up; - concord-server: option to disable audit logging; - concord-server: added limits for "encrypted strings" size; - concord-server: improved error reporting for non-process related errors; - concord-server, concord-console: repository metadata to processes; - concord-server: improved inventory query validation; - concord-console: display flow and ansible event duration; - ansible: record "pre" and "post" task events separately; - concord-console: new API key management UI; - concord-server: optional API key expiration; - concord-server: methods to list existing inventories and inventory queries; - concord-server, concord-console: new method to validate a repo without starting it; - concord-console: new form to encrypt values to use with `crypto.decryptString`; - concord-server: all secured endpoints will now return appropriate auth challenge headers; - concord-console: scroll back to top button on the process log page; - concord-console: highlighting of errors and warning messages in process logs; ### Changed - docker-images: increased nginx's default max request size to 32Mb; - concord-server: a single invalid trigger no longer prevents other triggers from firing; - concord-agent: now uses the same REST API endpoint to download process state snapshots as regular users. ## [0.78.1] - 2018-07-08 ### Changed - concord-server: fixed a bug that prevented the custom forms API endpoint from registering correctly. ## [0.78.0] - 2018-07-08 ### Added - concord-project-model: improved validation of `withItems` values; - concord-console: autoscrolling for process logs; - concord-console: ability to rename secrets and update their visibility; - concord-console: the list of registered triggers for a repository. # Changed - concord-server: configuration migrated into a single configuration file. - concord-runner: full stacktraces will be printed out for unhandled exceptions. ## [0.77.0] - 2018-07-01 ### Added - concord-server: project-scoped secrets; - http-tasks: support for custom headers. ### Changed - concord-client: better handling of server-side validation errors; - concord-server: merge `api` and `impl` modules; - concord-server: GitHub triggers will now correctly use the repo's organization and project names to match the events; - concord-server: now process state snapshots can only be accessed by admins, process owners and "global readers". ### Breaking - concord-server: removed the old project API endpoint `/api/v1/project`. ## [0.76.0] - 2018-06-24 ### Added - concord-server: multiple ldap groups support in forms ### Changed - concord-server: fixed the repository webhook registration when an individual repository is created or updated; - concord-console: project, team members and ansible host filters are now using simple substring search instead of regular expressions; - ansible: fixed a bug than prevented `outVars` from working for any variables other than the first in the list. ## [0.75.0] - 2018-06-17 ### Added - policy-engine: optional rule violation messages; - concord-console: the repository start popup now has a link to the process log if the process failed to start; - concord-console: the profile page and the API key form; - ansible: new task parameter `outVars` to save specific Ansible variables as Concord flow variables; - concord-agent: a local interface to enable maintenance mode; - concord-server: check template output for cycles to avoid serialization issues; - concord-server: size limit for binary data secrets. ### Changed - concord-console: clean up the error message when users try to access a form without a matching LDAP group; - policy-engine: fix calculation of workspace sizes; - concord-console: fixed the server error details parsing; - concord-rpc: module removed. ## [0.74.0] - 2018-06-10 ### Added - concord-server: initial support for roles; - concord-console: a field to filter the project list by name; - concord-project-model: support for `withItems`. Allows iterating over a list of elements; - concord-server: support for `Bearer ` authorization; - concord-console: the process page's organization and project links; - concord-server: optional git repository check for `concord.yml` being present. ## [0.73.0] - 2018-06-04 ### Added - concord-server: new `update` method for secrets. It allows changing of secret names and/or visibility; - concord-agent: retries for errors during download of process payloads; - concord-agent: `AGENT_ID` and `USER_AGENT` parameters to support persistent agent IDs. ### Changed - concord-server: fixed the secret access level endpoint not accepting team names; - concord-server: fixed "test repository connection" method. ## [0.72.0] - 2018-06-03 ### Added - concord-console: add `System` menu with `Documentation` and `About` links; - concord-agent: new `MAX_PREFORK_COUNT` configuration parameter to limit the number of processes in the pool; - ansible: new parameter `disableConcordCallbacks` to disable Concord-specific Ansible callbacks: stdout filtering, event recording, etc; - ansible, concord-console: handle Ansible's `ignore_errors` modifier; - ansible: ability to use password-less secrets in the secret lookup plugin; - concord-client, concord-agent, concord-runner: enable session cookies; - concord-task: fail the parent process if the subprocess has failed. Added new parameter `ignoreFailures: true` to revert the previous behavior. ### Changed - concord-project-model: external form definitions must work with and without a whitespace in the definition. ## [0.71.3] - 2018-05-31 ### Changed - concord-agent: fixed orphaned docker sweeper bug which caused live containers to be terminated. ## [0.71.2] - 2018-05-30 ### Changed - guava downgraded from 21.0 to 20.0 to avoid classpath issues with some of the plugins (e.g. jira). ## [0.71.1] - 2018-05-29 ### Changed - concord-server: fix form wizard redirect; - concord-server: improve `decryptString` error messages; - concord-runner: fix export of secrets w/o password; - concord-tasks: now uses `concord-client` instead of Resteasy. ## [0.71.0] - 2018-05-21 ### Added - ansible: limit the saved stdout/stderr size; - concord-server: log the agent's IP address when a process starts; - concord-server: a method to list all inventory items; - concord-server: added optional `replace` query parameter to the team users update operation. - concord-console: team management UI. ### Changed - concord-server-client: renamed to `concord-client`; - concord-server: reduced the amount of information on the "wait page" in the process API endpoint for browsers; - concord-server: fixed team management RBAC; - concord-console: fixed dropdowns not re-rendering after update; - concord-server: fixed incorrect filtering of inventory data. ## [0.70.0] - 2018-05-17 ### Added - concord-server, concord-agent: initial support for environmental `requirements` and agent capabilities. ### Changed - concord-runner, concord-server: improved error handling when working with secrets; - concord-client: renamed to `concord-tasks`; - concord-rpc: the KV store, heartbeat and secret gRPC services replaced with the REST API based services. ## [0.69.0] - 2018-05-09 ### Added - concord-agent: "maintenance mode" to suspend job acquisition; - resource-tasks: new methods to read/write JSON; - concord-server: an "in progress" page when the process is started using "browser" endpoints; - concord-console: a button to download raw process log. ### Changed - concord-console: new UI layout. ## [0.68.1] - 2018-05-09 ### Changed - concord-server: fixed the repository connection test method not getting secrets from the UI. ## [0.68.0] - 2018-05-04 ### Added - http-tasks: support for `PUT` requests; - concord-server: new LDAP configuration property `usernameProperty`. Defaults to `sAMAccountName`. STRDTORC-507 ## [0.67.0] - 2018-04-29 ### Added - concord-server-db: `bigserial` columns for forwarding log data; - concord-runner: record task `in` parameters in process events; - concord-runner, ansible: `correlationId` for task events; - concord-server: `runAs` form option. ## [0.66.0] - 2018-04-22 ### Added - concord-client: support for `activeProfiles`; - docker: ability to save `stdout` as a variable; - concord-project-model: `retry` support for tasks; - smtp: add support for multiple values in `to`, `cc` and `bcc` parameters; - concord-server: method to cancel a process including its subprocesses; - concord-server: method to delete an existing team. ### Changed - concord-server: skip invalid definitions on trigger activation. ## [0.65.3] - 2018-04-16 ### Added - concord-server: additional logging for authentication realms. ### Changed - concord-server: fixed session key conflicts when sub processes are used; - concord-console: load correct project processes; - concord-server: fixed `platform` filter for OneOps triggers. ## [0.65.2] - 2018-04-15 ### Changed - docker: fix nginx logging configuration. ## [0.65.1] - 2018-04-15 ### Added - concord-server: support for `cron` triggers; - concord-project-model: better handling of YAML parsing errors; - concord-server: process endpoints now return `childrenIds` - array of child process IDs. ### Changed - concord-server: fixed an issue when updating team role for an existing team member. ## [0.64.0] - 2018-04-11 ### Added - concord-sdk: `Context#suspend` method for suspending processes with programmatically-defined callback events; - concord-server: metadata for organizations; - concord-server: initial audit logging implementation; - keywhiz: initial support; - concord-server: support for symlinks on initial process state ingestion; - ansible: support for exporting secrets as `group_var` files; - concord-console: show Ansible host events in a modal popup. - ansible: new lookup plugins 'concord_data_secret' and 'concord_public_key_secret' ### Changed - ansible: deprecate `configuration.ansible`, `inventory` and `dynamicInventory` request parameters; - http-tasks: parse JSON responses; - http-tasks: use `${response}` as the default out variable. ## [0.63.0] - 2018-04-01 ### Added - concord-console: new UI layout; - concord-console: visualization of Ansible stats; - concord-server: make `orgName` optional when `teamName` is used when setting a resource's access level. ### Changed - ansible: make `privateKey`'s password optional; - concord-agent: ship `http-tasks` with the docker image; - concord-console: disable TLS 1.0. ## [0.62.1] - 2018-03-26 ### Changed - concord-server: send empty JSON if a project's cfg is empty; - concord-server: use basic auth for the interactive process endpoints. ## [0.61.0] - 2018-03-25 ### Added - concord-server, concord-agent, concord-runner: initial support for process policies; - concord-project-model: `exit` step to terminate execution of the flow w/o throwing an error; - concord-client: support for the new `startAt` parameter. ### Changed - concord-server: OWNER access level is now required to delete a project. ## [0.60.0] - 2018-03-22 ### Added - concord-server: a method to update access levels of inventories; - concord-server: `startAt` process parameter to schedule process executions; - concord-agent: a cleanup job to remove old Docker images (keeps two latest versions). - concord-server: an endpoint to download a single state file. ### Changed - concord-server: fixed an issue, preventing file upload fields from working with custom forms. ## [0.59.0] - 2018-03-11 ### Added - concord-server: the process event endpoint now accepts `limit` and `after` query parameters. ### Changed - concord-server: allow removal of secrets that are in use; - concord-server: trim usernames on login. - ansible: the task now prepends user-provided `callback_plugins` and `lookup_plugins` values with the default values. ## [0.58.0] - 2018-02-25 ### Added - new task: `http`. Provides a simple HTTP client with JSON support. ### Changed - ansible: fixed exporting of key pairs; - concord-server: fixed handling of secret requests w/o an organization; - concord-server: fixed retrieval of team users. ## [0.57.0] - 2018-02-22 ### Added - slack: support for the `task` syntax, includes new messaging options such as `icon_emoji` and `attachments`; - ansible: filter for removing sensitive data from the logs; - concord-client: `concord` task's `repo` is an alias for `repository` now; - concord-client: `project` task now works with organizations other than `Default`; - concord-client: `concord` task now accepts `payload` parameter instead of `archive`, which can be either a path to a ZIP archive or a path to a directory; - concord-server: additional logging in case of process statup errors. ### Changed - concord-server, concord-console: fix handling of multi-select dropdowns; ## [0.56.0] - 2018-02-14 ### Added - concord-server: new user's `type` attribute (`LOCAL` or `LDAP`); - crypto: ability to export secrets from organizations other than the current. ### Changed - concord-docker: fixed the `unable to find user concord: no matching entries in passwd file` issue. ## [0.55.0] - 2018-02-13 ### Added - concord-agent: the new `debug` configuration parameter to log the resolved dependencies of a process. ### Changed - concord-server: fixed incorrect inventory query filtering; - concord-client: use polling while waiting for the process to end; - ansible: updated to 2.4.3; - concord-client: `org` parameter was ignored; - concord-server: configurable max state age; - ansible, docker: use a non-root user to run all Docker processes (including `ansible-playbook`); - docker: run Docker containers in the host's network. ### Breaking - concord-server: the trigger endpoint address is made to conform path patterns of the rest of the organization endpoints. ## [0.54.0] - 2018-02-01 ### Added - ansible, docker: support for `--add-host` in the Ansible and Docker tasks. ### Changed - docker: automatically update `pip` to the latest version; - docker: more Walmart-specific CA certificates; - concord-server: when a new team is created, automatically add the current user as the team's `MAINTAINER`; - concord-server: apply RBAC to the process state download endpoint. ## [0.53.0] - 2018-01-28 ### Added - concord-server: support for nested paths when retrieving attachments; - docker: Walmart images are now include Walmart's Root CA SSL certificates. ### Changed - concord-console: embed the Semantic UI resources; - concord-server: JGit is replaced with the GIT CLI tool, improving the support for submodules and large repositories. ## [0.52.0] - 2018-01-23 ### Changed - concord-server: avoid creation of multiple webhooks for the same GIT repository urls registered in different projects; - docker: add non-root users for the server and agent containers; - dependency-manager: ignore checksums, cache the intermediate data. ## [0.51.0] - 2018-01-17 ### Added - inventory: the ansible wrapper now able to produce inventories with per-host variables; - crypto: a method to export a single value secret as a file; - concord-tasks: make the organization name parameter optional for the inventory task; - concord-server: an endpoint to export binary data secrets; - ansible: add a lookup plugin for retrieving "single value" secrets; - ansible: make the org parameter optional for the inventory lookup plugin. ### Changed - concord-server: form `values` are now correctly added to the process context after the form is submitted submit; - ansible: updated to 2.4.2; - ansible: `inventoryFile` and `extraEnv` were ignored when the plugin was invoked using the task syntax; - concord-server: the "Accept payload archives" flag now correctly updates; - concord-server: fixed the issue preventing the process from being marked as FAILED on YML syntax errors; - concord-server: fixed parsing of the `activeProfiles` property when the multipart process endpoint is used. ## [0.50.0] - 2018-01-10 ### Added - concord-server: limit the maximum allowed size of process files; - concord-server: configurable DB connection pool size. ### Changed - concord-console: fixed the issue with new projects overwriting the previously opened ones. ## [0.49.0] - 2018-01-07 ### Added - concord-server: allow hyphens and tildes in entity names; - concord-server, concord-console: initial support for file upload fields. ### Changed - concord-console: the repository refresh button now opens a pop-up window. ## [0.48.2] - 2017-12-28 ### Changed - concord-server: fixed key names in the GitHub configuration file. ## [0.48.1] - 2017-12-27 ### Added - concord-console: the button to manually refresh a project's repository cache. ### Changed - concord-server: skip the repository cache if the repository's webhook is not registered (yet); - concord-server: fixed an bug preventing startup errors from being logged in process logs. ## [0.48.0] - 2017-12-17 ### Added - concord-agent: display the list of dependencies when a process starts; - concord-console: new "visibility" field on the process and the secret forms; - ansible: `skipTags` support; - concord-server, concord-console: filter process queue by user organizations; - concord-console: add "organization" field to the process page; - concord-server: new provided variables in `projectInfo`: `repoCommitId`, `repoCommitAuthor` and `repoCommitMessage`; - concord-console: host the swagger-ui app; - concord-server: triggers, inventories and landing pages are moved into organizations; - concord-server, concord-console: an option to disallow raw payload archives for projects; - concord-server, concord-console: initial support for Organizations; - concord-server: public and private projects and secrets; - concord-server: project and secret now has owners; - concord-server: new endpoint for managing secrets; - concord-client: the `project` task - provides a method to create new projects using flows; - concord-server: methods to refresh triggers and LPs for all projects. ### Changed - concord-client: switched to resteasy-based client; - concord-runner: improved stability by using a separate classloader to load tasks; - concord-server: user permissions are effectively replaced with the Team RBAC feature; - concord-console: reworked the form for creating secrets; - concord-console: rename "Kill" button on the process page to "Cancel". ## [0.47.0] - 2017-11-15 ### Added - concord-project-model: `debug` option for the `docker` step; - docker: the task now automatically overrides the container's `entrypoint`; - concord-client: new module; - ansible: `ansible` alias for the `ansible2` task; - concord-server: new automatically-provided variable - `projectInfo`; - concord-runner: `script` task now supports URLs; - concord-server: initial support for process triggers. ## [0.46.0] - 2017-11-04 ### Added - kv: `getLong` and `putLong` methods; - concord-console: "download state" button to the process status page; - concord-agent, concord-server: detect orphaned or stalled processes; - concord-server, concord-console: process landing pages; - concord-server: show an error page when a "portal" process fails; - concord-project-model: alias `::` to `try:`; - concord-console: ability to start a new process from the project's form; - ansible: support for saving and using Ansible's "limit" files; - concord-server, concord-console: support for boolean form fields; - ansible: Inventory lookup plugin. ### Changed - concord-server: user can now create API keys only for themselves; - concord-server: fix empty secret name in project repository entries returned by the API; - concord-server: added environment variable to override the server's password. ## [0.45.1] - 2017-10-30 ### Added - concord-console: "terminated ssl" option. ## [0.45.0] - 2017-10-20 ### Added - concord-server: log the repository data when a process starts; - concord-server: Inventory API initial support; - concord-console: storybook integration; - concord-server: retrieve user's LDAP info when API key authentication is used; - concord-server: the API keys endpoint now accepts LDAP usernames as well as user UUIDs; - concord-server, concord-runner: support for process OUT variables; - concord-server: log process status updates; - concord-server: initial support for Teams; - concord-server: an endpoint to retrieve a list of attachments. ### Breaking - concord-dependency-manager: remove support for `includeOptional`. ### Changed - concord-dependency-manager: batch resolution of Maven dependencies. ## [0.44.0] - 2017-10-12 ### Added - concord-runner: Slack task can now be called using the full form; - concord-server: automatically remove orphaned data. ### Changed - concord-rpc: fix dispatching of agent commands when the server is restarted; - ansible: throw an exception if a private key file was not found. ### Breaking - concord-server: `/api/v1/process/{id}/subprocesses` changed to `/api/v1/process/{id}/subprocess`. ## [0.43.0] - 2017-10-05 ### Added - ansible: ability to specify a secret name and password as a `privateFile` value; - concord-console: a form to create secrets; - concord-server: validate uploaded SSH key pairs; - concord-server, concord-agent: support for pulling dependencies from Maven repositories; - concord-server: update repositories using GitHub webhooks; - concord-agent: automatic cleanup of orphaned Docker containers; - ansible: support for additional environment variables. ### Changed - concord-server: fixed the issue with incorrect credentials configuration when retrieving GIT submodules. ## [0.42.0] - 2017-10-01 ### Added - concord-sdk: new provided variable `parentInstanceId`; - concord-server: ability to suppress the execution of `onCancel` or `onFailure` flows; - concord-server: new API method to fork a process as its subprocess; - concord-server: support for GIT submodules; - concord-server, concord-console: process tags. ## [0.40.2] - 2017-09-24 ### Added - concord-server: if entry point is not set, use `default`; - concord-project-model: alias `variables` to `configuration`; - concord-server: pagination support for the process queue list; - concord-project-model: support for `switch`; - ansible: initial support for Ansible event streaming. ### Changed - concord-server: fix Jolokia JMX names; - concord-runner: fix `InjectVariable` for `JavaDelegate`-style tasks; - concord-runner: fix `JavaDelegate` handling; - concord-server: fix SSH key pair upload/create endpoint; - concord-server: process events cleanup; - concord-server: fixed a potential NPE while retrieving the process queue data. ## [0.39.0] - 2017-09-17 ### Added - concord-server, concord-runner: support for `onFailure`, `onCancel` flows; - concord-server, concord-runner: provide a way to access password-protected secrets from flows; - concord-server: allow starting a process with a POST request using a project, a repository and an entry point specified in `.concord.yml` file; - concord-server: allow starting a process by sending an empty POST request; - concord-server, concord-console: support for `yield` for non-custom forms; - ansible: support for external private key files; - ansible: support for external configuration files; - ansible: verbosity level can now be set using the task's arguments. ### Changed - concord-server: include GIT repository name into the cache key; - concord-agent: remove the working directory after the process finishes; - concord-runner: refresh the value of `${__attr_localPath}` after resuming a process; - concord-server: fixed variable merging when `activeProfiles` is an empty array or `null`. ## [0.38.3] - 2017-09-10 ### Changed - concord-runner: upgrade to the BPM engine 0.38.2. This fixes another variable interpolation issue. ## [0.38.0] - 2017-09-07 ### Added - concord-server, concord-console: support for GIT repository paths. ### Changed - concord-server: fix key pair generation for non-API key users. ## [0.37.0] - 2017-09-04 ### Changed - concord-agent: normalize dependency URLs, support for Nexus/WARM redirects; - concord-runner: fix the issue with tasks based on `concord-sdk`; - concord-project-model: fix serialization issues when `set` task used with nested structures. ## [0.36.0] - 2017-08-23 ### Added - concord-server: REST API method for exporting process state; - concord-sdk: support for full-form tasks; - concord-agent: log local IPs before starting a process. ### Changed - concord-server: fix evaluation of variables when one variable references another; - concord-server: accept any type of attachment as a file, except `text/plain`. This fixes the issue with the multipart requests with incorrect `Content-Type`. ## [0.35.0] - 2017-08-19 ### Added - concord-sdk: new module; - concord-runner: upgrade the BPM engine version to 0.34. This add the support recursive value interpolation in `variables` or `in` blocks; - concord-agent, concord-runner: keep a pool of JVM instances instead of starting a new one for each process. ## Changed - concord-server: preserve user input in `data.js` after a failed validation. ## [0.34.1] - 2017-08-14 ### Changed - concord-server: fixed an issue with custom forms and SSL. ## [0.34.0] - 2017-08-13 ### Added - concord-server: support for `shared` folders for custom forms; - concord-server: process files should overwrite template files; - ansible: static and dynamic inventory files can now be specified using `inventoryFile` and `dynamicInventoryFile` task parameters. ## [0.33.0] - 2017-08-10 ### Added - concord-server: add Jolokia agent; ### Changed - concord-runner: use copyAllCallActivityOutVariables to prevent losing subprocess variables when events are used; - concord-agent: slightly improved startup time of process JVMs; - concord-server: normalize LDAP usernames; - project-model: more robust yml-to-bpmn converter. ## [0.32.0] - 2017-08-01 ### Added - concord-runner: `@InjectVariable` annotation can now be used to inject process context variables as task fields or method arguments. ### Changed - ansible: fixed potential NPE. ## [0.31.0] - 2017-07-25 ### Added - project-model: support for in/out variables and `error` blocks for process calls (aka the full form of `CallActivity`); - ansible-tasks: a `JavaDelegate` version of the task. Allows use of IN/OUT variables. ### Changed - concord-server: use the native PostgreSQL UUID type. ## [0.30.1] - 2017-07-24 ### Changed - concord-server: fixed the merging of arguments while resuming a process. ## [0.30.0] - 2017-07-23 ### Added - concord-runner: environment variables support for `docker` task. - concord-server: allow uploading arbitrary files and override request parameters using `multipart/form-data` requests. ## [0.29.0] - 2017-07-23 ### Changed - concord-server: fixed the order of applying variable overrides; - concord-runner: docker task now requires `/bin/sh` to be available in all images. ### Breaking - concord-server: storing overrides in `_defaults.json` is not supported anymore. ## [0.28.3] - 2017-07-19 ### Changed - concord-runner: use `/workspace` instead of `/workplace` in the docker task; ### Breaking - concord-runner: docker task syntax is changed to ``` - docker: image-name cmd: my-cmd ``` ## [0.28.2] - 2017-07-18 ### Changed - concord-server: improve error handling - return error details, add optional stacktrace field to error messages; - concord-server: fixed NPE on retrieving an empty/non-existing log file; - project-model: fixed an issue with passing nested objects in IN-parameters of tasks. ## [0.28.1] - 2017-07-14 ### Changed - concord-server: fixed an data escaping issue with project configuration migration. ## [0.28.0] - 2017-07-13 ### Added - concord-server, concord-console: ability to override `type` in `` for non-branded forms (e.g. for `password` fields). ### Changed - concord-server: fix usage of`${requestInfo}` in non-"portal" calls. ## [0.27.0] - 2017-07-12 ### Added - concord-server: query parameters of the requests made using the "portal" endpoint are now accessible as `requestInfo.query.param` variables; - concord-runner: support for running docker images using `docker` flow command; - docker-images: new tool image - `ansible`. ### Changed - concord-server: fixed the issue with inability to use expressions as default values of form fields for non-branded forms. ## [0.26.0] - 2017-07-10 ### Added - concord-server: include all available form data for "success" and "process failed" pages. ### Changed - concord-server: return project configuration on `GET /api/v1/project/{projectName}` calls; - concord-server: improve project configuration handling. ## [0.25.0] - 2017-07-10 ### Added - concord-server: form calls can now override form values and/or provide additional data. - concord-server: both `flows` and `processes` directories can now be used to load flows definitions; - concord-server: `concord.yml` can now be used instead of `.concord.yml`; - concord-server: profiles can now be loaded from a `profiles` directory. ## [0.24.0] - 2017-07-05 ### Added - new task: `loadTasks`. Allows users to create their own tasks in Groovy, store them in process files and load dynamically; - concord-server: optional `activeProfiles` parameter can now be used in the Portal API. ### Changed - ansible: dependency URLs in the ansible template are temporary changed to use WARM. ## [0.23.0] - 2017-06-28 ### Added - concord-server: Slack integration; - concord-runner: new `slack` task to send notifications to a Slack channel. ### Changed - concord-server: templates now are referenced by URLs in project configuration. ## [0.22.0] - 2017-06-18 ### Added - concord-server: support for LDAPS; - concord-runner, concord-server: record process execution events. ### Changed - concord-server: store agent commands in the DB; - concord-server: store process logs in the DB; - concord-server: fixed an issue with `data.js` generation when the store directory does not exist. ### Breaking - concord-server: remove the support for H2 database. ## [0.20.1] - 2017-06-08 - concord-server: fix potential connection leak. ## [0.20.0] - 2017-06-06 ### Added - concord-console: the version information page; - concord-server: `/api/v1/server/version` API endpoint; - docker-images: optional SSL support for the console's nginx; - concord-server: new `PREPARING` process state; - concord-runner: upgrade the bpm engine to 0.31.1: - support for EL 3.0 in flow expressions; - form options now can use expressions. ### Changed - concord-server: `created` flags in the REST API responses are replaced with `actionType: CREATED|UPDATED`. Should be more obvious. - concord-server: fixed basic auth using passwords with `:` symbol; - concord-console: handle `ENQUEUED` status in the default form wizard; - concord-server: fixed the issue with custom form redirects when using HTTPS proxy; - concord-server: store process state in the DB; - concord-server: create the log file of a process as early as possible to log startup errors. ## [0.19.0] - 2017-05-30 ### Added - concord-server: add the first batch of metrics, expose with JMX; - concord-console: add "Test connection" button to the repository form; - concord-server: improve error handling for GIT repository cloning; - concord-server: add endpoint to encrypt values with a project's key; - concord-runner: add `crypto` task to decrypt previously encrypted values; ### Changed - remove "provisio" builds: use plain .sh startup scripts for the server and the agents; - concord-server: improve KV store to work in multi server setups; - concord-server: simplify the project configuration handling. ## [0.18.0] - 2017-05-26 ### Added - concord-console: the project form; - concord-server: add form field labels to the generated `data.js` files. ### Changed - upgrade the bpm engine to 0.29.0. It changes the form validation messages: now it uses labels instead of field names (when available). ## [0.17.3] - 2017-05-25 ### Changed - concord-server: fixed a bug preventing LDAP attributes from being collected. ## [0.17.2] - 2017-05-24 ### Changed - reverted back to jetty 9.2.11.v20150529 due to the issue with serving forms using `DefaultServlet`. ## [0.17.0] - 2017-05-24 ### Added - concord-server: simple KV store backed by the database; - concord-runner: `kv` task to use the simple KV store; - yaml: optional error code in the `return` command; - concord-server: ability to pull a repository using a `commitId`; - concord-console: add the project list; - concord-server: add project description field; - concord-server: return process JSON objects in sync mode regardless of success or failure; - concord-server: log incoming gRPC connections; - concord-server: improve request data validation for the Project REST API. ### Changed - concord-server: move the rpc module into the top-level directory; - concord-console: fix timestamps on the process page; - concord-server: suppress JOOQ banner. ## [0.16.0] - 2017-05-20 ### Added - concord-runner: add `workDir` variable (same as `__attr_localPath`); - ansible-tasks: allow passing a vault password using request variables. - upgrade the bpm engine to 0.28.0 which allow flows to access available tasks using expressions `${tasks.get('name')}` or in a script block. ### Changed - concord-console: wrap long lines in the log viewer; - concord-agent: do not cache `file://` and `SNAPSHOT` dependencies; - ansible-tasks: move inline inventory processing from the server into the plugin. ## [0.15.0] - 2017-05-19 ### Added - concord-server: process start errors now include `stacktrace` field; - concord-server: optional synchronous mode of the process start methods. All forms will be automatically submitted using the provided request data; - ansible: `defaults` replaced with `config` JSON object. Each key represents a section in the configuration file; - concord-runner: additional methods for `LoggingTask`. ### Changed - concord-console: disable the log button for the processes that don't have a log file; - concord-agent: fixed the state transfer for failed processes; - concord-server: make all LDAP attributes available in `${initiator.attributes}`. ## [0.14.1] - 2017-05-17 ### Changed - concord-agent: handle early process startup errors. ## [0.14.0] - 2017-05-17 ### Added - concord-server-db: indexes for the process queue; - yaml: `lastError` can now be used to access the last handled `BpmnError`. ### Changed - concord-console: fixed rendering of "Updated" and "Created" columns in the queue table. - concord-runner: all unhandled exceptions in a `ServiceTask` (YAML expressions) or in a `ScriptTask` (YAML script blocks) will be wrapped as `BpmnErrors`. ## [0.13.0] - 2017-05-16 ### Added - concord-server: process queue; - concord-server, concord-agent: support for multiple agents, async transport. ### Changed - boo, nexus-perf, teamrosters and oneops plugins are moved into a separate repository. ## [0.12.0] - 2017-05-12 ## Added - concord-console: support for "int" and "decimal" fields in the default form renderer; - added "project name" column to the process history; - concord-runner: support for external scripts. ## Changed - concord-server: fixed LDAP attributes retrieval (including `displayName`). ## [0.11.2] - 2017-05-09 ### Changed - updated oneops-client version, fixing the `NoSuchMethodError` issue with boo-task; - oneops-tasks: migrate to the official oneops-client. ## [0.11.1] - 2017-05-09 ### Changed - boo-task: fixed fatjar creation. ## [0.11.0] - 2017-05-09 ### Added - ansible: store exit code in the stats file. ### Changed - concord-agent: more reliable kill command; - ansible: allow overriding of connection timeout value. ## [0.10.0] - 2017-05-04 ### Added - boo-task: tag assembly with a cost center. ## [0.9.0] - 2017-05-04 ### Added - concord-server: GIT repository shallow cloning and caching. ### Changed - concord-server: improved error message when creating or updating a project with non-existing secret; - concord-server, concord-agent: fixed logging configuration conflicts; - concord-console: reduce wizard polling interval, "time to first form" improved; - runner.jar moved from the server to the agent, futher reducing the process statup time. ## [0.8.2] - 2017-05-01 ### Added - concord-agent: simple caching mechanism for dependencies. ### Changed - boo-task: bug fixes. ## [0.8.0] - 2017-05-01 ### Added - support for the form branding: custom forms with user-provided HTML/CSS/JS/etc. ## [0.7.0] - 2017-04-30 ### Added - boo-task: pull more deployment information into the context; - teamrosters-task: new task to retrieve Team Rosters data; ### Changed - fixed: missing line/column numbers when parsing YAML process definitions and project files; - fixed: `value` attribute ignored in form field declarations. ## [0.6.0] - 2017-04-27 ### Added - the Console now uses LDAP authentication; - expose (a configurable set of) LDAP attributes to processes; - smtp: support for Mustache templates; - yaml: simplify usage of external variables in script steps; - boo: return deployment status from the task. ## [0.5.1] - 2017-04-20 ### Changed - boo: use all provided variables as template variables; - fixed: `.concord.yml` variables are ignored when using a GIT repository; - fixed: project repositories should be ignored when starting a process using an archive. ## [0.5.0] - 2017-04-11 ### Added - concord-runner: support for expressions in variables. ### Breaking - project and process related constants moved to project-model module. ## [0.4.1] - 2017-04-10 ### Changed - fixed merging of profile variables with defaults and request's data. ## [0.4.0] - 2017-04-10 ### Added - support for `.concord.yml` files. Those files can contain flow and form definitions, default variables and "profiles"; - allow overriding`dependencies` in a project file, defaults or user requests; - concord-console: list of secrets. ### Breaking - concord-common: removed `Task#getKey`. Value of `javax.inject.@Named` annotation is used to resolve a task. - removed bpmn-format and yaml-format modules; - yaml2-format replaced with a single module: project-model. ## [0.3.2] - 2017-04-03 ### Changed - concord-server: merge existing process values with form values; - concord-server: fix the LDAP mapping update method; - concord-server: new method `/api/v1/ldap/query/{username}/group` to retrieve a list of LDAP user's groups; - concord-agent: fix a race condition in the log creation; - upgrade BPM version to 0.2.1. ## Breaking - concord-server: LDAP mappings methods moved to `/api/v1/ldap/mapping` path prefix. ## [0.3.1] - 2017-04-02 ### Changed - concord-console: host the landing page; - concord-console: simple process launcher using `/#/portal/start?entryPoint=abc` URLs. ## [0.3.0] - 2017-03-31 ### Changed - fix error logging in nexus-perf tasks. - support for single or multiple selection fields ### Breaking - `_deps` file is replaced with `dependencies` array in a project configuration, template, `_defaults.json` or user's request. ## [0.2.1] - 2017-03-29 ### Changed - boo upgraded to 1.0.3; - minor fixes in examples. ## [0.2.0] - 2017-03-28 ### Added - initial support for forms, including the new UI wizard; - the server now uses connection pooling to talk with the agent(s). - YAML DSL support for inline JSR-223 scripts; - now it's possible to use AD/LDAP fully-qualified usernames, e.g. `user@domain.com`; - new endpoint: `/api/v1/role` for managing mappings between roles and permissions; - new endpoint: `/api/v1/ldap` for managing mappings between LDAP groups and roles. ### Changed - logging configuration cleanup; - projects now can be created and updated using a single endpoint; - "get user" method now uses path parameter: `GET /api/v1/user/{username}`; - usernames containing backward slashes ("\\") are forbidden; - unauthenticated and unauthorized errors are now returned with `Content-Type: text/plain`. ### Breaking - removed `PUT /api/v1/user/{username}` method. ## [0.1.0] - 2017-03-22 First release. ================================================ FILE: LICENSE ================================================ Copyright (c) 2017-present, Walmart Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License." ================================================ FILE: NOTES.md ================================================ # Development Notes ## Code Style Prefer `var` in new code, but do not mix `var` and explicit local variable types in the same file. ## Git Prefer commit subjects in the existing `module: short description` style. Use comma-separated modules when needed, and keep the body minimal or omit it if the subject is already clear. ## Server Plugins Prefer explicit binding using `com.google.inject.Module` over `@Named` annotations. Use `@Named` for top-level modules and server plugins. Server and server plugin types that currently require explicit binding using `Multibinder.newSetBinder`: - ApiDescriptor - AuditLogListener - AuthenticationHandler - BackgroundTask - Component - ContextHandlerConfigurator - CustomEnqueueProcessor - DatabaseChangeLogProvider - ExceptionMapper - ExternalEventTriggerProcessor - Filter - FilterChainConfigurator - FilterHolder - GaugeProvider - GithubTriggerProcessor.EventEnricher - HttpServlet - ModeProcessor - PolicyApplier - ProcessEventListener - ProcessLogListener - ProcessStatusListener - ProcessWaitHandler - ProjectLoader - Realm - RepositoryRefreshListener - RequestErrorHandler - ScheduledTask - SecretStore - ServletContextListener - ServletHolder - UserInfoProvider ================================================ FILE: README.md ================================================ # Concord ![](https://img.shields.io/maven-central/v/com.walmartlabs.concord/parent.svg) - Website: https://concord.walmartlabs.com - [Installation guide](https://concord.walmartlabs.com/docs/getting-started/installation.html) - [Core Plugins](./plugins) - [Community Plugins](https://github.com/walmartlabs/concord-plugins/) ![](console2/public/images/concord.svg) Concord is a workflow server. It is the orchestration engine that connects different systems together using scenarios and plugins created by users. - [Building](#building) - [Console](#console) - [Integration tests](#integration-tests) * [Prerequisites](#prerequisites) * [Running tests](#running-tests) - [Repository Docs](#repository-docs) - [Examples](#examples) - [How To Release New Versions](#how-to-release-new-versions) - [Development Notes](#development-notes) ## Building Dependencies: - [Git](https://git-scm.com/) 2.18+ - [Java 17](https://adoptium.net/) - [Docker Community Edition](https://www.docker.com/community-edition) - [Docker Buildx](https://docs.docker.com/build/buildx/install/) - (Optional) [NodeJS and NPM](https://nodejs.org/en/download/) (Node 24 LTS) ```shell git clone https://github.com/walmartlabs/concord.git cd concord ./mvnw clean install -DskipTests ``` Available Maven profiles: - `docker` - build Docker images; - `it` - run integration tests; - `jdk17-aarch64` - use a different JDK version for building artifacts and Docker images. Profiles can be combined, e.g. ``` ./mvnw clean install -Pdocker -Pit -Pjdk17-aarch64 ``` ## Console See the [console2/README.md](./console2/README.md) file. ```shell cd ./console2 npm ci # Install dependencies ``` Start the console in dev mode by running: ```shell npm run start ``` ## Integration tests ### Prerequisites Prerequisites: - Git 2.18+ - Docker, listening on `tcp://127.0.0.1:2375`; - Ansible 2.6.0+ must be installed and available in `$PATH`. See [the official documentation](http://docs.ansible.com/ansible/intro_installation.html); - `requests` python module is required. It can be installed by using `pip install requests` or the system package manager; - Java must be available in `$PATH` as `java`; - [Chrome WebDriver](http://chromedriver.chromium.org/) available in `$PATH`. ### Running tests Integration tests are disabled by default. Use the `it` profile to enable them: ```shell ./mvnw verify -Pit ``` This will run ITs agains the locally running server and the agent. To automatically start and stop the server and the agent using docker, use the `docker` profile: ```shell ./mvnw verify -Pit -Pdocker ``` To run UI ITs in an IDE using the UI's dev mode: - start the UI's dev mode with `cd console2 && npm start`; - set up `IT_CONSOLE_BASE_URL=http://localhost:3000` environment variable before running any UI tests. ## Repository Docs For repo-specific development entrypoints, see: - [Integration tests](./it/README.md) - [Development notes](./NOTES.md) - [Server README](./server/README.md) - [Console UI README](./console2/README.md) - [Agent operator README](./agent-operator/README.md) ## Examples See the [examples](examples) directory. ## How To Release New Versions - perform a regular Maven release: ``` $ ./mvnw release:prepare release:perform ``` - update and commit the CHANGELOG.md file ``` $ git add CHANGELOG.md $ git commit -m 'update changelog' ``` - push the new tag and the master branch: ``` $ git push origin RELEASE_TAG $ git push origin master ``` - build and push the Docker images: ``` $ git checkout RELEASE_TAG $ gh workflow run docker-multiarch.yml --ref master -f ref=RELEASE_TAG -f docker_tag=RELEASE_TAG -f docker_namespace=walmartlabs ``` - sync to [Sonatype](https://oss.sonatype.org/); - check the Central repository if the sync is complete: ``` https://repo.maven.apache.org/maven2/com/walmartlabs/concord/parent/RELEASE_TAG ``` - once the sync is complete, push the `latest` Docker images: ``` $ gh workflow run docker-multiarch.yml --ref master -f ref=RELEASE_TAG -f docker_tag=latest -f docker_namespace=walmartlabs ``` ## Development Notes See [NOTES.md](NOTES.md). ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability To report a vulnerability please use [GitHub Issues](https://github.com/walmartlabs/concord/issues) or reach the team using the following email address: - [concord-team@wal-mart.com](mailto:concord-team@wal-mart.com). ================================================ FILE: agent/pom.xml ================================================ 4.0.0 com.walmartlabs.concord parent 2.40.1-SNAPSHOT ../pom.xml concord-agent jar ${project.groupId}:${project.artifactId} ${project.basedir}/../runtime/v1/impl/target/concord-runtime-impl-v1-${project.version}-jar-with-dependencies.jar ${project.basedir}/../runtime/v2/runner/target/concord-runner-v2-${project.version}-jar-with-dependencies.jar com.walmartlabs.concord concord-common com.walmartlabs.concord concord-github-app-installation com.walmartlabs.concord concord-client2 com.walmartlabs.concord concord-imports com.walmartlabs.concord concord-sdk com.walmartlabs.concord concord-dependency-manager com.walmartlabs.concord concord-policy-engine com.walmartlabs.concord.server concord-queue-client com.walmartlabs.concord concord-repository com.walmartlabs.concord.runtime concord-runtime-common javax.inject javax.inject org.eclipse.sisu org.eclipse.sisu.inject com.google.inject guice com.google.guava guava com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.datatype jackson-datatype-jdk8 com.fasterxml.jackson.core jackson-annotations org.slf4j slf4j-api ch.qos.logback logback-classic org.apache.commons commons-compress com.typesafe config com.walmartlabs.concord concord-config org.immutables value provided com.fasterxml.jackson.datatype jackson-datatype-guava com.google.code.findbugs jsr305 provided com.google.errorprone error_prone_annotations provided org.junit.jupiter junit-jupiter-api test org.mockito mockito-core test org.mockito mockito-junit-jupiter test com.walmartlabs.concord.runtime.v1 concord-runtime-impl-v1 ${project.version} pom test * * com.walmartlabs.concord.runtime.v2 concord-runner-v2 ${project.version} pom test * * false ${project.basedir}/src/main/resources **/* true ${project.basedir}/src/main/filtered-resources **/* org.eclipse.sisu sisu-maven-plugin org.apache.maven.plugins maven-dependency-plugin copy-runner process-resources copy com.walmartlabs.concord.runtime.v1 concord-runtime-impl-v1 ${project.version} jar-with-dependencies runner-v1.jar com.walmartlabs.concord.runtime.v2 concord-runner-v2 ${project.version} jar-with-dependencies runner-v2.jar ${project.build.directory}/runner org.apache.maven.plugins maven-assembly-plugin dist package single src/assembly/dist.xml posix ================================================ FILE: agent/src/assembly/default.conf ================================================ concord-agent { } ================================================ FILE: agent/src/assembly/dist.xml ================================================ dist tar.gz false lib src/assembly/start.sh . 755 src/assembly/default.conf . ${project.build.directory}/runner/runner-v1.jar runner ${project.build.directory}/runner/runner-v2.jar runner ================================================ FILE: agent/src/assembly/start.sh ================================================ #!/bin/bash BASE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" export RUNNER_V1_PATH="${BASE_DIR}/runner/runner-v1.jar" export RUNNER_V2_PATH="${BASE_DIR}/runner/runner-v2.jar" if [[ -z "${CONCORD_TMP_DIR}" ]]; then export CONCORD_TMP_DIR="/tmp" fi if [[ -z "${CONCORD_JAVA_OPTS}" ]]; then CONCORD_JAVA_OPTS="-Xmx256m" fi echo "CONCORD_JAVA_OPTS: ${CONCORD_JAVA_OPTS}" if [[ -z "${CONCORD_CFG_FILE}" ]]; then CONCORD_CFG_FILE="${BASE_DIR}/default.conf" fi echo "CONCORD_CFG_FILE: ${CONCORD_CFG_FILE}" echo "Using $(which java)" java -version JAVA_VERSION=$(java -version 2>&1 \ | head -1 \ | cut -d'"' -f2 \ | sed 's/^1\.//' \ | cut -d'.' -f1) JDK_SPECIFIC_OPTS="" if (( $JAVA_VERSION > 8 )); then echo "Applying JDK 9+ specific options..." JDK_SPECIFIC_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED" fi exec java \ ${CONCORD_JAVA_OPTS} \ ${JDK_SPECIFIC_OPTS} \ -Dfile.encoding=UTF-8 \ -Djava.net.preferIPv4Stack=true \ -Djava.security.egd=file:/dev/./urandom \ -Dlogback.configurationFile=com/walmartlabs/concord/agent/logback.xml \ -Dconcord.conf=${CONCORD_CFG_FILE} \ -cp "${BASE_DIR}/lib/*" \ com.walmartlabs.concord.agent.Main \ "$@" ================================================ FILE: agent/src/main/filtered-resources/com/walmartlabs/concord/agent/cfg/runnerV1.properties ================================================ path=${runnerV1.path} ================================================ FILE: agent/src/main/filtered-resources/com/walmartlabs/concord/agent/cfg/runnerV2.properties ================================================ path=${runnerV2.path} ================================================ FILE: agent/src/main/filtered-resources/com/walmartlabs/concord/agent/executors/runner/default-dependencies ================================================ mvn://com.walmartlabs.concord.plugins.basic:concord-tasks:${project.version} mvn://com.walmartlabs.concord.plugins.basic:slack-tasks:${project.version} mvn://com.walmartlabs.concord.plugins.basic:http-tasks:${project.version} ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/Agent.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.google.inject.Injector; import com.walmartlabs.concord.agent.Worker.CompletionCallback; import com.walmartlabs.concord.agent.cfg.AgentConfiguration; import com.walmartlabs.concord.agent.cfg.DockerConfiguration; import com.walmartlabs.concord.agent.docker.OrphanSweeper; import com.walmartlabs.concord.agent.guice.WorkerModule; import com.walmartlabs.concord.agent.mmode.MaintenanceModeListener; import com.walmartlabs.concord.agent.mmode.MaintenanceModeNotifier; import com.walmartlabs.concord.client2.ProcessEntry.StatusEnum; import com.walmartlabs.concord.common.PathUtils; import com.walmartlabs.concord.server.queueclient.QueueClient; import com.walmartlabs.concord.server.queueclient.message.ProcessRequest; import com.walmartlabs.concord.server.queueclient.message.ProcessResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.IOException; import java.nio.file.Path; import java.util.Map; import java.util.UUID; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; public class Agent { private static final Logger log = LoggerFactory.getLogger(Agent.class); private final Injector injector; private final AgentConfiguration agentCfg; private final DockerConfiguration dockerCfg; private final QueueClient queueClient; private final ExecutorService executor; private final Map activeWorkers = new ConcurrentHashMap<>(); private final AtomicBoolean maintenanceMode = new AtomicBoolean(false); // make the reference volatile as we check if for != null in different threads private volatile Semaphore workersAvailable; // NOSONAR @Inject public Agent(Injector injector, AgentConfiguration agentCfg, DockerConfiguration dockerCfg, QueueClient queueClient) { this.injector = injector; this.agentCfg = agentCfg; this.dockerCfg = dockerCfg; this.queueClient = queueClient; this.executor = Executors.newCachedThreadPool(); } public void start() { Runtime.getRuntime().addShutdownHook(new Thread(() -> { log.info("Received SIGTERM, stopping..."); Agent.this.stop(); }, "shutdown-hook")); executor.submit(() -> { run(); return null; }); log.info("start -> done"); } @SuppressWarnings("unused") public void stop() { queueClient.stop(); executor.shutdownNow(); log.info("stop -> done"); } private void run() throws Exception { int workersCount = agentCfg.getWorkersCount(); log.info("run -> using {} worker(s)", workersCount); workersAvailable = new Semaphore(workersCount); // listen for maintenance mode requests startMaintenanceModeNotifier(queueClient); if (dockerCfg.isOrphanSweeperEnabled()) { executor.submit(new OrphanSweeper(this::isAlive, dockerCfg.getOrphanSweeperPeriod())); } // start the command handler in a separate thread CommandHandler commandHandler = new CommandHandler(agentCfg.getAgentId(), queueClient, agentCfg.getPollInterval(), this::cancel); executor.submit(commandHandler); // main loop while (!Thread.currentThread().isInterrupted()) { // check if the maintenance mode is enabled. If so, hang there indefinitely validateMaintenanceMode(); // TODO parallel acquire? // wait for a free "slot" workersAvailable.acquire(); log.info("run -> acquired a slot, {}/{} remains", workersAvailable.availablePermits(), workersCount); // fetch the next job JobRequest jobRequest; try { jobRequest = take(queueClient); } catch (InterruptedException e) { log.info("run -> interrupted, exiting..."); return; } catch (Exception e) { log.error("run -> error while fetching a job: {}", e.getMessage(), e); workersAvailable.release(); // wait before retrying // the server is not reachable or unhealthy, no point retrying immediately Utils.sleep(AgentConstants.ERROR_DELAY); continue; } if (jobRequest == null) { // can happen on switching to maintenance mode or reconnecting, etc workersAvailable.release(); continue; } UUID instanceId = jobRequest.getInstanceId(); // worker will handle the process' lifecycle try { Worker w = injector.createChildInjector(new WorkerModule(agentCfg.getAgentId(), instanceId, jobRequest.getSessionToken())) .getInstance(WorkerFactory.class) .create(jobRequest, createStatusCallback(instanceId, workersAvailable)); // register the worker so we can cancel it later activeWorkers.put(instanceId, w); // start a new thread to process the job executor.submit(w); } catch (Exception e) { log.error("run -> error while submitting worker: {}", e.getMessage()); workersAvailable.release(); } } } private void startMaintenanceModeNotifier(QueueClient queueClient) { try { MaintenanceModeNotifier n = new MaintenanceModeNotifier(agentCfg.getMaintenanceModeListenerHost(), agentCfg.getMaintenanceModeListenerPort(), new MaintenanceModeListener() { @Override public Status onMaintenanceMode() { maintenanceMode.set(true); queueClient.maintenanceMode(); return getMaintenanceModeStatus(); } @Override public Status getMaintenanceModeStatus() { long availableWorkers = (workersAvailable != null) ? workersAvailable.availablePermits() : 0L; long cnt = agentCfg.getWorkersCount() - availableWorkers; return new Status(maintenanceMode.get(), cnt); } }); n.start(); } catch (IOException e) { log.warn("start -> can't start the maintenance mode notifier: {}", e.getMessage()); } } private void validateMaintenanceMode() throws InterruptedException { while (maintenanceMode.get()) { log.info("run -> switched to maintenance mode"); synchronized (maintenanceMode) { maintenanceMode.wait(); } // TODO option to switch mmode off? } } private boolean isAlive(UUID instanceId) { return activeWorkers.containsKey(instanceId); } private CompletionCallback createStatusCallback(UUID instanceId, Semaphore workersAvailable) { return new CompletionCallback() { // guard against misuse: the callback must be called only once - when // the process reaches its final status (successful or not) private volatile boolean called = false; @Override public void onStatusChange(StatusEnum status) { if (called) { throw new IllegalStateException("The completion callback already called once"); } called = true; activeWorkers.remove(instanceId); workersAvailable.release(); } }; } private JobRequest take(QueueClient queueClient) throws Exception { Future req = queueClient.request(new ProcessRequest(agentCfg.getCapabilities())); ProcessResponse resp = req.get(); if (resp == null) { return null; } Path workDir = PathUtils.createTempDir(agentCfg.getPayloadDir(), "workDir"); return JobRequest.from(resp, workDir); } private void cancel(UUID instanceId) { Worker w = activeWorkers.get(instanceId); if (w == null) { return; } w.cancel(); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/AgentAuthTokenProvider.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import com.walmartlabs.concord.agent.remote.ApiClientFactory; import com.walmartlabs.concord.common.AuthTokenProvider; import com.walmartlabs.concord.common.ExternalAuthToken; import com.walmartlabs.concord.github.appinstallation.GitHubAppInstallation; import com.walmartlabs.concord.sdk.Secret; import javax.annotation.Nullable; import javax.inject.Inject; import java.net.URI; import java.util.List; import java.util.Optional; public class AgentAuthTokenProvider implements AuthTokenProvider { private final List authTokenProviders; @Inject public AgentAuthTokenProvider(GitHubAppInstallation githubProvider, OauthTokenProvider oauthTokenProvider) { this.authTokenProviders = List.of(githubProvider, oauthTokenProvider); } @Override public boolean supports(URI repo, @Nullable Secret secret) { return authTokenProviders.stream() .anyMatch(p -> p.supports(repo, secret)); } public Optional getToken(URI repo, @Nullable Secret secret) { for (var tokenProvider : authTokenProviders) { if (tokenProvider.supports(repo, secret)) { return tokenProvider.getToken(repo, secret); } } return Optional.empty(); } public static class ConcordServerTokenProvider implements AuthTokenProvider { private final ApiClientFactory apiClientFactory; private final Config config; @Inject public ConcordServerTokenProvider(ApiClientFactory apiClientFactory, Config config) { this.apiClientFactory = apiClientFactory; this.config = config; } @Override public boolean supports(URI repo, @Nullable Secret secret) { // TODO implement return false; } @Override public Optional getToken(URI repo, @Nullable Secret secret) { // TODO implement return Optional.empty(); } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/AgentConstants.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public final class AgentConstants { public static final long ERROR_DELAY = 5000; public static final int API_CALL_MAX_RETRIES = 3; public static final long API_CALL_RETRY_DELAY = 3000; private AgentConstants() { } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/AgentModule.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Binder; import com.google.inject.Module; import com.typesafe.config.Config; import com.walmartlabs.concord.agent.cfg.*; import com.walmartlabs.concord.agent.executors.runner.DefaultDependencies; import com.walmartlabs.concord.agent.executors.runner.ProcessPool; import com.walmartlabs.concord.agent.remote.ApiClientFactory; import com.walmartlabs.concord.agent.remote.QueueClientProvider; import com.walmartlabs.concord.common.ObjectMapperProvider; import com.walmartlabs.concord.common.cfg.OauthTokenConfig; import com.walmartlabs.concord.config.ConfigModule; import com.walmartlabs.concord.github.appinstallation.cfg.GitHubAppInstallationConfig; import com.walmartlabs.concord.server.queueclient.QueueClient; import javax.inject.Named; import static com.google.inject.Scopes.SINGLETON; @Named public class AgentModule implements Module { private final Config config; public AgentModule() { this(loadDefaultConfig()); } public AgentModule(Config config) { this.config = config; } @Override public void configure(Binder binder) { binder.bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class); binder.bind(Config.class).toInstance(config); binder.bind(AgentConfiguration.class).in(SINGLETON); binder.bind(DockerConfiguration.class).in(SINGLETON); binder.bind(RuntimeConfiguration.class).asEagerSingleton(); binder.bind(GitConfiguration.class).in(SINGLETON); binder.bind(OauthTokenConfig.class).to(GitConfiguration.class).in(SINGLETON); binder.bind(GitHubConfiguration.class).in(SINGLETON); binder.bind(GitHubAppInstallationConfig.class).to(GitHubConfiguration.class).in(SINGLETON); binder.bind(AgentAuthTokenProvider.ConcordServerTokenProvider.class).in(SINGLETON); binder.bind(ImportConfiguration.class).in(SINGLETON); binder.bind(PreForkConfiguration.class).in(SINGLETON); binder.bind(RepositoryCacheConfiguration.class).in(SINGLETON); binder.bind(ServerConfiguration.class).in(SINGLETON); binder.bind(DefaultDependencies.class).in(SINGLETON); binder.bind(ProcessPool.class).in(SINGLETON); binder.bind(ApiClientFactory.class).in(SINGLETON); binder.bind(QueueClient.class).toProvider(QueueClientProvider.class).in(SINGLETON); binder.bind(Agent.class).in(SINGLETON); } private static Config loadDefaultConfig() { return ConfigModule.load("concord-agent"); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/CommandHandler.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.server.queueclient.QueueClient; import com.walmartlabs.concord.server.queueclient.message.CommandRequest; import com.walmartlabs.concord.server.queueclient.message.CommandResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class CommandHandler implements Runnable { private static final Logger log = LoggerFactory.getLogger(CommandHandler.class); private static final long ERROR_DELAY = 5000; private final ExecutorService executor; private final UUID agentId; private final QueueClient queueClient; private final long pollInterval; private final CancelHandler cancelHandler; public CommandHandler(String agentId, QueueClient queueClient, long pollInterval, CancelHandler cancelHandler) { this.executor = Executors.newCachedThreadPool(); this.agentId = UUID.fromString(agentId); this.queueClient = queueClient; this.pollInterval = pollInterval; this.cancelHandler = cancelHandler; } @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { CommandResponse cmd = take(); if (cmd == null) { sleep(pollInterval); continue; } executor.submit(() -> execute(cmd)); } catch (Exception e) { log.error("run -> error while processing a command: {}", e.getMessage(), e); sleep(ERROR_DELAY); } } } private CommandResponse take() throws Exception { try { return queueClient.request(new CommandRequest(agentId)).get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } } private void execute(CommandResponse cmd) { log.info("execute -> got a command: {}", cmd); CommandResponse.CommandType type = cmd.getType(); if (type == CommandResponse.CommandType.CANCEL_JOB) { UUID instanceId = UUID.fromString((String) cmd.getPayload().get("instanceId")); cancelHandler.cancel(instanceId); } else { log.warn("execute -> unsupported command type: {}", type); } } private static void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public interface CancelHandler { void cancel(UUID instanceId); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/ConfiguredJobRequest.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.walmartlabs.concord.sdk.Constants; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.Map; /** * A {@link JobRequest} plus the process configuration loaded from the request's * payload directory. */ public class ConfiguredJobRequest extends JobRequest { @SuppressWarnings("unchecked") public static ConfiguredJobRequest from(JobRequest req) throws ExecutionException { Map cfg = Collections.emptyMap(); Path p = req.getPayloadDir().resolve(Constants.Files.CONFIGURATION_FILE_NAME); if (Files.exists(p)) { try (InputStream in = Files.newInputStream(p)) { cfg = new ObjectMapper().readValue(in, Map.class); } catch (IOException e) { throw new ExecutionException("Error while reading process configuration", e); } } return new ConfiguredJobRequest(req, cfg); } private final Map processCfg; private ConfiguredJobRequest(JobRequest src, Map processCfg) { super(src); this.processCfg = processCfg; } public Map getProcessCfg() { return processCfg; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/DefaultStateFetcher.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.client2.ClientUtils; import com.walmartlabs.concord.client2.ProcessApi; import com.walmartlabs.concord.common.ZipUtils; import javax.inject.Inject; import java.io.InputStream; import java.nio.file.StandardCopyOption; public class DefaultStateFetcher implements StateFetcher { private final ProcessApi processApi; @Inject public DefaultStateFetcher(ProcessApi processApi) { this.processApi = processApi; } @Override public void downloadState(JobRequest job) throws Exception { try (InputStream is = ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> processApi.downloadState(job.getInstanceId()))){ ZipUtils.unzip(is, job.getPayloadDir(), StandardCopyOption.REPLACE_EXISTING); } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/ExecutionException.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public class ExecutionException extends Exception { private static final long serialVersionUID = 1L; public ExecutionException(String message) { super(message); } public ExecutionException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/JobInstance.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public interface JobInstance { /** * Wait for the job to finish. * Throws an {@link java.util.concurrent.ExecutionException} if the job finishes unsuccessfully. */ void waitForCompletion() throws Exception; /** * Cancel the job (unless it's already cancelled or done). */ void cancel(); /** * Returns {@code true} if the job was cancelled using the {@link #cancel()} method. */ boolean isCancelled(); } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/JobRequest.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.imports.Imports; import com.walmartlabs.concord.server.queueclient.message.ProcessResponse; import java.nio.file.Path; import java.util.UUID; public class JobRequest { public static JobRequest from(ProcessResponse resp, Path workDir) { return new JobRequest(Type.RUNNER, resp.getProcessId(), workDir, resp.getOrgName(), resp.getRepoUrl(), resp.getRepoPath(), resp.getCommitId(), resp.getRepoBranch(), resp.getSecretName(), resp.getImports(), resp.getSessionToken()); } private final Type type; private final UUID instanceId; private final Path payloadDir; private final String orgName; // TODO rename to secretOrgName private final String repoUrl; private final String repoPath; private final String commitId; private final String repoBranch; private final String secretName; private final Imports imports; private final String sessionToken; protected JobRequest(JobRequest src) { this(src.type, src.instanceId, src.payloadDir, src.orgName, src.repoUrl, src.repoPath, src.commitId, src.repoBranch, src.secretName, src.imports, src.sessionToken); } protected JobRequest(Type type, UUID instanceId, Path payloadDir, String orgName, String repoUrl, String repoPath, String commitId, String repoBranch, String secretName, Imports imports, String sessionToken) { this.type = type; this.instanceId = instanceId; this.payloadDir = payloadDir; this.orgName = orgName; this.repoUrl = repoUrl; this.repoPath = repoPath; this.commitId = commitId; this.repoBranch = repoBranch; this.secretName = secretName; this.imports = imports != null ? imports : Imports.builder().build(); this.sessionToken = sessionToken; } public Type getType() { return type; } public UUID getInstanceId() { return instanceId; } public Path getPayloadDir() { return payloadDir; } public String getOrgName() { return orgName; } public String getRepoUrl() { return repoUrl; } public String getRepoPath() { return repoPath; } public String getRepoBranch() { return repoBranch; } public String getCommitId() { return commitId; } public String getSecretName() { return secretName; } public Imports getImports() { return imports; } public String getSessionToken() { return sessionToken; } @Override public String toString() { return "JobRequest{" + "type=" + type + ", instanceId=" + instanceId + ", payloadDir=" + payloadDir + ", orgName='" + orgName + '\'' + ", repoUrl='" + repoUrl + '\'' + ", repoPath='" + repoPath + '\'' + ", commitId='" + commitId + '\'' + ", repoBranch='" + repoBranch + '\'' + ", secretName='" + secretName + '\'' + ", imports='" + imports + '\'' + '}'; } public enum Type { /** * A concord-runner based job. */ RUNNER } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/Main.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.google.inject.Guice; import org.eclipse.sisu.space.BeanScanning; import org.eclipse.sisu.space.SpaceModule; import org.eclipse.sisu.space.URLClassSpace; import org.eclipse.sisu.wire.WireModule; public class Main { public static void main(String[] args) throws Exception { // auto-wire all modules var classLoader = Main.class.getClassLoader(); var modules = new WireModule(new SpaceModule(new URLClassSpace(classLoader), BeanScanning.GLOBAL_INDEX)); var injector = Guice.createInjector(modules); if (args.length == 1) { // one-shot mode - read ProcessResponse directly from the command line, execute the process and exit // the current $PWD will be used as ${workDir} var oneShotRunner = injector.getInstance(OneShotRunner.class); oneShotRunner.run(args[0]); } else if (args.length == 0) { // agent mode - connect to the server's websocket and handle ProcessResponses var agent = injector.getInstance(Agent.class); agent.start(); } else { throw new IllegalArgumentException("Specify the entire ProcessResponse JSON as the first argument to run in " + "the one-shot mode or run without arguments for the default (agent) mode."); } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/OneShotRunner.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Injector; import com.walmartlabs.concord.agent.cfg.AgentConfiguration; import com.walmartlabs.concord.agent.guice.WorkerModule; import com.walmartlabs.concord.common.PathUtils; import com.walmartlabs.concord.server.queueclient.message.ProcessResponse; import javax.inject.Inject; import static java.util.Objects.requireNonNull; public class OneShotRunner { private final AgentConfiguration agentCfg; private final ObjectMapper objectMapper; private final Injector injector; @Inject public OneShotRunner(AgentConfiguration agentCfg, ObjectMapper objectMapper, Injector injector) { this.agentCfg = requireNonNull(agentCfg); this.objectMapper = requireNonNull(objectMapper); this.injector = requireNonNull(injector); } public void run(String processResponseJson) throws Exception { var processResponse = objectMapper.readValue(processResponseJson, ProcessResponse.class); var workDir = PathUtils.createTempDir(agentCfg.getPayloadDir(), "workDir"); var jobRequest = JobRequest.from(processResponse, workDir); var workerModule = new WorkerModule(agentCfg.getAgentId(), jobRequest.getInstanceId(), jobRequest.getSessionToken()); var workerFactory = injector.createChildInjector(workerModule).getInstance(WorkerFactory.class); var worker = workerFactory.create(jobRequest, status -> { }); worker.setThrowOnFailure(true); try { worker.run(); } catch (Exception e) { System.exit(1); } System.exit(0); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/RepositoryManager.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.walmartlabs.concord.agent.cfg.GitConfiguration; import com.walmartlabs.concord.agent.cfg.RepositoryCacheConfiguration; import com.walmartlabs.concord.client2.SecretClient; import com.walmartlabs.concord.imports.Import.SecretDefinition; import com.walmartlabs.concord.repository.*; import com.walmartlabs.concord.sdk.Secret; import com.walmartlabs.concord.dependencymanager.DependencyManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.IOException; import java.nio.file.Path; import java.util.List; public class RepositoryManager { private static final Logger log = LoggerFactory.getLogger(RepositoryManager.class); private final SecretClient secretClient; private final RepositoryProviders providers; private final RepositoryCache repositoryCache; private final GitConfiguration gitCfg; @Inject public RepositoryManager(SecretClient secretClient, GitConfiguration gitCfg, RepositoryCacheConfiguration cacheCfg, ObjectMapper objectMapper, DependencyManager dependencyManager, AgentAuthTokenProvider agentAuthTokenProvider) throws IOException { this.secretClient = secretClient; this.gitCfg = gitCfg; GitClientConfiguration clientCfg = GitClientConfiguration.builder() .defaultOperationTimeout(gitCfg.getDefaultOperationTimeout()) .fetchTimeout(gitCfg.getFetchTimeout()) .httpLowSpeedLimit(gitCfg.getHttpLowSpeedLimit()) .httpLowSpeedTime(gitCfg.getHttpLowSpeedTime()) .sshTimeout(gitCfg.getSshTimeout()) .sshTimeoutRetryCount(gitCfg.getSshTimeoutRetryCount()) .maxGitCliOutputBytes(gitCfg.maxGitCliOutputBytes()) .build(); this.providers = new RepositoryProviders(List.of( new MavenRepositoryProvider(dependencyManager), new GitCliRepositoryProvider(clientCfg, agentAuthTokenProvider) )); this.repositoryCache = new RepositoryCache(cacheCfg.getCacheDir(), cacheCfg.getInfoDir(), cacheCfg.getLockTimeout(), cacheCfg.getMaxAge(), cacheCfg.getLockCount(), objectMapper); } public void export(String repoUrl, String branch, String commitId, String repoPath, Path dest, SecretDefinition secretDefinition, List ignorePatterns) throws ExecutionException { if (gitCfg.isSkip()) { log.info("Skipping git export, using local state"); return; } Secret secret = getSecret(secretDefinition); Path cacheDir = repositoryCache.getPath(repoUrl); repositoryCache.withLock(repoUrl, () -> { Repository repo = providers.fetch( FetchRequest.builder() .url(repoUrl) .version(FetchRequest.Version.commitWithBranch(commitId, branch)) .secret(secret) .destination(cacheDir) .shallow(gitCfg.isShallowClone()) .checkAlreadyFetched(gitCfg.isCheckAlreadyFetched()) .build(), repoPath); repo.export(dest, ignorePatterns); return null; }); repositoryCache.cleanup(); } private Secret getSecret(SecretDefinition secret) throws ExecutionException { if (secret == null) { return null; } try { return secretClient.getData(secret.org(), secret.name(), secret.password(), null); } catch (Exception e) { throw new ExecutionException("Error while retrieving a secret '" + secret.name() + "' in org '" + secret.org() + "': " + e.getMessage(), e); } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/StateFetcher.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public interface StateFetcher { void downloadState(JobRequest jobRequest) throws Exception; } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/Utils.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; public final class Utils { private static final Logger log = LoggerFactory.getLogger(Utils.class); public static boolean kill(Process proc) { return kill(proc.toHandle()); } public static boolean kill(Process proc, boolean killDescendants) { List children = killDescendants ? proc.children().toList() : List.of(); // kill parent first which may gracefully clean up all descendents boolean killed = kill(proc.toHandle()); // clean up orphaned processes that are still running children.stream() .flatMap(ProcessHandle::descendants) .forEach(Utils::kill); return killed; } private static boolean kill(ProcessHandle handle) { if (!handle.isAlive()) { return false; } String p = toString(handle); log.info("kill ['{}'] -> attempting to stop...", p); handle.destroy(); if (handle.isAlive()) { sleep(1000); } while (handle.isAlive()) { log.warn("kill ['{}'] -> waiting for the process to die...", p); sleep(3000); handle.destroyForcibly(); } return true; } public static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private static String toString(ProcessHandle proc) { try { return "pid=" + proc.pid(); } catch (UnsupportedOperationException e) { return proc.toString(); } } private Utils() { } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/Worker.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.executors.JobExecutor; import com.walmartlabs.concord.agent.guice.AgentImportManager; import com.walmartlabs.concord.agent.logging.ProcessLog; import com.walmartlabs.concord.agent.remote.ProcessStatusUpdater; import com.walmartlabs.concord.client2.ProcessEntry.StatusEnum; import com.walmartlabs.concord.common.PathUtils; import com.walmartlabs.concord.imports.Import.SecretDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.IOException; import java.nio.file.Path; import java.util.Collections; import java.util.UUID; public class Worker implements Runnable { private static final Logger log = LoggerFactory.getLogger(Worker.class); private final RepositoryManager repositoryManager; private final AgentImportManager importManager; private final JobExecutor executor; private final CompletionCallback completionCallback; private final StateFetcher stateFetcher; private final ProcessStatusUpdater processStatusUpdater; private final ProcessLog processLog; private final JobRequest jobRequest; private JobInstance jobInstance; private boolean throwOnFailure; @Inject public Worker(RepositoryManager repositoryManager, AgentImportManager importManager, JobExecutor executor, CompletionCallback completionCallback, StateFetcher stateFetcher, ProcessStatusUpdater processStatusUpdater, ProcessLog processLog, JobRequest jobRequest) { this.repositoryManager = repositoryManager; this.importManager = importManager; this.executor = executor; this.completionCallback = completionCallback; this.stateFetcher = stateFetcher; this.processStatusUpdater = processStatusUpdater; this.processLog = processLog; this.jobRequest = jobRequest; } @Override public void run() { log.info("run -> starting {}", jobRequest); UUID instanceId = jobRequest.getInstanceId(); try { // fetch the git repo's data... fetchRepo(jobRequest); // ...and process imports processImports(jobRequest); // ...and download the saved process state from the server downloadState(jobRequest); // load the process' configuration ConfiguredJobRequest configuredJobRequest = ConfiguredJobRequest.from(jobRequest); // execute the job jobInstance = executor.exec(configuredJobRequest); jobInstance.waitForCompletion(); // successful completion log.info("run -> done with {}", configuredJobRequest); onStatusChange(instanceId, StatusEnum.FINISHED); } catch (Throwable e) { // unwrap the exception if needed Throwable t = unwrap(e); // handle any error during the startup or the execution handleError(instanceId, t); } finally { Path payloadDir = jobRequest.getPayloadDir(); try { log.info("exec ['{}'] -> removing the payload directory: {}", instanceId, payloadDir); PathUtils.deleteRecursively(payloadDir); } catch (IOException e) { log.warn("exec ['{}'] -> can't remove the payload directory: {}", instanceId, e.getMessage()); } } } public void cancel() { if (jobInstance == null) { return; } jobInstance.cancel(); } public void setThrowOnFailure(boolean throwOnFailure) { this.throwOnFailure = throwOnFailure; } private void handleError(UUID instanceId, Throwable error) { StatusEnum status = StatusEnum.FAILED; if (jobInstance != null && jobInstance.isCancelled()) { log.info("handleError ['{}'] -> job cancelled", instanceId); status = StatusEnum.CANCELLED; } else { log.error("handleError ['{}'] -> job failed", instanceId, error); } onStatusChange(instanceId, status); log.info("handleError ['{}'] -> done", instanceId); if (throwOnFailure) { if (error instanceof RuntimeException re) { throw re; } throw new RuntimeException(error); } } private void onStatusChange(UUID instanceId, StatusEnum status) { try { processStatusUpdater.update(instanceId, status); } finally { completionCallback.onStatusChange(status); } } private void fetchRepo(JobRequest r) throws Exception { if (r.getRepoUrl() == null || (r.getCommitId() == null && r.getRepoBranch() == null) || r.getRepoUrl().startsWith("classpath://")) { return; } processLog.info("Exporting the repository data: {} @ {}:{}, path: {}", r.getRepoUrl(), r.getRepoBranch() != null ? r.getRepoBranch() : "*", r.getCommitId(), r.getRepoPath() != null ? r.getRepoPath() : "/"); long dt; try { dt = withTimer(() -> repositoryManager.export( r.getRepoUrl(), r.getRepoBranch(), r.getCommitId(), r.getRepoPath(), r.getPayloadDir(), getSecret(r), Collections.emptyList())); } catch (Exception e) { processLog.error("Repository export error: {}", e.getMessage(), e); throw e; } processLog.info("Repository data export took {}ms", dt); } private static SecretDefinition getSecret(JobRequest r) { if (r.getSecretName() == null) { return null; } return SecretDefinition.builder() .org(r.getOrgName()) .name(r.getSecretName()) .build(); } private void downloadState(JobRequest r) throws Exception { processLog.info("Downloading the process state..."); long dt; try { dt = withTimer(() -> stateFetcher.downloadState(r)); } catch (Exception e) { processLog.error("State download error: {}", e.getMessage()); throw e; } processLog.info("Process state download took {}ms", dt); } private void processImports(JobRequest r) throws ExecutionException { if (r.getImports().isEmpty()) { return; } long dt; try { dt = withTimer(() -> importManager.process(r.getImports(), r.getPayloadDir())); } catch (Exception e) { processLog.error("Error while reading the process' imports: " + e.getMessage()); throw new ExecutionException("Error while reading the process' imports", e); } processLog.info("Import of external resources took {}ms", dt); } private static Throwable unwrap(Throwable t) { if (t instanceof ExecutionException && t.getCause() != null) { t = t.getCause(); } if (t instanceof RuntimeException && t.getCause() != null) { t = t.getCause(); } return t; } private static long withTimer(Fn f) throws Exception { long t1 = System.currentTimeMillis(); f.apply(); long t2 = System.currentTimeMillis(); return t2 - t1; } private interface Fn { void apply() throws Exception; } public interface CompletionCallback { void onStatusChange(StatusEnum status); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/WorkerFactory.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.executors.JobExecutor; import com.walmartlabs.concord.agent.executors.JobExecutorFactory; import com.walmartlabs.concord.agent.guice.AgentImportManager; import com.walmartlabs.concord.agent.logging.ProcessLog; import com.walmartlabs.concord.agent.remote.ProcessStatusUpdater; import javax.inject.Inject; public class WorkerFactory { private final RepositoryManager repositoryManager; private final AgentImportManager importManager; private final JobExecutorFactory jobExecutorFactory; private final StateFetcher stateFetcher; private final ProcessStatusUpdater statusUpdater; private final ProcessLog processLog; @Inject public WorkerFactory(RepositoryManager repositoryManager, AgentImportManager importManager, JobExecutorFactory jobExecutorFactory, StateFetcher stateFetcher, ProcessStatusUpdater statusUpdater, ProcessLog processLog) { this.repositoryManager = repositoryManager; this.importManager = importManager; this.jobExecutorFactory = jobExecutorFactory; this.stateFetcher = stateFetcher; this.statusUpdater = statusUpdater; this.processLog = processLog; } public Worker create(JobRequest jobRequest, Worker.CompletionCallback completionCallback) { JobExecutor executor = jobExecutorFactory.create(jobRequest.getType()); return new Worker(repositoryManager, importManager, executor, completionCallback, stateFetcher, statusUpdater, processLog, jobRequest); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/cfg/AgentConfiguration.java ================================================ package com.walmartlabs.concord.agent.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.nio.file.Path; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import static com.walmartlabs.concord.agent.cfg.Utils.getOrCreatePath; import static com.walmartlabs.concord.agent.cfg.Utils.getStringOrDefault; public class AgentConfiguration { private static final Logger log = LoggerFactory.getLogger(AgentConfiguration.class); private final String agentId; private final Map capabilities; private final Path dependencyCacheDir; private final Path dependencyListsDir; private final Duration dependencyResolveTimeout; private final boolean dependencyStrictRepositories; private final List dependencyExclusions; private final Path payloadDir; private final Path workDirBase; private final Path logDir; private final long logMaxDelay; private final boolean workDirMasking; private final int workersCount; private final long pollInterval; private final String maintenanceModeListenerHost; private final int maintenanceModeListenerPort; private final boolean explicitlyResolveV1Client; private final boolean mavenOfflineMode; @Inject public AgentConfiguration(Config cfg) { this.agentId = getStringOrDefault(cfg, "id", () -> UUID.randomUUID().toString()); log.info("Using agent ID: {}", this.agentId); this.capabilities = cfg.hasPath("capabilities") ? cfg.getObject("capabilities").unwrapped() : null; log.info("Using the capabilities: {}", this.capabilities); this.dependencyCacheDir = getOrCreatePath(cfg, "dependencyCacheDir"); this.dependencyListsDir = getOrCreatePath(cfg, "dependencyListsDir"); this.dependencyResolveTimeout = cfg.hasPath("dependencyResolveTimeout") ? cfg.getDuration("dependencyResolveTimeout") : null; this.dependencyStrictRepositories = cfg.hasPath("dependencyStrictRepositories") && cfg.getBoolean("dependencyStrictRepositories"); this.dependencyExclusions = cfg.getStringList("dependencyExclusions"); this.payloadDir = getOrCreatePath(cfg, "payloadDir"); this.workDirBase = getOrCreatePath(cfg, "workDirBase"); this.logDir = getOrCreatePath(cfg, "logDir"); this.logMaxDelay = cfg.getDuration("logMaxDelay", TimeUnit.MILLISECONDS); this.workDirMasking = cfg.getBoolean("workDirMasking"); this.workersCount = cfg.getInt("workersCount"); this.maintenanceModeListenerHost = cfg.getString("maintenanceModeListenerHost"); this.maintenanceModeListenerPort = cfg.getInt("maintenanceModeListenerPort"); this.pollInterval = cfg.getDuration("pollInterval", TimeUnit.MILLISECONDS); this.explicitlyResolveV1Client = cfg.getBoolean("explicitlyResolveV1Client"); this.mavenOfflineMode = cfg.getBoolean("mavenOfflineMode"); } public String getAgentId() { return agentId; } public Map getCapabilities() { return capabilities; } public Path getDependencyCacheDir() { return dependencyCacheDir; } public Path getDependencyListsDir() { return dependencyListsDir; } public Duration getDependencyResolveTimeout() { return dependencyResolveTimeout; } public boolean dependencyStrictRepositories() { return dependencyStrictRepositories; } public List dependencyExclusions() { return dependencyExclusions; } public Path getPayloadDir() { return payloadDir; } public Path getWorkDirBase() { return workDirBase; } public Path getLogDir() { return logDir; } public long getLogMaxDelay() { return logMaxDelay; } public boolean isWorkDirMaskings() { return workDirMasking; } public int getWorkersCount() { return workersCount; } public long getPollInterval() { return pollInterval; } public String getMaintenanceModeListenerHost() { return maintenanceModeListenerHost; } public int getMaintenanceModeListenerPort() { return maintenanceModeListenerPort; } public boolean isExplicitlyResolveV1Client() { return explicitlyResolveV1Client; } public boolean isMavenOfflineMode() { return mavenOfflineMode; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/cfg/DockerConfiguration.java ================================================ package com.walmartlabs.concord.agent.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import javax.inject.Inject; import java.util.List; import java.util.concurrent.TimeUnit; public class DockerConfiguration { private final String dockerHost; private final boolean orphanSweeperEnabled; private final long orphanSweeperPeriod; private final List extraVolumes; private final boolean exposeDockerDaemon; @Inject public DockerConfiguration(Config cfg) { this.dockerHost = cfg.getString("docker.host"); this.orphanSweeperEnabled = cfg.getBoolean("docker.orphanSweeperEnabled"); this.orphanSweeperPeriod = cfg.getDuration("docker.orphanSweeperPeriod", TimeUnit.MILLISECONDS); this.extraVolumes = cfg.getStringList("docker.extraVolumes"); this.exposeDockerDaemon = cfg.getBoolean("docker.exposeDockerDaemon"); } public String getDockerHost() { return dockerHost; } public boolean isOrphanSweeperEnabled() { return orphanSweeperEnabled; } public long getOrphanSweeperPeriod() { return orphanSweeperPeriod; } public List getExtraVolumes() { return extraVolumes; } public boolean exposeDockerDaemon() { return exposeDockerDaemon; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitConfiguration.java ================================================ package com.walmartlabs.concord.agent.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import com.walmartlabs.concord.common.cfg.MappingAuthConfig; import com.walmartlabs.concord.common.cfg.OauthTokenConfig; import javax.inject.Inject; import java.time.Duration; import java.util.List; import java.util.Optional; import static com.walmartlabs.concord.agent.cfg.Utils.getStringOrDefault; import static com.walmartlabs.concord.common.cfg.MappingAuthConfig.assertBaseUrlPattern; public class GitConfiguration implements OauthTokenConfig { private final String token; private final String oauthUsername; private final String oauthUrlPattern; private final boolean shallowClone; private final boolean checkAlreadyFetched; private final Duration defaultOperationTimeout; private final Duration fetchTimeout; private final int httpLowSpeedLimit; private final Duration httpLowSpeedTime; private final Duration sshTimeout; private final int sshTimeoutRetryCount; private final boolean skip; private final long maxGitCliOutputBytes; private final List authConfigs; @Inject public GitConfiguration(Config cfg) { this.token = getStringOrDefault(cfg, "git.oauth", () -> null); this.oauthUsername = getStringOrDefault(cfg, "git.oauthUsername", () -> null); this.oauthUrlPattern = getStringOrDefault(cfg, "git.oauthUrlPattern", () -> null); this.shallowClone = cfg.getBoolean("git.shallowClone"); this.checkAlreadyFetched = cfg.getBoolean("git.checkAlreadyFetched"); this.defaultOperationTimeout = cfg.getDuration("git.defaultOperationTimeout"); this.fetchTimeout = cfg.getDuration("git.fetchTimeout"); this.httpLowSpeedLimit = cfg.getInt("git.httpLowSpeedLimit"); this.httpLowSpeedTime = cfg.getDuration("git.httpLowSpeedTime"); this.sshTimeout = cfg.getDuration("git.sshTimeout"); this.sshTimeoutRetryCount = cfg.getInt("git.sshTimeoutRetryCount"); this.skip = cfg.getBoolean("git.skip"); this.maxGitCliOutputBytes = cfg.getLong("git.maxGitCliOutputBytes"); this.authConfigs = cfg.getConfigList("git.systemAuth"); } @Override public Optional getOauthToken() { return Optional.ofNullable(token); } @Override public Optional getOauthUsername() { return Optional.ofNullable(oauthUsername); } @Override public Optional getOauthUrlPattern() { return Optional.ofNullable(oauthUrlPattern); } public boolean isShallowClone() { return shallowClone; } public boolean isCheckAlreadyFetched() { return checkAlreadyFetched; } public Duration getDefaultOperationTimeout() { return defaultOperationTimeout; } public Duration getFetchTimeout() { return fetchTimeout; } public int getHttpLowSpeedLimit() { return httpLowSpeedLimit; } public Duration getHttpLowSpeedTime() { return httpLowSpeedTime; } public Duration getSshTimeout() { return sshTimeout; } public int getSshTimeoutRetryCount() { return sshTimeoutRetryCount; } public boolean isSkip() { return skip; } public long maxGitCliOutputBytes() { return maxGitCliOutputBytes; } public List getSystemAuth() { return authConfigs.stream() .map(o -> { AuthSource type = AuthSource.valueOf(o.getString("type").toUpperCase()); return (AuthConfig) switch (type) { case OAUTH_TOKEN -> OauthConfig.from(o); }; }) .map(AuthConfig::toGitAuth) .toList(); } enum AuthSource { OAUTH_TOKEN } public interface AuthConfig { MappingAuthConfig toGitAuth(); } public record OauthConfig(String id, String urlPattern, String token) implements AuthConfig { static OauthConfig from(Config cfg) { return new OauthConfig( getStringOrDefault(cfg, "id", () -> "system-oauth-token"), cfg.getString("urlPattern"), cfg.getString("token") ); } @Override public MappingAuthConfig.OauthAuthConfig toGitAuth() { return MappingAuthConfig.OauthAuthConfig.builder() .id(this.id()) .urlPattern(assertBaseUrlPattern(this.urlPattern())) .token(this.token()) .build(); } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/cfg/GitHubConfiguration.java ================================================ package com.walmartlabs.concord.agent.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.cfg.MappingAuthConfig; import com.walmartlabs.concord.github.appinstallation.cfg.GitHubAppInstallationConfig; import javax.inject.Inject; import java.time.Duration; import java.util.List; public class GitHubConfiguration implements GitHubAppInstallationConfig { private static final String CFG_APP_INSTALLATION = "github.appInstallation"; private final GitHubAppInstallationConfig appInstallation; @Inject public GitHubConfiguration(com.typesafe.config.Config config) { if (config.hasPath(CFG_APP_INSTALLATION)) { var raw = config.getConfig(CFG_APP_INSTALLATION); this.appInstallation = GitHubAppInstallationConfig.fromConfig(raw); } else { this.appInstallation = GitHubAppInstallationConfig.builder() .authConfigs(List.of()) .build(); } } @Override public List getAuthConfigs() { return appInstallation.getAuthConfigs(); } @Override public Duration getSystemAuthCacheDuration() { return appInstallation.getSystemAuthCacheDuration(); } @Override public long getSystemAuthCacheMaxWeight() { return appInstallation.getSystemAuthCacheMaxWeight(); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/cfg/ImportConfiguration.java ================================================ package com.walmartlabs.concord.agent.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import javax.inject.Inject; import java.util.Collections; import java.util.HashSet; import java.util.Set; public class ImportConfiguration { private final Set disabledProcessors; @Inject public ImportConfiguration(Config cfg) { this.disabledProcessors = Collections.unmodifiableSet(new HashSet<>(cfg.getStringList("imports.disabledProcessors"))); } public Set getDisabledProcessors() { return disabledProcessors; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/cfg/PreForkConfiguration.java ================================================ package com.walmartlabs.concord.agent.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import javax.inject.Inject; import java.util.concurrent.TimeUnit; public class PreForkConfiguration { private final boolean enabled; private final long maxAge; private final int maxCount; @Inject public PreForkConfiguration(Config cfg) { this.enabled = cfg.getBoolean("prefork.enabled"); this.maxAge = cfg.getDuration("prefork.maxAge", TimeUnit.MILLISECONDS); this.maxCount = cfg.getInt("prefork.maxCount"); } public boolean isEnabled() { return enabled; } public long getMaxAge() { return maxAge; } public int getMaxCount() { return maxCount; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/cfg/RepositoryCacheConfiguration.java ================================================ package com.walmartlabs.concord.agent.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import javax.inject.Inject; import java.nio.file.Path; import java.time.Duration; import static com.walmartlabs.concord.agent.cfg.Utils.getOrCreatePath; public class RepositoryCacheConfiguration { private final Path cacheDir; private final Duration lockTimeout; private final int lockCount; private final Duration maxAge; private final Path infoDir; @Inject public RepositoryCacheConfiguration(Config cfg) { this.cacheDir = getOrCreatePath(cfg, "repositoryCache.cacheDir"); this.lockTimeout = cfg.getDuration("repositoryCache.lockTimeout"); this.lockCount = cfg.getInt("repositoryCache.lockCount"); this.maxAge = cfg.getDuration("repositoryCache.maxAge"); this.infoDir = getOrCreatePath(cfg, "repositoryCache.cacheInfoDir"); } public Path getCacheDir() { return cacheDir; } public Duration getLockTimeout() { return lockTimeout; } public int getLockCount() { return lockCount; } public Duration getMaxAge() { return maxAge; } public Path getInfoDir() { return infoDir; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/cfg/RuntimeConfiguration.java ================================================ package com.walmartlabs.concord.agent.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import static com.walmartlabs.concord.agent.cfg.Utils.*; import static java.util.stream.Collectors.joining; public class RuntimeConfiguration { private static final Logger log = LoggerFactory.getLogger(RuntimeConfiguration.class); private final Map configs; @Inject public RuntimeConfiguration(Config config) { var runtimes = config.getObject("runtimes"); var configs = new HashMap(); for (var runtime : runtimes.keySet()) { var cfg = Entry.parse(config.getConfig("runtimes." + runtime)); configs.put(runtime, cfg); } log.info("Available runtimes: {}", configs.keySet().stream().sorted().collect(joining(", "))); this.configs = Collections.unmodifiableMap(configs); } public Optional getForRuntime(String runtime) { var cfg = configs.get(runtime); return Optional.ofNullable(cfg); } public record Entry(Path path, Path cfgDir, String javaCmd, List jvmParams, String mainClass, Path persistentWorkDir, boolean cleanRunnerDescendants, boolean segmentedLogs) { public static Entry parse(Config cfg) { var pathString = getStringOrDefault(cfg, "path", () -> { // support local development, use .properties files to get JAR paths // the .properties files are populated during build if (!cfg.hasPath("propertiesFile")) { throw new IllegalStateException(".path or .propertiesFile are required"); } var fallback = cfg.getString("propertiesFile"); var props = new Properties(); try (var in = Entry.class.getResourceAsStream(fallback)) { if (in == null) { throw new IllegalStateException("Resource not found: " + fallback); } props.load(in); } catch (IOException e) { throw new RuntimeException(e); } return props.getProperty("path"); }); var path = Paths.get(pathString); var cfgDir = getOrCreatePath(cfg, "cfgDir"); var javaCmd = getJavaCmd(cfg); var jvmParams = cfg.getStringList("jvmParams"); var mainClass = cfg.getString("mainClass"); var persistentWorkDir = getOptionalAbsolutePath(cfg, "persistentWorkDir"); var cleanRunnerDescendants = cfg.getBoolean("cleanRunnerDescendants"); var segmentedLogs = cfg.getBoolean("segmentedLogs"); return new Entry(path, cfgDir, javaCmd, jvmParams, mainClass, persistentWorkDir, cleanRunnerDescendants, segmentedLogs); } private static String getJavaCmd(Config cfg) { var path = "javaCmd"; if (cfg.hasPath(path)) { var s = cfg.getString(path); if (s != null) { return s; } } var javaHome = System.getProperty("java.home"); if (javaHome != null) { return javaHome + "/bin/java"; } return "java"; } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/cfg/ServerConfiguration.java ================================================ package com.walmartlabs.concord.agent.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.util.concurrent.TimeUnit; import static com.walmartlabs.concord.agent.cfg.Utils.getStringOrDefault; public class ServerConfiguration { private static final Logger log = LoggerFactory.getLogger(ServerConfiguration.class); private final String apiBaseUrl; private final String[] websocketUrls; private final String apiKey; private final long pingInterval; private final long maxNoActivityPeriod; private final boolean verifySsl; private final long connectTimeout; private final long readTimeout; private final String userAgent; private final long maxNoHeartbeatInterval; private final long processRequestDelay; private final long reconnectDelay; @Inject public ServerConfiguration(Config cfg, AgentConfiguration agentCfg) { this.apiBaseUrl = cfg.getString("server.apiBaseUrl"); log.info("Using the Server's API address: {}", apiBaseUrl); this.websocketUrls = getWebsocketUrls(cfg); log.info("Using the Server's websocket addresses: {}", (Object[]) websocketUrls); this.apiKey = cfg.getString("server.apiKey"); if (this.apiKey == null || this.apiKey.trim().isEmpty()) { throw new IllegalArgumentException("Configuration is missing value for server.apiKey!"); } this.pingInterval = cfg.getDuration("server.websocketPingInterval", TimeUnit.MILLISECONDS); this.maxNoActivityPeriod = cfg.getDuration("server.websocketMaxNoActivityPeriod", TimeUnit.MILLISECONDS); this.verifySsl = cfg.getBoolean("server.verifySsl"); this.connectTimeout = cfg.getDuration("server.connectTimeout", TimeUnit.MILLISECONDS); this.readTimeout = cfg.getDuration("server.readTimeout", TimeUnit.MILLISECONDS); this.userAgent = getStringOrDefault(cfg, "server.userAgent", () -> "Concord-Agent: id=" + agentCfg.getAgentId()); this.maxNoHeartbeatInterval = cfg.getDuration("server.maxNoHeartbeatInterval", TimeUnit.MILLISECONDS); this.processRequestDelay = cfg.getDuration("server.processRequestDelay", TimeUnit.MILLISECONDS); this.reconnectDelay = cfg.getDuration("server.reconnectDelay", TimeUnit.MILLISECONDS); } public String getApiBaseUrl() { return apiBaseUrl; } public String[] getWebsocketUrls() { return websocketUrls; } public String getApiKey() { return apiKey; } public long getPingInterval() { return pingInterval; } public long getMaxNoActivityPeriod() { return maxNoActivityPeriod; } public boolean isVerifySsl() { return verifySsl; } public long getConnectTimeout() { return connectTimeout; } public long getReadTimeout() { return readTimeout; } public String getUserAgent() { return userAgent; } public long getMaxNoHeartbeatInterval() { return maxNoHeartbeatInterval; } public long getProcessRequestDelay() { return processRequestDelay; } public long getReconnectDelay() { return reconnectDelay; } private static String[] getWebsocketUrls(Config cfg) { // we had a silly typo ("websockeR") in our configs, so for backward compatibility we must check the old variant first String oldKey = "server.websockerUrl"; if (cfg.hasPath(oldKey)) { String[] as = getCSV(cfg.getString(oldKey)); if (as != null) { return as; } } return getCSV(cfg.getString("server.websocketUrl")); } private static String[] getCSV(String s) { if (s == null) { return null; } String[] as = s.split(","); for (int i = 0; i < as.length; i++) { as[i] = as[i].trim(); } return as; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/cfg/Utils.java ================================================ package com.walmartlabs.concord.agent.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import com.walmartlabs.concord.common.PathUtils; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.function.Supplier; public final class Utils { public static String getStringOrDefault(Config cfg, String key, Supplier defaultValueSupplier) { if (cfg.hasPath(key)) { return cfg.getString(key); } return defaultValueSupplier.get(); } public static Path getOptionalAbsolutePath(Config cfg, String key) { if (!cfg.hasPath(key)) { return null; } String s = cfg.getString(key).trim(); if (s.isEmpty()) { return null; } if (!s.startsWith("/")) { throw new IllegalArgumentException(key + " must be an absolute path, got: " + s); } return Paths.get(s); } public static Path getOrCreatePath(Config cfg, String key) { try { if (!cfg.hasPath(key)) { return PathUtils.createTempDir(key); } String value = cfg.getString(key); if (value.startsWith("/")) { Path p = Paths.get(value) .normalize() .toAbsolutePath(); if (!Files.exists(p)) { Files.createDirectories(p); } return p; } return PathUtils.createTempDir(value); } catch (IOException e) { throw new RuntimeException(e); } } private Utils() { } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/docker/OrphanSweeper.java ================================================ package com.walmartlabs.concord.agent.docker; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.DockerProcessBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.Function; public class OrphanSweeper implements Runnable { private static final Logger log = LoggerFactory.getLogger(OrphanSweeper.class); private static final String[] PS_CMD = {"docker", "ps", "-a", "--filter", "label=" + DockerProcessBuilder.CONCORD_TX_ID_LABEL, "--format", "{{.Label \"" + DockerProcessBuilder.CONCORD_TX_ID_LABEL + "\"}} {{.ID}}"}; private static final long RETRY_DELAY = TimeUnit.SECONDS.toMillis(30); private final StatusChecker statusChecker; private final long period; public OrphanSweeper(StatusChecker statusChecker, long period) { this.statusChecker = statusChecker; this.period = period; } @Override public void run() { log.info("run -> removing orphaned Docker containers..."); while (!Thread.currentThread().isInterrupted()) { try { Map containers = findContainers(); log.debug("run -> found {} container(s)...", containers.size()); for (Map.Entry c : containers.entrySet()) { UUID instanceId = c.getKey(); if (statusChecker.isAlive(instanceId)) { continue; } String cId = c.getValue(); log.warn("run -> found an orphaned container {} (process {}), attempting to kill...", cId, instanceId); killContainer(cId); } sleep(period); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { log.warn("run -> error: {}, retrying in {}ms...", e.getMessage(), RETRY_DELAY); sleep(RETRY_DELAY); } } } private static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private static Map findContainers() throws IOException, InterruptedException { Map ids = new HashMap<>(); exec(PS_CMD, line -> { int idx = line.indexOf(" "); if (idx < 0 || idx + 1 >= line.length()) { log.warn("findContainers -> invalid line: {}", line); return null; } UUID k = UUID.fromString(line.substring(0, idx)); String v = line.substring(idx + 1); ids.put(k, v); return null; }); return ids; } private static void killContainer(String cId) throws IOException, InterruptedException { Process b = new ProcessBuilder() .command(createKillCommand(cId)) .start(); int code = b.waitFor(); if (code != 0) { throw new IOException("Error while removing a container " + cId + ": docker exit code " + code); } log.info("killContainer -> done, {} removed", cId); } private static String[] createKillCommand(String cId) { return new String[]{"docker", "rm", "-f", cId}; } private static void exec(String[] cmd, Function callback) throws IOException, InterruptedException { Process b = new ProcessBuilder() .command(cmd) .redirectErrorStream(true) .start(); int code = b.waitFor(); if (code != 0) { throw new IOException("Error while executing a command " + String.join(" ", cmd) + " : docker exit code " + code); } try (BufferedReader reader = new BufferedReader(new InputStreamReader(b.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { if (line.trim().isEmpty()) { continue; } callback.apply(line); } } } public interface StatusChecker { boolean isAlive(UUID instanceId); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/executors/JobExecutor.java ================================================ package com.walmartlabs.concord.agent.executors; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.ConfiguredJobRequest; import com.walmartlabs.concord.agent.JobInstance; public interface JobExecutor { JobInstance exec(ConfiguredJobRequest jobRequest) throws Exception; } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/executors/JobExecutorFactory.java ================================================ package com.walmartlabs.concord.agent.executors; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.ConfiguredJobRequest; import com.walmartlabs.concord.agent.JobRequest; import com.walmartlabs.concord.agent.cfg.*; import com.walmartlabs.concord.agent.executors.runner.DefaultDependencies; import com.walmartlabs.concord.agent.executors.runner.ProcessPool; import com.walmartlabs.concord.agent.executors.runner.RunnerJobExecutor; import com.walmartlabs.concord.agent.logging.ProcessLog; import com.walmartlabs.concord.agent.logging.ProcessLogFactory; import com.walmartlabs.concord.agent.remote.AttachmentsUploader; import com.walmartlabs.concord.dependencymanager.DependencyManager; import com.walmartlabs.concord.sdk.Constants; import com.walmartlabs.concord.sdk.MapUtils; import javax.inject.Inject; import javax.inject.Singleton; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @Singleton public class JobExecutorFactory { private final AgentConfiguration agentCfg; private final ServerConfiguration serverCfg; private final DockerConfiguration dockerCfg; private final PreForkConfiguration preForkCfg; private final RuntimeConfiguration runtimeCfg; private final DependencyManager dependencyManager; private final DefaultDependencies defaultDependencies; private final ProcessPool processPool; private final ProcessLog processLog; private final AttachmentsUploader attachmentsUploader; private final ProcessLogFactory processLogFactory; private final ExecutorService executor; @Inject public JobExecutorFactory(AgentConfiguration agentCfg, ServerConfiguration serverCfg, DockerConfiguration dockerCfg, PreForkConfiguration preForkCfg, RuntimeConfiguration runtimeCfg, DependencyManager dependencyManager, DefaultDependencies defaultDependencies, ProcessPool processPool, ProcessLog processLog, AttachmentsUploader attachmentsUploader, ProcessLogFactory processLogFactory) { this.agentCfg = agentCfg; this.serverCfg = serverCfg; this.dockerCfg = dockerCfg; this.preForkCfg = preForkCfg; this.runtimeCfg = runtimeCfg; this.dependencyManager = dependencyManager; this.defaultDependencies = defaultDependencies; this.processPool = processPool; this.processLog = processLog; this.attachmentsUploader = attachmentsUploader; this.processLogFactory = processLogFactory; this.executor = Executors.newCachedThreadPool(); } public JobExecutor create(JobRequest.Type jobType) { if (jobType != JobRequest.Type.RUNNER) { throw new RuntimeException("Unsupported job type: " + jobType); } return jobRequest -> { String runtimeName = getRuntimeName(jobRequest); RuntimeConfiguration.Entry runtimeCfg = this.runtimeCfg.getForRuntime(runtimeName) .orElseThrow(() -> new IllegalStateException("Runner configuration for '%s' not found.".formatted(runtimeName))); processLog.info("Runtime: {}", runtimeName); RunnerJobExecutor.RunnerJobExecutorConfiguration runnerExecutorCfg = RunnerJobExecutor.RunnerJobExecutorConfiguration.builder() .agentId(agentCfg.getAgentId()) .serverApiBaseUrl(serverCfg.getApiBaseUrl()) .javaCmd(runtimeCfg.javaCmd()) .jvmParams(runtimeCfg.jvmParams()) .dependencyListDir(agentCfg.getDependencyListsDir()) .dependencyCacheDir(agentCfg.getDependencyCacheDir()) .dependencyResolveTimeout(agentCfg.getDependencyResolveTimeout()) .workDirBase(agentCfg.getWorkDirBase()) .runnerPath(runtimeCfg.path()) .runnerCfgDir(runtimeCfg.cfgDir()) .runnerSecurityManagerEnabled(false) // SecurityManager is deprecated and should not be used .runnerMainClass(runtimeCfg.mainClass()) .extraDockerVolumes(dockerCfg.getExtraVolumes()) .exposeDockerDaemon(dockerCfg.exposeDockerDaemon()) .maxHeartbeatInterval(serverCfg.getMaxNoHeartbeatInterval()) .segmentedLogs(runtimeCfg.segmentedLogs()) .workDirMasking(agentCfg.isWorkDirMaskings()) .persistentWorkDir(runtimeCfg.persistentWorkDir()) .preforkEnabled(preForkCfg.isEnabled()) .cleanRunnerDescendants(runtimeCfg.cleanRunnerDescendants()) .build(); JobExecutor delegate = new RunnerJobExecutor(runnerExecutorCfg, dependencyManager, defaultDependencies, attachmentsUploader, processPool, processLogFactory, executor); return delegate.exec(jobRequest); }; } private static String getRuntimeName(ConfiguredJobRequest req) { return MapUtils.getString(req.getProcessCfg(), Constants.Request.RUNTIME_KEY, "concord-v1"); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/DefaultDependencies.java ================================================ package com.walmartlabs.concord.agent.executors.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; import java.util.stream.Stream; public class DefaultDependencies { private static final Logger log = LoggerFactory.getLogger(DefaultDependencies.class); private static final String CFG_KEY = "DEFAULT_DEPS_CFG"; private final List dependencies; public DefaultDependencies() { String path = System.getenv(CFG_KEY); if (path != null) { try (Stream lines = Files.lines(Paths.get(path))) { this.dependencies = lines.filter(s -> !s.isBlank()) .map(DefaultDependencies::parseUri) .toList(); } catch (IOException e) { throw new RuntimeException(e); } log.info("init -> using external default dependencies configuration: {}", path); } else { try (InputStream is = DefaultDependencies.class.getResourceAsStream("default-dependencies")) { if (is == null) { throw new RuntimeException("Can't find com/walmartlabs/concord/agent/executors/runner/default-dependencies. " + "This is most likely a bug or an issue with the local build and/or classpath."); } this.dependencies = new BufferedReader(new InputStreamReader(is)).lines() .filter(s -> !s.isBlank()) .map(DefaultDependencies::parseUri).toList(); } catch (IOException e) { throw new RuntimeException(e); } log.info("init -> using classpath default dependencies configuration"); } } public List getDependencies() { return dependencies; } private static URI parseUri(String s) { try { return new URI(s); } catch (URISyntaxException e) { throw new RuntimeException(e); } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/JobDependencies.java ================================================ package com.walmartlabs.concord.agent.executors.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.ExecutionException; import com.walmartlabs.concord.policyengine.PolicyEngine; import com.walmartlabs.concord.sdk.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.net.*; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; import static com.walmartlabs.concord.dependencymanager.DependencyManager.MAVEN_SCHEME; import static com.walmartlabs.concord.policyengine.DependencyVersionsPolicy.Dependency; public final class JobDependencies { private static final Logger log = LoggerFactory.getLogger(JobDependencies.class); public static Collection get(RunnerJob job) throws ExecutionException { Collection uris = getDependencyUris(job); if (uris.isEmpty()) { return Collections.emptyList(); } Map versions = getDependencyVersions(job); if (versions.isEmpty()) { return uris; } return updateVersions(job, uris, versions); } private static Collection updateVersions(RunnerJob job, Collection uris, Map versions) { List result = new ArrayList<>(); for (URI item : uris) { String scheme = item.getScheme(); if (MAVEN_SCHEME.equalsIgnoreCase(scheme)) { IdAndVersion idv = IdAndVersion.parse(item.getAuthority()); if (isLatestVersion(idv.version)) { String version = versions.get(idv.id); if (version != null) { item = URI.create(MAVEN_SCHEME + "://" + idv.id + ":" + assertVersion(idv.id, versions)); } else { job.getLog().warn("Can't determine the version of {}, using as-is...", item); } } } result.add(item); } return result; } @SuppressWarnings("unchecked") private static Collection getDependencyUris(RunnerJob job) throws ExecutionException { try { Map m = job.getProcessCfg(); Collection deps = (Collection) m.get(Constants.Request.DEPENDENCIES_KEY); return normalizeUrls(deps); } catch (URISyntaxException | IOException e) { throw new ExecutionException("Error while reading the list of dependencies: " + e.getMessage(), e); } } private static Collection normalizeUrls(Collection urls) throws IOException, URISyntaxException { if (urls == null || urls.isEmpty()) { return Collections.emptySet(); } Collection result = new HashSet<>(); for (String s : urls) { URI u = new URI(s); String scheme = u.getScheme(); if (MAVEN_SCHEME.equalsIgnoreCase(scheme)) { result.add(u); continue; } if (scheme == null || scheme.trim().isEmpty()) { throw new IOException("Invalid dependency URL. Missing URL scheme: " + s); } if (s.endsWith(".jar")) { result.add(u); continue; } URL url = u.toURL(); while (true) { if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) { URLConnection conn = url.openConnection(); if (conn instanceof HttpURLConnection) { HttpURLConnection httpConn = (HttpURLConnection) conn; httpConn.setInstanceFollowRedirects(false); int code = httpConn.getResponseCode(); if (code == HttpURLConnection.HTTP_MOVED_TEMP || code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_SEE_OTHER || code == 307) { String location = httpConn.getHeaderField("Location"); url = new URL(location); log.info("normalizeUrls -> using: {}", location); continue; } u = url.toURI(); } else { log.warn("normalizeUrls -> unexpected connection type: {} (for {})", conn.getClass(), s); } } break; } result.add(u); } return result; } private static Map getDependencyVersions(RunnerJob job) throws ExecutionException { Map result = getDependencyVersionsFromFile(job); PolicyEngine pe = job.getPolicyEngine(); if (pe != null) { result = new HashMap<>(result); // make mutable result.putAll(pe.getDefaultDependencyVersionsPolicy().get().stream() .collect(Collectors.toMap(Dependency::getArtifact, Dependency::getVersion))); } return result; } private static Map getDependencyVersionsFromFile(RunnerJob job) throws ExecutionException { Path workDir = job.getPayloadDir(); Path pluginsFile = workDir.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME) .resolve(Constants.Files.DEPENDENCY_VERSIONS_FILE_NAME); if (!Files.exists(pluginsFile)) { return Collections.emptyMap(); } try (InputStream is = Files.newInputStream(pluginsFile)) { Map result = new HashMap<>(); Properties p = new Properties(); p.load(is); for (String name : p.stringPropertyNames()) { result.put(name, p.getProperty(name)); } return result; } catch (IOException e) { throw new ExecutionException("Error while reading default dependency versions: " + e.getMessage(), e); } } private static boolean isLatestVersion(String v) { return v.equalsIgnoreCase("latest"); } private static String assertVersion(String dep, Map versions) { String version = versions.get(dep); if (version != null) { return version; } throw new IllegalArgumentException("Unofficial dependency '" + dep + "': version is required"); } private static class IdAndVersion { public static IdAndVersion parse(String s) { int i = s.lastIndexOf(':'); if (i >= 0 && i + 1 < s.length()) { String id = s.substring(0, i); String v = s.substring(i + 1); return new IdAndVersion(id, v); } throw new IllegalArgumentException("Invalid artifact ID format: " + s); } private final String id; private final String version; private IdAndVersion(String id, String version) { this.id = id; this.version = version; } } private JobDependencies() { } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/ProcessPool.java ================================================ package com.walmartlabs.concord.agent.executors.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.google.common.hash.HashCode; import com.walmartlabs.concord.agent.ExecutionException; import com.walmartlabs.concord.agent.Utils; import com.walmartlabs.concord.agent.cfg.PreForkConfiguration; import com.walmartlabs.concord.common.PathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.IOException; import java.nio.file.Path; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ProcessPool { private static final Logger log = LoggerFactory.getLogger(ProcessPool.class); private static final long CLEANUP_PERIOD = 30000; private final long maxEntryAge; private final int maxEntryCount; private final Map> pool = new HashMap<>(); private final ExecutorService executor = Executors.newCachedThreadPool(); @Inject public ProcessPool(PreForkConfiguration cfg) { this.maxEntryAge = cfg.getMaxAge(); this.maxEntryCount = cfg.getMaxCount(); init(); } public void init() { Thread t = new Thread(() -> { log.info("run -> starting cleanup thread, max entry age {}ms, max entry count {}", maxEntryAge, maxEntryCount); while (!Thread.currentThread().isInterrupted()) { Utils.sleep(CLEANUP_PERIOD); try { maintenance(); } catch (Exception e) { log.warn("pool -> error while performing maintenance: {}", e.getMessage()); } } }, "process-pool-cleanup"); t.start(); } public ProcessEntry take(HashCode hc, ProcessLauncher launcher) throws ExecutionException { synchronized (pool) { Queue q = pool.computeIfAbsent(hc, k -> new LinkedList<>()); ProcessEntry entry = q.poll(); if (entry == null) { try { entry = launcher.start(); } catch (IOException e) { throw new ExecutionException("Error while starting a new process", e); } log.info("take -> started a new process: {}", entry.workDir); } else { log.info("take -> using a pre-forked instance: {}", entry.workDir); } executor.submit(() -> populate(hc, launcher)); return entry; } } private void populate(HashCode hc, ProcessLauncher launcher) { synchronized (pool) { // calculate the total number of processes in the pool int total = 0; for (Map.Entry> e : pool.entrySet()) { total += e.getValue().size(); } if (total >= maxEntryCount) { // mark the oldest entry for removal ProcessEntry oldest = null; for (Map.Entry> q : pool.entrySet()) { for (ProcessEntry e : q.getValue()) { if (oldest == null || oldest.timestamp > e.timestamp) { oldest = e; } } } // it's never null assert oldest != null; oldest.remove = true; } // add a new pool entry Queue q = pool.computeIfAbsent(hc, k -> new LinkedList<>()); try { q.add(launcher.start()); } catch (IOException e) { log.error("populate -> error while starting a new process", e); } } } private void maintenance() { List queuesToRemove = new ArrayList<>(); List processesToKill = new ArrayList<>(); long t = System.currentTimeMillis(); synchronized (pool) { pool.forEach((hc, q) -> { q.removeIf(e -> { if (e.remove || t - e.timestamp >= maxEntryAge) { processesToKill.add(e); return true; } return false; }); if (q.isEmpty()) { queuesToRemove.add(hc); } }); for (HashCode hc : queuesToRemove) { pool.remove(hc); } } log.info("maintenance -> removed {} queues", queuesToRemove.size()); for (ProcessEntry p : processesToKill) { Utils.kill(p.process); cleanup(p); } log.info("maintenance -> killed {} processes", processesToKill.size()); } private static void cleanup(ProcessEntry process) { try { PathUtils.deleteRecursively(process.workDir); } catch (IOException e) { log.info("cleanup ['{}'] -> error: {}", process.workDir, e.getMessage()); // ignore } } public interface ProcessLauncher { ProcessEntry start() throws IOException; } public static final class ProcessEntry { private final long timestamp; private final Process process; private final Path workDir; private boolean remove = false; public ProcessEntry(Process process, Path workDir) { this.timestamp = System.currentTimeMillis(); this.process = process; this.workDir = workDir; } public Process getProcess() { return process; } public Path getWorkDir() { return workDir; } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/RunnerCommandBuilder.java ================================================ package com.walmartlabs.concord.agent.executors.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; public class RunnerCommandBuilder { private String javaCmd; private Path workDir; private Path runnerPath; private Path runnerCfgPath; private String logLevel; private Path extraDockerVolumesFile; private boolean exposeDockerDaemon; private List extraJvmParams; private String mainClass; private int majorJavaVersion; public RunnerCommandBuilder() { } public RunnerCommandBuilder javaCmd(String javaCmd) { this.javaCmd = javaCmd; return this; } public RunnerCommandBuilder workDir(Path workDir) { this.workDir = workDir; return this; } public RunnerCommandBuilder runnerPath(Path runnerPath) { this.runnerPath = runnerPath; return this; } public RunnerCommandBuilder runnerCfgPath(Path runnerCfgPath) { this.runnerCfgPath = runnerCfgPath; return this; } public RunnerCommandBuilder logLevel(String logLevel) { this.logLevel = logLevel; return this; } public RunnerCommandBuilder extraDockerVolumesFile(Path extraDockerVolumesFile) { this.extraDockerVolumesFile = extraDockerVolumesFile; return this; } public RunnerCommandBuilder exposeDockerDaemon(boolean exposeDockerDaemon) { this.exposeDockerDaemon = exposeDockerDaemon; return this; } public RunnerCommandBuilder jvmParams(List jvmParams) { this.extraJvmParams = jvmParams; return this; } public RunnerCommandBuilder mainClass(String mainClass) { this.mainClass = mainClass; return this; } public RunnerCommandBuilder majorJavaVersion(int majorJavaVersion) { this.majorJavaVersion = majorJavaVersion; return this; } public String[] build() { List l = new ArrayList<>(); l.add(javaCmd); // JVM arguments, can be customized if (extraJvmParams != null) { l.addAll(extraJvmParams); } // mandatory JVM parameters // speeds up the start, we don't care much about all potential optimizations done by HotSpot l.add("-client"); if (majorJavaVersion < 13) { // don't do bytecode verification l.add("-noverify"); } // enable support for calling vararg methods in JUEL l.add("-Djavax.el.varArgs=true"); // avoid blocking on crypto l.add("-Djava.security.egd=file:/dev/./urandom"); // avoid some performance issues by preferring IPv4 instead of IPv6 l.add("-Djava.net.preferIPv4Stack=true"); // workaround for JDK-8142508 l.add("-Dsun.zip.disableMemoryMapping=true"); // working directory if (workDir != null) { l.add("-Duser.dir=" + workDir); } // default to UTF-8 l.add("-Dfile.encoding=UTF-8"); // logback configuration looks for logLevel in JVM properties if (logLevel != null) { l.add("-DlogLevel=" + logLevel); } // additional Docker volumes to mount when running containers inside the flow if (extraDockerVolumesFile != null) { // TODO move into RunnerConfiguration l.add("-Dconcord.dockerExtraVolumes=" + extraDockerVolumesFile); } l.add("-Dconcord.exposeDockerDaemon=" + exposeDockerDaemon); // Java 9+ requires additional add-opens for compatibility if (majorJavaVersion >= 9) { l.add("--add-opens"); l.add("java.base/java.lang=ALL-UNNAMED"); l.add("--add-opens"); l.add("java.base/java.util=ALL-UNNAMED"); } // classpath l.add("-cp"); // the runner's runtime is stored somewhere in the agent's libraries String runner = runnerPath.toString(); l.add(runner); // main class if (mainClass == null) { mainClass = "com.walmartlabs.concord.runner.Main"; } l.add(mainClass); l.add(runnerCfgPath.toString()); return l.toArray(new String[0]); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/RunnerJob.java ================================================ package com.walmartlabs.concord.agent.executors.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.guava.GuavaModule; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.walmartlabs.concord.agent.ExecutionException; import com.walmartlabs.concord.agent.JobRequest; import com.walmartlabs.concord.agent.executors.runner.RunnerJobExecutor.RunnerJobExecutorConfiguration; import com.walmartlabs.concord.agent.logging.ProcessLogFactory; import com.walmartlabs.concord.policyengine.PolicyEngine; import com.walmartlabs.concord.policyengine.PolicyEngineRules; import com.walmartlabs.concord.runtime.common.cfg.*; import com.walmartlabs.concord.sdk.Constants; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.UUID; public class RunnerJob { @SuppressWarnings("unchecked") public static RunnerJob from(RunnerJobExecutorConfiguration runnerExecutorCfg, JobRequest jobRequest, ProcessLogFactory processLogFactory) throws ExecutionException, IOException { Map cfg = Collections.emptyMap(); Path payloadDir = jobRequest.getPayloadDir(); Path p = payloadDir.resolve(Constants.Files.CONFIGURATION_FILE_NAME); if (Files.exists(p)) { try (InputStream in = Files.newInputStream(p)) { cfg = new ObjectMapper().readValue(in, Map.class); } catch (IOException e) { throw new ExecutionException("Error while reading process configuration", e); } } RunnerConfiguration runnerCfg = createRunnerConfiguration(runnerExecutorCfg, cfg); RunnerLog log; try { log = new RunnerLog( processLogFactory.createRedirectedLog(jobRequest.getInstanceId(), runnerExecutorCfg.segmentedLogs()), processLogFactory.createRemoteLog(jobRequest.getInstanceId())); } catch (IOException e) { throw new ExecutionException("Error while creating the runner's log: " + e.getMessage(), e); } Path policyFile = payloadDir.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME) .resolve(Constants.Files.POLICY_FILE_NAME); PolicyEngine policyEngine = null; if (Files.exists(policyFile)) { PolicyEngineRules rules = createObjectMapper().readValue(policyFile.toFile(), PolicyEngineRules.class); if (rules != null) { policyEngine = new PolicyEngine(rules); } } return new RunnerJob(jobRequest.getInstanceId(), payloadDir, cfg, runnerCfg, log, policyEngine); } private final UUID instanceId; private final Path payloadDir; private final Map processCfg; private final RunnerConfiguration runnerCfg; private final boolean debugMode; private final RunnerLog log; private final PolicyEngine policyEngine; private RunnerJob(UUID instanceId, Path payloadDir, Map processCfg, RunnerConfiguration runnerCfg, RunnerLog log, PolicyEngine policyEngine) { this.instanceId = instanceId; this.payloadDir = payloadDir; this.processCfg = processCfg; this.runnerCfg = runnerCfg; this.debugMode = debugMode(processCfg); this.log = log; this.policyEngine = policyEngine; } public UUID getInstanceId() { return instanceId; } public Path getPayloadDir() { return payloadDir; } public Map getProcessCfg() { return processCfg; } public RunnerConfiguration getRunnerCfg() { return runnerCfg; } public boolean isDebugMode() { return debugMode; } public RunnerLog getLog() { return log; } public PolicyEngine getPolicyEngine() { return policyEngine; } private static boolean debugMode(Map processCfg) { Object v = processCfg.get(Constants.Request.DEBUG_KEY); if (v instanceof String) { // allows `curl ... -F debug=true` return Boolean.parseBoolean((String) v); } return Boolean.TRUE.equals(v); } public RunnerJob withDependencies(Collection resolvedDeps) { if (resolvedDeps == null) { resolvedDeps = Collections.emptyList(); } RunnerConfiguration cfg = RunnerConfiguration.builder().from(runnerCfg) .dependencies(resolvedDeps) .build(); // TODO replace with immutables? return new RunnerJob(instanceId, payloadDir, processCfg, cfg, log, policyEngine); } @Override public String toString() { return "RunnerJob{" + "instanceId=" + instanceId + ", debugMode=" + debugMode + '}'; } private static RunnerConfiguration createRunnerConfiguration(RunnerJobExecutorConfiguration execCfg, Map processCfg) { ImmutableRunnerConfiguration.Builder b = RunnerConfiguration.builder(); Object v = processCfg.get(Constants.Request.RUNNER_KEY); if (v != null) { RunnerConfiguration src = createObjectMapper().convertValue(v, RunnerConfiguration.class); b = b.from(src); } return b.agentId(execCfg.agentId()) .debug(debugMode(processCfg)) .api(ApiConfiguration.builder() .baseUrl(execCfg.serverApiBaseUrl()) .maxNoHeartbeatInterval(execCfg.maxHeartbeatInterval()) .build()) .docker(DockerConfiguration.builder() .extraVolumes(execCfg.extraDockerVolumes()) .exposeDockerDaemon(execCfg.exposeDockerDaemon()) .build()) .dependencyManager(DependencyManagerConfiguration.builder() .cacheDir(execCfg.dependencyCacheDir().toAbsolutePath().toString()) .build()) .logging(LoggingConfiguration.builder() .sendSystemOutAndErrToSLF4J(true) .segmentedLogs(execCfg.segmentedLogs()) .workDirMasking(execCfg.workDirMasking()) .build()) .build(); } // TODO reuse the same ObjectMapper instance? private static ObjectMapper createObjectMapper() { ObjectMapper om = new ObjectMapper(); om.registerModule(new GuavaModule()); om.registerModule(new Jdk8Module()); return om; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/RunnerJobExecutor.java ================================================ package com.walmartlabs.concord.agent.executors.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.google.common.base.Charsets; import com.google.common.hash.HashCode; import com.google.common.hash.HashFunction; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import com.walmartlabs.concord.agent.ConfiguredJobRequest; import com.walmartlabs.concord.agent.ExecutionException; import com.walmartlabs.concord.agent.JobInstance; import com.walmartlabs.concord.agent.Utils; import com.walmartlabs.concord.agent.executors.JobExecutor; import com.walmartlabs.concord.agent.executors.runner.ProcessPool.ProcessEntry; import com.walmartlabs.concord.agent.logging.ProcessLog; import com.walmartlabs.concord.agent.logging.ProcessLogFactory; import com.walmartlabs.concord.agent.remote.AttachmentsUploader; import com.walmartlabs.concord.common.PathUtils; import com.walmartlabs.concord.common.Posix; import com.walmartlabs.concord.dependencymanager.DependencyEntity; import com.walmartlabs.concord.dependencymanager.DependencyManager; import com.walmartlabs.concord.dependencymanager.ProgressListener; import com.walmartlabs.concord.policyengine.CheckResult; import com.walmartlabs.concord.policyengine.DependencyRule; import com.walmartlabs.concord.policyengine.PolicyEngine; import com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration; import com.walmartlabs.concord.sdk.Constants; import com.walmartlabs.concord.sdk.MapUtils; import org.immutables.value.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.time.Duration; import java.time.Instant; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import java.util.stream.Stream; import static com.walmartlabs.concord.common.DockerProcessBuilder.CONCORD_DOCKER_LOCAL_MODE_KEY; /** * Executes jobs using concord-runner runtime. */ public class RunnerJobExecutor implements JobExecutor { private static final Logger log = LoggerFactory.getLogger(RunnerJobExecutor.class); protected final DependencyManager dependencyManager; private final RunnerJobExecutorConfiguration cfg; private final DefaultDependencies defaultDependencies; private final AttachmentsUploader attachmentsUploader; private final ProcessPool processPool; private final ProcessLogFactory logFactory; private final ExecutorService executor; private final ObjectMapper objectMapper; private final int majorJavaVersion; public RunnerJobExecutor(RunnerJobExecutorConfiguration cfg, DependencyManager dependencyManager, DefaultDependencies defaultDependencies, AttachmentsUploader attachmentsUploader, ProcessPool processPool, ProcessLogFactory processLogFactory, ExecutorService executor) { this.cfg = cfg; this.dependencyManager = dependencyManager; this.defaultDependencies = defaultDependencies; this.attachmentsUploader = attachmentsUploader; this.processPool = processPool; this.logFactory = processLogFactory; this.executor = executor; // sort JSON keys for consistency this.objectMapper = new ObjectMapper() .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); this.majorJavaVersion = getMajorJavaVersion(cfg.javaCmd()); } private static int getMajorJavaVersion(String javaCmd) { try { Process process = new ProcessBuilder(javaCmd, "-version") .start(); int exitCode = process.waitFor(); if (exitCode != 0) { throw new RuntimeException("`java -version` exited with " + exitCode); } String version; try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { version = reader.readLine(); } int start = version.indexOf("\""); int end = version.indexOf(".", start + 1); if (start < 0 || start + 1 >= version.length() || end < 0) { throw new RuntimeException("Unknown version string: " + version); } int major; try { major = Integer.parseInt(version.substring(start + 1, end)); } catch (NumberFormatException e) { throw new RuntimeException("Unknown version string: " + version); } return major; } catch (Exception e) { throw new RuntimeException("Can't determine the target Java runtime version", e); } } @Override public JobInstance exec(ConfiguredJobRequest jobRequest) throws Exception { RunnerJob job = RunnerJob.from(cfg, jobRequest, logFactory); return exec(job); } private JobInstance exec(RunnerJob job) throws Exception { // prepare and start a new JVM of use a pre-forked one ProcessEntry pe; try { // resolve and download the dependencies Collection resolvedDeps; if (cfg.dependencyResolveTimeout() != null) { Duration timeout = Objects.requireNonNull(cfg.dependencyResolveTimeout()); RunnerJob finalJob = job; Instant startedAt = Instant.now(); Future> future = executor.submit(() -> resolveDeps(finalJob)); try { resolvedDeps = future.get(timeout.toMillis(), TimeUnit.MILLISECONDS); } catch (InterruptedException | TimeoutException e) { future.cancel(true); Duration elapsed = Duration.between(startedAt, Instant.now()); throw new RuntimeException("Timeout resolving dependencies (timeout=%sms, elapsed=%sms)".formatted(timeout.toMillis(), elapsed.toMillis())); } } else { resolvedDeps = resolveDeps(job); } job = job.withDependencies(resolvedDeps); pe = buildProcessEntry(job); } catch (Throwable e) { log.warn("exec ['{}'] -> process error: {}", job.getInstanceId(), e.getMessage(), e); job.getLog().error("Process startup error: {}", e.getMessage(), e); cleanup(job); throw e; } // continue the execution in a separate thread to make the process cancellable RunnerJob _job = job; Future f = executor.submit(() -> { boolean uploadAttachmentsOnError = true; try { exec(_job, pe); uploadAttachmentsOnError = false; uploadAttachments(_job.getInstanceId(), pe); } catch (Throwable t) { if (uploadAttachmentsOnError) { try { uploadAttachments(_job.getInstanceId(), pe); } catch (Exception e) { // ignore } } throw new RuntimeException(t); } finally { persistWorkDir(_job.getInstanceId(), pe.getWorkDir()); cleanup(_job.getInstanceId(), pe); cleanup(_job); } }); // return a handle that can be used to cancel the process or wait for its completion return new JobInstanceImpl(f, pe.getProcess(), cfg.cleanRunnerDescendants()); } private void persistWorkDir(UUID instanceId, Path src) { Path persistentWorkDir = cfg.persistentWorkDir(); if (persistentWorkDir == null) { return; } Path dst = persistentWorkDir.resolve(instanceId.toString()); try { if (!Files.exists(dst)) { Files.createDirectories(dst); } log.info("exec ['{}'] -> persisting the payload directory into {}...", instanceId, dst); PathUtils.copy(src, dst); // persistentWorkDir is mostly useful when the Agent is running in a container // typically it is running as PID 456 - all files created by the process // are created using PID 456 and won't be readable by the host user // therefore, we need to make all files readable by all users // and that's why runner.persistentWorkDir shouldn't be used in prod try(Stream walk = Files.walk(dst)) { walk.forEach(f -> { try { if (Files.isDirectory(f)) { Files.setPosixFilePermissions(f, Posix.posix(0755)); } else if (Files.isRegularFile(f)) { Files.setPosixFilePermissions(f, Posix.posix(0644)); } } catch (IOException e) { log.warn("persistWorkDir -> can't update permissions for {}: {}", f, e.getMessage()); } }); } } catch (IOException e) { log.warn("persistWorkDir -> failed to copy {} into {}: {}", src, dst, e.getMessage()); } } private void uploadAttachments(UUID instanceId, ProcessEntry pe) { try { attachmentsUploader.upload(instanceId, pe.getWorkDir()); } catch (Exception e) { log.error("uploadAttachments ['{}'] -> error: {}", instanceId, e.getMessage()); throw new RuntimeException("Error while uploading attachments: " + e.getMessage()); } } private void cleanup(UUID instanceId, ProcessEntry pe) { Path workDir = pe.getWorkDir(); try { log.info("exec ['{}'] -> removing the working directory: {}", instanceId, workDir); PathUtils.deleteRecursively(workDir); } catch (IOException e) { log.warn("exec ['{}'] -> can't remove the working directory: {}", instanceId, e.getMessage()); } } protected ProcessEntry buildProcessEntry(RunnerJob job) throws Exception { List jvmParams = getJvmParams(job.getPayloadDir(), job.getProcessCfg()); String[] cmd = createCmd(job, jvmParams); boolean prefork = canUsePrefork(job); if (prefork) { log.info("start ['{}'] -> using a pre-forked instances", job.getInstanceId()); return fork(job, cmd); } else { return startOneTime(job, cmd); } } private void exec(RunnerJob job, ProcessEntry pe) throws Exception { // the actual OS process Process proc = pe.getProcess(); UUID instanceId = job.getInstanceId(); ProcessLog processLog = job.getLog(); // start the log's maintenance thread (e.g. streaming to the server) LogStream logStream = new LogStream(job, proc); logStream.start(); try { // save the process' log processLog.log(proc.getInputStream()); // wait for the process to finish int code; try { code = proc.waitFor(); } catch (Exception e) { // wait for the log to finish logStream.waitForCompletion(); handleError(job, proc, e.getMessage()); throw new ExecutionException("Error while executing a job: " + e.getMessage()); } // wait for the log to finish logStream.waitForCompletion(); if (code != 0) { log.warn("exec ['{}'] -> finished with {}", instanceId, code); handleError(job, proc, "Process exit code: " + code); throw new ExecutionException("Error while executing a job, process exit code: " + code); } log.info("exec ['{}'] -> finished with {}", instanceId, code); processLog.info("Process finished with: {}", code); } finally { // wait for the log to finish logStream.waitForCompletion(); } } private void handleError(RunnerJob job, Process proc, String error) { job.getLog().error(error); if (Utils.kill(proc)) { log.warn("handleError ['{}'] -> killed by agent", job.getInstanceId()); } } private Collection resolveDeps(RunnerJob job) throws Exception { job.getLog().info("Resolving process dependencies..."); long t1 = System.currentTimeMillis(); // combine the default dependencies and the process' dependencies Collection uris = Stream.concat(defaultDependencies.getDependencies().stream(), JobDependencies.get(job).stream()) .collect(Collectors.toList()); uris = rewriteDependencies(job, uris); Collection deps = dependencyManager.resolve(uris, new ProgressListener() { private final List errors = Collections.synchronizedList(new ArrayList<>()); @Override public void onRetry(int retryCount, int maxRetry, long interval, String cause) { synchronized (errors) { for (String error : errors) { job.getLog().warn(error); } } job.getLog().warn("Error while downloading dependencies: {}", cause); job.getLog().info("Retrying in {}ms", interval); } @Override public void onTransferFailed(String error) { if (job.isDebugMode()) { job.getLog().warn(error); } else { errors.add(error); } } }); // check the resolved dependencies against the current policy validateDependencies(job, deps); // sort dependencies to maintain consistency in runner configurations Collection paths = deps.stream() .map(DependencyEntity::getPath) .map(p -> p.toAbsolutePath().toString()) .sorted() .collect(Collectors.toList()); long t2 = System.currentTimeMillis(); if (job.isDebugMode()) { job.getLog().info("Dependency resolution took {}ms", (t2 - t1)); logDependencies(job, paths); } else { logDependencies(job, uris); } return paths; } private Collection rewriteDependencies(RunnerJob job, Collection uris) { PolicyEngine policyEngine = job.getPolicyEngine(); if (policyEngine == null) { return uris; } return policyEngine.getDependencyRewritePolicy() .rewrite(uris, (msg, from, to) -> { job.getLog().info("Updating dependency from '{}' to '{}'", from, to); if (msg != null) { job.getLog().warn(msg); } }); } private void validateDependencies(RunnerJob job, Collection resolvedDepEntities) throws ExecutionException { PolicyEngine policyEngine = job.getPolicyEngine(); if (policyEngine == null) { return; } ProcessLog processLog = job.getLog(); processLog.info("Checking the dependency policy..."); CheckResult result = policyEngine.getDependencyPolicy().check(resolvedDepEntities); result.getWarn().forEach(d -> processLog.warn("Potentially restricted artifact '{}' (dependency policy: {})", d.getEntity(), d.getRule().msg())); result.getDeny().forEach(d -> processLog.warn("Artifact '{}' is forbidden by the dependency policy {}", d.getEntity(), d.getRule().msg())); if (!result.getDeny().isEmpty()) { throw new ExecutionException("Found restricted dependencies"); } } private void logDependencies(RunnerJob job, Collection deps) { if (deps == null || deps.isEmpty()) { job.getLog().info("No external dependencies."); return; } List l = deps.stream() .map(Object::toString) .collect(Collectors.toList()); StringBuilder b = new StringBuilder(); for (String s : l) { b.append("\n\t").append(s); } job.getLog().info("Dependencies: {}", b); } private String[] createCmd(RunnerJob job, List jvmParams) throws IOException { Path runnerCfgFile = storeRunnerCfg(cfg.runnerCfgDir(), job.getRunnerCfg()); return new RunnerCommandBuilder() .javaCmd(cfg.javaCmd()) .logLevel(getLogLevel(job)) .extraDockerVolumesFile(createExtraDockerVolumesFile(job)) .exposeDockerDaemon(cfg.exposeDockerDaemon()) .runnerPath(cfg.runnerPath().toAbsolutePath()) .runnerCfgPath(runnerCfgFile.toAbsolutePath()) .mainClass(cfg.runnerMainClass()) .jvmParams(jvmParams) .majorJavaVersion(this.majorJavaVersion) .build(); } private ProcessEntry fork(RunnerJob job, String[] cmd) throws ExecutionException, IOException { long t1 = System.currentTimeMillis(); HashCode hc = hash(cmd); // take a "pre-forked" JVM from the pool or start a new one ProcessEntry entry = processPool.take(hc, () -> { // can't use workDirBase, "preforks" start before they receive their process payload // create a new temporary directory Path workDir = PathUtils.createTempDir("workDir"); return start(workDir, cmd); }); // the job's payload directory containing all files from the process' state snapshot and/or the repository's data Path src = job.getPayloadDir(); // the process' workDir Path dst = entry.getWorkDir(); // TODO use move PathUtils.copy(src, dst); writeInstanceId(job.getInstanceId(), dst); long t2 = System.currentTimeMillis(); if (job.isDebugMode()) { job.getLog().info("Forking a VM took {}ms", (t2 - t1)); } return entry; } protected ProcessEntry startOneTime(RunnerJob job, String[] cmd) throws IOException { // create the parent directory of the process' ${workDir} Path workDir = cfg.workDirBase().resolve(job.getInstanceId().toString()); if (!Files.exists(workDir)) { Files.createDirectories(workDir); } // the job's payload directory, contains all files from the state snapshot including imports Path src = job.getPayloadDir(); try { Files.move(src, workDir, StandardCopyOption.ATOMIC_MOVE); } catch (AtomicMoveNotSupportedException e) { log.error("startOneTime ['{}'] -> unable to move {} to {} atomically", job.getInstanceId(), src, workDir); throw e; } writeInstanceId(job.getInstanceId(), workDir); return start(workDir, cmd); } private ProcessEntry start(Path workDir, String[] cmd) throws IOException { log.info("start -> {}, {}", workDir, String.join(" ", cmd)); ProcessBuilder b = new ProcessBuilder() .directory(workDir.toFile()) .command(cmd) .redirectErrorStream(true); // TODO constants Map env = b.environment(); env.put(PathUtils.TMP_DIR_KEY, PathUtils.TMP_DIR.toAbsolutePath().toString()); env.put("_CONCORD_ATTACHMENTS_DIR", workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME) .toAbsolutePath().toString()); // pass through the docker mode String dockerMode = System.getenv(CONCORD_DOCKER_LOCAL_MODE_KEY); if (dockerMode != null) { log.debug("start -> using Docker mode: {}", dockerMode); env.put(CONCORD_DOCKER_LOCAL_MODE_KEY, dockerMode); } Process p = b.start(); return new ProcessEntry(p, workDir); } protected Path storeRunnerCfg(Path baseDir, RunnerConfiguration runnerCfg) throws IOException { if (!Files.exists(baseDir)) { Files.createDirectories(baseDir); } byte[] data = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(runnerCfg); HashCode hc = Hashing.sha256().hashBytes(data); Path cfgFile = baseDir.resolve(hc + ".json"); if (!Files.exists(cfgFile)) { Files.write(cfgFile, data); } return cfgFile; } @Override public String toString() { return "RunnerJobExecutor"; } private Path createExtraDockerVolumesFile(RunnerJob job) throws IOException { List l = cfg.extraDockerVolumes(); if (l.isEmpty()) { return null; } Path workDir = job.getPayloadDir(); Path p = workDir.resolve(".extraDockerVolumes"); Files.write(p, l, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); return workDir.relativize(p); } @SuppressWarnings("unchecked") private List getJvmParams(Path workDir, Map processCfg) { // the _agent.json file takes precedence Path p = workDir.resolve(Constants.Agent.AGENT_PARAMS_FILE_NAME); if (Files.exists(p)) { try (InputStream in = Files.newInputStream(p)) { Map m = objectMapper.readValue(in, Map.class); List l = MapUtils.getList(m, Constants.Agent.JVM_ARGS_KEY, null); if (l != null) { return l; } } catch (IOException e) { throw new RuntimeException(e); } } // check the `configuration.requirements.jvm` next List l = getJvmArgsFromConfig(processCfg); if (l != null) { return l; } // fallback to the default parameters return cfg.jvmParams(); } private boolean canUsePrefork(RunnerJob job) { if (!cfg.preforkEnabled()) { return false; } Path workDir = job.getPayloadDir(); if (Files.exists(workDir.resolve(Constants.Files.LIBRARIES_DIR_NAME))) { // the process supplied its own libraries, can't use preforking return false; } // the process supplied its own JVM parameters in concord.yml, can't use preforking List jvmExtraArgs = getJvmArgsFromConfig(job.getProcessCfg()); if (jvmExtraArgs != null) { return false; } // the process supplied its own JVM parameters in _agent.json, can't use preforking return !Files.exists(workDir.resolve(Constants.Agent.AGENT_PARAMS_FILE_NAME)); } private static List getJvmArgsFromConfig(Map processCfg) { Map requirements = MapUtils.get(processCfg, Constants.Request.REQUIREMENTS, null); if (requirements == null) { return null; } // TODO constants? Map jvm = MapUtils.get(requirements, "jvm", null); if (jvm == null) { return null; } // TODO constants? List extraArgs = MapUtils.getList(jvm, "extraArgs", null); if (extraArgs == null || extraArgs.isEmpty()) { return null; } return extraArgs; } private static String getLogLevel(RunnerJob job) { RunnerConfiguration cfg = job.getRunnerCfg(); if (cfg == null) { return null; } String logLevel = cfg.logLevel(); if (logLevel == null) { return null; } return logLevel.toUpperCase(); } private static HashCode hash(String[] as) { HashFunction f = Hashing.sha256(); Hasher h = f.newHasher(); for (String s : as) { h.putString(s, Charsets.UTF_8); } return h.hash(); } private static void cleanup(RunnerJob job) { try { job.getLog().delete(); } catch (Exception e) { log.warn("cleanup [{}] -> error while cleaning up the process logs: {}", job.getInstanceId(), e.getMessage()); } } /** * A tiny wrapper to simplify working with the log streaming. */ private class LogStream { private final RunnerJob job; private final Process proc; private Future f; private transient boolean doStop = false; private LogStream(RunnerJob job, Process proc) { this.job = job; this.proc = proc; } /** * Starts the log streaming in a separate thread. */ public void start() { RunnerLog processLog = job.getLog(); f = executor.submit(() -> { try { processLog.run(() -> doStop); } catch (Exception e) { handleError(job, proc, e.getMessage()); } }); } /** * Waits for the log stream to finish and removes the log file. */ public void waitForCompletion() { this.doStop = true; try { f.get(1, TimeUnit.MINUTES); } catch (Exception e) { log.warn("waitForCompletion -> timeout waiting for the log stream of {}", job.getInstanceId()); } } } private static void writeInstanceId(UUID instanceId, Path dst) throws IOException { Path idPath = dst.resolve(Constants.Files.INSTANCE_ID_FILE_NAME); Files.write(idPath, instanceId.toString().getBytes(), StandardOpenOption.CREATE, StandardOpenOption.SYNC); } @Value.Immutable @Value.Style(jdkOnly = true) public interface RunnerJobExecutorConfiguration { String agentId(); String serverApiBaseUrl(); String javaCmd(); List jvmParams(); Path dependencyListDir(); Path dependencyCacheDir(); @Nullable Duration dependencyResolveTimeout(); Path workDirBase(); Path runnerPath(); Path runnerCfgDir(); String runnerMainClass(); boolean runnerSecurityManagerEnabled(); boolean segmentedLogs(); boolean workDirMasking(); @Value.Default default List extraDockerVolumes() { return Collections.emptyList(); } @Value.Default default Boolean exposeDockerDaemon() { return true; } long maxHeartbeatInterval(); @Nullable Path persistentWorkDir(); boolean preforkEnabled(); boolean cleanRunnerDescendants(); static ImmutableRunnerJobExecutorConfiguration.Builder builder() { return ImmutableRunnerJobExecutorConfiguration.builder(); } } private static class JobInstanceImpl implements JobInstance { private final Future f; private final Process proc; private final boolean cleanRunnerDescendants; private transient boolean cancelled = false; private JobInstanceImpl(Future f, Process proc, boolean cleanRunnerDescendants) { this.f = f; this.proc = proc; this.cleanRunnerDescendants = cleanRunnerDescendants; } @Override public void waitForCompletion() throws Exception { f.get(); } @Override public void cancel() { if (f.isCancelled() || f.isDone()) { return; } cancelled = true; Utils.kill(proc, cleanRunnerDescendants); } @Override public boolean isCancelled() { return cancelled; } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/executors/runner/RunnerLog.java ================================================ package com.walmartlabs.concord.agent.executors.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.logging.ProcessLog; import com.walmartlabs.concord.agent.logging.RedirectedProcessLog; import com.walmartlabs.concord.agent.logging.RemoteProcessLog; import java.io.IOException; import java.io.InputStream; import java.util.function.Supplier; public class RunnerLog implements ProcessLog { private final RedirectedProcessLog redirectedLog; private final RemoteProcessLog remoteLog; public RunnerLog(RedirectedProcessLog redirectedLog, RemoteProcessLog remoteLog) { this.redirectedLog = redirectedLog; this.remoteLog = remoteLog; } public void run(Supplier stopCondition) throws Exception { redirectedLog.run(stopCondition); } @Override public void delete() { redirectedLog.delete(); remoteLog.delete(); } @Override public void log(InputStream src) throws IOException { redirectedLog.log(src); } @Override public void info(String log, Object... args) { remoteLog.info(log, args); } @Override public void warn(String log, Object... args) { remoteLog.warn(log, args); } @Override public void error(String log, Object... args) { remoteLog.error(log, args); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/guice/AgentDependencyManagerConfigurationProvider.java ================================================ package com.walmartlabs.concord.agent.guice; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.cfg.AgentConfiguration; import com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; @Singleton public class AgentDependencyManagerConfigurationProvider implements Provider { private final AgentConfiguration cfg; @Inject public AgentDependencyManagerConfigurationProvider(AgentConfiguration cfg) { this.cfg = cfg; } @Override public DependencyManagerConfiguration get() { return DependencyManagerConfiguration.builder() .cacheDir(cfg.getDependencyCacheDir()) .strictRepositories(cfg.dependencyStrictRepositories()) .exclusions(cfg.dependencyExclusions()) .explicitlyResolveV1Client(cfg.isExplicitlyResolveV1Client()) .offlineMode(cfg.isMavenOfflineMode()) .build(); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/guice/AgentImportManager.java ================================================ package com.walmartlabs.concord.agent.guice; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.imports.ImportManager; import com.walmartlabs.concord.imports.Imports; import com.walmartlabs.concord.imports.ImportsListener; import com.walmartlabs.concord.repository.Snapshot; import java.nio.file.Path; import java.util.List; /** * A wrapper type to avoid clashes with the Server's instance of a {@link ImportManager}. * TODO replace with a common Guice module */ public class AgentImportManager { private final ImportManager delegate; public AgentImportManager(ImportManager delegate) { this.delegate = delegate; } public List process(Imports imports, Path dest) throws Exception { return delegate.process(imports, dest, ImportsListener.NOP_LISTENER); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/guice/AgentImportManagerProvider.java ================================================ package com.walmartlabs.concord.agent.guice; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.RepositoryManager; import com.walmartlabs.concord.agent.cfg.ImportConfiguration; import com.walmartlabs.concord.dependencymanager.DependencyManager; import com.walmartlabs.concord.imports.ImportManagerFactory; import com.walmartlabs.concord.imports.RepositoryExporter; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; import java.nio.file.Path; import java.util.Objects; @Singleton public class AgentImportManagerProvider implements Provider { private final ImportManagerFactory factory; @Inject public AgentImportManagerProvider(ImportConfiguration cfg, RepositoryManager repositoryManager, DependencyManager dependencyManager) { RepositoryExporter exporter = (entry, workDir) -> { Path dst = workDir; String entryDest = entry.dest(); if (entry.dest() != null) { dst = dst.resolve(Objects.requireNonNull(entryDest)); } repositoryManager.export(entry.url(), entry.version(), null, entry.path(), dst, entry.secret(), entry.exclude()); return null; }; this.factory = new ImportManagerFactory(dependencyManager, exporter, cfg.getDisabledProcessors()); } @Override public AgentImportManager get() { return new AgentImportManager(factory.create()); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/guice/WorkerModule.java ================================================ package com.walmartlabs.concord.agent.guice; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.Singleton; import com.google.inject.multibindings.Multibinder; import com.walmartlabs.concord.client2.*; import com.walmartlabs.concord.agent.DefaultStateFetcher; import com.walmartlabs.concord.agent.StateFetcher; import com.walmartlabs.concord.agent.logging.*; import com.walmartlabs.concord.agent.remote.ApiClientFactory; import com.walmartlabs.concord.agent.remote.ProcessStatusUpdater; import com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.UUID; public class WorkerModule extends AbstractModule { private static final Logger log = LoggerFactory.getLogger(WorkerModule.class); private static final String REDIRECT_PROCESS_LOGS_TO_STDOUT_KEY = "REDIRECT_PROCESS_LOGS_TO_STDOUT"; private final String agentId; private final UUID instanceId; private final String sessionToken; public WorkerModule(String agentId, UUID instanceId, String sessionToken) { this.agentId = agentId; this.instanceId = instanceId; this.sessionToken = sessionToken; } @Provides @Singleton ApiClient getApiClient(ApiClientFactory factory) throws IOException { return factory.create(sessionToken); } @Provides @Singleton ProcessLog getProcessLog(ApiClient apiClient) { return new RemoteProcessLog(instanceId, new RemoteLogAppender(apiClient)); } @Provides @Singleton SecretClient getSecretClient(ApiClient apiClient) { return new SecretClient(apiClient); } @Provides @Singleton ProcessApi getProcessApi(ApiClient apiClient) { return new ProcessApi(apiClient); } @Provides @Singleton ProcessStatusUpdater getProcessStatusUpdater(ProcessApi processApi) { return new ProcessStatusUpdater(agentId, processApi); } @Override protected void configure() { bind(StateFetcher.class).to(DefaultStateFetcher.class); Multibinder logAppenders = Multibinder.newSetBinder(binder(), LogAppender.class); logAppenders.addBinding().to(RemoteLogAppender.class); if (Boolean.parseBoolean(System.getenv(REDIRECT_PROCESS_LOGS_TO_STDOUT_KEY))) { log.info("Redirecting process logs into the agent's stdout..."); logAppenders.addBinding().to(StdOutLogAppender.class); } bind(LogAppender.class).to(CombinedLogAppender.class); bind(AgentImportManager.class).toProvider(AgentImportManagerProvider.class); bind(DependencyManagerConfiguration.class).toProvider(AgentDependencyManagerConfigurationProvider.class); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/AbstractProcessLog.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.LogUtils; public abstract class AbstractProcessLog implements ProcessLog { @Override public void delete() { // do nothing } @Override public void info(String log, Object... args) { log(LogUtils.formatMessage(LogUtils.LogLevel.INFO, log, args)); } @Override public void warn(String log, Object... args) { log(LogUtils.formatMessage(LogUtils.LogLevel.WARN, log, args)); } @Override public void error(String log, Object... args) { log(LogUtils.formatMessage(LogUtils.LogLevel.ERROR, log, args)); } protected abstract void log(String message); } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/CombinedLogAppender.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2021 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import javax.inject.Inject; import java.util.Set; import java.util.UUID; public class CombinedLogAppender implements LogAppender { private final Set appenders; @Inject public CombinedLogAppender(Set appenders) { this.appenders = appenders; } @Override public void appendLog(UUID instanceId, byte[] ab) { appenders.forEach(a -> a.appendLog(instanceId, ab)); } @Override public boolean appendLog(UUID instanceId, long segmentId, byte[] ab) { boolean result = true; for (LogAppender a : appenders) { boolean done = a.appendLog(instanceId, segmentId, ab); result = result && done; } return result; } @Override public boolean updateSegment(UUID instanceId, long segmentId, LogSegmentStats stats) { boolean result = true; for (LogAppender a : appenders) { boolean done = a.updateSegment(instanceId, segmentId, stats); result = result && done; } return result; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/LocalProcessLog.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; /** * Local log file. Typically used as a temporary buffer to store process logs * before sending them to the server. */ public class LocalProcessLog extends AbstractProcessLog { private static final Logger log = LoggerFactory.getLogger(LocalProcessLog.class); private final Path baseDir; public LocalProcessLog(Path baseDir) throws IOException { this.baseDir = baseDir; Files.createFile(logFile()); } @Override public void delete() { Path p = logFile(); if (Files.exists(p)) { try { Files.delete(p); } catch (IOException e) { log.warn("delete -> error while removing a log file: {}", p); } } } @Override public void log(InputStream src) throws IOException { Path f = logFile(); try (OutputStream dst = Files.newOutputStream(f, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { src.transferTo(dst); } } @Override protected void log(String message) { Path f = logFile(); try (OutputStream out = Files.newOutputStream(f, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { out.write(message.getBytes()); out.flush(); } catch (IOException e) { throw new RuntimeException("Error writing to a log file: " + f, e); } } public Path logFile() { return baseDir.resolve("system" + ".log"); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/LogAppender.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.UUID; public interface LogAppender { void appendLog(UUID instanceId, byte[] ab); boolean appendLog(UUID instanceId, long segmentId, byte[] ab); boolean updateSegment(UUID instanceId, long segmentId, LogSegmentStats stats); } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/LogSegmentStats.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.annotation.JsonInclude; import com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus; import javax.annotation.Nullable; @JsonInclude(JsonInclude.Include.NON_EMPTY) public record LogSegmentStats(@Nullable LogSegmentStatus status, @Nullable Integer errors, @Nullable Integer warnings) { } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/ProcessLog.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.IOException; import java.io.InputStream; public interface ProcessLog { /** * Removed the associated resources. */ void delete(); /** * Copies the specified stream into the log. */ void log(InputStream src) throws IOException; void info(String log, Object... args); void warn(String log, Object... args); void error(String log, Object... args); } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/ProcessLogFactory.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.cfg.AgentConfiguration; import javax.inject.Inject; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.UUID; import java.util.function.Consumer; public class ProcessLogFactory { private final Path logDir; private final long logStreamMaxDelay; private final LogAppender logAppender; @Inject public ProcessLogFactory(AgentConfiguration cfg, LogAppender logAppender) { this.logDir = cfg.getLogDir(); this.logStreamMaxDelay = cfg.getLogMaxDelay(); this.logAppender = logAppender; } public RedirectedProcessLog createRedirectedLog(UUID instanceId, boolean segmented) throws IOException { Path dst = logDir.resolve(instanceId.toString()); if (Files.notExists(dst)) { Files.createDirectories(dst); } Consumer logConsumer; if (segmented) { logConsumer = new SegmentedLogsConsumer(instanceId, logAppender); } else { logConsumer = chunk -> { byte[] ab = new byte[chunk.len()]; System.arraycopy(chunk.bytes(), 0, ab, 0, chunk.len()); logAppender.appendLog(instanceId, ab); }; } return new RedirectedProcessLog(dst, logStreamMaxDelay, logConsumer); } public RemoteProcessLog createRemoteLog(UUID instanceId) { return new RemoteProcessLog(instanceId, logAppender); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/RedirectedProcessLog.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.function.Consumer; import java.util.function.Supplier; /** * Log that uses a local file as a buffer before sending the data into the specified {@link LogAppender}. * Typically, {@link #run(Supplier)} method should be executed in a separate thread. */ public class RedirectedProcessLog implements ProcessLog { protected final long logSteamMaxDelay; private final LocalProcessLog localLog; private final Consumer consumer; public RedirectedProcessLog(Path baseDir, long logSteamMaxDelay, Consumer consumer) throws IOException { this.localLog = new LocalProcessLog(baseDir); this.logSteamMaxDelay = logSteamMaxDelay; this.consumer = consumer; } public void run(Supplier stopCondition) throws Exception { streamLog(localLog.logFile(), stopCondition, logSteamMaxDelay, consumer); } @Override public void delete() { this.localLog.delete(); } @Override public void log(InputStream src) throws IOException { this.localLog.log(src); } @Override public void info(String log, Object... args) { this.localLog.info(log, args); } @Override public void warn(String log, Object... args) { this.localLog.warn(log, args); } @Override public void error(String log, Object... args) { this.localLog.error(log, args); } private static void streamLog(Path p, Supplier stopCondition, long maxDelay, Consumer sink) throws IOException { long total = 0; byte[] ab = new byte[8192]; try (InputStream in = Files.newInputStream(p, StandardOpenOption.READ)) { while (true) { int read = in.read(ab, 0, ab.length); if (read > 0) { sink.accept(new Chunk(ab, read)); total += read; } if (read < ab.length) { if (stopCondition.get() && total >= Files.size(p)) { // the log and the job are finished break; } // job is still running, wait for more data try { Thread.sleep(maxDelay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } } } public static class Chunk { private final byte[] ab; private final int len; protected Chunk(byte[] ab, int len) { // NOSONAR this.ab = ab; this.len = len; } public byte[] bytes() { return ab; } public int len() { return len; } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/RemoteLogAppender.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.AgentConstants; import com.walmartlabs.concord.client2.*; import com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.ByteArrayInputStream; import java.util.UUID; public class RemoteLogAppender implements LogAppender { private static final Logger log = LoggerFactory.getLogger(RemoteLogAppender.class); private final ProcessApi processApi; private final ProcessLogV2Api processLogV2Api; @Inject public RemoteLogAppender(ApiClient apiClient) { this.processApi = new ProcessApi(apiClient); this.processLogV2Api = new ProcessLogV2Api(apiClient); } @Override public void appendLog(UUID instanceId, byte[] ab) { try { ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> { processApi.appendProcessLog(instanceId, new ByteArrayInputStream(ab)); return null; }); } catch (ApiException e) { // TODO handle errors log.warn("appendLog ['{}'] -> error: {}", instanceId, e.getMessage()); } } @Override public boolean appendLog(UUID instanceId, long segmentId, byte[] ab) { try { ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> { processLogV2Api.appendProcessLogSegment(instanceId, segmentId, new ByteArrayInputStream(ab)); return null; }); return true; } catch (ApiException e) { log.warn("appendLog ['{}'] -> error: {}", instanceId, e.getMessage()); return e.getCode() >= 400 && e.getCode() < 500; } } @Override public boolean updateSegment(UUID instanceId, long segmentId, LogSegmentStats stats) { LogSegmentUpdateRequest request = new LogSegmentUpdateRequest() .status(convertStatus(stats.status())) .warnings(stats.warnings()) .errors(stats.errors()); try { ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> processLogV2Api.updateProcessLogSegment(instanceId, segmentId, request)); return true; } catch (Exception e) { log.warn("updateSegment ['{}', '{}', '{}'] -> error: {}", instanceId, segmentId, stats, e.getMessage()); } return false; } private static LogSegmentUpdateRequest.StatusEnum convertStatus(LogSegmentStatus status) { if (status == null) { return null; } return switch (status) { case ERROR -> LogSegmentUpdateRequest.StatusEnum.FAILED; case OK -> LogSegmentUpdateRequest.StatusEnum.OK; case RUNNING -> LogSegmentUpdateRequest.StatusEnum.RUNNING; case SUSPENDED -> LogSegmentUpdateRequest.StatusEnum.SUSPENDED; }; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/RemoteProcessLog.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.InputStream; import java.util.UUID; /** * Simple log implementation that sends all data into a {@link LogAppender} directly. */ public class RemoteProcessLog extends AbstractProcessLog { private final UUID instanceId; private final LogAppender appender; public RemoteProcessLog(UUID instanceId, LogAppender appender) { this.instanceId = instanceId; this.appender = appender; } @Override public void log(InputStream src) { throw new IllegalStateException("Not supported"); } @Override protected void log(String message) { appender.appendLog(instanceId, message.getBytes()); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/SegmentHeaderParser.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2021 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.common.logger.ImmutableLogSegmentHeader; import com.walmartlabs.concord.runtime.common.logger.LogSegmentDeserializer; import com.walmartlabs.concord.runtime.common.logger.LogSegmentHeader; import org.immutables.value.Value; import java.nio.ByteBuffer; import java.util.List; public class SegmentHeaderParser { private static final int MAX_FIELD_BYTES = String.valueOf(Long.MAX_VALUE).getBytes().length; // msgLength|segmentId|status|warnings|errors|msg public static int parse(byte[] ab, List segments, List invalidSegments) { Field field = Field.MSG_LENGTH; StringBuilder fieldData = new StringBuilder(); int mark = -1; ImmutableLogSegmentHeader.Builder headerBuilder = LogSegmentHeader.builder(); boolean continueParse = true; State state = State.FIND_HEADER; ByteBuffer bb = ByteBuffer.wrap(ab); while (continueParse) { switch (state) { case FIND_HEADER: { if (bb.remaining() <= 0) { continueParse = false; break; } char ch = (char) bb.get(); if (ch == '|') { if (mark != -1) { invalidSegments.add(new Position(mark, bb.position() - 1)); } mark = bb.position() - 1; state = State.FIELD_DATA; } else { if (mark == -1) { mark = bb.position() - 1; } } break; } case FIELD_DATA: { if (bb.remaining() <= 0) { continueParse = false; break; } char ch = (char)bb.get(); if (ch == '|') { state = State.END_FIELD; break; } if (fieldData.length() > MAX_FIELD_BYTES || !Character.isDigit(ch)) { // reset fieldData.setLength(0); field = Field.MSG_LENGTH; state = State.FIND_HEADER; break; } fieldData.append(ch); break; } case END_FIELD: { String fieldValue = fieldData.toString(); if (fieldData.isEmpty()) { // reset field = Field.MSG_LENGTH; state = State.FIND_HEADER; bb.position(bb.position() - 1); break; } field.process(fieldValue, headerBuilder); field = field.next(); if (field == null) { LogSegmentHeader h = headerBuilder.build(); segments.add(new Segment(h, bb.position())); int actualLength = Math.min(h.length(), bb.remaining()); bb.position(bb.position() + actualLength); // reset field = Field.MSG_LENGTH; mark = -1; state = State.FIND_HEADER; } else { state = State.FIELD_DATA; } fieldData.setLength(0); break; } } } int result; if (mark != -1) { if (state == State.FIND_HEADER) { invalidSegments.add(new Position(mark, bb.position())); result = bb.position(); } else { result = mark; } } else { result = bb.position(); } return result; } public record Position(int start, int end) { } public record Segment(LogSegmentHeader header, int msgStart) { } enum State { FIND_HEADER, FIELD_DATA, END_FIELD } enum Field { MSG_LENGTH { @Override public Field next() { return SEGMENT_ID; } @Override public void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder) { headerBuilder.length(Integer.parseInt(fieldValue)); } }, SEGMENT_ID { @Override public Field next() { return STATUS; } @Override public void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder) { headerBuilder.segmentId(Long.parseLong(fieldValue)); } }, STATUS { @Override public Field next() { return WARNINGS; } @Override public void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder) { headerBuilder.status(LogSegmentDeserializer.deserializeStatus(fieldValue)); } }, WARNINGS { @Override public Field next() { return ERRORS; } @Override public void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder) { headerBuilder.warnCount(Integer.parseInt(fieldValue)); } }, ERRORS { @Override public Field next() { return null; } @Override public void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder) { headerBuilder.errorCount(Integer.parseInt(fieldValue)); } }; public abstract Field next(); public abstract void process(String fieldValue, ImmutableLogSegmentHeader.Builder headerBuilder); } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/SegmentedLogsConsumer.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2021 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.common.logger.LogSegmentHeader; import com.walmartlabs.concord.runtime.common.logger.LogSegmentSerializer; import com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus; import java.util.*; import java.util.function.Consumer; import static com.walmartlabs.concord.agent.logging.SegmentHeaderParser.Position; import static com.walmartlabs.concord.agent.logging.SegmentHeaderParser.Segment; public class SegmentedLogsConsumer implements Consumer { private static final byte[] EMPTY = new byte[0]; private final UUID instanceId; private final LogAppender logAppender; private byte[] unparsed = EMPTY; public SegmentedLogsConsumer(UUID instanceId, LogAppender logAppender) { this.instanceId = instanceId; this.logAppender = logAppender; } @Override public void accept(RedirectedProcessLog.Chunk chunk) { byte[] ab = new byte[unparsed.length + chunk.len()]; if (unparsed.length > 0) { System.arraycopy(unparsed, 0, ab, 0, unparsed.length); } System.arraycopy(chunk.bytes(), 0, ab, unparsed.length, chunk.len()); unparsed = EMPTY; List segments = new ArrayList<>(); List invalidSegments = new ArrayList<>(); int pos = SegmentHeaderParser.parse(ab, segments, invalidSegments); invalidSegmentsToSystemSegments(invalidSegments, segments); Map> segmentsById = byId(segments); for (Map.Entry> e : segmentsById.entrySet()) { int buffLength = e.getValue().stream().mapToInt(h -> actualLength(h, ab.length)).sum(); byte[] segmentBuffer = new byte[buffLength]; fillBuffer(e.getValue(), ab, segmentBuffer); if (segmentBuffer.length > 0) { // TODO: retry? logAppender.appendLog(instanceId, e.getKey(), segmentBuffer); } LogSegmentStats stats = findStats(e.getValue()); if (stats != null) { logAppender.updateSegment(instanceId, e.getKey(), stats); } } Segment partialSegment = findPartialSegment(segments, ab.length); if (partialSegment != null) { unparsed = LogSegmentSerializer.serializeHeader( partialSegment.header(), partialSegment.header().length() - actualLength(partialSegment, ab.length)); } if (pos < ab.length) { if (unparsed != EMPTY) { throw new RuntimeException("Unexpected partial segment and unparsed tail"); } unparsed = Arrays.copyOfRange(ab, pos, ab.length); } } private void invalidSegmentsToSystemSegments(List invalidSegments, List segments) { for (Position s : invalidSegments) { LogSegmentHeader header = LogSegmentHeader.builder() .status(LogSegmentStatus.RUNNING) .segmentId(0) .errorCount(0) .warnCount(0) .length(s.end() - s.start()) .build(); segments.add(new Segment(header, s.start())); } } private static int actualLength(Segment segment, int chunkLength) { return Math.min(chunkLength - segment.msgStart(), segment.header().length()); } private static Map> byId(List segments) { Map> result = new LinkedHashMap<>(); for (Segment s : segments) { result.computeIfAbsent(s.header().segmentId(), id -> new ArrayList<>()) .add(s); } return result; } private static void fillBuffer(List segments, byte[] from, byte[] to) { int i = 0; for (Segment s : segments) { int actualLength = actualLength(s, from.length); for (int j = 0; j < actualLength; j++) { to[i++] = from[j + s.msgStart()]; } } } private static Segment findPartialSegment(List segments, int chunkLength) { Segment result = null; for (Segment segment : segments) { if (actualLength(segment, chunkLength) != segment.header().length()) { if (result != null) { throw new RuntimeException("Unexpected second partial segment"); } result = segment; } } return result; } private static LogSegmentStats findStats(List segments) { boolean done = false; LogSegmentStatus status = null; int errorCount = 0; int warnCount = 0; ListIterator it = segments.listIterator(segments.size()); while (it.hasPrevious()) { Segment segment = it.previous(); if (segment.header().status() != LogSegmentStatus.RUNNING) { done = true; status = segment.header().status(); } if (warnCount == 0 && segment.header().warnCount() > 0) { warnCount = segment.header().warnCount(); } if (errorCount == 0 && segment.header().errorCount() > 0) { errorCount = segment.header().errorCount(); } } if (done || errorCount > 0 || warnCount > 0) { return new LogSegmentStats(status, errorCount, warnCount); } return null; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/logging/StdOutLogAppender.java ================================================ package com.walmartlabs.concord.agent.logging; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2021 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.UUID; public class StdOutLogAppender implements LogAppender { private static final String PREFIX = "RUNNER: "; @Override public void appendLog(UUID instanceId, byte[] ab) { System.out.print(PREFIX + new String(ab)); } @Override public boolean appendLog(UUID instanceId, long segmentId, byte[] ab) { System.out.print(PREFIX + new String(ab)); return true; } @Override public boolean updateSegment(UUID instanceId, long segmentId, LogSegmentStats stats) { return true; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/mmode/MaintenanceModeListener.java ================================================ package com.walmartlabs.concord.agent.mmode; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public interface MaintenanceModeListener { Status onMaintenanceMode(); Status getMaintenanceModeStatus(); class Status { private final boolean maintenanceMode; private final long workersAlive; public Status(boolean maintenanceMode, long workersAlive) { this.maintenanceMode = maintenanceMode; this.workersAlive = workersAlive; } public boolean isMaintenanceMode() { return maintenanceMode; } public long getWorkersAlive() { return workersAlive; } @Override public String toString() { return "Status{" + "maintenanceMode=" + maintenanceMode + ", workersAlive=" + workersAlive + '}'; } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/mmode/MaintenanceModeNotifier.java ================================================ package com.walmartlabs.concord.agent.mmode; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; public class MaintenanceModeNotifier { private static final Logger log = LoggerFactory.getLogger(MaintenanceModeNotifier.class); private final HttpServer server; public MaintenanceModeNotifier(String host, Integer port, MaintenanceModeListener listener) throws IOException { this.server = HttpServer.create(new InetSocketAddress(host, port), 0); this.server.createContext("/maintenance-mode", new MaintenanceModeHandler(listener)); } public void start() { server.start(); log.info("start -> done, listening on {}", server.getAddress()); } public void stop() { server.stop(0); log.info("stop -> done"); } private static class MaintenanceModeHandler implements HttpHandler { private static final String NOT_FOUND_RESPONSE = "404 (Not Found)\n"; private final ObjectMapper objectMapper = new ObjectMapper(); private final MaintenanceModeListener listener; private MaintenanceModeHandler(MaintenanceModeListener listener) { this.listener = listener; } @Override public void handle(HttpExchange httpExchange) throws IOException { MaintenanceModeListener.Status status = null; if ("GET".equals(httpExchange.getRequestMethod())) { status = onMaintenanceModeStatus(); } else if ("POST".equals(httpExchange.getRequestMethod())) { status = onMaintenanceMode(); } if (status != null) { httpExchange.getResponseHeaders().set("Content-Type", "application/json"); response(httpExchange, 200, objectMapper.writeValueAsBytes(status)); return; } response(httpExchange, 404, NOT_FOUND_RESPONSE.getBytes()); } private void response(HttpExchange httpExchange, int code, byte[] response) throws IOException { httpExchange.sendResponseHeaders(code, response.length); try (OutputStream os = httpExchange.getResponseBody()) { os.write(response); } } private MaintenanceModeListener.Status onMaintenanceMode() { MaintenanceModeListener.Status status = listener.onMaintenanceMode(); log.info("onMaintenanceMode -> {}", status); return status; } private MaintenanceModeListener.Status onMaintenanceModeStatus() { MaintenanceModeListener.Status status = listener.getMaintenanceModeStatus(); log.info("onMaintenanceModeStatus -> {}", status); return status; } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/remote/ApiClientFactory.java ================================================ package com.walmartlabs.concord.agent.remote; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.client2.*; import com.walmartlabs.concord.agent.cfg.ServerConfiguration; import javax.inject.Inject; import java.io.IOException; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.List; import java.util.Map; public class ApiClientFactory { private static final String SESSION_COOKIE_NAME = "JSESSIONID"; private final ServerConfiguration cfg; private final DefaultApiClientFactory clientFactory; @Inject public ApiClientFactory(ServerConfiguration cfg) throws Exception { this.cfg = cfg; this.clientFactory = new DefaultApiClientFactory(cfg.getApiBaseUrl(), Duration.of(cfg.getConnectTimeout(), ChronoUnit.MILLIS), cfg.isVerifySsl()); } public ApiClient create(String sessionToken) throws IOException { ImmutableApiClientConfiguration.Builder clientCfgBuilder = ApiClientConfiguration.builder() .baseUrl(cfg.getApiBaseUrl()); if (sessionToken != null) { clientCfgBuilder.sessionToken(sessionToken); } else { clientCfgBuilder.apiKey(cfg.getApiKey()); } ApiClient client = clientFactory.create(clientCfgBuilder.build()) .setReadTimeout(Duration.of(cfg.getReadTimeout(), ChronoUnit.MILLIS)) .setUserAgent(cfg.getUserAgent()); Map cookieJar = new HashMap<>(); client.setResponseInterceptor(response -> { List cookies = response.headers().allValues("Set-Cookie"); if (cookies.isEmpty()) { return; } for (String cookie : cookies) { if (cookie.startsWith(SESSION_COOKIE_NAME)) { cookieJar.put(SESSION_COOKIE_NAME, cookie); } } }); client.setRequestInterceptor(builder -> { for (Map.Entry cookie : cookieJar.entrySet()) { builder.header("Cookie", cookie.getValue()); } }); return client; } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/remote/AttachmentsUploader.java ================================================ package com.walmartlabs.concord.agent.remote; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.AgentConstants; import com.walmartlabs.concord.client2.*; import com.walmartlabs.concord.common.PathUtils; import com.walmartlabs.concord.common.TemporaryPath; import com.walmartlabs.concord.common.ZipUtils; import com.walmartlabs.concord.sdk.Constants; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import javax.inject.Inject; import java.nio.file.Files; import java.nio.file.Path; import java.util.UUID; public class AttachmentsUploader { private final ApiClient apiClient; @Inject public AttachmentsUploader(ApiClient apiClient) { this.apiClient = apiClient; } public void upload(UUID instanceId, Path workDir) throws Exception { Path attachmentsDir = workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME); if (!Files.exists(attachmentsDir)) { return; } try (TemporaryPath tmp = PathUtils.tempFile("attachments", ".zip")) { try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(Files.newOutputStream(tmp.path()))) { ZipUtils.zip(zip, attachmentsDir); } ProcessApi api = new ProcessApi(apiClient); ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> { api.uploadProcessAttachments(instanceId, Files.newInputStream(tmp.path())); return null; }); } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/remote/ProcessStatusUpdater.java ================================================ package com.walmartlabs.concord.agent.remote; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.AgentConstants; import com.walmartlabs.concord.client2.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.UUID; public class ProcessStatusUpdater { private static final Logger log = LoggerFactory.getLogger(ProcessStatusUpdater.class); private final String agentId; private final ProcessApi processApi; public ProcessStatusUpdater(String agentId, ProcessApi processApi) { this.agentId = agentId; this.processApi = processApi; } public void update(UUID instanceId, ProcessEntry.StatusEnum status) { try { ClientUtils.withRetry(AgentConstants.API_CALL_MAX_RETRIES, AgentConstants.API_CALL_RETRY_DELAY, () -> { processApi.updateStatus(instanceId, agentId, status.name()); return null; }); } catch (ApiException e) { log.warn("updateStatus ['{}'] -> error while updating status of a job: {}", instanceId, e.getMessage()); } } } ================================================ FILE: agent/src/main/java/com/walmartlabs/concord/agent/remote/QueueClientProvider.java ================================================ package com.walmartlabs.concord.agent.remote; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agent.cfg.AgentConfiguration; import com.walmartlabs.concord.agent.cfg.ServerConfiguration; import com.walmartlabs.concord.server.queueclient.QueueClient; import com.walmartlabs.concord.server.queueclient.QueueClientConfiguration; import javax.inject.Inject; import javax.inject.Provider; import java.net.URISyntaxException; public class QueueClientProvider implements Provider { private final AgentConfiguration agentCfg; private final ServerConfiguration serverCfg; @Inject public QueueClientProvider(AgentConfiguration agentCfg, ServerConfiguration serverCfg) { this.agentCfg = agentCfg; this.serverCfg = serverCfg; } @Override public QueueClient get() { try { QueueClient queueClient = new QueueClient(new QueueClientConfiguration.Builder(serverCfg.getWebsocketUrls()) .agentId(agentCfg.getAgentId()) .apiKey(serverCfg.getApiKey()) .userAgent(serverCfg.getUserAgent()) .connectTimeout(serverCfg.getConnectTimeout()) .pingInterval(serverCfg.getPingInterval()) .maxNoActivityPeriod(serverCfg.getMaxNoActivityPeriod()) .processRequestDelay(serverCfg.getProcessRequestDelay()) .reconnectDelay(serverCfg.getReconnectDelay()) .build()); queueClient.start(); return queueClient; } catch (URISyntaxException e) { throw new RuntimeException(e); } } } ================================================ FILE: agent/src/main/resources/com/walmartlabs/concord/agent/logback.xml ================================================ %d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n ================================================ FILE: agent/src/main/resources/concord-agent.conf ================================================ # Concord Agent # # Note: most of path parameters accept either absolute paths or a directory # name. With the latter, the effective path is ${CONCORD_TMP_DIR}/${value} # # E.g. # # workDirBase = "workDirs" # means "/tmp/workDirs", created automatically # # dependencyCacheDir = "/data/concord/dependencyCache" # path to an existing directory # concord-agent { # unique ID of the agent # a string value, 36 characters max, typically an UUID # generated on start if not specified id = ${?AGENT_ID} # agent capabilities, JSON object capabilities = { } # directory to cache dependencies dependencyCacheDir = "dependencyCache" # directory to store process dependency lists dependencyListsDir = "dependencyLists" # timeout to resolve process dependencies dependencyResolveTimeout = "10 minutes" # use repositories from `CONCORD_MAVEN_CFG` file only # (allow/ignore repositories from artifact descriptor) dependencyStrictRepositories = false # artifact exclude patterns dependencyExclusions = [] # explicitly resolve v1 version of the concord HTTP client explicitlyResolveV1Client = true # resolve Maven artifacts in the offline mode mavenOfflineMode = false # base directory to store the process payload # created automatically if not specified payloadDir = "payload" # base directory for the process' ${workDir} # # Use the same value and use absolute paths when running multiple Agents. # If the process keeps ${workDir} value as a part of another variable, # the value might not longer be valid if the process restarts and # gets a new Agent. workDirBase = "/tmp/concord-agent/workDirs" workDirBase = ${?WORK_DIR_BASE} # directory to store the process logs # created automatically if not specified logDir = "logs" # maximum delay between log chunks # determines how ofter the logs are send back to the server logMaxDelay = "2 seconds" # replace the current process' workDir in logs with literal "$WORK_DIR" workDirMasking = true # maximum number of concurrent processes workersCount = 3 workersCount = ${?WORKERS_COUNT} # host/ip of the maintenance mode endpoint maintenanceModeListenerHost = "localhost" # port of the maintenance mode endpoint maintenanceModeListenerPort = 8010 maintenanceModeListenerPort = ${?MM_PORT} # interval between new payload requests pollInterval = "2 seconds" # JVM prefork settings prefork { # enable/disabled the use of "preforks" # # When enabled, Agent keeps a copy of the process' JVM as a "spare". # If Agent receives another process with the same classpath, JVM and # Concord Runtime parameters, the "spare" is used. This can sometimes # minimize the cost of JVM startup and classpath scanning. # # Note, enabling this mechanism can have other side-effects. # The effective ${workDir} might exist before the process ID is known # (in order to keep a "spare" running there, with all Java dependencies # loaded). # If any process keeps a copy of ${workDir} value as a part of another # variable, the value might get stale, e.g. if the process restarts # (or resumes after suspend) and, subsequently, gets a "fresh" workDir. # # When "false", the process' ${workDir} is always ${workDirBase}/${instanceId} enabled = false # maximum time to keep a preforked JVM maxAge = "30 seconds" # maximum number of preforks maxCount = 3 } # server connection settings server { apiBaseUrl = "http://localhost:8001" apiBaseUrl = ${?SERVER_API_BASE_URL} # comma-separated list or URLs websocketUrl = "ws://localhost:8001/websocket" websocketUrl = ${?SERVER_WEBSOCKET_URL} verifySsl = false connectTimeout = "30 seconds" readTimeout = "1 minute" retryCount = 5 retryInterval = "30 seconds" # User-Agent header to use with API requests userAgent = null userAgent = ${?USER_AGENT} # interval between WS ping requests in case of no other activity websocketPingInterval = "10 seconds" # maximum period of no activity before reconnect websocketMaxNoActivityPeriod = "30 seconds" # API key to use # Generated on Server first start or defined in server.conf at db.changeLogParameters.defaultAgentToken # IMPORTANT! After initialization, create a new token via API and delete initial token apiKey = "" apiKey = ${?SERVER_API_KEY} # maximum time interval without a heartbeat before the process fails maxNoHeartbeatInterval = "5 minutes" # delay between successful polling attempts processRequestDelay = "1 seconds" # delay between re-connection attempts if the server is unreachable or unhealthy reconnectDelay = "5 seconds" } docker { host = "tcp://127.0.0.1:2375" host = ${?DOCKER_HOST} orphanSweeperEnabled = false orphanSweeperPeriod = "15 minutes" # list of volumes mounted into the process' containers in addition to the /workspace # affects only the plugins, such as `docker` and `ansible` extraVolumes = [] # expose docker daemon to containers started by DockerService exposeDockerDaemon = true } repositoryCache { # directory to store the local repo cache # created automatically if not specified # cacheDir = "/tmp/concord/repos" # timeout for checkout operations (ms) lockTimeout = "3 minutes" # directory to store the local repo cache info # created automatically if not specified #cacheInfoDir = "/tmp/concord/repos_info" # the allowed concurrency level when pulling Git data lockCount = 8 # max cached repo age in ms maxAge = "1 day" } # git clone config git { # if true, skip Git fetch, use workspace state only skip = false # GitHub auth token to use when cloning repositories without explicitly # configured authentication. Deprecated in favor of systemAuth list of # tokens or service-specific app config (e.g. github) # oauth = "..." # specific username to use for auth # oauthUsername = "" # regex to match against git server's hostname + port + path so oauth # token isn't used for and unexpected host # oauthUrlPattern = "" # List of system-provided auth token configs # { # "token" = "...", # "username" = "...", # optional, username to send with auth token # "urlPattern" = "..." # required, regex to match against target git host + port + path # } systemAuth = [] # use GIT's shallow clone shallowClone = true # do not execute fetch if the current HEAD is the latest commit ID checkAlreadyFetched = true # default timeout duration for any git operation defaultOperationTimeout = "10 minutes" # fetch timeout duration fetchTimeout = "10 minutes" # see GIT documentation for GIT_HTTP_LOW_SPEED_LIMIT and GIT_HTTP_LOW_SPEED_TIME # use with caution, can cause performance issues httpLowSpeedLimit = 0 httpLowSpeedTime = "10 minutes" sshTimeoutRetryCount = 1 sshTimeout = "10 minutes" # max bytes to keep from a git cli process output (distinct for stdout and stderr) maxGitCliOutputBytes = 512 } # github app settings. While this works on the agent, it's preferable to # get auth token from concord-server via externalTokenProvider github { # App installation settings. Multiple auth (private key) definitions are supported, # as each is matched to a particular url pattern. appInstallation { # { # type = "GITHUB_APP_INSTALLATION", # urlPattern = "github.com", # regex # username = "...", # optional, defaults to "x-access-token" # apiUrl = "https://api.github.com", # github api url, usually *not* the same as the repo url host/path # clientId = "...", # privateKey = "/path/to/pk.pem" # } # or static oauth config. Not exactly a "GitHub App", but can do some # API interactions and cloning. Less preferred to actual app. # { # type = "OAUTH_TOKEN", # token = "...", # username = "...", # optional, usually not necessary # urlPattern = "..." # regex to match against git server's hostname + port + path # } auth = [] } } imports { # base git url for imports src = "" # list of disabled import processors # e.g. "dir" is useful for local development, but potentially a security issue disabledProcessors = [ "dir" ] } # configuration of "runners" -- JARs that are responsible for the actual process execution # common configuration for all runners runner { # directory to store process configuration files cfgDir = null # reserved for the future use securityManagerEnabled = false # command to use to run the runner JAR # '${java.home}/bin/java' by default #javaCmd = "java" # default JVM parameters jvmParams = [ "-Xmx128m", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:+ExitOnOutOfMemoryError", "-XX:HeapDumpPath=/tmp" ] # if set, the Agent copies all process files into a persistentWorkDir's subdirectory # after the process ends (regardless of the status) # should not be used in production environments # persistentWorkDir = /path/to/dir # if true, agent will forcibly kill any remaining child PIDs (i.e. zombies) # of the runner process cleanRunnerDescendants = false } # the default v1 runtime configuration runnerV1 = ${runner} runnerV1 { # path to the runner v1 JAR, must be local to the agent path = null path = ${?RUNNER_V1_PATH} mainClass = "com.walmartlabs.concord.runner.Main" # local dev only propertiesFile = "runnerV1.properties" # use the segmented logs API segmentedLogs = false } # the v2 runtime configuration runnerV2 = ${runner} runnerV2 { # path to the runner v2 JAR, must be local to the agent path = null path = ${?RUNNER_V2_PATH} mainClass = "com.walmartlabs.concord.runtime.v2.runner.Main" segmentedLogs = true # local dev only propertiesFile = "runnerV2.properties" } runtimes = { "concord-v1" = ${runnerV1}, "concord-v2" = ${runnerV2}, } development { } production { } } ================================================ FILE: agent/src/main/resources/logback.xml ================================================ %d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n ================================================ FILE: agent/src/test/java/com/walmartlabs/concord/agent/AgentAuthTokenProviderTest.java ================================================ package com.walmartlabs.concord.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.AuthTokenProvider; import com.walmartlabs.concord.common.ExternalAuthToken; import com.walmartlabs.concord.github.appinstallation.GitHubAppInstallation; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.net.URI; import java.time.OffsetDateTime; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class AgentAuthTokenProviderTest { @Mock GitHubAppInstallation ghApp; @Mock AuthTokenProvider.OauthTokenProvider oauthTokenProvider; @Test void testGitHubApp() { when(ghApp.getToken(any(), any())). thenReturn(Optional.of(ExternalAuthToken.SimpleToken.builder() .token("gh-installation-token") .expiresAt(OffsetDateTime.now().plusMinutes(60)) .build())); when(ghApp.supports(any(), any())).thenReturn(true); var provider = new AgentAuthTokenProvider(ghApp, oauthTokenProvider); // -- assertTrue(provider.supports(URI.create("https://github.local/owner/repo.git"), null)); var o = provider.getToken(URI.create("https://github.local/owner/repo.git"), null); // -- assertTrue(o.isPresent()); var result = assertInstanceOf(ExternalAuthToken.class, o.get()); assertEquals("gh-installation-token", result.token()); } @Test void testOauth() { when(oauthTokenProvider.supports(any(), any())).thenReturn(true); when(oauthTokenProvider.getToken(any(), any())) .thenReturn(Optional.of(ExternalAuthToken.StaticToken.builder() .token("oauth-token") .build())); var provider = new AgentAuthTokenProvider(ghApp, oauthTokenProvider); // -- assertTrue(provider.supports(URI.create("https://github.local/owner/repo.git"), null)); var o = provider.getToken(URI.create("https://github.local/owner/repo.git"), null); // -- assertTrue(o.isPresent()); var result = assertInstanceOf(ExternalAuthToken.class, o.get()); assertEquals("oauth-token", result.token()); } @Test void testNoAuth() { when(ghApp.supports(any(), any())).thenReturn(false); when(oauthTokenProvider.supports(any(), any())).thenReturn(false); var provider = new AgentAuthTokenProvider(ghApp, oauthTokenProvider); // -- assertFalse(provider.supports(URI.create("https://github.local/owner/repo.git"), null)); var o = provider.getToken(URI.create("https://github.local/owner/repo.git"), null); // -- assertFalse(o.isPresent()); } } ================================================ FILE: agent/src/test/java/com/walmartlabs/concord/agent/executors/runner/JobDependenciesTest.java ================================================ package com.walmartlabs.concord.agent.executors.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.sdk.Constants; import org.junit.jupiter.api.Test; import java.io.InputStream; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.*; public class JobDependenciesTest { @Test public void test() throws Exception { Path payloadDir = Files.createTempDirectory("test"); Path versionsFile = payloadDir.resolve(Constants.Files.CONCORD_SYSTEM_DIR_NAME) .resolve(Constants.Files.DEPENDENCY_VERSIONS_FILE_NAME); Files.createDirectories(versionsFile.getParent()); try (InputStream in = JobDependenciesTest.class.getResourceAsStream("versions.properties")) { Files.copy(in, versionsFile); } Map cfg = new HashMap<>(); cfg.put(Constants.Request.DEPENDENCIES_KEY, Arrays.asList( "file://something.jar", "mvn://org.codehaus.groovy:groovy-all:pom:2.5.8", "mvn://aaa:aaa:1.0", "mvn://bbb:bbb:latest", "mvn://ccc:ccc:1.0.1-20190214.203609-21", "mvn://ddd:ddd:latest" )); RunnerJob j = mock(RunnerJob.class); when(j.getProcessCfg()).thenReturn(cfg); when(j.getPayloadDir()).thenReturn(payloadDir); RunnerLog log = mock(RunnerLog.class); when(j.getLog()).thenReturn(log); Collection uris = JobDependencies.get(j); assertEquals(6, uris.size()); assertContains("mvn://aaa:aaa:1.0", uris); assertContains("mvn://bbb:bbb:1.0", uris); assertContains("mvn://ccc:ccc:1.0.1-20190214.203609-21", uris); assertContains("mvn://ddd:ddd:latest", uris); verify(log, times(1)).warn(anyString(), any()); } private static void assertContains(String s, Collection uris) { for (URI u : uris) { if (u.toString().equals(s)) { return; } } fail("Expected to find " + s); } } ================================================ FILE: agent/src/test/java/com/walmartlabs/concord/agent/executors/runner/SegmentHeaderParserTest.java ================================================ package com.walmartlabs.concord.agent.executors.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2021 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.google.common.primitives.Bytes; import com.walmartlabs.concord.agent.logging.SegmentHeaderParser; import com.walmartlabs.concord.runtime.common.logger.LogSegmentHeader; import com.walmartlabs.concord.runtime.common.logger.LogSegmentSerializer; import com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static com.walmartlabs.concord.agent.logging.SegmentHeaderParser.Position; import static com.walmartlabs.concord.agent.logging.SegmentHeaderParser.Segment; import static org.junit.jupiter.api.Assertions.assertEquals; public class SegmentHeaderParserTest { /** * in: |5|2|1|1|2|hello */ @Test public void test1() { String log = "hello"; byte[] ab = bb(1, log); List segments = new ArrayList<>(); List invalidSegments = new ArrayList<>(); int result = SegmentHeaderParser.parse(ab, segments, invalidSegments); assertEquals(ab.length, result); assertEquals(1, segments.size()); assertEquals(log, msg(ab, segments.get(0))); assertEquals(0, invalidSegments.size()); } /** * in: |7|1|1|1|2|hello-1|8|2|1|1|2|hello-21 */ @Test public void test1_1() { byte[] ab = Bytes.concat(bb(1, "hello-1"), bb(2, "hello-21")); List segments = new ArrayList<>(); List invalidSegments = new ArrayList<>(); int result = SegmentHeaderParser.parse(ab, segments, invalidSegments); assertEquals(ab.length, result); assertEquals(2, segments.size()); assertEquals("hello-1", msg(ab, segments.get(0))); assertEquals("hello-21", msg(ab, segments.get(1))); assertEquals(0, invalidSegments.size()); } /** * in: hello */ @Test public void test2() { String log = "hello"; byte[] ab = log.getBytes(); List segments = new ArrayList<>(); List invalidSegments = new ArrayList<>(); int result = SegmentHeaderParser.parse(ab, segments, invalidSegments); assertEquals(ab.length, result); assertEquals(0, segments.size()); assertEquals(1, invalidSegments.size()); Position i = invalidSegments.get(0); assertEquals(log, new String(Arrays.copyOfRange(ab, i.start(), i.end()))); } /** * in: 123|5|2|1|1|2|hello */ @Test public void test3() { String log = "hello"; byte[] ab = {'1', '2', '3'}; ab = Bytes.concat(ab, bb(2, log)); List segments = new ArrayList<>(); List invalidSegments = new ArrayList<>(); int result = SegmentHeaderParser.parse(ab, segments, invalidSegments); assertEquals(ab.length, result); assertEquals(1, segments.size()); assertEquals("hello", msg(ab, segments.get(0))); assertEquals(1, invalidSegments.size()); Position i = invalidSegments.get(0); assertEquals("123", new String(Arrays.copyOfRange(ab, i.start(), i.end()))); } /** * in: |5|2|1|1|2|hello123 */ @Test public void test4() { String log = "hello"; byte[] ab = {'1', '2', '3'}; ab = Bytes.concat(bb(2, log), ab); List segments = new ArrayList<>(); List invalidSegments = new ArrayList<>(); int result = SegmentHeaderParser.parse(ab, segments, invalidSegments); assertEquals(ab.length, result); assertEquals(1, segments.size()); assertEquals(1, invalidSegments.size()); Position i = invalidSegments.get(0); assertEquals("123", new String(Arrays.copyOfRange(ab, i.start(), i.end()))); } /** * in: |5|2|1 */ @Test public void test5() { byte[] ab = {'|', '5', '|', '2', '|', '1'}; List segments = new ArrayList<>(); List invalidSegments = new ArrayList<>(); int result = SegmentHeaderParser.parse(ab, segments, invalidSegments); assertEquals(0, result); assertEquals(0, segments.size()); assertEquals(0, invalidSegments.size()); } /** * in: abc|5|2|1 */ @Test public void test6() { byte[] ab = {'a', 'b', 'c', '|', '5', '|', '2', '|', '1'}; List segments = new ArrayList<>(); List invalidSegments = new ArrayList<>(); int result = SegmentHeaderParser.parse(ab, segments, invalidSegments); assertEquals(3, result); assertEquals(0, segments.size()); assertEquals(1, invalidSegments.size()); Position i = invalidSegments.get(0); assertEquals("abc", new String(Arrays.copyOfRange(ab, i.start(), i.end()))); } /** * in: |5|2|1|1|2|he */ @Test public void test7() { String log = "hello"; byte[] full = bb(1, log); byte[] ab = Arrays.copyOfRange(full, 0, full.length - 3); List segments = new ArrayList<>(); List invalidSegments = new ArrayList<>(); int result = SegmentHeaderParser.parse(ab, segments, invalidSegments); assertEquals(ab.length, result); assertEquals(1, segments.size()); assertEquals("he", msg(ab, segments.get(0))); assertEquals(0, invalidSegments.size()); } /** * * |0|552|1|0|0| */ @Test public void testParseSegmentEndMarker() { String log = "|0|552|1|0|0|"; byte[] ab = log.getBytes(StandardCharsets.UTF_8); List segments = new ArrayList<>(); List invalidSegments = new ArrayList<>(); int result = SegmentHeaderParser.parse(ab, segments, invalidSegments); assertEquals(ab.length, result); assertEquals(1, segments.size()); Segment s = segments.get(0); assertEquals(0, s.header().length()); assertEquals(LogSegmentStatus.OK, s.header().status()); } private static String msg(byte[] ab, Segment segment) { int to = Math.min(ab.length, segment.msgStart() + segment.header().length()); return new String(Arrays.copyOfRange(ab, segment.msgStart(), to)); } private static byte[] bb(int segmentId, String msg) { byte[] ab = msg.getBytes(); byte[] header = LogSegmentSerializer.serializeHeader(LogSegmentHeader.builder() .segmentId(segmentId) .length(ab.length) .warnCount(1) .errorCount(2) .status(LogSegmentStatus.RUNNING) .build()); return Bytes.concat(header, ab); } } ================================================ FILE: agent/src/test/java/com/walmartlabs/concord/agent/executors/runner/SegmentedLogsConsumerTest.java ================================================ package com.walmartlabs.concord.agent.executors.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2021 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.google.common.primitives.Bytes; import com.walmartlabs.concord.agent.logging.LogAppender; import com.walmartlabs.concord.agent.logging.LogSegmentStats; import com.walmartlabs.concord.agent.logging.SegmentedLogsConsumer; import com.walmartlabs.concord.runtime.common.logger.LogSegmentHeader; import com.walmartlabs.concord.runtime.common.logger.LogSegmentSerializer; import com.walmartlabs.concord.runtime.common.logger.LogSegmentStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.UUID; import static com.walmartlabs.concord.agent.logging.RedirectedProcessLog.Chunk; import static org.mockito.Mockito.*; public class SegmentedLogsConsumerTest { private LogAppender logAppender; private SegmentedLogsConsumer consumer; @BeforeEach public void init() { this.logAppender = mock(LogAppender.class); consumer = new SegmentedLogsConsumer(UUID.randomUUID(), logAppender); } @Test public void test1() { String msg = "hello"; byte[] ab = bb(1, msg); consumer.accept(toChunk(ab)); verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(msg.getBytes())); verify(logAppender, times(1)).updateSegment(any(), eq(1L), eq(new LogSegmentStats(null, 2, 1))); } /** * in: |7|1|1|1|2|hello1\n|8|1|1|1|2|hello223 */ @Test public void test2() { String msg1 = "hello1\n"; byte[] s1 = bb(1, msg1); String msg2 = "hello223"; byte[] s2 = bb(1, msg2); consumer.accept(toChunk(Bytes.concat(s1, s2))); verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(Bytes.concat(msg1.getBytes(), msg2.getBytes()))); verify(logAppender, times(1)).updateSegment(any(), eq(1L), eq(new LogSegmentStats(null, 2, 1))); } /** * in: |7|1|1|1|2|hello1\n|8|2|1|1|2|hello223 */ @Test public void test3() { String msg1 = "hello1\n"; byte[] s1 = bb(1, msg1); String msg2 = "hello223"; byte[] s2 = bb(2, msg2); consumer.accept(toChunk(Bytes.concat(s1, s2))); verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(msg1.getBytes())); verify(logAppender, times(1)).appendLog(any(), eq(2L), eq(msg2.getBytes())); verify(logAppender, times(1)).updateSegment(any(), eq(1L), eq(new LogSegmentStats(null, 2, 1))); } /** * in: |7|1|1|1|2|hello1\n|8|2|1|1|2|hello223 */ @Test public void test4() { String msg = "hello"; byte[] s = bb(1, msg); consumer.accept(toChunk(Arrays.copyOfRange(s, 0, s.length - 3))); byte[] p1 = {'h', 'e'}; verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(p1)); byte[] p2 = {'l', 'l', 'o'}; consumer.accept(toChunk(p2)); verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(p2)); verify(logAppender, times(2)).updateSegment(any(), eq(1L), eq(new LogSegmentStats(null, 2, 1))); } /** * in: |5|1|1|0|0|hello */ @Test public void test5() { String msg = "hello"; byte[] ab = bb(1, msg, 0, 0); consumer.accept(toChunk(ab)); verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(msg.getBytes())); verifyNoMoreInteractions(logAppender); } /** * in: |5|1|1|0|0|hellotrash|3|2|1|0|0|bye */ @Test public void test6() { String msg1 = "hello"; String msg2 = "bye"; byte[] s1 = bb(1, msg1, 0, 0); byte[] s2 = bb(2, msg2, 0, 0); byte[] ab = Bytes.concat(s1, "trash".getBytes(), s2); System.out.println(">>>" + new String(ab)); consumer.accept(toChunk(ab)); verify(logAppender, times(1)).appendLog(any(), eq(1L), eq(msg1.getBytes())); verify(logAppender, times(1)).appendLog(any(), eq(0L), eq("trash".getBytes())); verify(logAppender, times(1)).appendLog(any(), eq(2L), eq(msg2.getBytes())); verifyNoMoreInteractions(logAppender); } private static Chunk toChunk(byte[] ab) { return new Chunk(ab, ab.length) { }; } private static byte[] bb(int segmentId, String msg) { return bb(segmentId, msg, 2, 1); } private static byte[] bb(int segmentId, String msg, int errorCount, int warnCount) { byte[] ab = msg.getBytes(); byte[] header = LogSegmentSerializer.serializeHeader(LogSegmentHeader.builder() .segmentId(segmentId) .length(ab.length) .warnCount(warnCount) .errorCount(errorCount) .status(LogSegmentStatus.RUNNING) .build()); return Bytes.concat(header, ab); } } ================================================ FILE: agent/src/test/resources/com/walmartlabs/concord/agent/executors/runner/versions.properties ================================================ aaa\:aaa=2.0 bbb\:bbb=1.0 ================================================ FILE: agent-operator/README.md ================================================ # Kubernetes Operator for Concord Agents Takes care of deploying and scaling [Concord](https://concord.walmartlabs.com) Agents based on the current Process Queue usage. ## Prerequisites Build the parent Concord repo to install the latest artifacts and Docker images: ``` $ cd concord/ $ ./mvnw clean install -DskipTests ``` ## Running in Minikube Below are the steps to deploy the concord agent operator to the `default` namespace in any local/dev k8s cluster (in this case minikube). Before deploying the operator's resources, please ensure the Concord Server URL and API token fields in the specs files are correctly pointing to a running instance on your dev or local machine. Make sure the API token used in the `operator.yml` is valid and working. 1. Start the cluster: ``` $ minikube start $ minikube ``` 2. Build the operator's image: ``` $ eval $(minikube docker-env) $ cd concord/agent-operator $ docker build . -t walmartlabs/concord-agent-operator:latest ``` 3. Build the app's images (might take a while, depending on cached layers present in your minikube instance): ``` $ eval $(minikube docker-env) $ cd concord $ ./mvnw -f docker-images clean install -Pdocker ``` 4. Deploy the necessary resources: ``` $ minikube kubectl -- create -f deploy/cluster_role.yml -n default $ minikube kubectl -- create -f deploy/service_account.yml -n default $ minikube kubectl -- create -f deploy/cluster_role_binding.yml -n default ``` 5. Create the custom resource: ``` $ minikube kubectl -- create -f deploy/crds/crd.yml -n default $ minikube kubectl -- create -f deploy/crds/example.agentpool.yml -n default ``` 6. Start the operator: ``` $ minikube kubectl -- create -f deploy/operator.yml -n default ``` If everything is correct you should see this line in the operator's pod log: ``` [INFO ] c.w.concord.agentoperator.Operator - main -> my watch begins... (namespace=default) [INFO ] c.w.c.a.p.CreateConfigMapChange - apply -> created a configmap example-agentpool-cfg [INFO ] c.w.c.a.planner.CreatePodChange - apply -> created a pod example-agentpool/example-agentpool-00000 ``` There should be no other errors or warnings. ## Running in an IDE Repeat all steps from the [Running in Minikube](#running-in-minikube) section except for "Start the operator". Start the operator directly in your IDE by using `com.walmartlabs.concord.agentoperator.Operator` as the main class. Specify `CONCORD_BASE_URL` and `CONCORD_API_TOKEN` if you wish to test the autoscaling feature. #### How to Verify 1. Check the pods running in the `default` namespace: ``` minikube kubectl -- get po -n default ``` The output should show two pods - the concord agent operator pod (with 1 container) and the agentpool pod (with 2 containers). 2. Verify the logs of both these pods using: ``` minikube kubectl -- logs -f -c -n default ``` ## Running in Production 1. Deploy the service account, the cluster role and the cluster role binding (modify the commands according to your namespace): ``` $ minikube kubectl -- create -f deploy/cluster_role.yml $ minikube kubectl -- create -f deploy/service_account.yml $ minikube kubectl -- create -f deploy/cluster_role_binding.yml ``` 2. Update the `CONCORD_BASE_URL` and `CONCORD_API_TOKEN` in `deploy/operator.yml`. Verify the image name and tag (version); 3. Deploy the operator: ``` $ minikube kubectl -- create -f deploy/operator.yml ``` 4. Check the operator's pod logs; 5. Deploy one or more CRs using `deploy/crds/example.agentpool.yml` as a template. ## How To Release New Versions - build the image; - push the image to Docker Hub: ``` $ docker push walmartlabs/concord-agent-operator:latest ``` ## TODO - automatically add the necessary labels to pods; - add validation rules for CRDs and CRs; - use secrets to store Concord API keys; - make the queue connection optional if the auto scaling is disabled. ================================================ FILE: agent-operator/deploy/cluster_role.yml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: concord-agent-operator rules: - apiGroups: - "" resources: - pods - events - configmaps - pods/exec verbs: - '*' - apiGroups: - apiextensions.k8s.io resources: - customresourcedefinitions verbs: - '*' - apiGroups: - "" resources: - namespaces verbs: - get - apiGroups: - concord.walmartlabs.com resources: - '*' verbs: - '*' ================================================ FILE: agent-operator/deploy/cluster_role_binding.yml ================================================ kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: concord-agent-operator subjects: - kind: ServiceAccount name: concord-agent-operator namespace: 'default' roleRef: kind: ClusterRole name: concord-agent-operator apiGroup: rbac.authorization.k8s.io ================================================ FILE: agent-operator/deploy/crds/agentpools.concord.walmartlabs.com-v1.yml ================================================ # Generated by Fabric8 CRDGenerator, manual edits might get overwritten! apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: agentpools.concord.walmartlabs.com spec: group: concord.walmartlabs.com names: kind: AgentPool plural: agentpools shortNames: - aps singular: agentpool scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: properties: spec: properties: autoScale: type: boolean maxSize: type: integer minSize: type: integer size: type: integer queueQueryLimit: type: integer scaleUpDelayMs: type: integer scaleDownDelayMs: type: integer percentIncrement: type: number percentDecrement: type: number incrementThresholdFactor: type: number decrementThresholdFactor: type: number queueSelector: additionalProperties: type: object x-kubernetes-preserve-unknown-fields: true type: object configMap: properties: apiVersion: type: string binaryData: additionalProperties: type: string type: object data: additionalProperties: type: string type: object immutable: type: boolean kind: type: string metadata: properties: annotations: additionalProperties: type: string type: object clusterName: type: string creationTimestamp: type: string deletionGracePeriodSeconds: type: integer deletionTimestamp: type: string finalizers: items: type: string type: array generateName: type: string generation: type: integer labels: additionalProperties: type: string type: object managedFields: items: properties: apiVersion: type: string fieldsType: type: string fieldsV1: type: object manager: type: string operation: type: string subresource: type: string time: type: string type: object type: array name: type: string namespace: type: string ownerReferences: items: properties: apiVersion: type: string blockOwnerDeletion: type: boolean controller: type: boolean kind: type: string name: type: string uid: type: string type: object type: array resourceVersion: type: string selfLink: type: string uid: type: string type: object type: object pod: properties: apiVersion: type: string kind: type: string metadata: properties: annotations: additionalProperties: type: string type: object clusterName: type: string creationTimestamp: type: string deletionGracePeriodSeconds: type: integer deletionTimestamp: type: string finalizers: items: type: string type: array generateName: type: string generation: type: integer labels: additionalProperties: type: string type: object managedFields: items: properties: apiVersion: type: string fieldsType: type: string fieldsV1: type: object manager: type: string operation: type: string subresource: type: string time: type: string type: object type: array name: type: string namespace: type: string ownerReferences: items: properties: apiVersion: type: string blockOwnerDeletion: type: boolean controller: type: boolean kind: type: string name: type: string uid: type: string type: object type: array resourceVersion: type: string selfLink: type: string uid: type: string type: object spec: properties: activeDeadlineSeconds: type: integer affinity: properties: nodeAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: preference: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array type: object weight: type: integer type: object type: array requiredDuringSchedulingIgnoredDuringExecution: properties: nodeSelectorTerms: items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array type: object type: array type: object type: object podAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchLabels: additionalProperties: type: string type: object type: object namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchLabels: additionalProperties: type: string type: object type: object namespaces: items: type: string type: array topologyKey: type: string type: object weight: type: integer type: object type: array requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchLabels: additionalProperties: type: string type: object type: object namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchLabels: additionalProperties: type: string type: object type: object namespaces: items: type: string type: array topologyKey: type: string type: object type: array type: object podAntiAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchLabels: additionalProperties: type: string type: object type: object namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchLabels: additionalProperties: type: string type: object type: object namespaces: items: type: string type: array topologyKey: type: string type: object weight: type: integer type: object type: array requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchLabels: additionalProperties: type: string type: object type: object namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchLabels: additionalProperties: type: string type: object type: object namespaces: items: type: string type: array topologyKey: type: string type: object type: array type: object type: object automountServiceAccountToken: type: boolean containers: items: properties: args: items: type: string type: array command: items: type: string type: array env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: type: string optional: type: boolean type: object fieldRef: properties: apiVersion: type: string fieldPath: type: string type: object resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true resource: type: string type: object secretKeyRef: properties: key: type: string name: type: string optional: type: boolean type: object type: object type: object type: array envFrom: items: properties: configMapRef: properties: name: type: string optional: type: boolean type: object prefix: type: string secretRef: properties: name: type: string optional: type: boolean type: object type: object type: array image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object type: object preStop: properties: exec: properties: command: items: type: string type: array type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object type: object type: object livenessProbe: properties: exec: properties: command: items: type: string type: array type: object failureThreshold: type: integer grpc: properties: port: type: integer service: type: string type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object initialDelaySeconds: type: integer periodSeconds: type: integer successThreshold: type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object terminationGracePeriodSeconds: type: integer timeoutSeconds: type: integer type: object name: type: string ports: items: properties: containerPort: type: integer hostIP: type: string hostPort: type: integer name: type: string protocol: type: string type: object type: array readinessProbe: properties: exec: properties: command: items: type: string type: array type: object failureThreshold: type: integer grpc: properties: port: type: integer service: type: string type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object initialDelaySeconds: type: integer periodSeconds: type: integer successThreshold: type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object terminationGracePeriodSeconds: type: integer timeoutSeconds: type: integer type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object type: object securityContext: properties: allowPrivilegeEscalation: type: boolean capabilities: properties: add: items: type: string type: array drop: items: type: string type: array type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: type: integer runAsNonRoot: type: boolean runAsUser: type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array type: object failureThreshold: type: integer grpc: properties: port: type: integer service: type: string type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object initialDelaySeconds: type: integer periodSeconds: type: integer successThreshold: type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object terminationGracePeriodSeconds: type: integer timeoutSeconds: type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string type: object type: array volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean subPath: type: string subPathExpr: type: string type: object type: array workingDir: type: string type: object type: array dnsConfig: properties: nameservers: items: type: string type: array options: items: properties: name: type: string value: type: string type: object type: array searches: items: type: string type: array type: object dnsPolicy: type: string enableServiceLinks: type: boolean ephemeralContainers: items: properties: args: items: type: string type: array command: items: type: string type: array env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: type: string optional: type: boolean type: object fieldRef: properties: apiVersion: type: string fieldPath: type: string type: object resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true resource: type: string type: object secretKeyRef: properties: key: type: string name: type: string optional: type: boolean type: object type: object type: object type: array envFrom: items: properties: configMapRef: properties: name: type: string optional: type: boolean type: object prefix: type: string secretRef: properties: name: type: string optional: type: boolean type: object type: object type: array image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object type: object preStop: properties: exec: properties: command: items: type: string type: array type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object type: object type: object livenessProbe: properties: exec: properties: command: items: type: string type: array type: object failureThreshold: type: integer grpc: properties: port: type: integer service: type: string type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object initialDelaySeconds: type: integer periodSeconds: type: integer successThreshold: type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object terminationGracePeriodSeconds: type: integer timeoutSeconds: type: integer type: object name: type: string ports: items: properties: containerPort: type: integer hostIP: type: string hostPort: type: integer name: type: string protocol: type: string type: object type: array readinessProbe: properties: exec: properties: command: items: type: string type: array type: object failureThreshold: type: integer grpc: properties: port: type: integer service: type: string type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object initialDelaySeconds: type: integer periodSeconds: type: integer successThreshold: type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object terminationGracePeriodSeconds: type: integer timeoutSeconds: type: integer type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object type: object securityContext: properties: allowPrivilegeEscalation: type: boolean capabilities: properties: add: items: type: string type: array drop: items: type: string type: array type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: type: integer runAsNonRoot: type: boolean runAsUser: type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array type: object failureThreshold: type: integer grpc: properties: port: type: integer service: type: string type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object initialDelaySeconds: type: integer periodSeconds: type: integer successThreshold: type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object terminationGracePeriodSeconds: type: integer timeoutSeconds: type: integer type: object stdin: type: boolean stdinOnce: type: boolean targetContainerName: type: string terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string type: object type: array volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean subPath: type: string subPathExpr: type: string type: object type: array workingDir: type: string type: object type: array hostAliases: items: properties: hostnames: items: type: string type: array ip: type: string type: object type: array hostIPC: type: boolean hostNetwork: type: boolean hostPID: type: boolean hostname: type: string imagePullSecrets: items: properties: name: type: string type: object type: array initContainers: items: properties: args: items: type: string type: array command: items: type: string type: array env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: type: string optional: type: boolean type: object fieldRef: properties: apiVersion: type: string fieldPath: type: string type: object resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true resource: type: string type: object secretKeyRef: properties: key: type: string name: type: string optional: type: boolean type: object type: object type: object type: array envFrom: items: properties: configMapRef: properties: name: type: string optional: type: boolean type: object prefix: type: string secretRef: properties: name: type: string optional: type: boolean type: object type: object type: array image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object type: object preStop: properties: exec: properties: command: items: type: string type: array type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object type: object type: object livenessProbe: properties: exec: properties: command: items: type: string type: array type: object failureThreshold: type: integer grpc: properties: port: type: integer service: type: string type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object initialDelaySeconds: type: integer periodSeconds: type: integer successThreshold: type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object terminationGracePeriodSeconds: type: integer timeoutSeconds: type: integer type: object name: type: string ports: items: properties: containerPort: type: integer hostIP: type: string hostPort: type: integer name: type: string protocol: type: string type: object type: array readinessProbe: properties: exec: properties: command: items: type: string type: array type: object failureThreshold: type: integer grpc: properties: port: type: integer service: type: string type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object initialDelaySeconds: type: integer periodSeconds: type: integer successThreshold: type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object terminationGracePeriodSeconds: type: integer timeoutSeconds: type: integer type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object type: object securityContext: properties: allowPrivilegeEscalation: type: boolean capabilities: properties: add: items: type: string type: array drop: items: type: string type: array type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: type: integer runAsNonRoot: type: boolean runAsUser: type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array type: object failureThreshold: type: integer grpc: properties: port: type: integer service: type: string type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string type: object type: array path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string type: object initialDelaySeconds: type: integer periodSeconds: type: integer successThreshold: type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object terminationGracePeriodSeconds: type: integer timeoutSeconds: type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string type: object type: array volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean subPath: type: string subPathExpr: type: string type: object type: array workingDir: type: string type: object type: array nodeName: type: string nodeSelector: additionalProperties: type: string type: object os: properties: name: type: string type: object overhead: additionalProperties: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object preemptionPolicy: type: string priority: type: integer priorityClassName: type: string readinessGates: items: properties: conditionType: type: string type: object type: array restartPolicy: type: string runtimeClassName: type: string schedulerName: type: string securityContext: properties: fsGroup: type: integer fsGroupChangePolicy: type: string runAsGroup: type: integer runAsNonRoot: type: boolean runAsUser: type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string type: object supplementalGroups: items: type: integer type: array sysctls: items: properties: name: type: string value: type: string type: object type: array windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object serviceAccount: type: string serviceAccountName: type: string setHostnameAsFQDN: type: boolean shareProcessNamespace: type: boolean subdomain: type: string terminationGracePeriodSeconds: type: integer tolerations: items: properties: effect: type: string key: type: string operator: type: string tolerationSeconds: type: integer value: type: string type: object type: array topologySpreadConstraints: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchLabels: additionalProperties: type: string type: object type: object maxSkew: type: integer minDomains: type: integer topologyKey: type: string whenUnsatisfiable: type: string type: object type: array volumes: items: properties: awsElasticBlockStore: properties: fsType: type: string partition: type: integer readOnly: type: boolean volumeID: type: string type: object azureDisk: properties: cachingMode: type: string diskName: type: string diskURI: type: string fsType: type: string kind: type: string readOnly: type: boolean type: object azureFile: properties: readOnly: type: boolean secretName: type: string shareName: type: string type: object cephfs: properties: monitors: items: type: string type: array path: type: string readOnly: type: boolean secretFile: type: string secretRef: properties: name: type: string type: object user: type: string type: object cinder: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: type: string type: object volumeID: type: string type: object configMap: properties: defaultMode: type: integer items: items: properties: key: type: string mode: type: integer path: type: string type: object type: array name: type: string optional: type: boolean type: object csi: properties: driver: type: string fsType: type: string nodePublishSecretRef: properties: name: type: string type: object readOnly: type: boolean volumeAttributes: additionalProperties: type: string type: object type: object downwardAPI: properties: defaultMode: type: integer items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string type: object mode: type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true resource: type: string type: object type: object type: array type: object emptyDir: properties: medium: type: string sizeLimit: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object ephemeral: properties: volumeClaimTemplate: properties: metadata: properties: annotations: additionalProperties: type: string type: object clusterName: type: string creationTimestamp: type: string deletionGracePeriodSeconds: type: integer deletionTimestamp: type: string finalizers: items: type: string type: array generateName: type: string generation: type: integer labels: additionalProperties: type: string type: object managedFields: items: properties: apiVersion: type: string fieldsType: type: string fieldsV1: type: object manager: type: string operation: type: string subresource: type: string time: type: string type: object type: array name: type: string namespace: type: string ownerReferences: items: properties: apiVersion: type: string blockOwnerDeletion: type: boolean controller: type: boolean kind: type: string name: type: string uid: type: string type: object type: array resourceVersion: type: string selfLink: type: string uid: type: string type: object spec: properties: accessModes: items: type: string type: array dataSource: properties: apiGroup: type: string kind: type: string name: type: string type: object dataSourceRef: properties: apiGroup: type: string kind: type: string name: type: string type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true type: object type: object selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array type: object type: array matchLabels: additionalProperties: type: string type: object type: object storageClassName: type: string volumeMode: type: string volumeName: type: string type: object type: object type: object fc: properties: fsType: type: string lun: type: integer readOnly: type: boolean targetWWNs: items: type: string type: array wwids: items: type: string type: array type: object flexVolume: properties: driver: type: string fsType: type: string options: additionalProperties: type: string type: object readOnly: type: boolean secretRef: properties: name: type: string type: object type: object flocker: properties: datasetName: type: string datasetUUID: type: string type: object gcePersistentDisk: properties: fsType: type: string partition: type: integer pdName: type: string readOnly: type: boolean type: object gitRepo: properties: directory: type: string repository: type: string revision: type: string type: object glusterfs: properties: endpoints: type: string path: type: string readOnly: type: boolean type: object hostPath: properties: path: type: string type: type: string type: object iscsi: properties: chapAuthDiscovery: type: boolean chapAuthSession: type: boolean fsType: type: string initiatorName: type: string iqn: type: string iscsiInterface: type: string lun: type: integer portals: items: type: string type: array readOnly: type: boolean secretRef: properties: name: type: string type: object targetPortal: type: string type: object name: type: string nfs: properties: path: type: string readOnly: type: boolean server: type: string type: object persistentVolumeClaim: properties: claimName: type: string readOnly: type: boolean type: object photonPersistentDisk: properties: fsType: type: string pdID: type: string type: object portworxVolume: properties: fsType: type: string readOnly: type: boolean volumeID: type: string type: object projected: properties: defaultMode: type: integer sources: items: properties: configMap: properties: items: items: properties: key: type: string mode: type: integer path: type: string type: object type: array name: type: string optional: type: boolean type: object downwardAPI: properties: items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string type: object mode: type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true resource: type: string type: object type: object type: array type: object secret: properties: items: items: properties: key: type: string mode: type: integer path: type: string type: object type: array name: type: string optional: type: boolean type: object serviceAccountToken: properties: audience: type: string expirationSeconds: type: integer path: type: string type: object type: object type: array type: object quobyte: properties: group: type: string readOnly: type: boolean registry: type: string tenant: type: string user: type: string volume: type: string type: object rbd: properties: fsType: type: string image: type: string keyring: type: string monitors: items: type: string type: array pool: type: string readOnly: type: boolean secretRef: properties: name: type: string type: object user: type: string type: object scaleIO: properties: fsType: type: string gateway: type: string protectionDomain: type: string readOnly: type: boolean secretRef: properties: name: type: string type: object sslEnabled: type: boolean storageMode: type: string storagePool: type: string system: type: string volumeName: type: string type: object secret: properties: defaultMode: type: integer items: items: properties: key: type: string mode: type: integer path: type: string type: object type: array optional: type: boolean secretName: type: string type: object storageos: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: type: string type: object volumeName: type: string volumeNamespace: type: string type: object vsphereVolume: properties: fsType: type: string storagePolicyID: type: string storagePolicyName: type: string volumePath: type: string type: object type: object type: array type: object status: properties: conditions: items: properties: lastProbeTime: type: string lastTransitionTime: type: string message: type: string reason: type: string status: type: string type: type: string type: object type: array containerStatuses: items: properties: containerID: type: string image: type: string imageID: type: string lastState: properties: running: properties: startedAt: type: string type: object terminated: properties: containerID: type: string exitCode: type: integer finishedAt: type: string message: type: string reason: type: string signal: type: integer startedAt: type: string type: object waiting: properties: message: type: string reason: type: string type: object type: object name: type: string ready: type: boolean restartCount: type: integer started: type: boolean state: properties: running: properties: startedAt: type: string type: object terminated: properties: containerID: type: string exitCode: type: integer finishedAt: type: string message: type: string reason: type: string signal: type: integer startedAt: type: string type: object waiting: properties: message: type: string reason: type: string type: object type: object type: object type: array ephemeralContainerStatuses: items: properties: containerID: type: string image: type: string imageID: type: string lastState: properties: running: properties: startedAt: type: string type: object terminated: properties: containerID: type: string exitCode: type: integer finishedAt: type: string message: type: string reason: type: string signal: type: integer startedAt: type: string type: object waiting: properties: message: type: string reason: type: string type: object type: object name: type: string ready: type: boolean restartCount: type: integer started: type: boolean state: properties: running: properties: startedAt: type: string type: object terminated: properties: containerID: type: string exitCode: type: integer finishedAt: type: string message: type: string reason: type: string signal: type: integer startedAt: type: string type: object waiting: properties: message: type: string reason: type: string type: object type: object type: object type: array hostIP: type: string initContainerStatuses: items: properties: containerID: type: string image: type: string imageID: type: string lastState: properties: running: properties: startedAt: type: string type: object terminated: properties: containerID: type: string exitCode: type: integer finishedAt: type: string message: type: string reason: type: string signal: type: integer startedAt: type: string type: object waiting: properties: message: type: string reason: type: string type: object type: object name: type: string ready: type: boolean restartCount: type: integer started: type: boolean state: properties: running: properties: startedAt: type: string type: object terminated: properties: containerID: type: string exitCode: type: integer finishedAt: type: string message: type: string reason: type: string signal: type: integer startedAt: type: string type: object waiting: properties: message: type: string reason: type: string type: object type: object type: object type: array message: type: string nominatedNodeName: type: string phase: type: string podIP: type: string podIPs: items: properties: ip: type: string type: object type: array qosClass: type: string reason: type: string startTime: type: string type: object type: object type: object status: type: object type: object served: true storage: true subresources: status: {} ================================================ FILE: agent-operator/deploy/crds/example.agentpool.yml ================================================ ## Concord Agent Pool definition ## ## "%%var%%" are template variables which will be replaces by the operator ## ## %%configMapName%% - name of the ConfigMap resource used by the Agent's pod ## %%podName%% - Agent's pod name ## %%app%% - the operator's label ## %%poolName%% - name of the pool, the operator automatically uses the CR's name as the pool name ## %%concordCfgHash%% - configuration hash ## %%preStopHook%% - content of the preStop script apiVersion: concord.walmartlabs.com/v1alpha1 kind: AgentPool metadata: name: example spec: queueSelector: agent: flavor: "k8s-test" autoScale: false minSize: 1 maxSize: 10 size: 1 configMap: apiVersion: v1 kind: ConfigMap metadata: name: "%%configMapName%%" data: mvn.json: | { "repositories": [ { "id": "central", "url": "https://repo.maven.apache.org/maven2/" } ] } agent.conf: | concord-agent { capabilities = { flavor = "k8s-test" k8s { cluster = "minikube" namespace = "default" pod = ${MY_POD_NAME} } } workersCount = 1 server { apiBaseUrl = "http://11.12.13.14:8001" websocketUrl = "ws://11.12.13.14:8001/websocket" readTimeout = "10 minutes" } } preStopHook.sh: "%%preStopHook%%" pod: apiVersion: v1 kind: Pod metadata: name: "%%podName%%" labels: app: "%%app%%" poolName: "%%poolName%%" concordCfgHash: "%%concordCfgHash%%" spec: terminationGracePeriodSeconds: 3600 containers: - name: dind image: "docker:dind" args: [ "-H tcp://0.0.0.0:6666" ] resources: requests: cpu: 1 memory: "2G" ephemeral-storage: "2G" limits: cpu: 2 memory: "3G" ephemeral-storage: "3G" volumeMounts: - name: "process-tmp" mountPath: "/tmp" - mountPath: "/hooks/preStopHook.sh" name: cfg subPath: preStopHook.sh securityContext: privileged: true lifecycle: preStop: exec: command: - "sh" - "/hooks/preStopHook.sh" - name: agent image: "walmartlabs/concord-agent:latest" imagePullPolicy: Never volumeMounts: - mountPath: "/opt/concord/conf/agent.conf" name: cfg subPath: agent.conf - mountPath: "/opt/concord/conf/mvn.json" name: cfg subPath: mvn.json - mountPath: "/opt/concord/hooks/preStopHook.sh" name: cfg subPath: preStopHook.sh - mountPath: "/tmp" name: process-tmp resources: requests: cpu: 1 memory: "1G" limits: cpu: 2 memory: "2G" env: - name: CONCORD_TMP_DIR value: "/tmp/concord" - name: CONCORD_DOCKER_LOCAL_MODE value: "false" - name: DOCKER_HOST value: "tcp://localhost:6666" - name: CONCORD_CFG_FILE value: "/opt/concord/conf/agent.conf" - name: CONCORD_MAVEN_CFG value: "/opt/concord/conf/mvn.json" - name: MY_POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: MY_POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIP - name: USER_AGENT value: "k8s-agent $(MY_POD_NAMESPACE)/$(MY_POD_NAME) @ $(MY_POD_IP)" lifecycle: preStop: exec: command: - "/bin/bash" - "/opt/concord/hooks/preStopHook.sh" volumes: - name: cfg configMap: name: "%%configMapName%%" - name: process-tmp emptyDir: { } ================================================ FILE: agent-operator/deploy/operator.yml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: concord-agent-operator spec: replicas: 1 selector: matchLabels: name: concord-agent-operator template: metadata: labels: name: concord-agent-operator spec: serviceAccountName: concord-agent-operator containers: - name: concord-agent-operator image: "walmartlabs/concord-agent-operator:latest" imagePullPolicy: Never env: - name: WATCH_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: SCALE_UP_DELAY_MS value: "15000" - name: SCALE_DOWN_DELAY_MS value: "180000" - name: INCREMENT_PERCENTAGE value: "50" - name: DECREMENT_PERCENTAGE value: "10" - name: INCREMENT_THRESHOLD_FACTOR value: "1.5" - name: DECREMENT_THRESHOLD_FACTOR value: "1.0" - name: OPERATOR_NAME value: "concord-agent-operator" - name: CONCORD_BASE_URL value: "http://host.minikube.internal:8001" # replace with local IP - name: CONCORD_API_TOKEN value: "...API token..." # replace with an actual Concord API token ================================================ FILE: agent-operator/deploy/service_account.yml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: concord-agent-operator namespace: default ================================================ FILE: agent-operator/pom.xml ================================================ 4.0.0 com.walmartlabs.concord parent 2.40.1-SNAPSHOT ../pom.xml com.walmartlabs.concord.k8s concord-agent-operator jar ${project.groupId}:${project.artifactId} com.walmartlabs.concord concord-common io.fabric8 kubernetes-client io.fabric8 kubernetes-model-apiextensions io.fabric8 kubernetes-model-core io.fabric8 kubernetes-model-common org.bouncycastle bcprov-jdk18on org.bouncycastle bcpkix-jdk18on org.bouncycastle bcutil-jdk18on com.fasterxml.jackson.core jackson-core com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.core jackson-annotations com.google.guava guava org.slf4j slf4j-api ch.qos.logback logback-classic javax.xml.bind jaxb-api com.sun.xml.bind jaxb-impl org.immutables value compile org.junit.jupiter junit-jupiter-api test org.apache.maven.plugins maven-shade-plugin package shade operator true uber com.walmartlabs.concord.agentoperator.Operator *:* META-INF/*.SF META-INF/*.RSA ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/HashUtils.java ================================================ package com.walmartlabs.concord.agentoperator; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; import javax.xml.bind.DatatypeConverter; import java.io.IOException; import java.nio.charset.StandardCharsets; public final class HashUtils { private static final ObjectMapper objectMapper = new ObjectMapper() .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); public static String hashAsHexString(Object v) throws IOException { String s = objectMapper.writeValueAsString(v); HashCode hc = Hashing.sha1().hashString(s, StandardCharsets.UTF_8); return DatatypeConverter.printHexBinary(hc.asBytes()); } private HashUtils() { } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/Operator.java ================================================ package com.walmartlabs.concord.agentoperator; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.crd.AgentPool; import com.walmartlabs.concord.agentoperator.crd.AgentPoolList; import com.walmartlabs.concord.agentoperator.scheduler.AutoScalerFactory; import com.walmartlabs.concord.agentoperator.scheduler.Scheduler; import io.fabric8.kubernetes.client.DefaultKubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; import java.util.concurrent.Executors; import static com.walmartlabs.concord.agentoperator.scheduler.Event.Type.DELETED; import static com.walmartlabs.concord.agentoperator.scheduler.Event.Type.MODIFIED; public class Operator { private static final Logger log = LoggerFactory.getLogger(Operator.class); private static final long RESYNC_PERIOD = Duration.ofSeconds(10).toMillis(); public static void main(String[] args) { var namespace = getEnv("WATCH_NAMESPACE", "default"); var concordBaseUrl = getEnv("CONCORD_BASE_URL", "http://192.168.99.1:8001"); // use minikube/vbox host's default address var concordApiToken = getEnv("CONCORD_API_TOKEN", null); var useMaintenanceMode = Boolean.parseBoolean(getEnv("USE_AGENT_MAINTENANCE_MODE", "false")); var k8sClient = new DefaultKubernetesClient().inNamespace(namespace); var executor = Executors.newSingleThreadExecutor(); var autoScalerFactory = new AutoScalerFactory(concordBaseUrl, concordApiToken, k8sClient); var scheduler = new Scheduler(autoScalerFactory, k8sClient, useMaintenanceMode); var handler = new ResourceEventHandler() { @Override public void onAdd(AgentPool resource) { executor.submit(() -> scheduler.onEvent(MODIFIED, resource)); } @Override public void onUpdate(AgentPool oldResource, AgentPool newResource) { if (oldResource == newResource) { return; } executor.submit(() -> scheduler.onEvent(MODIFIED, newResource)); } @Override public void onDelete(AgentPool resource, boolean deletedFinalStateUnknown) { executor.submit(() -> scheduler.onEvent(DELETED, resource)); } }; var informer = k8sClient.resources(AgentPool.class, AgentPoolList.class) .inAnyNamespace() .inform(handler, RESYNC_PERIOD); scheduler.start(); try { informer.run(); } catch (Exception e) { log.error("Error while watching for CRs (namespace={})", namespace, e); System.exit(2); } log.info("main -> and so my watch begins... (namespace={})", namespace); } private static String getEnv(String key, String defaultValue) { String s = System.getenv(key); if (s == null) { return defaultValue; } return s; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/PodLabels.java ================================================ package com.walmartlabs.concord.agentoperator; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; public final class PodLabels { private static final Logger log = LoggerFactory.getLogger(PodLabels.class); public static void applyTag(KubernetesClient client, String podName, String tagName, String tagValue) { Pod pod = client.pods().withName(podName).get(); if (pod == null) { log.warn("['{}']: apply tag ['{}': '{}'] -> pod doesn't exist, nothing to do", podName, tagName, tagValue); return; } Map labels = pod.getMetadata().getLabels(); if (labels.containsKey(tagName)) { return; } try { labels.put(tagName, tagValue); client.pods().withName(podName).patch(pod); log.info("['{}']: apply tag ['{}': '{}'] -> done", podName, tagName, tagValue); } catch (KubernetesClientException e) { if (e.getCode() == 404) { log.warn("['{}']: apply tag ['{}': '{}'] -> pod doesn't exist, nothing to do", podName, tagName, tagValue); } else { log.warn("['{}']: apply tag ['{}': '{}'] -> error", podName, tagName, tagValue, e); } } } private PodLabels() { } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/agent/AgentClient.java ================================================ package com.walmartlabs.concord.agentoperator.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public interface AgentClient { void enableMaintenanceMode() throws Exception; boolean hasBusyWorkers() throws Exception; } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/agent/AgentClientFactory.java ================================================ package com.walmartlabs.concord.agentoperator.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import io.fabric8.kubernetes.api.model.Pod; import java.net.http.HttpClient; import java.time.Duration; import static java.net.http.HttpClient.Redirect.NEVER; public class AgentClientFactory { private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10); private final boolean useMaintenanceMode; private final HttpClient httpClient; public AgentClientFactory(boolean useMaintenanceMode) { this.useMaintenanceMode = useMaintenanceMode; if (useMaintenanceMode) { this.httpClient = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(NEVER) .connectTimeout(CONNECT_TIMEOUT) .build(); } else { httpClient = null; } } public AgentClient create(Pod pod) { if (useMaintenanceMode && isRunning(pod) && hasIP(pod)) { return new DefaultAgentClient(httpClient, pod.getStatus().getPodIP()); } else { return new NopAgentClient(); } } private static boolean isRunning(Pod pod) { return pod.getStatus() != null && "Running".equals(pod.getStatus().getPhase()); } private static boolean hasIP(Pod pod) { return pod.getStatus() != null && pod.getStatus().getPodIP() != null; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/agent/DefaultAgentClient.java ================================================ package com.walmartlabs.concord.agentoperator.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.immutables.value.Value; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class DefaultAgentClient implements AgentClient { private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10); private final ObjectMapper objectMapper = new ObjectMapper(); private final HttpClient client; private final String url; public DefaultAgentClient(HttpClient client, String podIp) { this.client = client; this.url = "http://%s:8010/maintenance-mode".formatted(podIp); } @Override public void enableMaintenanceMode() throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .POST(HttpRequest.BodyPublishers.noBody()) .timeout(REQUEST_TIMEOUT) .build(); client.send(request, HttpResponse.BodyHandlers.ofString()); } @Override public boolean hasBusyWorkers() throws Exception { HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .GET() .timeout(REQUEST_TIMEOUT) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); MaintenanceMode entity = objectMapper.readValue(response.body(), MaintenanceMode.class); return entity.maintenanceMode() && entity.workersAlive() > 0; } @Value.Immutable @JsonSerialize(as = ImmutableMaintenanceMode.class) @JsonDeserialize(as = ImmutableMaintenanceMode.class) public interface MaintenanceMode { boolean maintenanceMode(); int workersAlive(); } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/agent/NopAgentClient.java ================================================ package com.walmartlabs.concord.agentoperator.agent; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public class NopAgentClient implements AgentClient { @Override public void enableMaintenanceMode() { // do nothing } @Override public boolean hasBusyWorkers() { return false; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/crd/AgentPool.java ================================================ package com.walmartlabs.concord.agentoperator.crd; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import io.fabric8.kubernetes.api.model.KubernetesResource; import io.fabric8.kubernetes.api.model.Namespaced; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.*; @Version(AgentPool.VERSION) @Group(AgentPool.CONCORD_GROUP) @Singular(AgentPool.SERVICE_SINGULAR_NAME) @Plural(AgentPool.SERVICE_PLURAL_NAME) @ShortNames("aps") @Kind(AgentPool.SERVICE_KIND) public class AgentPool extends CustomResource implements Namespaced { public static final String VERSION = "v1alpha1"; private static final long serialVersionUID = 1L; public static final String CONCORD_GROUP = "concord.walmartlabs.com"; public static final String SERVICE_KIND = "AgentPool"; public static final String SERVICE_LIST_KIND = "AgentPoolList"; public static final String SERVICE_SINGULAR_NAME = "agentpool"; public static final String SERVICE_PLURAL_NAME = "agentpools"; public static final String SERVICE_FULL_NAME = SERVICE_PLURAL_NAME + "." + CONCORD_GROUP; } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/crd/AgentPoolConfiguration.java ================================================ package com.walmartlabs.concord.agentoperator.crd; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.scheduler.DefaultAutoScaler; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Pod; import java.io.Serializable; import java.util.Map; public class AgentPoolConfiguration implements Serializable { private static final long serialVersionUID = 1L; private static final long DEFAULT_SCALE_UP_DELAY_MS = 30000; private static final long DEFAULT_SCALE_DOWN_DELAY_MS = 180000; private static final String ENV_SCALE_UP_DELAY_MS = "SCALE_UP_DELAY_MS"; private static final String ENV_SCALE_DOWN_DELAY_MS = "SCALE_DOWN_DELAY_MS"; private static final String ENV_INCREMENT_PERCENTAGE = "INCREMENT_PERCENTAGE"; private static final String ENV_DECREMENT_PERCENTAGE = "DECREMENT_PERCENTAGE"; private static final String ENV_INCREMENT_THRESHOLD_FACTOR = "INCREMENT_THRESHOLD_FACTOR"; private static final String ENV_DECREMENT_THRESHOLD_FACTOR = "DECREMENT_THRESHOLD_FACTOR"; private static final int DEFAULT_MAX_SIZE = 10; private static final int DEFAULT_MIN_SIZE = 1; private static final int DEFAULT_SIZE = 1; private static final int DEFAULT_SIZE_INCREMENT = 1; private static final double DEFAULT_INCREMENT_THRESHOLD_FACTOR = 1.5; private static final double DEFAULT_DECREMENT_THRESHOLD_FACTOR = 1.0; private static final double DEFAULT_INCREMENT_PERCENTAGE = 50; private static final double DEFAULT_DECREMENT_PERCENTAGE = 10; private static final int DEFAULT_QUEUE_QUERY_LIMIT = 300; private boolean autoScale = true; private int maxSize = DEFAULT_MAX_SIZE; private int minSize = DEFAULT_MIN_SIZE; private int size = DEFAULT_SIZE; private String autoScaleStrategy = DefaultAutoScaler.NAME; private int sizeIncrement = DEFAULT_SIZE_INCREMENT; private int queueQueryLimit = DEFAULT_QUEUE_QUERY_LIMIT; /** * Minimum time that should elapse between one scale up operation to the next */ private long scaleUpDelayMs = getLongFromEnv(ENV_SCALE_UP_DELAY_MS, DEFAULT_SCALE_UP_DELAY_MS); /** * Minimum time that should elapse between one scale down operation to the next */ private long scaleDownDelayMs = getLongFromEnv(ENV_SCALE_DOWN_DELAY_MS, DEFAULT_SCALE_DOWN_DELAY_MS); /** * Percentage of current pool size by which the poolsize has to be increased */ private double percentIncrement = getDoubleFromEnv(ENV_INCREMENT_PERCENTAGE, DEFAULT_INCREMENT_PERCENTAGE); /** * Percentage of current pool size by which the poolsize has to be decreased */ private double percentDecrement = getDoubleFromEnv(ENV_DECREMENT_PERCENTAGE, DEFAULT_DECREMENT_PERCENTAGE); /** * Factor that determines the threshold above which the operator can scale up the agent pods */ private double incrementThresholdFactor = getDoubleFromEnv(ENV_INCREMENT_THRESHOLD_FACTOR, DEFAULT_INCREMENT_THRESHOLD_FACTOR); /** * Factor that determines the threshold below which the operator can scale down the agent pods. */ private double decrementThresholdFactor = getDoubleFromEnv(ENV_DECREMENT_THRESHOLD_FACTOR, DEFAULT_DECREMENT_THRESHOLD_FACTOR); private Map queueSelector; private ConfigMap configMap; private Pod pod; public boolean isAutoScale() { return autoScale; } public int getSizeIncrement() { return sizeIncrement; } public void setSizeIncrement(int sizeIncrement) { this.sizeIncrement = sizeIncrement; } public void setAutoScale(boolean autoScale) { this.autoScale = autoScale; } public String getAutoScaleStrategy() { return autoScaleStrategy; } public void setAutoScaleStrategy(String autoScaleStrategy) { this.autoScaleStrategy = autoScaleStrategy; } public long getScaleUpDelayMs() { return scaleUpDelayMs; } public void setScaleUpDelayMs(long scaleUpDelayMs) { this.scaleUpDelayMs = scaleUpDelayMs; } public long getScaleDownDelayMs() { return scaleDownDelayMs; } public void setScaleDownDelayMs(long scaleDownDelayMs) { this.scaleDownDelayMs = scaleDownDelayMs; } public int getMaxSize() { return maxSize; } public void setMaxSize(int maxSize) { this.maxSize = maxSize; } public int getMinSize() { return minSize; } public void setMinSize(int minSize) { this.minSize = minSize; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } public Map getQueueSelector() { return queueSelector; } public void setQueueSelector(Map queueSelector) { this.queueSelector = queueSelector; } public double getPercentIncrement() { return percentIncrement; } public void setPercentIncrement(double percentIncrement) { this.percentIncrement = percentIncrement; } public double getPercentDecrement() { return percentDecrement; } public void setPercentDecrement(double percentDecrement) { this.percentDecrement = percentDecrement; } public double getIncrementThresholdFactor() { return incrementThresholdFactor; } public void setIncrementThresholdFactor(double incrementThresholdFactor) { this.incrementThresholdFactor = incrementThresholdFactor; } public double getDecrementThresholdFactor() { return decrementThresholdFactor; } public void setDecrementThresholdFactor(double decrementThresholdFactor) { this.decrementThresholdFactor = decrementThresholdFactor; } public int getQueueQueryLimit() { return queueQueryLimit; } public void setQueueQueryLimit(int queueQueryLimit) { this.queueQueryLimit = queueQueryLimit; } public ConfigMap getConfigMap() { return configMap; } public void setConfigMap(ConfigMap configMap) { this.configMap = configMap; } public Pod getPod() { return pod; } public void setPod(Pod pod) { this.pod = pod; } private static long getLongFromEnv(String key, Long defaultValue) { String envValue = System.getenv(key); return envValue != null ? Long.parseLong(envValue) : defaultValue; } private static double getDoubleFromEnv(String key, double defaultValue) { String envValue = System.getenv(key); return envValue != null ? Double.parseDouble(envValue) : defaultValue; } private static String getStringFromEnv(String key, String defaultValue) { String envValue = System.getenv(key); return envValue != null ? envValue : defaultValue; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/crd/AgentPoolList.java ================================================ package com.walmartlabs.concord.agentoperator.crd; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import io.fabric8.kubernetes.api.model.DefaultKubernetesResourceList; public class AgentPoolList extends DefaultKubernetesResourceList { } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/Change.java ================================================ package com.walmartlabs.concord.agentoperator.planner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import io.fabric8.kubernetes.client.KubernetesClient; public interface Change { void apply(KubernetesClient client); } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/CreateConfigMapChange.java ================================================ package com.walmartlabs.concord.agentoperator.planner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.resources.AgentConfigMap; import com.walmartlabs.concord.agentoperator.scheduler.AgentPoolInstance; import io.fabric8.kubernetes.client.KubernetesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; public class CreateConfigMapChange implements Change { private static final Logger log = LoggerFactory.getLogger(CreateConfigMapChange.class); private final AgentPoolInstance poolInstance; private final String configMapName; public CreateConfigMapChange(AgentPoolInstance poolInstance, String configMapName) { this.poolInstance = poolInstance; this.configMapName = configMapName; } @Override public void apply(KubernetesClient client) { try { AgentConfigMap.create(client, poolInstance, configMapName); log.info("apply -> created a configmap {}", configMapName); } catch (IOException e) { log.error("apply -> error while creating a configmap {}: {}", configMapName, e.getMessage()); } } @Override public String toString() { return "CreateConfigMapChange{" + "configMapName='" + configMapName + '\'' + '}'; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/CreatePodChange.java ================================================ package com.walmartlabs.concord.agentoperator.planner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.resources.AgentPod; import com.walmartlabs.concord.agentoperator.scheduler.AgentPoolInstance; import io.fabric8.kubernetes.client.KubernetesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; public class CreatePodChange implements Change { private static final Logger log = LoggerFactory.getLogger(CreatePodChange.class); private final AgentPoolInstance poolInstance; private final String podName; private final String configMapName; private final String hash; public CreatePodChange(AgentPoolInstance poolInstance, String podName, String configMapName, String hash) { this.poolInstance = poolInstance; this.podName = podName; this.configMapName = configMapName; this.hash = hash; } @Override public void apply(KubernetesClient client) { try { AgentPod.create(client, poolInstance, podName, configMapName, hash); log.info("apply -> created a pod {}/{}", poolInstance.getName(), podName); } catch (IOException e) { log.error("apply -> error while creating a pod {}/{}: {}", poolInstance.getName(), podName, e.getMessage()); } } @Override public String toString() { return "CreatePodChange{" + "podName='" + podName + '\'' + ", configMapName='" + configMapName + '\'' + ", hash='" + hash + '\'' + '}'; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/DeleteConfigMapChange.java ================================================ package com.walmartlabs.concord.agentoperator.planner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.resources.AgentConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DeleteConfigMapChange implements Change { private static final Logger log = LoggerFactory.getLogger(DeleteConfigMapChange.class); private final String configMapName; public DeleteConfigMapChange(String configMapName) { this.configMapName = configMapName; } @Override public void apply(KubernetesClient client) { AgentConfigMap.delete(client, configMapName); log.info("apply -> removed a configmap {}", configMapName); } @Override public String toString() { return "DeleteConfigMapChange{" + "configMapName='" + configMapName + '\'' + '}'; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/Planner.java ================================================ package com.walmartlabs.concord.agentoperator.planner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.HashUtils; import com.walmartlabs.concord.agentoperator.agent.AgentClient; import com.walmartlabs.concord.agentoperator.agent.AgentClientFactory; import com.walmartlabs.concord.agentoperator.resources.AgentConfigMap; import com.walmartlabs.concord.agentoperator.resources.AgentPod; import com.walmartlabs.concord.agentoperator.scheduler.AgentPoolInstance; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.client.KubernetesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class Planner { private static final Logger log = LoggerFactory.getLogger(Planner.class); private final KubernetesClient client; private final AgentClientFactory agentClientFactory; public Planner(KubernetesClient client, AgentClientFactory agentClientFactory) { this.client = client; this.agentClientFactory = agentClientFactory; } public List plan(AgentPoolInstance poolInstance) throws IOException { String resourceName = poolInstance.getName(); List changes = new ArrayList<>(); // process pods marked for removal first AgentPod.listMarkedForRemoval(client, resourceName) .forEach(n -> changes.add(new TryToDeletePodChange(n.getMetadata().getName(), agentClientFactory.create(n)))); List pods = AgentPod.list(client, resourceName); int currentSize = pods.size(); int currentNotMarkedForDeleteSize = (int) pods.stream() .filter(p -> !p.getMetadata().getLabels().containsKey(AgentPod.TAGGED_FOR_REMOVAL_LABEL)) .count(); // hash of the Agent Pod configuration, will be used to determine which resources should be updated String newHash = HashUtils.hashAsHexString(poolInstance.getResource().getSpec().getPod()); // calculate the configmap changes boolean recreateAllPods = false; String configMapName = configMapName(resourceName); ConfigMap m = AgentConfigMap.get(client, configMapName); int targetSize = poolInstance.getTargetSize(); log.info("plan ['{}'] -> currentSize = {}, currentNotMarkedForDeleteSize = {}, targetSize= {}, configMap = {}", resourceName, currentSize, currentNotMarkedForDeleteSize, targetSize, m != null); AgentPoolInstance.Status poolStatus = poolInstance.getStatus(); if (poolStatus == AgentPoolInstance.Status.DELETED) { targetSize = 0; } /* Set the flag to recreate all pods to true, when there is a change to Config Map in the AgentPool definition, or when a new ConfigMap is added. Delete the Config Map if there are no pods present in the pool. */ if (m == null) { if (targetSize > 0) { changes.add(new CreateConfigMapChange(poolInstance, configMapName)); recreateAllPods = true; } } else { if (targetSize <= 0 && currentSize == 0) { changes.add(new DeleteConfigMapChange(configMapName)); } else if (AgentConfigMap.hasChanges(client, poolInstance, m)) { changes.add(new DeleteConfigMapChange(configMapName)); changes.add(new CreateConfigMapChange(poolInstance, configMapName)); recreateAllPods = true; } } /* check all pods for cfg changes. Delete the pod only if there is a change in the Hash of Agent Pod, that is change to Pod definition in the AgentPool resource definition. Changes include change to the image, mem requirements, env variable changes, docker changes, or anything that requires the Agent Pod to be restarted. Do not delete and recreate all pods when there is a change to the agentpool sizes - min, max and current size. */ for (Pod p : pods) { String currentHash = p.getMetadata().getLabels().get(AgentPod.CONFIG_HASH_LABEL); if (!newHash.equals(currentHash)) { changes.add(new TagForRemovalChange(p.getMetadata().getName(), agentClientFactory.create(p))); } } // recreate all pods if the configmap changed if (recreateAllPods) { pods.forEach(p -> changes.add(new TagForRemovalChange(p.getMetadata().getName(), agentClientFactory.create(p)))); } // create or remove pods according to the configured pool size if (targetSize > currentSize) { Set podNames = pods.stream().map(p -> p.getMetadata().getName()).collect(Collectors.toSet()); for (int i = 0; i < targetSize - currentSize; i++) { String podName = generatePodName(resourceName, podNames); changes.add(new CreatePodChange(poolInstance, podName, configMapName(resourceName), newHash)); podNames.add(podName); } } if (currentNotMarkedForDeleteSize > targetSize) { int podsToDelete = currentNotMarkedForDeleteSize - targetSize; for (Pod pod : pods) { if (pod.getMetadata().getLabels().containsKey(AgentPod.TAGGED_FOR_REMOVAL_LABEL)) { continue; } String podName = pod.getMetadata().getName(); AgentClient agentClient = agentClientFactory.create(pod); changes.add(new TagForRemovalChange(podName, agentClient)); changes.add(new TryToDeletePodChange(podName, agentClient)); podsToDelete--; if (podsToDelete == 0) { break; } } } if (!changes.isEmpty()) { log.info("plan ['{}'] -> changes: {}", resourceName, changes); } return changes; } private static String generatePodName(String resourceName, Set podNames) { for (int i = 0; i < podNames.size() + 1; i++) { String podName = podName(resourceName, i); if (!podNames.contains(podName)) { return podName; } } throw new RuntimeException("Can't generate pod name for '" + resourceName + "', current pods: " + podNames); } private static String configMapName(String resourceName) { return resourceName + "-cfg"; } private static String podName(String resourceName, int i) { return String.format("%s-%05d", resourceName, i); } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/TagForRemovalChange.java ================================================ package com.walmartlabs.concord.agentoperator.planner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.agent.AgentClient; import com.walmartlabs.concord.agentoperator.PodLabels; import com.walmartlabs.concord.agentoperator.resources.AgentPod; import io.fabric8.kubernetes.client.KubernetesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TagForRemovalChange implements Change { private static final Logger log = LoggerFactory.getLogger(TagForRemovalChange.class); private final String podName; private final AgentClient agentClient; public TagForRemovalChange(String podName, AgentClient agentClient) { this.podName = podName; this.agentClient = agentClient; } @Override public void apply(KubernetesClient client) { try { agentClient.enableMaintenanceMode(); } catch (Exception e) { log.error("Error enabling maintenance mode for pod '{}'", podName, e); return; } PodLabels.applyTag(client, podName, AgentPod.TAGGED_FOR_REMOVAL_LABEL, "true"); } @Override public String toString() { return "TagForRemovalChange{" + "podName='" + podName + '\'' + '}'; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/planner/TryToDeletePodChange.java ================================================ package com.walmartlabs.concord.agentoperator.planner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.agent.AgentClient; import com.walmartlabs.concord.agentoperator.PodLabels; import com.walmartlabs.concord.agentoperator.resources.AgentPod; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.client.KubernetesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; public class TryToDeletePodChange implements Change { private static final Logger log = LoggerFactory.getLogger(TryToDeletePodChange.class); private final String podName; private final AgentClient agentClient; public TryToDeletePodChange(String podName, AgentClient agentClient) { this.podName = podName; this.agentClient = agentClient; } /** * When the agent pod is being deleted, Kubernetes calls the prestop hook configured. * The prestop hook script configured for agent container enables maintenance mode on the agent, * and waits for the number of workers in use to go to 0, before the pod gets terminated. *

* Whenever the scheduler calls this `apply` method, the following conditions are checked, and * corresponding actions are executed. *

* - If the pod has a label `preStopHookTermination: true`, do nothing and exit, as the pod is being * terminated, waiting for the prestop hook script to complete (that is, last running process on the agent * container to complete). *

* - Otherwise if the pod is in `RUNNING` phase, call the kubernetes client `delete` method on the agent pod, * which will put the pod in `Terminating` state and start executing the prestop hook script on the * agent container. Add the label `preStopHookTermination: true` (this will be checked on * subsequent executions). * * @param client instance of Kubernetes client */ @Override public void apply(KubernetesClient client) { Pod pod = client.pods().withName(podName).get(); if (pod == null) { log.warn("apply ['{}'] -> pod doesn't exist, nothing to do", podName); return; } Map labels = pod.getMetadata().getLabels(); if ("true".equals(labels.getOrDefault(AgentPod.PRE_STOP_HOOK_TERMINATION_LABEL, "false"))) { log.debug("['{}'] -> has already been marked for termination", podName); return; } try { if (agentClient.hasBusyWorkers()) { return; } } catch (Exception e) { log.error("Error while checking agent workers count for pod '{}'", podName, e); return; } // agent pod in maintenance mode and all workers done client.pods().withName(podName).delete(); PodLabels.applyTag(client, podName, AgentPod.PRE_STOP_HOOK_TERMINATION_LABEL, "true"); log.info("apply ['{}'] -> Marked for termination (former phase: {})", podName, pod.getStatus().getPhase()); } @Override public String toString() { return "TryToDeletePodChange{" + "podName='" + podName + '\'' + '}'; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/processqueue/ProcessQueueClient.java ================================================ package com.walmartlabs.concord.agentoperator.processqueue; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.escape.Escaper; import com.google.common.net.UrlEscapers; import com.walmartlabs.concord.agentoperator.scheduler.QueueSelector; import com.walmartlabs.concord.sdk.Constants; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.net.CookieManager; import java.net.CookiePolicy; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.util.List; public class ProcessQueueClient { private static final TypeReference> LIST_OF_PROCESS_QUEUE_ENTRIES = new TypeReference<>() { }; private final String baseUrl; private final String apiToken; private final ObjectMapper objectMapper; private final HttpClient client; public ProcessQueueClient(String baseUrl, String apiToken) { this.baseUrl = baseUrl; this.apiToken = apiToken; this.objectMapper = new ObjectMapper(); this.client = initClient(); } public List query(String processStatus, int limit, QueueSelector queueSelector) throws IOException { StringBuilder queryUrl = new StringBuilder(baseUrl + "/api/v2/process/requirements?status=" + processStatus + "&limit=" + limit + "&startAt.len="); String flavor = queueSelector.getFlavor(); if (flavor != null) { queryUrl.append("&requirements.agent.flavor.eq=").append(flavor); } List queryParams = queueSelector.getQueryParams(); if (queryParams != null) { for (String queryParam : queryParams) { queryUrl.append("&").append(escapeQueryParam(queryParam)); } } HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(queryUrl.toString())) .header("Authorization", apiToken) .header("User-Agent", "k8s-agent-operator") .header(Constants.Headers.ENABLE_HTTP_SESSION, "true") .GET() .build(); HttpResponse response; try { response = client.send(request, HttpResponse.BodyHandlers.ofString()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException("Interrupted while fetching the queue data", e); } if (response.statusCode() != 200) { throw new IOException("Error while fetching the process queue data: " + response.statusCode()); } return objectMapper.readValue(response.body(), LIST_OF_PROCESS_QUEUE_ENTRIES); } private static HttpClient initClient() { try { TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(X509Certificate[] certs, String authType) { } public void checkServerTrusted(X509Certificate[] certs, String authType) { } } }; SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(null, trustAllCerts, new SecureRandom()); CookieManager cookieManager = new CookieManager(); cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); return HttpClient.newBuilder() .sslContext(sslContext) .cookieHandler(cookieManager) .build(); } catch (NoSuchAlgorithmException | KeyManagementException e) { throw new RuntimeException("Error while initializing the HTTP client", e); } } @VisibleForTesting static String escapeQueryParam(String s) { Escaper escaper = UrlEscapers.urlPathSegmentEscaper(); int i = s.indexOf("="); if (i < 0) { return escaper.escape(s); } String key = s.substring(0, i); String value = s.substring(i + 1); return escaper.escape(key) + "=" + escaper.escape(value); } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/processqueue/ProcessQueueEntry.java ================================================ package com.walmartlabs.concord.agentoperator.processqueue; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; import java.util.Map; @JsonIgnoreProperties(ignoreUnknown = true) public class ProcessQueueEntry implements Serializable { private static final long serialVersionUID = 1L; private final Map requirements; @JsonCreator public ProcessQueueEntry(@JsonProperty("requirements") Map requirements) { this.requirements = requirements; } public Map getRequirements() { return requirements; } @Override public String toString() { return "ProcessQueueEntry{" + "requirements=" + requirements + '}'; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/resources/AgentConfigMap.java ================================================ package com.walmartlabs.concord.agentoperator.resources; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.walmartlabs.concord.agentoperator.HashUtils; import com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration; import com.walmartlabs.concord.agentoperator.scheduler.AgentPoolInstance; import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; public final class AgentConfigMap { private static final Logger log = LoggerFactory.getLogger(AgentConfigMap.class); private static final ObjectMapper objectMapper = new ObjectMapper(); public static ConfigMap get(KubernetesClient client, String configMapName) { try { return client.configMaps().withName(configMapName).get(); } catch (KubernetesClientException e) { log.warn("get ['{}'] -> error while getting a configmap: {}", configMapName, e.getMessage()); throw e; } } public static void create(KubernetesClient client, AgentPoolInstance poolInstance, String configMapName) throws IOException { try { ConfigMap m = prepare(client, poolInstance, configMapName); client.configMaps().resource(m).create(); } catch (KubernetesClientException e) { log.warn("create ['{}', '{}'] -> error while creating a configmap: {}", poolInstance.getName(), configMapName, e.getMessage()); throw e; } } public static void delete(KubernetesClient client, String configMapName) { try { client.configMaps().withName(configMapName).delete(); // wait till it's actually removed while (client.configMaps().withName(configMapName).get() != null) { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } } catch (KubernetesClientException e) { log.warn("delete ['{}'] -> error while deleting a configmap: {}", configMapName, e.getMessage()); throw e; } } public static boolean hasChanges(KubernetesClient client, AgentPoolInstance poolInstance, ConfigMap a) throws IOException { ConfigMap b = prepare(client, poolInstance, a.getMetadata().getName()); String hashA = HashUtils.hashAsHexString(a.getData()); String hashB = HashUtils.hashAsHexString(b.getData()); return !hashA.equals(hashB); } private static ConfigMap prepare(KubernetesClient client, AgentPoolInstance poolInstance, String configMapName) throws IOException { try { AgentPoolConfiguration spec = poolInstance.getResource().getSpec(); String configMapYaml = objectMapper.writeValueAsString(spec.getConfigMap()) .replaceAll("%%configMapName%%", configMapName) .replace("%%preStopHook%%", escape(Resources.get("/prestop-hook.sh"))); return client.configMaps().load(new ByteArrayInputStream(configMapYaml.getBytes())).item(); } catch (KubernetesClientException e) { log.warn("prepare ['{}', '{}'] -> error while preparing a configmap: {}", poolInstance.getName(), configMapName, e.getMessage()); throw e; } } private static String escape(String str) throws JsonProcessingException { String result = objectMapper.writeValueAsString(str); return result.substring(1, result.length() - 1); } private AgentConfigMap() { } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/resources/AgentPod.java ================================================ package com.walmartlabs.concord.agentoperator.resources; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.walmartlabs.concord.agentoperator.crd.AgentPool; import com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration; import com.walmartlabs.concord.agentoperator.scheduler.AgentPoolInstance; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.List; public final class AgentPod { private static final Logger log = LoggerFactory.getLogger(AgentPod.class); public static final String TAGGED_FOR_REMOVAL_LABEL = "concordTaggedForRemoval"; public static final String PRE_STOP_HOOK_TERMINATION_LABEL = "preStopHookTermination"; public static final String POOL_NAME_LABEL = "poolName"; public static final String CONFIG_HASH_LABEL = "concordCfgHash"; private static final ObjectMapper objectMapper = new ObjectMapper(); public static List listMarkedForRemoval(KubernetesClient client, String resourceName) { try { return client.pods() .withLabel(AgentPod.TAGGED_FOR_REMOVAL_LABEL) .withLabel(AgentPod.POOL_NAME_LABEL, resourceName) .list() .getItems(); } catch (KubernetesClientException e) { log.warn("listMarkedForRemoval ['{}'] -> error while listing marked for removal pods: {}", resourceName, e.getMessage()); throw e; } } public static List list(KubernetesClient client, String resourceName) { try { return client.pods() .withLabel(POOL_NAME_LABEL, resourceName) .list() .getItems(); } catch (KubernetesClientException e) { log.warn("list ['{}'] -> error while listing pool pods: {}", resourceName, e.getMessage()); throw e; } } public static void create(KubernetesClient client, AgentPoolInstance poolInstance, String podName, String configMapName, String hash) throws IOException { try { AgentPoolConfiguration spec = poolInstance.getResource().getSpec(); String podYaml = objectMapper.writeValueAsString(spec.getPod()) .replaceAll("%%podName%%", podName) .replaceAll("%%app%%", AgentPool.SERVICE_FULL_NAME) .replaceAll("%%" + POOL_NAME_LABEL + "%%", poolInstance.getName()) .replaceAll("%%configMapName%%", configMapName) .replaceAll("%%" + CONFIG_HASH_LABEL + "%%", hash); client.pods().load(new ByteArrayInputStream(podYaml.getBytes())).create(); } catch (KubernetesClientException e) { log.warn("create ['{}', '{}', '{}', '{}'] -> error while creating a pod: {}", poolInstance.getName(), podName, configMapName, hash, e.getMessage()); throw e; } } private AgentPod() { } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/resources/Resources.java ================================================ package com.walmartlabs.concord.agentoperator.resources; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.google.common.base.Charsets; import com.google.common.io.CharStreams; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public final class Resources { private static final Map resources = new ConcurrentHashMap<>(); public static String get(String name) { return resources.computeIfAbsent(name, Resources::load); } private static String load(String name) { try (InputStream in = Resources.class.getResourceAsStream(name)) { if (in == null) { throw new RuntimeException("Resource '" + name + "' not found"); } return CharStreams.toString(new InputStreamReader(in, Charsets.UTF_8)); } catch (IOException e) { throw new RuntimeException(e); } } private Resources() { } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/AgentPoolInstance.java ================================================ package com.walmartlabs.concord.agentoperator.scheduler; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.crd.AgentPool; public class AgentPoolInstance { public static AgentPoolInstance updateStatus(AgentPoolInstance i, Status status) { return new AgentPoolInstance(i.name, i.resource, status, i.targetSize, System.currentTimeMillis(), i.getLastScaleUpTimestamp(), i.getLastScaleDownTimeStamp()); } public static AgentPoolInstance updateTargetSize(AgentPoolInstance i, int targetSize, long scaleUptimeStamp, long scaleDownTimeStamp) { return new AgentPoolInstance(i.name, i.resource, i.status, targetSize, System.currentTimeMillis(), scaleUptimeStamp, scaleDownTimeStamp); } private final String name; private final AgentPool resource; private final Status status; private final int targetSize; private final long lastUpdateTimestamp; private final long lastScaleUpTimestamp; private final long lastScaleDownTimeStamp; public AgentPoolInstance(String name, AgentPool resource, Status status, int targetSize, long lastUpdateTimestamp, long lastScaleUpTimestamp, long lastScaleDownTimeStamp) { this.name = name; this.resource = resource; this.status = status; this.targetSize = targetSize; this.lastUpdateTimestamp = lastUpdateTimestamp; this.lastScaleUpTimestamp = lastScaleUpTimestamp; this.lastScaleDownTimeStamp = lastScaleDownTimeStamp; } public String getName() { return name; } public AgentPool getResource() { return resource; } public Status getStatus() { return status; } public int getTargetSize() { return targetSize; } public long getLastUpdateTimestamp() { return lastUpdateTimestamp; } public long getLastScaleUpTimestamp() { return lastScaleUpTimestamp; } public long getLastScaleDownTimeStamp() { return lastScaleDownTimeStamp; } public enum Status { ACTIVE, DELETED } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/AutoScaler.java ================================================ package com.walmartlabs.concord.agentoperator.scheduler; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.IOException; public interface AutoScaler { AgentPoolInstance apply(AgentPoolInstance i) throws IOException; } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/AutoScalerFactory.java ================================================ package com.walmartlabs.concord.agentoperator.scheduler; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration; import com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient; import com.walmartlabs.concord.agentoperator.resources.AgentPod; import io.fabric8.kubernetes.client.KubernetesClient; import java.util.function.Function; public class AutoScalerFactory { private final Function podCounter; private final ProcessQueueClient processQueueClient; public AutoScalerFactory(String concordBaseUrl, String concordApiToken, KubernetesClient k8sClient) { this.podCounter = n -> AgentPod.list(k8sClient, n).size(); this.processQueueClient = new ProcessQueueClient(concordBaseUrl, concordApiToken); } public AutoScaler create(AgentPoolInstance poolInstance) { AgentPoolConfiguration cfg = poolInstance.getResource().getSpec(); if (LinearAutoScaler.NAME.equals(cfg.getAutoScaleStrategy())) { return new LinearAutoScaler(processQueueClient, podCounter); } return new DefaultAutoScaler(processQueueClient, podCounter); } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/DefaultAutoScaler.java ================================================ package com.walmartlabs.concord.agentoperator.scheduler; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration; import com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient; import com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueEntry; import com.walmartlabs.concord.common.Matcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.function.Function; public class DefaultAutoScaler implements AutoScaler { public static final String NAME = "default"; private static final Logger log = LoggerFactory.getLogger(DefaultAutoScaler.class); private final ProcessQueueClient processQueueClient; private final Function podCounter; private final Function canBeScaledUp; private final Function canBeScaledDown; private long scaleUpTimeStamp; private long scaleDownTimeStamp; public DefaultAutoScaler(ProcessQueueClient processQueueClient, Function podCounter) { this(processQueueClient, podCounter, i -> { long t = System.currentTimeMillis(); return t - i.getLastScaleUpTimestamp() > i.getResource().getSpec().getScaleUpDelayMs(); }, i -> { long t = System.currentTimeMillis(); return t - i.getLastScaleDownTimeStamp() > i.getResource().getSpec().getScaleDownDelayMs(); }); } public DefaultAutoScaler(ProcessQueueClient processQueueClient, Function podCounter, Function canBeScaledUp, Function canBeScaledDown) { this.processQueueClient = processQueueClient; this.podCounter = podCounter; this.canBeScaledUp = canBeScaledUp; this.canBeScaledDown = canBeScaledDown; this.scaleUpTimeStamp = System.currentTimeMillis(); this.scaleDownTimeStamp = System.currentTimeMillis(); } /** * Scale up or Scale down the number of agent pods depending on various conditions *

* If enqueued process count is greater than the threshold defined for incrementing to max * pool size, increase the pool size to the maximum size. * Otherwise, if the enqueued process count is greater than the threshold defined for incrementing * the pool size, increase the pool size by the increment percentage defined *

* Decrease the pool size by the decrement percentage defined only if, * - enqueued process count is lesser than the minimum pool size threshold defined * - running process count is lesser than the threshold defined (which depends on current size of the pool * running the processes, and a constant factor specified - default to 1. Simplified to * runningCount < podsCount) * * @param i Agent pool on which the scaling activity is to be performed */ public AgentPoolInstance apply(AgentPoolInstance i) throws IOException { int queueQueryLimit = i.getResource().getSpec().getQueueQueryLimit(); QueueSelector queueSelector = QueueSelector.parse(i.getResource().getSpec().getQueueSelector()); List queueEntries = processQueueClient.query("ENQUEUED", queueQueryLimit, queueSelector); scaleUpTimeStamp = i.getLastScaleUpTimestamp(); scaleDownTimeStamp = i.getLastScaleDownTimeStamp(); AgentPoolConfiguration cfg = i.getResource().getSpec(); boolean canBeScaled = canBeScaledUp.apply(i) || canBeScaledDown.apply(i); if (!canBeScaled) { // was updated recently, skipping return i; } // count the currently running pods int podsCount = podCounter.apply(i.getName()); log.info("['{}']: Current pool size: {}", i.getName(), podsCount); // the number of processes waiting for an agent in the current pool int enqueuedCount = getProcessCount(cfg, queueEntries); log.info("['{}']: Enqueued process count: {}", i.getName(), enqueuedCount); if (podsCount < cfg.getMinSize()) { return AgentPoolInstance.updateTargetSize(i, cfg.getMinSize(), System.currentTimeMillis(), System.currentTimeMillis()); } // The threshold above which the operator can scale up the agent pods to the defined maximum pool size double maxPoolSizeThreshold = cfg.getMaxSize() * cfg.getIncrementThresholdFactor(); // The threshold above which the pool size can be increased by the increment percentage defined double incrementThreshold = cfg.getIncrementThresholdFactor() * podsCount; // The threshold, combined with threshold for running processes determine if the pool size can be // reduced by the decrement percentage defined double minPoolSizeThreshold = cfg.getDecrementThresholdFactor() * cfg.getMinSize(); // Initial target size of the agent pool before updation int targetSize = i.getTargetSize(); // Try scaling up if the time elapsed after last scale up operation // is greater than the scale up delay defined (default: 15s) if (canBeScaledUp.apply(i)) { targetSize = tryScaleUp(cfg, i, podsCount, enqueuedCount, targetSize, maxPoolSizeThreshold, incrementThreshold); // Reset scaledown delay counter if enqueued count is greater than min threshold. // Scale down should happen only if enqueued count is less than // min threshold consistently for scaledown delay defined (default: 180s) if (enqueuedCount >= minPoolSizeThreshold) { log.info("['{}']: Resetting scale down delay counter - (enqueued count({}) >= minimum threshold({}))...", i.getName(), enqueuedCount, minPoolSizeThreshold); scaleDownTimeStamp = System.currentTimeMillis(); } } // Try scaling down if the time elapsed after last scale down operation // is greater than the scale down delay defined (default: 180s) if (canBeScaledDown.apply(i)) { targetSize = tryScaleDown(cfg, i, podsCount, enqueuedCount, targetSize, minPoolSizeThreshold); } if (targetSize == i.getTargetSize()) { log.info("['{}']: Not changing the pool size.", i.getName()); } else { log.info("apply ['{}'] -> updated to {}", i.getName(), targetSize); } return AgentPoolInstance.updateTargetSize(i, targetSize, scaleUpTimeStamp, scaleDownTimeStamp); } private int tryScaleUp(AgentPoolConfiguration cfg, AgentPoolInstance i, int podsCount, int enqueuedCount, int poolSize, double maxPoolSizeThreshold, double incrementThreshold) { // To prevent scale up before previous scale down action is completed podsCount = Math.min(podsCount, i.getTargetSize()); // Reset scaleup delay counter for every attempt to scale up scaleUpTimeStamp = System.currentTimeMillis(); if (podsCount < cfg.getMaxSize()) { if (enqueuedCount >= maxPoolSizeThreshold) { poolSize = cfg.getMaxSize(); log.info("['{}']: Incrementing to max size - {}", i.getName(), poolSize); } else if (enqueuedCount >= incrementThreshold) { poolSize = (int) Math.round(podsCount * (1 + cfg.getPercentIncrement() / 100)); // Limit to maximum pool size if the computed target size is more than the max size if (poolSize > cfg.getMaxSize()) { log.warn("['{}']: Target pool size exceeds the allowed maximum: {} > {}. Updating to maximum size - {}", i.getName(), poolSize, cfg.getMaxSize(), cfg.getMaxSize()); poolSize = cfg.getMaxSize(); } log.info("['{}']: Scaling up to {}...", i.getName(), poolSize); } } else { log.warn("['{}']: Target pool size already the allowed maximum size: {}. Not updating.", i.getName(), cfg.getMaxSize()); } return poolSize; } private int tryScaleDown(AgentPoolConfiguration cfg, AgentPoolInstance i, int podsCount, int enqueuedCount, int poolSize, double minPoolSizeThreshold) { // To prevent scale down before previous scale up action is completed podsCount = Math.max(podsCount, i.getTargetSize()); // Reset scaledown delay counter for every attempt to scale down scaleDownTimeStamp = System.currentTimeMillis(); if (podsCount > cfg.getMinSize()) { if (enqueuedCount < minPoolSizeThreshold) { poolSize = (int) Math.floor(podsCount * (1 - cfg.getPercentDecrement() / 100)); log.info("['{}']: Scaling down - (enqueued count({}) < minimum threshold({})) for more than {} seconds...", i.getName(), enqueuedCount, minPoolSizeThreshold, (i.getResource().getSpec().getScaleDownDelayMs() / 1000)); // Limit to minimum pool size if the computed target size is less than the min size if (poolSize < cfg.getMinSize()) { log.warn("['{}']: Target pool size lesser than the allowed minimum: {} < {}. Updating to minimum size - {}", i.getName(), poolSize, cfg.getMinSize(), cfg.getMinSize()); poolSize = cfg.getMinSize(); } else { log.info("['{}']: Scaling down to {}...", i.getName(), poolSize); } } } else { log.warn("['{}']: Target pool size already the allowed minimum size: {}. Not updating.", i.getName(), cfg.getMinSize()); } return poolSize; } private static int getProcessCount(AgentPoolConfiguration cfg, List processQueueEntries) { return (int) processQueueEntries.stream() .map(ProcessQueueEntry::getRequirements) .filter(a -> isEmpty(cfg.getQueueSelector()) || Matcher.matches(a, cfg.getQueueSelector())) .count(); } private static boolean isEmpty(Map m) { return m == null || m.isEmpty(); } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/Event.java ================================================ package com.walmartlabs.concord.agentoperator.scheduler; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.crd.AgentPool; public class Event { private final Type type; private final AgentPool resource; public Event(Type type, AgentPool resource) { this.type = type; this.resource = resource; } public Type getType() { return type; } public AgentPool getResource() { return resource; } public enum Type { MODIFIED, DELETED } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/LinearAutoScaler.java ================================================ package com.walmartlabs.concord.agentoperator.scheduler; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration; import com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient; import com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; import java.util.function.Function; public class LinearAutoScaler implements AutoScaler { private static final Logger log = LoggerFactory.getLogger(LinearAutoScaler.class); public static final String NAME = "linear"; private final ProcessQueueClient processQueueClient; private final Function podCounter; private final Function canBeScaledUp; private final Function canBeScaledDown; public LinearAutoScaler(ProcessQueueClient processQueueClient, Function podCounter) { this(processQueueClient, podCounter, i -> { long t = System.currentTimeMillis(); return t - i.getLastScaleUpTimestamp() > i.getResource().getSpec().getScaleUpDelayMs(); }, i -> { long t = System.currentTimeMillis(); return t - i.getLastScaleDownTimeStamp() > i.getResource().getSpec().getScaleDownDelayMs(); }); } public LinearAutoScaler(ProcessQueueClient processQueueClient, Function podCounter, Function canBeScaledUp, Function canBeScaledDown) { this.processQueueClient = processQueueClient; this.podCounter = podCounter; this.canBeScaledUp = canBeScaledUp; this.canBeScaledDown = canBeScaledDown; } @Override public AgentPoolInstance apply(AgentPoolInstance i) throws IOException { if (!canBeScaledUp.apply(i) && !canBeScaledDown.apply(i)) { log.info("apply [{}] -> not a time. up: {}, down: {}, delay up: {}, delay down: {}", i.getName(), (System.currentTimeMillis() - i.getLastScaleUpTimestamp()), (System.currentTimeMillis() - i.getLastScaleDownTimeStamp()), i.getResource().getSpec().getScaleUpDelayMs(), i.getResource().getSpec().getScaleDownDelayMs()); // was updated recently, skipping return i; } long scaleUpTimeStamp = i.getLastScaleUpTimestamp(); long scaleDownTimeStamp = i.getLastScaleDownTimeStamp(); AgentPoolConfiguration cfg = i.getResource().getSpec(); QueueSelector queueSelector = QueueSelector.parse(cfg.getQueueSelector()); List queueEntries = processQueueClient.query("ENQUEUED", cfg.getMaxSize(), queueSelector); // count the currently running pods int podsCount = podCounter.apply(i.getName()); // the number of processes waiting for an agent in the current pool int enqueuedCount = queueEntries.size(); int runningCount = processQueueClient.query("RUNNING", cfg.getMaxSize(), queueSelector).size(); int freePodsCount = Math.max(podsCount - runningCount, 0); int increment = 0; if (enqueuedCount > freePodsCount) { increment = cfg.getSizeIncrement(); scaleUpTimeStamp = System.currentTimeMillis(); scaleDownTimeStamp = System.currentTimeMillis(); } else if (enqueuedCount < freePodsCount) { increment = -cfg.getSizeIncrement(); scaleDownTimeStamp = System.currentTimeMillis(); } int targetSize = Math.max(cfg.getMinSize(), podsCount + increment); if (i.getTargetSize() == targetSize) { log.info("apply ['{}'] -> targetSize = {}, enqueuedCount = {}, increment = {}, podsCount = {}", i.getName(), targetSize, enqueuedCount, increment, podsCount); // no changes needed return i; } if (increment > 0 && !canBeScaledUp.apply(i)) { log.info("apply ['{}'] -> not a time to scale up to {}", i.getName(), targetSize); return i; } if (increment < 0 && !canBeScaledDown.apply(i)) { log.info("apply ['{}'] -> not a time to scale down to {}", i.getName(), targetSize); return i; } if (targetSize > cfg.getMaxSize()) { log.warn("apply ['{}'] -> target pool size exceeds the allowed maximum: {} > {}", i.getName(), enqueuedCount, cfg.getMaxSize()); } targetSize = Math.min(targetSize, cfg.getMaxSize()); log.info("apply ['{}'] -> updated to {}, pods: {}, free: {}, enqueued: {}, running: {}", i.getName(), targetSize, podsCount, freePodsCount, enqueuedCount, runningCount); return AgentPoolInstance.updateTargetSize(i, targetSize, scaleUpTimeStamp, scaleDownTimeStamp); } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/QueueSelector.java ================================================ package com.walmartlabs.concord.agentoperator.scheduler; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.ConfigurationUtils; import java.util.List; import java.util.Map; public class QueueSelector { public static QueueSelector parse(Map queueSelector) { String flavor; Object maybeFlavor = ConfigurationUtils.get(queueSelector, "agent", "flavor"); if (maybeFlavor != null && !(maybeFlavor instanceof String)) { throw new IllegalArgumentException("Expected a string value as 'agent.flavor', got: " + maybeFlavor); } flavor = (String) maybeFlavor; List queryParams; Object maybeQueryParams = ConfigurationUtils.get(queueSelector, "queryParams"); if (maybeQueryParams != null) { if (!(maybeQueryParams instanceof List)) { throw new IllegalArgumentException("Expected a list value as 'queryParams', got: " + maybeQueryParams); } ((List) maybeQueryParams).forEach(qp -> { if (!(qp instanceof String)) { throw new IllegalArgumentException("Expected a string value as 'queryParams' item, got: " + qp); } }); } //noinspection unchecked queryParams = (List) maybeQueryParams; return new QueueSelector(flavor, queryParams); } private final String flavor; private final List queryParams; private QueueSelector(String flavor, List queryParams) { this.flavor = flavor; this.queryParams = queryParams; } /** * "Flavor" of the current agent. Translates to the "requirements.agent.flavor.eq" * query parameter when fetching the process queue. */ public String getFlavor() { return flavor; } /** * Additional query parameters to be used when fetching the process queue. Appended * as-is to the query URL. */ public List getQueryParams() { return queryParams; } } ================================================ FILE: agent-operator/src/main/java/com/walmartlabs/concord/agentoperator/scheduler/Scheduler.java ================================================ package com.walmartlabs.concord.agentoperator.scheduler; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.agent.AgentClientFactory; import com.walmartlabs.concord.agentoperator.crd.AgentPool; import com.walmartlabs.concord.agentoperator.planner.Change; import com.walmartlabs.concord.agentoperator.planner.Planner; import com.walmartlabs.concord.agentoperator.resources.AgentPod; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.client.KubernetesClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.*; public class Scheduler { private static final Logger log = LoggerFactory.getLogger(Scheduler.class); private static final long POLL_DELAY = 10000; private static final long ERROR_DELAY = 30000; private final AutoScalerFactory autoScalerFactory; private final KubernetesClient k8sClient; private final Planner planner; private final Map pools; private final List events; public Scheduler(AutoScalerFactory autoScalerFactory, KubernetesClient k8sClient, boolean useMaintenanceMode) { this.autoScalerFactory = autoScalerFactory; this.k8sClient = k8sClient; this.planner = new Planner(k8sClient, new AgentClientFactory(useMaintenanceMode)); this.pools = new HashMap<>(); this.events = new LinkedList<>(); } public void onEvent(Event.Type type, AgentPool resource) { log.info("onEvent -> handling {} for {}/{}", type, resource.getMetadata().getNamespace(), resource.getMetadata().getName()); synchronized (events) { events.add(new Event(type, resource)); } } public void start() { new Thread(new Worker(), "scheduler-worker").start(); } /** * Process the recent events and update the cluster state. */ private void doRun() { // drain the event queue List evs; synchronized (events) { evs = new ArrayList<>(events); events.clear(); } for (Event e : evs) { AgentPool resource = e.getResource(); String resourceName = resource.getMetadata().getName(); switch (e.getType()) { case MODIFIED: { onAdd(resourceName, resource); break; } case DELETED: { onDelete(resourceName); break; } default: throw new IllegalArgumentException("Unknown event type: " + e.getType()); } } // process the pool List todo; synchronized (pools) { todo = new ArrayList<>(pools.values()); } if (todo.isEmpty()) { return; } // fetch the process queue status todo.parallelStream().forEach(i -> { try { switch (i.getStatus()) { case ACTIVE: { AgentPoolInstance updated = updateTargetSize(i); processActive(updated); break; } case DELETED: { processDeleted(i); break; } default: throw new IllegalArgumentException("Unknown pool status: " + i.getStatus()); } } catch (IOException e) { log.error("doRun -> error while processing a registered pool {} ({}): {}", i.getName(), i.getStatus(), e.getMessage()); } }); } private void onAdd(String resourceName, AgentPool resource) { int targetSize = resource.getSpec().getSize(); synchronized (pools) { long currentTimeStamp = System.currentTimeMillis(); pools.put(resourceName, new AgentPoolInstance(resourceName, resource, AgentPoolInstance.Status.ACTIVE, targetSize, currentTimeStamp, currentTimeStamp, currentTimeStamp)); } } private void onDelete(String resourceName) { synchronized (pools) { AgentPoolInstance i = pools.get(resourceName); if (i == null) { return; } pools.put(resourceName, AgentPoolInstance.updateStatus(i, AgentPoolInstance.Status.DELETED)); } } private AgentPoolInstance updateTargetSize(AgentPoolInstance i) throws IOException { if (!i.getResource().getSpec().isAutoScale()) { return i; } AgentPoolInstance result = autoScalerFactory.create(i).apply(i); synchronized (pools) { pools.put(i.getName(), result); } return result; } private void processActive(AgentPoolInstance i) throws IOException { log.info("processActive ['{}']", i.getName()); List changes = planner.plan(i); apply(changes); } private void processDeleted(AgentPoolInstance i) throws IOException { log.info("processDeleted ['{}']", i.getName()); String resourceName = i.getName(); // remove all pool's pods List changes = planner.plan(i); apply(changes); // if no pods left - remove the pool List pods = AgentPod.list(k8sClient, resourceName); if (pods.isEmpty()) { synchronized (pools) { pools.remove(resourceName); log.info("processDeleted ['{}'] -> no pods left, the pool was removed", resourceName); } } else { log.info("processDeleted ['{}'] -> {} pod(s) left, will be deleted on the next iteration", resourceName, pods.size()); } } private void apply(List changes) { changes.forEach(c -> c.apply(k8sClient)); } private static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private class Worker implements Runnable { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { doRun(); sleep(POLL_DELAY); } catch (Exception e) { log.error("run -> error while running the scheduler: {}", e.getMessage(), e); sleep(ERROR_DELAY); } } } } } ================================================ FILE: agent-operator/src/main/resources/logback.xml ================================================ %d{HH:mm:ss.SSS} [%thread] [%-5level] %logger{36} - %msg%n ================================================ FILE: agent-operator/src/main/resources/prestop-hook.sh ================================================ #!/usr/bin/env bash echo "PreStop hook started at $(date)" MAX_RETRIES=5 RETRY_DELAY=1 current_workers="1" num_retries=0 while [ "$current_workers" != "0" ] && [ "$num_retries" -lt "$MAX_RETRIES" ] do echo "[$HOSTNAME]: Agent is still executing a process.. enabling maintenance mode and checking the number of current_workers" response=$(exec 3<>/dev/tcp/127.0.0.1/8010; echo -e "POST /maintenance-mode HTTP/1.1\r\nHost: 127.0.0.1:8010\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" >&3; cat <&3; exec 3>&-) mmode_response=$(echo "$response" | sed -n '/^\r*$/,$p' | tail -n +2) mmode_enabled=$(echo "$mmode_response" | sed -n 's/^.*\"maintenanceMode\":\([a-z]*\).*$/\1/p') if [ "$mmode_enabled" == "true" ]; then echo "[$HOSTNAME]: Maintenance mode enabled: $mmode_enabled" current_workers=$(echo "$mmode_response" | sed -n 's/^.*\"workersAlive\":\([0-9]*\).*$/\1/p') echo "[$HOSTNAME]: Number of current_workers: $current_workers" else echo "[$HOSTNAME]: trouble enabling maintenance mode" num_retries=$(("$num_retries" + 1)) fi sleep ${RETRY_DELAY} done if [ "$num_retries" -ge "$MAX_RETRIES" ]; then echo "[$HOSTNAME]: Number of retries to enable exceeded $MAX_RETRIES times. Exiting ..." exit 1 fi echo "[$HOSTNAME]: There are no processes running on this agent. Terminating..." ================================================ FILE: agent-operator/src/test/java/com/walmartlabs/concord/agentoperator/processqueue/ProcessQueueClientTest.java ================================================ package com.walmartlabs.concord.agentoperator.processqueue; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.Test; import static com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient.escapeQueryParam; import static org.junit.jupiter.api.Assertions.assertEquals; public class ProcessQueueClientTest { @Test public void testEscapeQueryParam() { assertEquals("foo%20bar", escapeQueryParam("foo bar")); assertEquals("foo=bar", escapeQueryParam("foo=bar")); assertEquals("foo=bar%20baz", escapeQueryParam("foo=bar baz")); assertEquals("foo.bar.baz=.*()%23%2F%2F&++$", escapeQueryParam("foo.bar.baz=.*()#//&++$")); } } ================================================ FILE: agent-operator/src/test/java/com/walmartlabs/concord/agentoperator/scheduler/DefaultAutoScalerTest.java ================================================ package com.walmartlabs.concord.agentoperator.scheduler; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.crd.AgentPool; import com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration; import com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient; import com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueEntry; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; public class DefaultAutoScalerTest { @Test public void testStill() throws Exception { AtomicInteger podCount = new AtomicInteger(1); List queue = new ArrayList<>(); DefaultAutoScaler as = new DefaultAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true); AgentPoolConfiguration spec = new AgentPoolConfiguration(); spec.setPercentIncrement(50); spec.setDecrementThresholdFactor(1.0); spec.setIncrementThresholdFactor(1.5); spec.setPercentDecrement(10); spec.setQueueSelector(Collections.singletonMap("test", 123)); AgentPool resource = new AgentPool(); resource.setSpec(spec); AgentPoolInstance pool = new AgentPoolInstance("test", resource, AgentPoolInstance.Status.ACTIVE, 1, 0, 0, 0); // --- pool = as.apply(pool); assertEquals(1, pool.getTargetSize()); pool = as.apply(pool); assertEquals(1, pool.getTargetSize()); } @Test public void testZeroStart() throws IOException { AtomicInteger podCount = new AtomicInteger(0); List queue = new ArrayList<>(); DefaultAutoScaler as = new DefaultAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true); AgentPoolConfiguration spec = new AgentPoolConfiguration(); spec.setPercentIncrement(50); spec.setDecrementThresholdFactor(1.0); spec.setIncrementThresholdFactor(1.5); spec.setPercentDecrement(10); spec.setQueueSelector(Collections.singletonMap("test", 123)); AgentPool resource = new AgentPool(); resource.setSpec(spec); AgentPoolInstance pool = new AgentPoolInstance("test", resource, AgentPoolInstance.Status.ACTIVE, 1, 0, 0, 0); // --- pool = as.apply(pool); assertEquals(1, pool.getTargetSize()); podCount.set(2); pool = as.apply(pool); assertEquals(1, pool.getTargetSize()); } private ProcessQueueClient mockProcessQueueClient(List queue) { return new ProcessQueueClient("test", "test") { @Override public List query(String processStatus, int limit, QueueSelector queueSelector) throws IOException { return queue; } }; } @Test public void testQueue() throws IOException { AtomicInteger podCount = new AtomicInteger(1); List queue = new ArrayList<>(); for (int i = 0; i < 10; i++) { queue.add(new ProcessQueueEntry(Collections.singletonMap("test", 123))); } DefaultAutoScaler as = new DefaultAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true); AgentPoolConfiguration spec = new AgentPoolConfiguration(); spec.setPercentIncrement(50); spec.setDecrementThresholdFactor(1.0); spec.setIncrementThresholdFactor(1.5); spec.setPercentDecrement(10); spec.setQueueSelector(Collections.singletonMap("test", 123)); AgentPool resource = new AgentPool(); resource.setSpec(spec); AgentPoolInstance pool = new AgentPoolInstance("test", resource, AgentPoolInstance.Status.ACTIVE, 1, 0, 0, 0); // --- pool = as.apply(pool); assertEquals(2, pool.getTargetSize()); podCount.set(2); pool = as.apply(pool); assertEquals(3, pool.getTargetSize()); podCount.set(3); pool = as.apply(pool); assertEquals(5, pool.getTargetSize()); podCount.set(5); pool = as.apply(pool); assertEquals(8, pool.getTargetSize()); podCount.set(8); // --- queue.clear(); pool = as.apply(pool); assertEquals(7, pool.getTargetSize()); podCount.set(7); pool = as.apply(pool); assertEquals(6, pool.getTargetSize()); } } ================================================ FILE: agent-operator/src/test/java/com/walmartlabs/concord/agentoperator/scheduler/LinearAutoScalerTest.java ================================================ package com.walmartlabs.concord.agentoperator.scheduler; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.agentoperator.crd.AgentPool; import com.walmartlabs.concord.agentoperator.crd.AgentPoolConfiguration; import com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueClient; import com.walmartlabs.concord.agentoperator.processqueue.ProcessQueueEntry; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; public class LinearAutoScalerTest { @Test public void testStill() throws Exception { AtomicInteger podCount = new AtomicInteger(1); List queue = new ArrayList<>(); LinearAutoScaler as = new LinearAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true); AgentPoolConfiguration spec = new AgentPoolConfiguration(); spec.setQueueSelector(Collections.singletonMap("test", 123)); AgentPool resource = new AgentPool(); resource.setSpec(spec); AgentPoolInstance pool = new AgentPoolInstance("test", resource, AgentPoolInstance.Status.ACTIVE, 1, 0, 0, 0); // --- pool = as.apply(pool); assertEquals(1, pool.getTargetSize()); pool = as.apply(pool); assertEquals(1, pool.getTargetSize()); } @Test public void testZeroStart() throws IOException { AtomicInteger podCount = new AtomicInteger(0); List queue = new ArrayList<>(); AutoScaler as = new LinearAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true); AgentPoolConfiguration spec = new AgentPoolConfiguration(); spec.setMinSize(0); spec.setSize(0); spec.setQueueSelector(Collections.singletonMap("test", 123)); AgentPool resource = new AgentPool(); resource.setSpec(spec); AgentPoolInstance pool = new AgentPoolInstance("test", resource, AgentPoolInstance.Status.ACTIVE, 0, 0, 0, 0); // --- pool = as.apply(pool); assertEquals(0, pool.getTargetSize()); // 1 enqueued process -> inc pods count queue.add(new ProcessQueueEntry(Collections.singletonMap("test", 123))); pool = as.apply(pool); assertEquals(1, pool.getTargetSize()); podCount.set(1); // no processes -> 0 pods queue.clear(); pool = as.apply(pool); assertEquals(0, pool.getTargetSize()); } private ProcessQueueClient mockProcessQueueClient(List queue) { return new ProcessQueueClient("test", "test") { @Override public List query(String processStatus, int limit, QueueSelector queueSelector) throws IOException { return queue; } }; } @Test public void testQueue() throws IOException { AtomicInteger podCount = new AtomicInteger(1); List queue = new ArrayList<>(); for (int i = 0; i < 10; i++) { queue.add(new ProcessQueueEntry(Collections.singletonMap("test", 123))); } DefaultAutoScaler as = new DefaultAutoScaler(mockProcessQueueClient(queue), n -> podCount.get(), i -> true, i -> true); AgentPoolConfiguration spec = new AgentPoolConfiguration(); spec.setPercentIncrement(50); spec.setDecrementThresholdFactor(1.0); spec.setIncrementThresholdFactor(1.5); spec.setPercentDecrement(10); spec.setQueueSelector(Collections.singletonMap("test", 123)); AgentPool resource = new AgentPool(); resource.setSpec(spec); AgentPoolInstance pool = new AgentPoolInstance("test", resource, AgentPoolInstance.Status.ACTIVE, 1, 0, 0, 0); // --- pool = as.apply(pool); assertEquals(2, pool.getTargetSize()); podCount.set(2); pool = as.apply(pool); assertEquals(3, pool.getTargetSize()); podCount.set(3); pool = as.apply(pool); assertEquals(5, pool.getTargetSize()); podCount.set(5); pool = as.apply(pool); assertEquals(8, pool.getTargetSize()); podCount.set(8); // --- queue.clear(); pool = as.apply(pool); assertEquals(7, pool.getTargetSize()); podCount.set(7); pool = as.apply(pool); assertEquals(6, pool.getTargetSize()); } } ================================================ FILE: checkstyle.xml ================================================ ================================================ FILE: cli/pom.xml ================================================ 4.0.0 com.walmartlabs.concord parent 2.40.1-SNAPSHOT ../pom.xml concord-cli jar ${project.groupId}:${project.artifactId} com.walmartlabs.concord.cli.Main com.walmartlabs.concord.runtime concord-runtime-common com.walmartlabs.concord.runtime.v1 concord-runtime-model-v1 com.walmartlabs.concord.runtime.v2 concord-runtime-model-v2 com.walmartlabs.concord.runtime.v2 concord-runner-v2 com.walmartlabs.concord.runtime.v2 concord-runtime-sdk-v2 com.walmartlabs.concord.runtime.v2 concord-runtime-vm-v2 com.walmartlabs.concord concord-common com.walmartlabs.concord.runtime concord-runtime-loader com.walmartlabs.concord concord-imports com.walmartlabs.concord concord-repository com.walmartlabs.concord concord-dependency-manager com.walmartlabs.concord concord-sdk com.walmartlabs.concord concord-client2 javax.inject javax.inject com.google.inject guice org.glassfish javax.el info.picocli picocli com.fasterxml.jackson.core jackson-core com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.dataformat jackson-dataformat-yaml org.fusesource.jansi jansi org.eclipse.jgit org.eclipse.jgit org.junit.jupiter junit-jupiter-api test false ${project.basedir}/src/main/resources **/* true ${project.basedir}/src/main/filtered-resources **/* false ${project.basedir}/src/test/resources **/* true ${project.basedir}/src/test/filtered-resources **/* org.apache.maven.plugins maven-shade-plugin package shade true executable META-INF/sisu/javax.inject.Named META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider ${main.class} java.base/java.lang java.base/java.util *:* META-INF/*.SF META-INF/*.RSA org.skife.maven really-executable-jar-maven-plugin package really-executable-jar -Xmx128m -client executable true concord-cli-${project.version} sh true ================================================ FILE: cli/src/main/filtered-resources/defaultCfg.yml ================================================ configuration: dependencies: - "mvn://com.walmartlabs.concord.plugins.basic:http-tasks:${project.version}" - "mvn://com.walmartlabs.concord.plugins.basic:slack-tasks:${project.version}" - "mvn://com.walmartlabs.concord.plugins.basic:concord-tasks:${project.version}" ================================================ FILE: cli/src/main/filtered-resources/project.properties ================================================ project.version=${project.version} ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/AbortException.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public class AbortException extends RuntimeException { public AbortException() { super("Aborted"); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/App.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import picocli.CommandLine.Command; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Option; import picocli.CommandLine.Spec; @Command(name = "concord", subcommands = {Lint.class, Run.class, Resume.class, RemoteRun.class, SelfUpdate.class}) public class App implements Runnable { @Spec private CommandSpec spec; @Option(names = {"-h", "--help"}, usageHelp = true, description = "display a help message") boolean helpRequested = false; @Option(names = {"--version"}, description = "display version") boolean versionRequested = false; @Override public void run() { if (versionRequested) { System.out.println(Version.getVersion()); return; } spec.commandLine().usage(System.out); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/CliConfig.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import java.util.Optional; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Objects.requireNonNull; import static org.fusesource.jansi.Ansi.ansi; public record CliConfig(Map contexts) { private static final Logger log = LoggerFactory.getLogger(CliConfig.class); public static CliConfig.CliConfigContext load(Verbosity verbosity, String context, Overrides overrides) { try { return loadOrThrow(verbosity, context, overrides); } catch (Exception e) { handleCliConfigErrorAndBail(resolveCliConfigPath().toAbsolutePath().toString(), e); throw new IllegalStateException("should be unreachable"); } } public static CliConfig.CliConfigContext loadOrThrow(Verbosity verbosity, String context, Overrides overrides) throws IOException { var cfgFile = resolveCliConfigPath(); if (Files.notExists(cfgFile)) { var cfg = CliConfig.create(); return requireCliConfigContext(cfg, context, false).withOverrides(overrides); } if (verbosity.verbose()) { log.info("Using CLI configuration file: {} (\"{}\" context)", cfgFile, context); } var cfg = loadConfigFile(cfgFile); return requireCliConfigContext(cfg, context, true).withOverrides(overrides); } private static void handleCliConfigErrorAndBail(String cfgPath, Throwable e) { // unwrap runtime exceptions if (e instanceof RuntimeException ex) { if (ex.getCause() instanceof IllegalArgumentException) { e = ex.getCause(); } } if (e instanceof MissingContextException) { System.out.println(ansi().fgRed().a(e.getMessage())); System.exit(1); } // handle YAML errors if (e instanceof IllegalArgumentException) { if (e.getCause() instanceof UnrecognizedPropertyException ex) { System.out.println(ansi().fgRed().a("Invalid format of the CLI configuration file ").a(cfgPath).a(". ").a(ex.getMessage())); System.exit(1); } System.out.println(ansi().fgRed().a("Invalid format of the CLI configuration file ").a(cfgPath).a(". ").a(e.getMessage())); System.exit(1); } // all other errors System.out.println(ansi().fgRed().a("Failed to read the CLI configuration file ").a(cfgPath).a(". ").a(e.getMessage())); System.exit(1); } private static CliConfig.CliConfigContext requireCliConfigContext(CliConfig config, String context, boolean userConfigLoaded) { var result = config.contexts().get(context); if (result == null) { throw new MissingContextException(context, userConfigLoaded); } return result; } static final class MissingContextException extends IllegalArgumentException { private MissingContextException(String context, boolean userConfigLoaded) { super(message(context, userConfigLoaded)); } private static String message(String context, boolean userConfigLoaded) { if (userConfigLoaded) { return "Configuration context not found: " + context + ". Check the CLI configuration file."; } return "Configuration context not found: " + context + ". No CLI configuration file was found in ~/.concord; only the built-in 'default' context is available."; } } @VisibleForTesting static CliConfig loadConfigFile(Path path) throws IOException { var mapper = new YAMLMapper() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); JsonNode defaults = mapper.readTree(readDefaultConfig()); JsonNode cfg; try (var reader = Files.newBufferedReader(path)) { cfg = mapper.readTree(reader); } // merge the loaded config file with the default built-in config var cfgWithDefaults = mapper.updateValue(defaults, cfg); // merge each non-default context with the default context var contexts = assertContexts(cfgWithDefaults); var defaultCtx = contexts.get("default"); if (defaultCtx == null) { throw new IllegalArgumentException("Missing 'default' context."); } contexts.fieldNames().forEachRemaining(ctxName -> { if ("default".equals(ctxName)) { return; } var ctx = contexts.get(ctxName); try { var mergedCtx = mapper.updateValue(defaultCtx, ctx); contexts.set(ctxName, mergedCtx); } catch (JsonMappingException e) { throw new RuntimeException(e); } }); return mapper.convertValue(cfgWithDefaults, CliConfig.class); } private static ObjectNode assertContexts(JsonNode cfg) { var maybeContexts = cfg.get("contexts"); if (maybeContexts == null) { throw new IllegalArgumentException("Missing 'contexts' object."); } if (!maybeContexts.isObject()) { throw new IllegalArgumentException("The 'contexts' field must be an object."); } return (ObjectNode) maybeContexts; } public static CliConfig create() { var mapper = new YAMLMapper(); try { return mapper.readValue(readDefaultConfig(), CliConfig.class); } catch (IOException e) { throw new IllegalStateException("Can't parse the default CLI config file. " + e.getMessage()); } } public record Overrides(@Nullable Path secretStoreDir, @Nullable Path vaultDir, @Nullable String vaultId) { } static boolean hasUserConfig() { return Files.exists(Paths.get(System.getProperty("user.home"), ".concord", "cli.yaml")) || Files.exists(Paths.get(System.getProperty("user.home"), ".concord", "cli.yml")); } private static Path resolveCliConfigPath() { var baseDir = Paths.get(System.getProperty("user.home"), ".concord"); var cfgFile = baseDir.resolve("cli.yaml"); if (Files.exists(cfgFile)) { return cfgFile; } return baseDir.resolve("cli.yml"); } public record CliConfigContext(@Nullable RemoteRunConfiguration remoteRun, SecretsConfiguration secrets) { public CliConfigContext withOverrides(@Nullable Overrides overrides) { if (overrides == null) { return this; } var remoteRun = this.remoteRun(); var secrets = this.secrets().withOverrides(overrides); return new CliConfigContext(remoteRun, secrets); } } public record SecretRef(String orgName, String secretName) { public SecretRef(String orgName, String secretName) { this.orgName = orgName == null ? "Default" : orgName; if (this.orgName.isBlank()) { throw new IllegalArgumentException("'orgName' is required"); } this.secretName = requireNonNull(secretName); if (this.secretName.isBlank()) { throw new IllegalArgumentException("'secretName' is required"); } } } public record RemoteRunConfiguration(@Nullable String baseUrl, @Nullable SecretRef apiKeyRef) { } public record SecretsConfiguration(VaultConfiguration vault, FileSecretsProviderConfiguration local, RemoteSecretsProviderConfiguration remote) { public SecretsConfiguration withOverrides(@Nullable Overrides overrides) { if (overrides == null) { return this; } var vault = this.vault().withOverrides(overrides); var localFiles = this.local().withOverrides(overrides); return new SecretsConfiguration(vault, localFiles, this.remote); } public record VaultConfiguration(Path dir, String id) { public VaultConfiguration withOverrides(@Nullable Overrides overrides) { if (overrides == null) { return this; } return new VaultConfiguration( Optional.ofNullable(overrides.vaultDir()).orElse(this.dir()), Optional.ofNullable(overrides.vaultId()).orElse(this.id())); } } public record FileSecretsProviderConfiguration(boolean enabled, boolean writable, Path dir) { public FileSecretsProviderConfiguration withOverrides(@Nullable Overrides overrides) { if (overrides == null) { return this; } return new FileSecretsProviderConfiguration( this.enabled, this.writable, Optional.ofNullable(overrides.secretStoreDir()).orElse(this.dir())); } } public record RemoteSecretsProviderConfiguration(boolean enabled, boolean writable, @Nullable String baseUrl, @Nullable String apiKey, boolean confirmAccess) { } } private static String readDefaultConfig() { try (var in = CliConfig.class.getResourceAsStream("defaultCliConfig.yaml")) { if (in == null) { throw new IllegalStateException("defaultCliConfig.yaml resource not found"); } var ab = in.readAllBytes(); var s = new String(ab, UTF_8); var dotConcordPath = Paths.get(System.getProperty("user.home")).resolve(".concord"); return s.replace("${configDir}", dotConcordPath.normalize().toAbsolutePath().toString()); } catch (IOException e) { throw new IllegalStateException("Can't load the default CLI config file. " + e.getMessage()); } } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/CliExitCodes.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ final class CliExitCodes { static final int SUCCESS = 0; static final int ERROR = 1; static final int USAGE = 2; static final int SUSPENDED = 20; static final int INPUT_REQUIRED = 21; static final int NON_INTERACTIVE_UNSUPPORTED = 22; static final int PROCESS_FAILED = -1; private CliExitCodes() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/CliPaths.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.nio.file.Path; public final class CliPaths { public static final String DEFAULT_TARGET_DIR_NAME = "target"; public static Path defaultTargetDir(Path sourceDir) { return sourceDir.resolve(DEFAULT_TARGET_DIR_NAME); } public static Path preferredResumeDir(Path sourceDir, Path workDir) { var normalizedSourceDir = sourceDir.normalize().toAbsolutePath(); var normalizedWorkDir = workDir.normalize().toAbsolutePath(); if (normalizedWorkDir.equals(defaultTargetDir(normalizedSourceDir))) { return normalizedSourceDir; } return normalizedWorkDir; } private CliPaths() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/Confirmation.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.IOException; import static org.fusesource.jansi.Ansi.ansi; public final class Confirmation { public static boolean confirm(String message) throws IOException { return confirm(message, false); } public static boolean confirm(String message, boolean defaultValue) throws IOException { System.out.println(ansi().fgBrightYellow().bold().a(message).reset()); var response = new StringBuilder(); while (true) { int ch = System.in.read(); if (ch == -1 || ch == '\n') { break; } if (ch != '\r') { response.append((char) ch); } } var value = response.toString().trim().toLowerCase(); if (value.isEmpty()) { return defaultValue; } return "y".equals(value) || "yes".equals(value); } private Confirmation() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/GitIgnoreFilter.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.eclipse.jgit.ignore.IgnoreNode; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; /** * Utility class for filtering files based on .gitignore patterns. * Supports hierarchical .gitignore files in subdirectories per git spec. */ public class GitIgnoreFilter { private final Map ignoreNodes; private GitIgnoreFilter(Map ignoreNodes) { this.ignoreNodes = ignoreNodes; } /** * Loads all .gitignore files from the given directory and its subdirectories. * * @param baseDir the base directory to scan for .gitignore files * @return a GitIgnoreFilter instance, or null if no .gitignore files exist */ public static GitIgnoreFilter load(Path baseDir) throws IOException { var nodes = new HashMap(); Files.walkFileTree(baseDir, new SimpleFileVisitor<>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { var gitignore = dir.resolve(".gitignore"); if (Files.isRegularFile(gitignore)) { var node = new IgnoreNode(); try (var in = Files.newInputStream(gitignore)) { node.parse(in); } if (!node.getRules().isEmpty()) { var relDir = baseDir.relativize(dir); nodes.put(relDir, node); } } return FileVisitResult.CONTINUE; } }); if (nodes.isEmpty()) { return null; } return new GitIgnoreFilter(nodes); } /** * Checks if the given path should be ignored based on .gitignore rules. * * @param relativePath the path relative to the base directory * @param isDirectory true if the path is a directory * @return true if the path should be ignored */ public boolean isIgnored(Path relativePath, boolean isDirectory) { var pathStr = relativePath.toString().replace('\\', '/'); Boolean ignored = null; // Check from root down to parent directory for (var ancestor : getAncestorPaths(relativePath)) { var node = ignoreNodes.get(ancestor); if (node != null) { // Make path relative to this ignore node's directory String relativeToNode; if (ancestor.toString().isEmpty()) { relativeToNode = pathStr; } else { var ancestorStr = ancestor.toString().replace('\\', '/'); relativeToNode = pathStr.substring(ancestorStr.length() + 1); } var result = node.isIgnored(relativeToNode, isDirectory); if (result == IgnoreNode.MatchResult.IGNORED) { ignored = true; } else if (result == IgnoreNode.MatchResult.NOT_IGNORED) { ignored = false; } // CHECK_PARENT means no match, keep current value } } return Boolean.TRUE.equals(ignored); } /** * Returns all ancestor paths from root (empty path) to the parent of the given path. */ private List getAncestorPaths(Path relativePath) { var ancestors = new ArrayList(); ancestors.add(Paths.get("")); // root var current = Paths.get(""); for (var i = 0; i < relativePath.getNameCount() - 1; i++) { current = current.resolve(relativePath.getName(i)); ancestors.add(current); } return ancestors; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/Lint.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.cli.lint.*; import com.walmartlabs.concord.cli.lint.LintResult.Type; import com.walmartlabs.concord.cli.runner.CliImportsListener; import com.walmartlabs.concord.imports.ImportManager; import com.walmartlabs.concord.imports.NoopImportManager; import com.walmartlabs.concord.process.loader.DelegatingProjectLoader; import com.walmartlabs.concord.runtime.model.ProcessDefinition; import com.walmartlabs.concord.runtime.model.SourceMap; import com.walmartlabs.concord.runtime.v1.ProjectLoaderV1; import com.walmartlabs.concord.runtime.v2.ProjectLoaderV2; import org.fusesource.jansi.Ansi; import picocli.CommandLine.Command; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import picocli.CommandLine.Spec; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import static org.fusesource.jansi.Ansi.ansi; @Command(name = "lint", description = "Parse and validate Concord YAML files") public class Lint implements Callable { @Spec private CommandSpec spec; @Option(names = {"-h", "--help"}, usageHelp = true, description = "display the command's help message") boolean helpRequested = false; @Option(names = {"-v", "--verbose"}, description = "Verbose output") boolean verbose = false; @Parameters(arity = "0..1") Path targetDir = Paths.get(System.getProperty("user.dir")); @Override public Integer call() throws Exception { targetDir = targetDir.normalize().toAbsolutePath(); if (!Files.isDirectory(targetDir)) { throw new IllegalArgumentException("Not a directory: " + targetDir); } ImportManager importManager = new NoopImportManager(); ProjectLoaderV1 v1 = new ProjectLoaderV1(importManager); ProjectLoaderV2 v2 = new ProjectLoaderV2(importManager); DelegatingProjectLoader loader = new DelegatingProjectLoader(Set.of(v1, v2)); ProcessDefinition pd = loader.loadProject(targetDir, new DummyImportsNormalizer(), verbose ? new CliImportsListener() : null).projectDefinition(); List lintResults = new ArrayList<>(); linters().forEach(l -> lintResults.addAll(l.apply(pd))); if (!lintResults.isEmpty()) { print(lintResults); println(); } println("Found:"); println(" imports: " + pd.imports().items().size()); println(" profiles: " + pd.profiles().size()); println(" flows: " + pd.flows().size()); println(" forms: " + pd.forms().size()); println(" triggers: " + pd.triggers().size()); println(" (not counting dynamically imported resources)"); println(); printStats(lintResults); println(); boolean hasErrors = hasErrors(lintResults); if (hasErrors) { System.out.println(ansi().fgBrightRed().bold().a("INVALID").reset()); } else { System.out.println(ansi().fgBrightGreen().bold().a("VALID").reset()); } return hasErrors ? 10 : 0; } private List linters() { return Arrays.asList( new ExpressionLinter(verbose), new TaskCallLinter(verbose) ); } private void print(List results) { for (LintResult r : results) { Ansi msg = ansi(); switch (r.getType()) { case ERROR: { ansi().fgBrightRed().a("ERROR:").reset(); break; } case WARNING: { ansi().fgBrightYellow().a("WARN:").reset(); break; } default: throw new IllegalArgumentException("Unsupported result type: " + r.getType()); } SourceMap sm = r.getSourceMap(); if (sm != null) { msg.a("@ [").a(sm.source()).a("] line: ").a(sm.line()).a(", col: ").a(sm.column()); } msg.append("\n\t").append(r.getMessage()); println(msg); println("------------------------------------------------------------"); } } private void printStats(List results) { long errors = results.stream().filter(r -> r.getType() == Type.ERROR).count(); long warns = results.stream().filter(r -> r.getType() == Type.WARNING).count(); println("Result: " + errors + " error(s), " + warns + " warning(s)"); } private void println(Object o) { System.out.println(o); } private void println() { System.out.println(); } private static boolean hasErrors(List results) { return results.stream().anyMatch(l -> l.getType() == Type.ERROR); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/LocalCliRuntime.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.google.inject.Injector; import com.walmartlabs.concord.cli.CliConfig.CliConfigContext; import com.walmartlabs.concord.cli.runner.CliServicesModule; import com.walmartlabs.concord.dependencymanager.DependencyManager; import com.walmartlabs.concord.dependencymanager.DependencyManagerConfiguration; import com.walmartlabs.concord.dependencymanager.DependencyManagerRepositories; import com.walmartlabs.concord.imports.ImportsListener; import com.walmartlabs.concord.imports.NoopImportManager; import com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration; import com.walmartlabs.concord.runtime.v2.NoopImportsNormalizer; import com.walmartlabs.concord.runtime.v2.ProjectLoaderV2; import com.walmartlabs.concord.runtime.v2.runner.InjectorFactory; import com.walmartlabs.concord.runtime.v2.runner.guice.ProcessDependenciesModule; import com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration; import com.walmartlabs.concord.runtime.v2.sdk.WorkingDirectory; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; final class LocalCliRuntime { static DependencyManager createDependencyManager(Path depsCacheDir) throws IOException { var cfgFile = Paths.get(System.getProperty("user.home"), ".concord", "mvn.json"); if (Files.exists(cfgFile)) { return new DependencyManager(DependencyManagerConfiguration.of(depsCacheDir, DependencyManagerRepositories.get(cfgFile))); } return new DependencyManager(DependencyManagerConfiguration.of(depsCacheDir)); } static Injector createInjector(Path workDir, RunnerConfiguration runnerCfg, ProcessConfiguration processCfg, CliConfigContext cliConfigContext, Path defaultTaskVars, DependencyManager dependencyManager, Verbosity verbosity) { return new InjectorFactory(new WorkingDirectory(workDir), runnerCfg, () -> processCfg, new ProcessDependenciesModule(workDir, runnerCfg.dependencies(), processCfg.debug()), new CliServicesModule(cliConfigContext, workDir, defaultTaskVars, dependencyManager, verbosity)) .create(); } static void notifyProjectLoaded(Path workDir) throws Exception { var loader = new ProjectLoaderV2(new NoopImportManager()); loader.load(workDir, new NoopImportsNormalizer(), ImportsListener.NOP_LISTENER); } private LocalCliRuntime() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/LocalFormInputs.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.forms.DefaultFormValidator; import com.walmartlabs.concord.forms.DefaultFormValidatorLocale; import com.walmartlabs.concord.forms.Form; import com.walmartlabs.concord.forms.FormField; import com.walmartlabs.concord.forms.FormFields; import com.walmartlabs.concord.forms.FormUtils; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static com.walmartlabs.concord.forms.Constants.FORM_FILES; final class LocalFormInputs { private static final DefaultFormValidatorLocale LOCALE = new DefaultFormValidatorLocale(); private static final DefaultFormValidator VALIDATOR = new DefaultFormValidator(LOCALE); static Converted convertAndValidate(Form form, Map rawValues) throws InputException { return convertAndValidate(form, rawValues, false); } static Converted convertAndValidate(Form form, Map input, boolean unwrapFormName) throws InputException { var rawValues = unwrapFormName ? unwrapFormValues(form, input) : input; var tmpFiles = new LinkedHashMap(); var opened = new ArrayList(); try { var convertedInput = prepareInput(form, rawValues, opened); var converted = new LinkedHashMap<>(FormUtils.convert(LOCALE, form, convertedInput, tmpFiles)); var errors = VALIDATOR.validate(form, converted); if (!errors.isEmpty()) { cleanupTempFiles(tmpFiles); throw new InputException(errors.stream().map(e -> e.error()).toList()); } return new Converted(converted, tmpFiles); } catch (FormUtils.ValidationException e) { cleanupTempFiles(tmpFiles); throw new InputException(List.of(e.getMessage()), e); } catch (IOException e) { cleanupTempFiles(tmpFiles); throw new InputException(List.of(e.getMessage()), e); } finally { close(opened); } } private static Map prepareInput(Form form, Map rawValues, List opened) throws IOException { var convertedInput = new LinkedHashMap(); for (var field : form.fields()) { var value = rawValues.get(field.name()); if (value == null) { continue; } convertedInput.put(field.name(), toFormInput(value, field, opened)); } return convertedInput; } private static Object toFormInput(Object value, FormField field, List opened) throws IOException { if (!FormFields.FileField.TYPE.equals(field.type())) { return value; } if (value instanceof Path path) { var in = Files.newInputStream(path); opened.add(in); return in; } if (value instanceof Collection values) { var result = new ArrayList<>(); for (var item : values) { result.add(toFormInput(item, field, opened)); } return result; } return value; } private static Map unwrapFormValues(Form form, Map input) throws InputException { var value = input.get(form.name()); if (value == null) { return input; } if (!(value instanceof Map values)) { throw new InputException(List.of("Expected an object value for form '" + form.name() + "'")); } var result = new LinkedHashMap(); for (var e : values.entrySet()) { if (!(e.getKey() instanceof String key)) { throw new InputException(List.of("Expected string field names for form '" + form.name() + "'")); } result.put(key, e.getValue()); } return result; } static void cleanupTempFiles(Map tmpFiles) { for (var tmpFile : tmpFiles.values()) { try { Files.deleteIfExists(Path.of(tmpFile)); } catch (IOException ignored) { // best effort cleanup for validation retries } } } private static void close(List opened) { for (var in : opened) { try { in.close(); } catch (IOException e) { throw new UncheckedIOException(e); } } } record Converted(Map values, Map tmpFiles) { Map payload(Form form) { var values = new LinkedHashMap<>(this.values); values.remove(FORM_FILES); var payload = new LinkedHashMap(); payload.put(form.name(), values); return payload; } void cleanupTempFiles() { LocalFormInputs.cleanupTempFiles(tmpFiles); } } static final class InputException extends Exception { private final List messages; private InputException(List messages) { this(messages, null); } private InputException(List messages, Throwable cause) { super(String.join(", ", messages), cause); this.messages = List.copyOf(messages); } List messages() { return messages; } } private LocalFormInputs() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/LocalFormPrompts.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.forms.Form; import com.walmartlabs.concord.forms.FormField; import com.walmartlabs.concord.forms.FormFields; import com.walmartlabs.concord.forms.FormFields.FileField; import java.io.BufferedReader; import java.io.Console; import java.io.IOException; import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; import static com.walmartlabs.concord.forms.Constants.FORM_FILES; import static org.fusesource.jansi.Ansi.ansi; final class LocalFormPrompts { private static final String HIDDEN_VALUE = ""; private final Path workDir; private final boolean printHeader; private final PromptIo promptIo; LocalFormPrompts(Path workDir) { this(workDir, true); } LocalFormPrompts(Path workDir, boolean printHeader) { this.workDir = workDir; this.printHeader = printHeader; this.promptIo = new PromptIo(); } Map prompt(Form form) throws Exception { var rawValues = new LinkedHashMap(); while (true) { if (printHeader) { printHeader(form); } collectRawValues(form, rawValues); try { var converted = LocalFormInputs.convertAndValidate(form, rawValues); try { moveFormFiles(converted.values()); } catch (IOException e) { converted.cleanupTempFiles(); throw e; } return converted.payload(form); } catch (LocalFormInputs.InputException e) { clearSensitiveValues(form, rawValues); printInputErrors(e); } } } private void printHeader(Form form) { System.out.println("Pending form input:"); System.out.println(" " + form.name() + " -> " + form.eventName()); if (form.options().isYield()) { printWarning("'yield' is informational only in local CLI."); } if (form.options().saveSubmittedBy()) { printWarning("'saveSubmittedBy' is ignored in local CLI."); } if (!form.options().runAs().isEmpty()) { printWarning("'runAs' restrictions are not enforced in local CLI."); } } private void collectRawValues(Form form, Map rawValues) { for (var field : form.fields()) { if (Boolean.TRUE.equals(field.getOption(FormFields.CommonFieldOptions.READ_ONLY))) { continue; } var value = promptField(field, rawValues.get(field.name())); if (value == MissingValue.INSTANCE) { rawValues.remove(field.name()); } else { rawValues.put(field.name(), value); } } } private Object promptField(FormField field, Object currentValue) { if (isRepeated(field)) { return promptRepeatedField(field, currentValue); } while (true) { var line = readValue(field, currentValue); if (line == null) { return currentValue != null ? currentValue : MissingValue.INSTANCE; } if (FileField.TYPE.equals(field.type())) { var path = Path.of(line); if (!Files.isRegularFile(path)) { printError("File not found: " + path); continue; } return path; } return normalizeRawValue(field, line); } } private Object promptRepeatedField(FormField field, Object currentValue) { if (currentValue instanceof Collection && !((Collection) currentValue).isEmpty()) { var current = isPasswordField(field) ? HIDDEN_VALUE : currentValue; System.out.println("Current values for " + label(field) + ": " + current); } var values = new ArrayList<>(); while (true) { var line = readValue(field, null, values.size() + 1); if (line == null) { if (values.isEmpty()) { return currentValue != null ? currentValue : MissingValue.INSTANCE; } return values; } if (FileField.TYPE.equals(field.type())) { var path = Path.of(line); if (!Files.isRegularFile(path)) { printError("File not found: " + path); continue; } values.add(path); } else { values.add(normalizeRawValue(field, line)); } } } private String readValue(FormField field, Object currentValue) { return readValue(field, currentValue, null); } private String readValue(FormField field, Object currentValue, Integer idx) { var prompt = buildPrompt(field, currentValue, idx); var password = "password".equals(field.getOption(FormFields.StringField.INPUT_TYPE)); var line = password ? promptIo.readPassword(prompt) : promptIo.readLine(prompt); if (line == null || line.trim().isEmpty()) { return null; } return line; } @SuppressWarnings("unchecked") private void moveFormFiles(Map converted) throws IOException { var formFiles = (Map) converted.get(FORM_FILES); if (formFiles == null || formFiles.isEmpty()) { return; } for (var e : formFiles.entrySet()) { var destination = workDir.resolve(e.getKey()); var parent = destination.getParent(); if (parent != null && Files.notExists(parent)) { Files.createDirectories(parent); } Files.move(Path.of(e.getValue()), destination, StandardCopyOption.REPLACE_EXISTING); } } private static void printInputErrors(LocalFormInputs.InputException e) { for (var message : e.messages()) { printError(message); } } private static void printError(String message) { System.err.println(ansi().fgBrightRed().a("Error: ").a(message).reset()); } private static void printWarning(String message) { System.out.println(ansi().fgBrightYellow().a("Warning: ").a(message).reset()); } private static String buildPrompt(FormField field, Object currentValue, Integer idx) { var details = new ArrayList(); details.add(field.type()); details.add(isOptional(field) ? "optional" : "required"); if (field.allowedValue() != null) { details.add("allowed: " + field.allowedValue()); } if (currentValue != null) { details.add("current: " + promptValue(field, currentValue)); } else if (field.defaultValue() != null) { details.add("default: " + promptValue(field, field.defaultValue())); } if (FormFields.DateField.TYPE.equals(field.type()) || FormFields.DateTimeField.TYPE.equals(field.type())) { details.add("format: ISO-8601"); } var name = idx != null ? label(field) + " [" + idx + "]" : label(field); return name + " [" + String.join(", ", details) + "]: "; } private static String promptValue(FormField field, Object value) { return isPasswordField(field) ? HIDDEN_VALUE : String.valueOf(value); } private static boolean isPasswordField(FormField field) { return "password".equals(field.getOption(FormFields.StringField.INPUT_TYPE)); } private static void clearSensitiveValues(Form form, Map rawValues) { for (var field : form.fields()) { if (isPasswordField(field)) { rawValues.remove(field.name()); } } } private static boolean isRepeated(FormField field) { return field.cardinality() == FormField.Cardinality.ANY || field.cardinality() == FormField.Cardinality.AT_LEAST_ONE; } private static boolean isOptional(FormField field) { return field.cardinality() == FormField.Cardinality.ONE_OR_NONE || field.cardinality() == FormField.Cardinality.ANY; } private static Object normalizeRawValue(FormField field, String line) { if (FormFields.BooleanField.TYPE.equals(field.type())) { var normalized = line.trim().toLowerCase(); if ("y".equals(normalized) || "yes".equals(normalized)) { return "true"; } if ("n".equals(normalized) || "no".equals(normalized)) { return "false"; } return normalized; } if (!FormFields.StringField.TYPE.equals(field.type())) { return line.trim(); } return line; } private static String label(FormField field) { return field.label() != null ? field.label() : field.name(); } private enum MissingValue { INSTANCE } private static final class PromptIo { private final Console console = System.console(); private final BufferedReader reader = console == null ? new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)) : null; String readLine(String prompt) { try { if (console != null) { return console.readLine("%s", prompt); } System.out.print(prompt); System.out.flush(); var line = reader.readLine(); if (line == null) { throw new IllegalStateException("End of input while reading form values"); } return line; } catch (IOException e) { throw new UncheckedIOException(e); } } String readPassword(String prompt) { if (console != null) { var chars = console.readPassword("%s", prompt); return chars != null ? new String(chars) : ""; } return readLine(prompt); } } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/LocalFormSession.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot; import com.walmartlabs.concord.runtime.v2.runner.Runner; import java.nio.file.Path; import java.util.Collections; final class LocalFormSession { static ProcessSnapshot resumePendingForms(Path workDir, Runner runner, ProcessSnapshot snapshot, LocalSuspendPersistence.ResumeMetadata metadata) throws Exception { return resumePendingForms(workDir, runner, snapshot, metadata, true); } static ProcessSnapshot resumePendingForms(Path workDir, Runner runner, ProcessSnapshot snapshot, LocalSuspendPersistence.ResumeMetadata metadata, boolean printHeader) throws Exception { var formPrompts = new LocalFormPrompts(workDir, printHeader); while (LocalSuspendPersistence.isSuspended(snapshot)) { var events = LocalSuspendPersistence.getEvents(snapshot); LocalSuspendPersistence.save(workDir, snapshot, metadata); var pendingForms = LocalFormState.syncPendingForms(workDir, events); if (pendingForms.isEmpty()) { return snapshot; } LocalFormState.assertSupported(workDir, pendingForms); var form = pendingForms.get(0); var input = formPrompts.prompt(form); snapshot = runner.resume(snapshot, Collections.singleton(form.eventName()), input); } LocalFormState.syncPendingForms(workDir, Collections.emptySet()); return snapshot; } private LocalFormSession() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/LocalFormState.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.forms.Form; import com.walmartlabs.concord.runtime.common.FormService; import com.walmartlabs.concord.sdk.Constants; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; final class LocalFormState { static List

syncPendingForms(Path workDir, Set waitingEvents) throws IOException { var formsDir = formsDir(workDir); if (Files.notExists(formsDir)) { return List.of(); } var forms = new FormService(formsDir).list(); for (var form : forms) { if (!waitingEvents.contains(form.eventName())) { Files.deleteIfExists(formPath(workDir, form.name())); } } return forms.stream() .filter(form -> waitingEvents.contains(form.eventName())) .sorted(Comparator.comparing(Form::name).thenComparing(Form::eventName)) .collect(Collectors.toList()); } static void assertSupported(Path workDir, Collection forms) { for (var form : forms) { var customAssets = customAssetsPath(workDir, form.name()); if (Files.exists(customAssets)) { throw new IllegalArgumentException("Custom form assets are not supported in local CLI resume: " + customAssets); } } } static Set formEvents(Collection forms) { return forms.stream() .map(Form::eventName) .collect(Collectors.toCollection(TreeSet::new)); } static Path formFilesDir(Path workDir) { return workDir.resolve(Constants.Files.FORM_FILES); } private static Path formsDir(Path workDir) { return workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME) .resolve(Constants.Files.JOB_STATE_DIR_NAME) .resolve(Constants.Files.JOB_FORMS_V2_DIR_NAME); } private static Path formPath(Path workDir, String formName) { return formsDir(workDir).resolve(formName); } private static Path customAssetsPath(Path workDir, String formName) { return workDir.resolve(Constants.Files.JOB_FORMS_DIR_NAME).resolve(formName); } private LocalFormState() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/LocalSuspendPersistence.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.walmartlabs.concord.cli.CliConfig.CliConfigContext; import com.walmartlabs.concord.common.PathUtils; import com.walmartlabs.concord.common.TemporaryPath; import com.walmartlabs.concord.forms.Form; import com.walmartlabs.concord.runtime.common.StateManager; import com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration; import com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot; import com.walmartlabs.concord.runtime.v2.runner.guice.ObjectMapperProvider; import com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration; import com.walmartlabs.concord.sdk.Constants; import com.walmartlabs.concord.svm.ThreadStatus; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.TreeSet; final class LocalSuspendPersistence { private static final String METADATA_FILE_NAME = "_cliResume.json"; static boolean isSuspended(ProcessSnapshot snapshot) { return snapshot.vmState().threadStatus().entrySet().stream() .anyMatch(e -> e.getValue() == ThreadStatus.SUSPENDED); } static void save(Path workDir, ProcessSnapshot snapshot, ResumeMetadata metadata) throws IOException { var events = getEvents(snapshot); StateManager.finalizeSuspendedState(workDir, snapshot, events); writeMetadata(workDir, metadata); } static ResumeMetadata readMetadata(Path workDir) throws IOException { var metadataPath = metadataPath(workDir); if (Files.notExists(metadataPath)) { return null; } try { return objectMapper().readValue(metadataPath.toFile(), ResumeMetadata.class); } catch (IOException e) { throw new IOException("Error while reading CLI resume metadata: " + e.getMessage(), e); } } static Set readWaitingEvents(Path workDir) throws IOException { var suspendMarker = suspendMarkerPath(workDir); if (Files.notExists(suspendMarker)) { return null; } return new LinkedHashSet<>(Files.readAllLines(suspendMarker)); } static boolean hasSnapshot(Path workDir) { return Files.exists(snapshotPath(workDir)); } static boolean hasMetadata(Path workDir) { return Files.exists(metadataPath(workDir)); } static void cleanup(Path workDir) throws IOException { StateManager.cleanupState(workDir); } static void printResumeGuidance(Path resumeDir, Set events, Collection pendingForms, boolean interactiveAvailable) { LocalSuspendPrinter.printSuspendGuidance(resumeDir, events, pendingForms, interactiveAvailable); } static Set getEvents(ProcessSnapshot snapshot) { var eventRefs = snapshot.vmState().getEventRefs().values(); var events = new TreeSet<>(eventRefs); if (events.size() != eventRefs.size()) { throw new IllegalStateException("Non-unique event refs: " + eventRefs + ". This is most likely a bug."); } return events; } private static void writeMetadata(Path workDir, ResumeMetadata metadata) throws IOException { var metadataPath = metadataPath(workDir); var stateDir = metadataPath.getParent(); if (Files.notExists(stateDir)) { Files.createDirectories(stateDir); } try (TemporaryPath tmp = PathUtils.tempFile("cli-resume", ".json")) { objectMapper().writerWithDefaultPrettyPrinter().writeValue(tmp.path().toFile(), metadata); Files.move(tmp.path(), metadataPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING); } } private static ObjectMapper objectMapper() { return new ObjectMapperProvider().get(); } private static Path stateDir(Path workDir) { return workDir.resolve(Constants.Files.JOB_ATTACHMENTS_DIR_NAME) .resolve(Constants.Files.JOB_STATE_DIR_NAME); } private static Path metadataPath(Path workDir) { return stateDir(workDir).resolve(METADATA_FILE_NAME); } private static Path suspendMarkerPath(Path workDir) { return stateDir(workDir).resolve(Constants.Files.SUSPEND_MARKER_FILE_NAME); } private static Path snapshotPath(Path workDir) { return stateDir(workDir).resolve("instance"); } record ResumeMetadata(ProcessConfiguration processConfiguration, RunnerConfiguration runnerConfiguration, List activeProfiles, String resumeDir, String workDir, String defaultTaskVars, String depsCacheDir, CliConfigData cliConfig) { static ResumeMetadata from(Path workDir, Path resumeDir, Path defaultTaskVars, Path depsCacheDir, String contextName, CliConfig.Overrides cliConfigOverrides, List activeProfiles, ProcessConfiguration processConfiguration, RunnerConfiguration runnerConfiguration) { return new ResumeMetadata(processConfiguration, runnerConfiguration, List.copyOf(activeProfiles), pathToString(resumeDir), pathToString(workDir), pathToString(defaultTaskVars), pathToString(depsCacheDir), CliConfigData.from(contextName, cliConfigOverrides)); } CliConfigContext loadCliConfigContext(Verbosity verbosity) throws Exception { return Objects.requireNonNull(cliConfig, "cliConfig").load(verbosity, resumeDirPath()); } Path defaultTaskVarsPath() { return stringToPath(defaultTaskVars, resumeDirPath()); } Path depsCacheDirPath() { return stringToPath(depsCacheDir, resumeDirPath()); } Path resumeDirPath() { if (resumeDir != null) { return Path.of(resumeDir); } var path = Path.of(workDir); var parent = path.getParent(); if (parent != null && CliPaths.DEFAULT_TARGET_DIR_NAME.equals(path.getFileName().toString())) { return parent; } return path; } } record CliConfigData(String contextName, boolean requiresUserConfig, String secretStoreDir, String vaultDir, String vaultId) { static CliConfigData from(String contextName, CliConfig.Overrides overrides) { return new CliConfigData(contextName, CliConfig.hasUserConfig(), pathToString(overrides.secretStoreDir()), pathToString(overrides.vaultDir()), overrides.vaultId()); } CliConfigContext load(Verbosity verbosity, Path fallbackBaseDir) throws Exception { try { if (requiresUserConfig && !CliConfig.hasUserConfig()) { throw new IllegalArgumentException("CLI configuration file is missing from ~/.concord. Resume requires the stored '" + contextName + "' context."); } return CliConfig.loadOrThrow(verbosity, contextName, toOverrides(fallbackBaseDir)); } catch (Exception e) { throw new IllegalArgumentException("Unable to reload CLI configuration context '" + contextName + "' for resume: " + e.getMessage(), e); } } private CliConfig.Overrides toOverrides(Path fallbackBaseDir) { return new CliConfig.Overrides(stringToPath(secretStoreDir, fallbackBaseDir), stringToPath(vaultDir, fallbackBaseDir), vaultId); } } private static String pathToString(Path path) { return path != null ? path.normalize().toAbsolutePath().toString() : null; } private static Path stringToPath(String value, Path fallbackBaseDir) { if (value == null) { return null; } var path = Path.of(value); if (path.isAbsolute()) { return path.normalize(); } return fallbackBaseDir.resolve(path).normalize().toAbsolutePath(); } private LocalSuspendPersistence() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/LocalSuspendPrinter.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; import com.walmartlabs.concord.forms.Form; import com.walmartlabs.concord.forms.FormField; import com.walmartlabs.concord.forms.FormFields; import java.io.Serializable; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; final class LocalSuspendPrinter { private static final ObjectMapper YAML_OBJECT_MAPPER = new ObjectMapper(new YAMLFactory() .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)); static void printSuspendGuidance(Path resumeDir, Set events, Collection pendingForms, boolean interactiveAvailable) { System.out.println("Process suspended."); System.out.println(); printResumeContext(resumeDir, System.out); System.out.println(); printPendingForms(pendingForms); printNonInteractiveSupport(pendingForms, interactiveAvailable, System.out); printAdditionalEvents(additionalEvents(events, pendingForms)); printContinueWith(pendingForms, additionalEvents(events, pendingForms), interactiveAvailable, false); } static void printInputRequired(Path resumeDir, Set events, Collection pendingForms, boolean interactiveAvailable) { printResumeContext(resumeDir, System.err); System.err.println(); if (pendingForms.size() == 1) { System.err.println("Pending form requires input in non-interactive mode."); } else { System.err.println("Pending forms require input or explicit event selection."); } System.err.println(); printPendingForms(pendingForms, System.err); printNonInteractiveSupport(pendingForms, interactiveAvailable, System.err); printAdditionalEvents(additionalEvents(events, pendingForms), System.err); printContinueWith(pendingForms, additionalEvents(events, pendingForms), interactiveAvailable, true); } static void printDescribeSelectionRequired(Path resumeDir, Set events, Collection pendingForms) { printResumeContext(resumeDir, System.err); System.err.println(); System.err.println("Pending forms require explicit event selection before describing input."); System.err.println(); printPendingForms(pendingForms, System.err); printAdditionalEvents(additionalEvents(events, pendingForms), System.err); System.err.println("Continue with:"); System.err.println(" Describe input:"); for (var form : pendingForms) { System.err.println(" " + describeInputCommand(form)); } } static void printEventSelectionRequired(Path resumeDir, Set events) { printResumeContext(resumeDir, System.err); System.err.println(); System.err.println("Multiple waiting events require explicit event selection."); System.err.println(); printAdditionalEvents(new TreeSet<>(events), System.err); System.err.println("Continue with:"); System.err.println(" Resume event:"); for (var event : new TreeSet<>(events)) { System.err.println(" " + resumeCommand() + " --event " + shellQuote(event)); } } static void printDescribeInput(Path resumeDir, Form form) throws Exception { printResumeDir(resumeDir); System.out.println("Pending form input:"); System.out.println(" " + formMapping(form)); var requiredFields = userFields(form).stream() .filter(f -> !isOptional(f)) .toList(); var optionalFields = userFields(form).stream() .filter(LocalSuspendPrinter::isOptional) .toList(); var fileFields = userFields(form).stream() .filter(LocalSuspendPrinter::isFileField) .toList(); printFieldList("Required fields:", requiredFields, System.out); printFieldList("Optional fields:", optionalFields, System.out); if (!fileFields.isEmpty()) { System.out.println("File-upload fields:"); for (var field : fileFields) { System.out.println(" " + field.name()); } System.out.println("Non-interactive submission:"); System.out.println(" not supported for file-upload fields"); } var example = examplePayload(form); if (!example.isEmpty()) { System.out.println("Example input file:"); for (var line : YAML_OBJECT_MAPPER.writeValueAsString(example).stripTrailing().split("\\R")) { System.out.println(" " + line); } } } static void printUnsupportedNonInteractiveForm(Path resumeDir, Form form, boolean interactiveAvailable) { printResumeContext(resumeDir, System.err); System.err.println(); System.err.println("Pending form cannot be submitted non-interactively because it contains file-upload fields."); System.err.println(); printPendingForms(List.of(form), System.err); System.err.println("Continue with:"); System.err.println(" Describe input:"); System.err.println(" " + describeInputCommand(form)); if (interactiveAvailable) { System.err.println(" Fill interactively:"); System.err.println(" " + resumeCommand()); } } static boolean supportsNonInteractiveInput(Form form) { return userFields(form).stream().noneMatch(LocalSuspendPrinter::isFileField); } private static void printContinueWith(Collection pendingForms, Set additionalEvents, boolean interactiveAvailable, boolean toErr) { var out = toErr ? System.err : System.out; out.println("Continue with:"); if (interactiveAvailable && !pendingForms.isEmpty()) { out.println(" Fill interactively:"); out.println(" " + resumeCommand()); } if (!pendingForms.isEmpty()) { out.println(" Describe input:"); for (var form : pendingForms) { out.println(" " + describeInputCommand(form)); } } if (pendingForms.stream().anyMatch(LocalSuspendPrinter::supportsNonInteractiveInput)) { out.println(" Submit input:"); } for (var form : pendingForms) { if (!supportsNonInteractiveInput(form)) { continue; } out.println(" " + inputFileCommand(form)); } if (!additionalEvents.isEmpty()) { out.println(" Resume event:"); for (var event : additionalEvents) { out.println(" " + resumeCommand() + " --event " + shellQuote(event)); } } } private static void printResumeContext(Path resumeDir, java.io.PrintStream out) { out.println("Resume dir: " + resumeDir.normalize().toAbsolutePath()); out.println("Commands below assume you are in that directory."); } private static void printResumeDir(Path resumeDir) { System.out.println("Resume dir: " + resumeDir.normalize().toAbsolutePath()); } private static void printPendingForms(Collection pendingForms) { printPendingForms(pendingForms, System.out); } private static void printPendingForms(Collection pendingForms, java.io.PrintStream out) { if (pendingForms.isEmpty()) { return; } out.println("Pending forms:"); var formKeyWidth = "Form key".length(); for (var form : pendingForms) { formKeyWidth = Math.max(formKeyWidth, form.name().length()); } out.printf(" %-" + formKeyWidth + "s %s%n", "Form key", "Event ID"); for (var form : pendingForms) { out.printf(" %-" + formKeyWidth + "s %s%n", form.name(), form.eventName()); } out.println(); } private static void printAdditionalEvents(Set events) { printAdditionalEvents(events, System.out); } private static void printAdditionalEvents(Set events, java.io.PrintStream out) { if (events.isEmpty()) { return; } out.println("Additional waiting events:"); for (var event : events) { out.println(" " + event); } out.println(); } private static void printFieldList(String header, List fields, java.io.PrintStream out) { if (fields.isEmpty()) { return; } out.println(header); for (var field : fields) { out.println(" " + field.name()); } } private static void printNonInteractiveSupport(Collection pendingForms, boolean interactiveAvailable, java.io.PrintStream out) { if (interactiveAvailable || pendingForms.size() != 1) { return; } var form = pendingForms.iterator().next(); if (supportsNonInteractiveInput(form)) { return; } out.println("Non-interactive submission:"); out.println(" not supported for file-upload fields"); out.println(); } private static String formMapping(Form form) { return form.name() + " -> " + form.eventName(); } private static Set additionalEvents(Set events, Collection pendingForms) { var result = new TreeSet<>(events); result.removeAll(LocalFormState.formEvents(pendingForms)); return result; } private static String describeInputCommand(Form form) { return resumeCommand() + " --event " + shellQuote(form.eventName()) + " --describe-input"; } private static String inputFileCommand(Form form) { return resumeCommand() + " --event " + shellQuote(form.eventName()) + " --input-file " + shellQuote(exampleInputFileName(form)); } private static List userFields(Form form) { return form.fields().stream() .filter(f -> !Boolean.TRUE.equals(f.getOption(FormFields.CommonFieldOptions.READ_ONLY))) .toList(); } private static boolean isOptional(FormField field) { return field.cardinality() == FormField.Cardinality.ONE_OR_NONE || field.cardinality() == FormField.Cardinality.ANY; } private static boolean isRepeated(FormField field) { return field.cardinality() == FormField.Cardinality.ANY || field.cardinality() == FormField.Cardinality.AT_LEAST_ONE; } private static boolean isFileField(FormField field) { return FormFields.FileField.TYPE.equals(field.type()); } private static String exampleInputFileName(Form form) { return form.name().replaceAll("[^A-Za-z0-9._-]+", "_") + ".yml"; } private static Object firstAllowedValue(Serializable value) { if (value instanceof List l && !l.isEmpty()) { return l.get(0); } return value; } private static Map examplePayload(Form form) { var values = new LinkedHashMap(); for (var field : userFields(form)) { values.put(field.name(), exampleValue(field)); } if (values.isEmpty()) { return Map.of(); } return Map.of(form.name(), values); } private static Object exampleValue(FormField field) { var baseValue = field.defaultValue() != null ? field.defaultValue() : firstAllowedValue(field.allowedValue()); if (baseValue == null) { baseValue = switch (field.type()) { case "boolean" -> Boolean.TRUE; case "int" -> 0; case "decimal" -> 0.0d; case "file" -> "path/to/file"; default -> ""; }; } if (!isRepeated(field)) { return baseValue; } if (baseValue instanceof Collection c) { return new ArrayList<>(c); } return List.of(baseValue); } private static String resumeCommand() { return "concord resume"; } private static String shellQuote(String value) { if (value.matches("[A-Za-z0-9_./:=+-]+")) { return value; } return "'" + value.replace("'", "'\"'\"'") + "'"; } private LocalSuspendPrinter() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/Main.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.fusesource.jansi.AnsiConsole; import picocli.CommandLine; public class Main { public static void main(String[] args) { AnsiConsole.systemInstall(); CommandLine cli = new CommandLine(new App()); int code = cli.execute(args); System.exit(code); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/PromptSupport.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ final class PromptSupport { static final String ALLOW_STDIN_PROMPTS_PROPERTY = "concord.cli.allowStdInPrompts"; static boolean canPromptInteractively() { return System.console() != null || Boolean.getBoolean(ALLOW_STDIN_PROMPTS_PROPERTY); } private PromptSupport() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/RemoteRun.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.walmartlabs.concord.cli.secrets.CliSecretService; import com.walmartlabs.concord.client2.ApiClient; import com.walmartlabs.concord.client2.ApiException; import com.walmartlabs.concord.client2.ProcessApi; import com.walmartlabs.concord.common.PathUtils; import com.walmartlabs.concord.common.TemporaryPath; import com.walmartlabs.concord.common.ZipUtils; import com.walmartlabs.concord.sdk.Constants; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import java.io.IOException; import java.net.http.HttpClient; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Callable; import static java.nio.charset.StandardCharsets.UTF_8; import static org.fusesource.jansi.Ansi.ansi; @Command(name = "remote-run", description = "Execute flows remotely. Sends the specified as the process payload.") public class RemoteRun implements Callable { private static final long LARGE_PAYLOAD_SIZE_BYTES = 16 * 1024 * 1024; private static final String LARGE_PAYLOAD_SIZE_HUMAN = "16MB"; @Option(names = {"--context"}, description = "Configuration context to use") String context = "default"; @Option(names = {"-v", "--verbose"}, description = "Use verbose output") boolean[] verbosity = new boolean[0]; @Option(names = {"--profiles"}, description = "A comma-separated list of Concord profiles to use", split = ",") String[] activeProfiles; @Option(names = {"--org"}, description = "Start the process in the specified Concord organization") String orgName; @Option(names = {"--project"}, description = "Start the process in the specified Concord project") String projectName; @Option(names = {"--entry-point"}, description = "Name of the starting flow") String entryPoint; @Option(names = {"--args"}, description = "Process arguments (flow variables)") Map processArgs = new LinkedHashMap<>(); @Option(names = {"--cfg"}, description = "Process configuration in JSON format (dependencies, runtime, arguments, etc)") String processCfg; @Option(names = {"--no-gitignore"}, description = "Do not use .gitignore patterns when filtering files") boolean noGitIgnore = false; @Parameters(arity = "1", description = "A path to a single concord.yaml (or .yml) file or a directory with flows") Path workDir = Paths.get(System.getProperty("user.dir")); @Override public Integer call() { var verbosity = new Verbosity(this.verbosity); var mapper = new ObjectMapper(); if (processCfg != null && !processCfg.isBlank()) { try { mapper.readValue(processCfg, ObjectNode.class); } catch (JsonProcessingException e) { return err("Expected a valid JSON object in , got: " + processCfg); } } var configContext = CliConfig.load(verbosity, context, null); var remoteRunConfig = configContext.remoteRun(); if (remoteRunConfig == null) { return missingConfig(context, "remoteRun"); } if (remoteRunConfig.baseUrl() == null) { return missingConfig(context, "remoteRun.baseUrl"); } if (remoteRunConfig.apiKeyRef() == null) { return missingConfig(context, "remoteRun.apiKeyRef"); } var secretService = CliSecretService.create(configContext, workDir, verbosity); String apiKey; try { apiKey = secretService.exportAsString(remoteRunConfig.apiKeyRef().orgName(), remoteRunConfig.apiKeyRef().secretName(), null); } catch (Exception e) { return err("Unable to fetch the API key. " + e.getMessage()); } var client = new ApiClient(HttpClient.newBuilder().build()) .setBaseUrl(remoteRunConfig.baseUrl()) .setApiKey(apiKey); var processApi = new ProcessApi(client); info("Preparing the payload..."); if (!Files.exists(workDir)) { return err(" not found: " + workDir); } try { var payloadSize = getDirectorySize(workDir); if (payloadSize >= LARGE_PAYLOAD_SIZE_BYTES) { if (!Confirmation.confirm("The specified is larger than %s. Continue? (y/N)".formatted(LARGE_PAYLOAD_SIZE_HUMAN))) { return err("Aborting."); } } } catch (IOException e) { return err("Unable to determine the payload size: " + e.getMessage()); } try (var archive = prepareArchive(workDir, noGitIgnore)) { var input = new HashMap(); input.put("archive", archive.path()); if (orgName != null) { input.put(Constants.Multipart.ORG_NAME, orgName); } if (projectName != null) { input.put(Constants.Multipart.PROJECT_NAME, projectName); } if (activeProfiles != null) { input.put(Constants.Request.ACTIVE_PROFILES_KEY, activeProfiles); } if (entryPoint != null) { input.put(Constants.Request.ENTRY_POINT_KEY, entryPoint); } if (processArgs != null) { processArgs.forEach((key, value) -> input.put(Constants.Request.ARGUMENTS_KEY + "." + key, value)); } if (processCfg != null && !processCfg.isBlank()) { input.put("request", processCfg.getBytes(UTF_8)); } info("Starting a new process..."); var response = processApi.startProcess(input); info("Started %s/#/process/%s/log".formatted(remoteRunConfig.baseUrl(), response.getInstanceId())); } catch (ApiException e) { return handleApiException(e); } catch (IOException e) { return err("Failed to start a process. " + e.getMessage()); } return 0; } private static TemporaryPath prepareArchive(Path src, boolean noGitIgnore) throws IOException { var badSrc = false; if (Files.isRegularFile(src)) { var fileName = src.getFileName().toString(); badSrc = !fileName.equals("concord.yml") && !fileName.equals("concord.yaml") && !fileName.endsWith(".yml") && !fileName.endsWith(".yaml"); } else if (!Files.isDirectory(src)) { badSrc = true; } if (badSrc) { throw new IOException("Expected a path to a single concord.yaml (or .yml) file or a directory with flows, got " + src); } var dst = PathUtils.tempFile("payload", ".zip"); try (var zip = new ZipArchiveOutputStream(dst.path(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { if (Files.isRegularFile(src)) { ZipUtils.zipFile(zip, src, src.getFileName().toString()); } else if (Files.isDirectory(src)) { GitIgnoreFilter gitIgnoreFilter = noGitIgnore ? null : GitIgnoreFilter.load(src); zipWithGitIgnore(zip, src, gitIgnoreFilter); } } return dst; } private static void zipWithGitIgnore(ZipArchiveOutputStream zip, Path srcDir, GitIgnoreFilter filter) throws IOException { Files.walkFileTree(srcDir, new SimpleFileVisitor<>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { if (dir.equals(srcDir)) { return FileVisitResult.CONTINUE; } String name = dir.getFileName().toString(); // Always skip .git and .concord directories if (name.equals(".git") || name.equals(".concord")) { return FileVisitResult.SKIP_SUBTREE; } // Check gitignore Path rel = srcDir.relativize(dir); if (filter != null && filter.isIgnored(rel, true)) { return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Path rel = srcDir.relativize(file); if (filter != null && filter.isIgnored(rel, false)) { return FileVisitResult.CONTINUE; } String name = rel.toString(); ZipUtils.zipFile(zip, file, name); return FileVisitResult.CONTINUE; } }); } private static long getDirectorySize(Path src) throws IOException { try (var walker = Files.walk(src)) { return walker.filter(Files::isRegularFile) .mapToLong(path -> { try { return Files.size(path); } catch (IOException e) { throw new RuntimeException(e); } }).sum(); } } private static int handleApiException(ApiException apiException) { if (apiException.getCode() != 400) { return err("Failed to start a process. " + apiException.getMessage()); } var contentType = apiException.getResponseHeaders().firstValue("Content-Type"); if (contentType.filter(t -> t.contains("vnd.concord-validation-errors-v1+json")).isEmpty()) { return unexpectedErrorBody(apiException); } JsonNode validationErrors; try { var mapper = new ObjectMapper(); validationErrors = mapper.readTree(apiException.getResponseBody()); } catch (IOException e) { return unexpectedErrorBody(apiException); } if (validationErrors == null || !validationErrors.isArray()) { return unexpectedErrorBody(apiException); } var text = new StringBuilder(); for (var node : validationErrors) { if (!node.isObject()) { return unexpectedErrorBody(apiException); } var message = node.get("message"); if (message != null && !message.asText().isBlank()) { text.append(message.asText()); } } validationErrors.forEach(e -> text.append(e.asText()).append(". ")); return err("Failed to start a process. " + text); } private static void info(String msg) { System.out.println(msg); } private static int unexpectedErrorBody(ApiException e) { warn("Unable to parse the API error response body: " + e.getResponseBody()); return err("Failed to start a process. " + e.getMessage()); } private static int missingConfig(String context, String key) { return err("Missing '%s' configuration in the '%s' context".formatted(key, context)); } private static void warn(String msg) { System.out.println(ansi().fgYellow().a(msg).reset()); } private static int err(String msg) { System.out.println(ansi().fgRed().a(msg).reset()); return -1; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/Resume.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.inject.Key; import com.google.inject.name.Names; import com.walmartlabs.concord.common.ConfigurationUtils; import com.walmartlabs.concord.forms.Form; import com.walmartlabs.concord.runtime.common.StateManager; import com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot; import com.walmartlabs.concord.runtime.v2.runner.Runner; import com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders; import com.walmartlabs.concord.runtime.v2.runner.vm.ParallelExecutionException; import com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; @Command(name = "resume", description = "Resume a previously suspended local runtime-v2 workspace.") public class Resume implements Callable { private static final ObjectMapper INPUT_OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()); private static final TypeReference> MAP_TYPE = new TypeReference<>() { }; @Option(names = {"-h", "--help"}, usageHelp = true, description = "display the command's help message") boolean helpRequested = false; @Option(names = {"-v", "--verbose"}, description = { "Specify multiple -v options to increase verbosity. For example, `-v -v -v` or `-vvv`", "-v log flow steps", "-vv log task input/output args", "-vvv runner debug logs"}) boolean[] verbosity = new boolean[0]; @Option(names = {"--event"}, description = "Waiting event reference to resume") String event; @Option(names = {"--input-file"}, description = "Resume input payload in JSON or YAML format") Path inputFile; @Option(names = {"-e", "--extra-vars"}, description = "inline resume input values (key=value)") Map extraVars = new LinkedHashMap<>(); @Option(names = {"--save-as"}, description = "Wrap the payload under the specified variable path") String saveAs; @Option(names = {"--describe-input"}, description = "Describe the expected input shape for a pending form") boolean describeInput = false; @Option(names = {"--no-prompt"}, description = "Disable interactive prompts and print recovery commands instead") boolean noPrompt = false; @Parameters(arity = "0..1", description = "Prepared workspace directory containing suspended local state (default: current directory or ./target).") Path workDir; @Override public Integer call() throws Exception { var verbosity = new Verbosity(this.verbosity); var workDir = resolveWorkDir(); if (saveAs != null && inputFile == null && extraVars.isEmpty()) { return err(CliExitCodes.USAGE, "--save-as requires --input-file or -e/--extra-vars"); } if (describeInput && hasManualInputFlags()) { return err(CliExitCodes.USAGE, "--describe-input can't be combined with --input-file, -e/--extra-vars or --save-as"); } try { var metadata = loadMetadata(workDir); var waitingEvents = loadWaitingEvents(workDir); var pendingForms = LocalFormState.syncPendingForms(workDir, waitingEvents); var resumeDir = metadata.resumeDirPath(); var interactiveAvailable = canPromptInteractively(); var formMode = usesFormMode(pendingForms); if (describeInput) { return describeInput(resumeDir, waitingEvents, pendingForms); } if (formMode && !interactiveAvailable) { if (pendingForms.size() == 1 && waitingEvents.size() == 1 && !LocalSuspendPrinter.supportsNonInteractiveInput(pendingForms.get(0))) { LocalSuspendPrinter.printUnsupportedNonInteractiveForm(resumeDir, pendingForms.get(0), false); return CliExitCodes.NON_INTERACTIVE_UNSUPPORTED; } LocalSuspendPrinter.printInputRequired(resumeDir, waitingEvents, pendingForms, false); return CliExitCodes.INPUT_REQUIRED; } var selectedEvent = formMode ? null : selectEvent(resumeDir, waitingEvents, pendingForms); if (!formMode && selectedEvent.exitCode() != CliExitCodes.SUCCESS) { return selectedEvent.exitCode(); } var dependencyManager = LocalCliRuntime.createDependencyManager(metadata.depsCacheDirPath()); var injector = LocalCliRuntime.createInjector(workDir, metadata.runnerConfiguration(), metadata.processConfiguration(), metadata.loadCliConfigContext(verbosity), metadata.defaultTaskVarsPath(), dependencyManager, verbosity); LocalCliRuntime.notifyProjectLoaded(workDir); if (metadata.processConfiguration().debug()) { System.out.println("Available tasks: " + injector.getInstance(TaskProviders.class).names()); } var classLoader = injector.getInstance(Key.get(ClassLoader.class, Names.named("runtime"))); var snapshot = loadSnapshot(workDir, classLoader); if (snapshot == null) { return err(CliExitCodes.ERROR, "Missing suspended snapshot in " + workDir); } var runner = injector.getInstance(Runner.class); try { if (formMode) { LocalFormState.assertSupported(workDir, pendingForms); snapshot = LocalFormSession.resumePendingForms(workDir, runner, snapshot, metadata); } else { var selectedForm = findPendingForm(pendingForms, selectedEvent.event()); if (selectedForm != null && !hasManualInputFlags()) { LocalSuspendPrinter.printInputRequired(resumeDir, waitingEvents, pendingForms, interactiveAvailable); return CliExitCodes.INPUT_REQUIRED; } if (selectedForm != null && !LocalSuspendPrinter.supportsNonInteractiveInput(selectedForm)) { LocalSuspendPrinter.printUnsupportedNonInteractiveForm(resumeDir, selectedForm, interactiveAvailable); return CliExitCodes.NON_INTERACTIVE_UNSUPPORTED; } var input = loadInput(); if (selectedForm != null) { try { input = LocalFormInputs.convertAndValidate(selectedForm, input, true).payload(selectedForm); } catch (LocalFormInputs.InputException e) { printFormInputErrors(e); LocalSuspendPrinter.printInputRequired(resumeDir, waitingEvents, pendingForms, interactiveAvailable); return CliExitCodes.INPUT_REQUIRED; } } snapshot = runner.resume(snapshot, Collections.singleton(selectedEvent.event()), input); } } catch (ParallelExecutionException | UserDefinedException e) { return CliExitCodes.PROCESS_FAILED; } catch (Exception e) { Run.logException(verbosity, e); return CliExitCodes.ERROR; } if (LocalSuspendPersistence.isSuspended(snapshot)) { LocalSuspendPersistence.save(workDir, snapshot, metadata); var events = LocalSuspendPersistence.getEvents(snapshot); var nextPendingForms = LocalFormState.syncPendingForms(workDir, events); LocalSuspendPersistence.printResumeGuidance(resumeDir, events, nextPendingForms, interactiveAvailable); return CliExitCodes.SUSPENDED; } LocalSuspendPersistence.cleanup(workDir); System.out.println("...done!"); return CliExitCodes.SUCCESS; } catch (Exception e) { return err(CliExitCodes.ERROR, e.getMessage()); } } private int describeInput(Path resumeDir, Set waitingEvents, List pendingForms) throws Exception { if (pendingForms.isEmpty()) { return err(CliExitCodes.USAGE, "--describe-input requires a pending form"); } if (event == null || event.isBlank()) { if (pendingForms.size() > 1) { LocalSuspendPrinter.printDescribeSelectionRequired(resumeDir, waitingEvents, pendingForms); return CliExitCodes.INPUT_REQUIRED; } LocalSuspendPrinter.printDescribeInput(resumeDir, pendingForms.get(0)); return CliExitCodes.SUCCESS; } var form = findPendingForm(pendingForms, event); if (form == null) { if (waitingEvents.contains(event)) { return err(CliExitCodes.USAGE, "--describe-input is only available for pending forms"); } return err(CliExitCodes.USAGE, "Unknown event: " + event + ". Available events: " + String.join(", ", waitingEvents)); } LocalSuspendPrinter.printDescribeInput(resumeDir, form); return CliExitCodes.SUCCESS; } private SelectionResult selectEvent(Path resumeDir, Set waitingEvents, List pendingForms) { if (event == null || event.isBlank()) { if (waitingEvents.size() == 1) { return SelectionResult.success(waitingEvents.iterator().next()); } if (!pendingForms.isEmpty()) { LocalSuspendPrinter.printInputRequired(resumeDir, waitingEvents, pendingForms, canPromptInteractively()); return SelectionResult.error(CliExitCodes.INPUT_REQUIRED); } else { LocalSuspendPrinter.printEventSelectionRequired(resumeDir, waitingEvents); return SelectionResult.error(CliExitCodes.INPUT_REQUIRED); } } if (!waitingEvents.contains(event)) { err(CliExitCodes.USAGE, "Unknown event: " + event + ". Available events: " + String.join(", ", waitingEvents)); return SelectionResult.error(CliExitCodes.USAGE); } return SelectionResult.success(event); } private Map loadInput() throws Exception { var input = new LinkedHashMap(); if (inputFile != null) { if (!Files.exists(inputFile)) { throw new IllegalArgumentException("Input file not found: " + inputFile); } var node = INPUT_OBJECT_MAPPER.readTree(inputFile.toFile()); if (node == null || !node.isObject()) { throw new IllegalArgumentException("Expected a JSON or YAML object in " + inputFile); } input.putAll(INPUT_OBJECT_MAPPER.convertValue(node, MAP_TYPE)); } if (!extraVars.isEmpty()) { input.putAll(ConfigurationUtils.deepMerge(input, loadInlineInput())); } if (input.isEmpty()) { return Collections.emptyMap(); } if (saveAs == null || saveAs.isBlank()) { return input; } return wrapInput(input, saveAs); } private Map loadInlineInput() throws Exception { var input = new LinkedHashMap(); for (var e : extraVars.entrySet()) { var key = e.getKey().trim(); if (key.isEmpty()) { throw new IllegalArgumentException("Invalid inline input key"); } var nested = ConfigurationUtils.toNested(key, parseInlineValue(e.getValue())); input.putAll(ConfigurationUtils.deepMerge(input, nested)); } return input; } private static Object parseInlineValue(String value) throws Exception { return INPUT_OBJECT_MAPPER.readValue(value, Object.class); } private static Map wrapInput(Map input, String saveAs) { var segments = saveAs.split("\\."); var current = input; for (int i = segments.length - 1; i >= 0; i--) { var segment = segments[i].trim(); if (segment.isEmpty()) { throw new IllegalArgumentException("Invalid --save-as path: " + saveAs); } var wrapper = new LinkedHashMap(); wrapper.put(segment, current); current = wrapper; } return current; } private static int err(int exitCode, String message) { System.err.println(message); return exitCode; } private static void printFormInputErrors(LocalFormInputs.InputException e) { for (var message : e.messages()) { System.err.println("Invalid form input: " + message); } System.err.println(); } private boolean usesFormMode(List pendingForms) { return !hasGenericManualFlags() && !pendingForms.isEmpty(); } private boolean hasGenericManualFlags() { return event != null || hasManualInputFlags(); } private boolean hasManualInputFlags() { return inputFile != null || saveAs != null || !extraVars.isEmpty(); } private boolean canPromptInteractively() { return !noPrompt && PromptSupport.canPromptInteractively(); } private static Form findPendingForm(List pendingForms, String event) { if (event == null) { return null; } return pendingForms.stream() .filter(f -> event.equals(f.eventName())) .findFirst() .orElse(null); } private Path resolveWorkDir() { var baseDir = workDir != null ? workDir : Paths.get(System.getProperty("user.dir")); var normalized = baseDir.normalize().toAbsolutePath(); if (hasSuspendedState(normalized)) { return normalized; } var targetDir = CliPaths.defaultTargetDir(normalized); if (hasSuspendedState(targetDir)) { return targetDir; } return normalized; } private static boolean hasSuspendedState(Path workDir) { return LocalSuspendPersistence.hasMetadata(workDir) && LocalSuspendPersistence.hasSnapshot(workDir); } private static LocalSuspendPersistence.ResumeMetadata loadMetadata(Path workDir) throws Exception { var metadata = LocalSuspendPersistence.readMetadata(workDir); if (metadata == null) { throw new IllegalArgumentException("Missing CLI resume metadata in " + workDir); } if (!LocalSuspendPersistence.hasSnapshot(workDir)) { throw new IllegalArgumentException("Missing suspended snapshot in " + workDir); } return metadata; } private static Set loadWaitingEvents(Path workDir) throws Exception { Set waitingEvents; try { waitingEvents = LocalSuspendPersistence.readWaitingEvents(workDir); } catch (Exception e) { throw new IllegalArgumentException("Error while reading waiting events: " + e.getMessage(), e); } if (waitingEvents == null || waitingEvents.isEmpty()) { throw new IllegalArgumentException("Missing suspend marker in " + workDir); } return waitingEvents; } private record SelectionResult(String event, int exitCode) { static SelectionResult success(String event) { return new SelectionResult(event, CliExitCodes.SUCCESS); } static SelectionResult error(int exitCode) { return new SelectionResult(null, exitCode); } } private static ProcessSnapshot loadSnapshot(Path workDir, ClassLoader classLoader) { // This branch is the first producer of local CLI suspended state, so direct reads are intentional for now. // Revisit compatibility handling here if the on-disk local suspended-state format changes after release. return StateManager.readProcessState(workDir, classLoader); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/Run.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; import com.google.inject.Injector; import com.walmartlabs.concord.cli.CliConfig.CliConfigContext; import com.walmartlabs.concord.cli.runner.*; import com.walmartlabs.concord.common.ConfigurationUtils; import com.walmartlabs.concord.common.FileVisitor; import com.walmartlabs.concord.common.PathUtils; import com.walmartlabs.concord.dependencymanager.DependencyManager; import com.walmartlabs.concord.forms.Form; import com.walmartlabs.concord.imports.*; import com.walmartlabs.concord.runtime.common.cfg.ApiConfiguration; import com.walmartlabs.concord.runtime.common.cfg.RunnerConfiguration; import com.walmartlabs.concord.runtime.model.EffectiveConfiguration; import com.walmartlabs.concord.runtime.v2.ProjectLoaderV2; import com.walmartlabs.concord.runtime.v2.ProjectSerializerV2; import com.walmartlabs.concord.runtime.v2.model.Flow; import com.walmartlabs.concord.runtime.v2.model.ProcessDefinition; import com.walmartlabs.concord.runtime.v2.model.ProcessDefinitionConfiguration; import com.walmartlabs.concord.runtime.v2.model.Profile; import com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot; import com.walmartlabs.concord.runtime.v2.runner.Runner; import com.walmartlabs.concord.runtime.v2.sdk.ImmutableProcessConfiguration; import com.walmartlabs.concord.runtime.v2.runner.tasks.TaskProviders; import com.walmartlabs.concord.runtime.v2.runner.vm.ParallelExecutionException; import com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration; import com.walmartlabs.concord.runtime.v2.sdk.ProcessInfo; import com.walmartlabs.concord.runtime.v2.sdk.ProjectInfo; import com.walmartlabs.concord.runtime.v2.sdk.UserDefinedException; import com.walmartlabs.concord.runtime.v2.wrapper.ProcessDefinitionV2; import com.walmartlabs.concord.sdk.Constants; import com.walmartlabs.concord.sdk.MapUtils; import picocli.CommandLine.Command; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; import picocli.CommandLine.Spec; import java.io.IOException; import java.io.InputStream; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; import java.util.concurrent.Callable; import java.util.stream.Stream; import static org.fusesource.jansi.Ansi.ansi; @Command(name = "run", description = "Execute flows locally. Sends the specified as the process payload.") public class Run implements Callable { @Spec private CommandSpec spec; @Option(names = {"-h", "--help"}, usageHelp = true, description = "display the command's help message") boolean helpRequested = false; @Option(names = {"--context"}, description = "Configuration context to use") String context = "default"; @Option(names = {"-e", "--extra-vars"}, description = "additional process variables") Map extraVars = new LinkedHashMap<>(); @Option(names = {"--default-cfg"}, description = "default Concord configuration file") Path defaultCfg = Paths.get(System.getProperty("user.home")).resolve(".concord").resolve("defaultCfg.yml"); @Option(names = {"--default-task-vars"}, description = "default task variables configuration file") Path defaultTaskVars = Paths.get(System.getProperty("user.home")).resolve(".concord").resolve("defaultTaskVars.json"); @Option(names = {"--deps-cache-dir"}, description = "process dependencies cache dir") Path depsCacheDir = Paths.get(System.getProperty("user.home")).resolve(".concord").resolve("depsCache"); @Option(names = {"--repo-cache-dir"}, description = "repository cache dir") Path repoCacheDir = Paths.get(System.getProperty("user.home")).resolve(".concord").resolve("repoCache"); @Option(names = {"--secret-dir"}, description = "secret store dir") Path secretStoreDir; @Option(names = {"--vault-dir"}, description = "vault dir") Path vaultDir; @Option(names = {"--vault-id"}, description = "vault id") String vaultId; @Option(names = {"--imports-source"}, description = "default imports source") String importsSource = "https://github.com"; @Option(names = {"--entry-point"}, description = "entry point") String entryPoint = Constants.Request.DEFAULT_ENTRY_POINT_NAME; @Option(names = {"-p", "--profile"}, description = "active profile") List profiles = new ArrayList<>(); @Option(names = {"-c", "--clean"}, description = "remove the target directory before starting the process") boolean cleanup = false; @Option(names = {"-v", "--verbose"}, description = { "Specify multiple -v options to increase verbosity. For example, `-v -v -v` or `-vvv`", "-v log flow steps", "-vv log task input/output args", "-vvv runner debug logs"}) boolean[] verbosity = new boolean[0]; @Option(names = {"--effective-yaml"}, description = "generate the effective YAML (skips execution)") boolean effectiveYaml = false; @Option(names = {"--default-import-version"}, description = "default import version or repo branch") String defaultVersion = "main"; @Option(names = {"--no-default-cfg"}, description = "Do not load default configuration (including standard dependencies)") boolean noDefaultCfg = false; @Option(names = {"--no-prompt"}, description = "Disable interactive prompts and print recovery commands instead") boolean noPrompt = false; @Option(names = {"--no-gitignore"}, description = "Do not use .gitignore patterns when filtering files") boolean noGitIgnore = false; @Option(names = {"--target-dir"}, description = "Target directory for the assembled payload (default: /target)") Path targetDir; @Parameters(arity = "0..1", description = "Directory with Concord files or a path to a single Concord YAML file.") Path sourceDirOrFile = Paths.get(System.getProperty("user.dir")); @Option(names = {"--dry-run"}, description = "execute process in dry-run mode?") boolean dryRunMode = false; @Override public Integer call() throws Exception { var verbosity = new Verbosity(this.verbosity); var cliConfigOverrides = new CliConfig.Overrides(secretStoreDir, vaultDir, vaultId); CliConfigContext cliConfigContext = CliConfig.load(verbosity, context, cliConfigOverrides); var sourceDir = resolveSourceDir(sourceDirOrFile.normalize().toAbsolutePath()); var workDir = prepareWorkDir(sourceDirOrFile.normalize().toAbsolutePath(), verbosity); if (!noDefaultCfg) { copyDefaultCfg(workDir, defaultCfg, verbosity.verbose()); } var dependencyManager = LocalCliRuntime.createDependencyManager(depsCacheDir); var importManager = new ImportManagerFactory(dependencyManager, new CliRepositoryExporter(repoCacheDir), Collections.emptySet()) .create(); ProjectLoaderV2.Result loadResult; try { loadResult = new ProjectLoaderV2(importManager) .load(workDir, new CliImportsNormalizer(importsSource, verbosity.verbose(), defaultVersion), verbosity.verbose() ? new CliImportsListener() : null); } catch (ImportProcessingException e) { ObjectMapper om = new ObjectMapper(); System.err.println("Error while processing import " + om.writeValueAsString(e.getImport()) + ": " + e.getMessage()); return CliExitCodes.PROCESS_FAILED; } catch (Exception e) { System.err.println("Error while loading " + workDir); e.printStackTrace(); return CliExitCodes.PROCESS_FAILED; } var processDefinition = loadResult.getProjectDefinition(); var instanceId = UUID.randomUUID(); if (verbosity.verbose() && !extraVars.isEmpty()) { System.out.println("Additional variables: " + extraVars); } if (verbosity.verbose() && !profiles.isEmpty()) { System.out.println("Active profiles: " + profiles); } // "deps" are the "dependencies" of the last profile in the list of active profiles (if present) var overlayCfg = EffectiveConfiguration.getEffectiveConfiguration(new ProcessDefinitionV2(processDefinition), profiles); List deps = MapUtils.getList(overlayCfg, Constants.Request.DEPENDENCIES_KEY, Collections.emptyList()); // "extraDependencies" are additive: ALL extra dependencies from ALL ACTIVE profiles are added to the list var extraDeps = profiles.stream() .flatMap(profileName -> Stream.ofNullable(processDefinition.profiles().get(profileName))) .flatMap(profile -> profile.configuration().extraDependencies().stream()) .toList(); var allDeps = new ArrayList(deps); allDeps.addAll(extraDeps); var resolver = new DependencyResolver(dependencyManager, verbosity.verbose()); Collection resolvedDependencies; try { resolvedDependencies = resolver.resolveDeps(allDeps); } catch (Exception e) { System.err.println(e.getMessage()); return CliExitCodes.PROCESS_FAILED; } var runnerCfg = RunnerConfiguration.builder() .api(buildApiConfiguration(cliConfigContext)) .dependencies(resolvedDependencies) .debug(processDefinition.configuration().debug()) .build(); if (verbosity.verbose()) { System.out.println("Using '" + runnerCfg.api().baseUrl() + "' as API base URL"); } Map overlayArgs = MapUtils.getMap(overlayCfg, Constants.Request.ARGUMENTS_KEY, Collections.emptyMap()); var args = ConfigurationUtils.deepMerge(processDefinition.configuration().arguments(), overlayArgs, extraVars); args.put(Constants.Context.TX_ID_KEY, instanceId.toString()); args.put(Constants.Context.WORK_DIR_KEY, workDir.toAbsolutePath().toString()); if (verbosity.verbose()) { dumpArguments(args); } if (effectiveYaml) { var flows = new HashMap(processDefinition.flows()); for (var ap : profiles) { var p = processDefinition.profiles().get(ap); if (p != null) { flows.putAll(p.flows()); } } var pd = ProcessDefinition.builder().from(processDefinition) .configuration(ProcessDefinitionConfiguration.builder().from(processDefinition.configuration()) .arguments(args) .dependencies(allDeps) .build()) .flows(flows) .imports(Imports.builder().build()) .profiles(Collections.emptyMap()) .build(); var serializer = new ProjectSerializerV2(); serializer.write(pd, System.out); return CliExitCodes.SUCCESS; } System.out.println(ansi().fgBrightGreen().a("Starting...").reset()); if (dryRunMode) { System.out.println("Running in the dry-run mode."); } ProcessInfo processInfo = ProcessInfo.builder() .activeProfiles(profiles) .sessionToken("") .build(); ProcessConfiguration cfg = from(processDefinition.configuration(), processInfo, projectInfo(args)) .entryPoint(entryPoint) .instanceId(instanceId) .dryRun(dryRunMode) .build(); Injector injector = LocalCliRuntime.createInjector(workDir, runnerCfg, cfg, cliConfigContext, defaultTaskVars, dependencyManager, verbosity); // Just to notify listeners LocalCliRuntime.notifyProjectLoaded(workDir); Runner runner = injector.getInstance(Runner.class); if (cfg.debug()) { System.out.println("Available tasks: " + injector.getInstance(TaskProviders.class).names()); } ProcessSnapshot snapshot; try { snapshot = runner.start(cfg, processDefinition, args); } catch (ParallelExecutionException | UserDefinedException e) { return CliExitCodes.PROCESS_FAILED; } catch (Exception e) { logException(verbosity, e); return CliExitCodes.ERROR; } if (LocalSuspendPersistence.isSuspended(snapshot)) { var resumeDir = CliPaths.preferredResumeDir(sourceDir, workDir); var metadata = LocalSuspendPersistence.ResumeMetadata.from(workDir, resumeDir, defaultTaskVars, depsCacheDir, context, cliConfigOverrides, profiles, cfg, runnerCfg); LocalSuspendPersistence.save(workDir, snapshot, metadata); Set events = LocalSuspendPersistence.getEvents(snapshot); List pendingForms = LocalFormState.syncPendingForms(workDir, events); var interactiveAvailable = canPromptInteractively(); if (pendingForms.isEmpty()) { LocalSuspendPersistence.printResumeGuidance(resumeDir, events, pendingForms, false); return CliExitCodes.SUSPENDED; } if (!interactiveAvailable) { LocalSuspendPersistence.printResumeGuidance(resumeDir, events, pendingForms, false); return CliExitCodes.SUSPENDED; } if (!Confirmation.confirm("Fill pending form now? (Y/n)", true)) { LocalSuspendPersistence.printResumeGuidance(resumeDir, events, pendingForms, true); return CliExitCodes.SUSPENDED; } try { snapshot = LocalFormSession.resumePendingForms(workDir, runner, snapshot, metadata, false); } catch (ParallelExecutionException | UserDefinedException e) { return CliExitCodes.PROCESS_FAILED; } catch (Exception e) { logException(verbosity, e); return CliExitCodes.ERROR; } if (!LocalSuspendPersistence.isSuspended(snapshot)) { LocalSuspendPersistence.cleanup(workDir); System.out.println(ansi().fgBrightGreen().a("...done!").reset()); return CliExitCodes.SUCCESS; } LocalSuspendPersistence.save(workDir, snapshot, metadata); events = LocalSuspendPersistence.getEvents(snapshot); pendingForms = LocalFormState.syncPendingForms(workDir, events); LocalSuspendPersistence.printResumeGuidance(resumeDir, events, pendingForms, true); return CliExitCodes.SUSPENDED; } LocalSuspendPersistence.cleanup(workDir); System.out.println(ansi().fgBrightGreen().a("...done!").reset()); return CliExitCodes.SUCCESS; } private boolean canPromptInteractively() { return !noPrompt && PromptSupport.canPromptInteractively(); } private ApiConfiguration buildApiConfiguration(CliConfigContext cliConfigContext) { CliConfig.RemoteRunConfiguration remoteRun = cliConfigContext.remoteRun(); if (remoteRun == null || remoteRun.baseUrl() == null) { return ApiConfiguration.builder().build(); } return ApiConfiguration.builder() .baseUrl(remoteRun.baseUrl()) .build(); } @SuppressWarnings("unchecked") private static ProjectInfo projectInfo(Map args) { Object projectInfoObject = args.get("projectInfo"); if (projectInfoObject == null) { projectInfoObject = fromExtraVars("projectInfo", args); } Map projectInfo = Collections.emptyMap(); if (projectInfoObject instanceof Map) { projectInfo = (Map) projectInfoObject; } return ProjectInfo.builder() .orgName(MapUtils.getString(projectInfo, "orgName")) .projectName(MapUtils.getString(projectInfo, "projectName")) .repoName(MapUtils.getString(projectInfo, "repoName")) .repoUrl(MapUtils.getString(projectInfo, "repoUrl")) .repoBranch(MapUtils.getString(projectInfo, "repoBranch")) .repoPath(MapUtils.getString(projectInfo, "repoPath")) .repoCommitId(MapUtils.getString(projectInfo, "repoCommitId")) .repoCommitAuthor(MapUtils.getString(projectInfo, "repoCommitAuthor")) .repoCommitMessage(MapUtils.getString(projectInfo, "repoCommitMessage")) .build(); } private static ImmutableProcessConfiguration.Builder from(ProcessDefinitionConfiguration cfg, ProcessInfo processInfo, ProjectInfo projectInfo) { return ProcessConfiguration.builder() .debug(cfg.debug()) .entryPoint(cfg.entryPoint()) .arguments(cfg.arguments()) .meta(cfg.meta()) .events(cfg.events()) .processInfo(processInfo) .projectInfo(projectInfo) .out(cfg.out()); } private static Map fromExtraVars(String key, Map args) { Map result = new HashMap<>(); for (String k : args.keySet()) { if (k.startsWith(key + ".")) { result.put(k.substring(key.length() + 1), args.get(k)); } } return result; } private static void copyDefaultCfg(Path targetDir, Path defaultCfg, boolean verbose) throws IOException { final Path destDir = targetDir.resolve("concord"); final Path destFile = destDir.resolve("_defaultCfg.concord.yml"); // Don't overwrite existing file is given project dir if (Files.exists(destFile)) { if (verbose) { System.out.println("Default configuration already exists: " + defaultCfg); } return; } if (!Files.exists(destDir)) { Files.createDirectory(destDir); } if (Files.exists(defaultCfg)) { if (Files.isRegularFile(defaultCfg)) { Files.copy(defaultCfg, destFile); } else { System.err.println("Default configuration must be a file!"); } } else { try (InputStream in = Run.class.getClassLoader().getResourceAsStream("defaultCfg.yml")) { if (in == null) { throw new IllegalStateException("Failed to load embedded default concord configuration."); } Files.copy(in, destFile); } } } private Path prepareWorkDir(Path sourceDir, Verbosity verbosity) throws IOException { Path srcDir = resolveSourceDir(sourceDir); Path target = targetDir != null ? targetDir.normalize().toAbsolutePath() : CliPaths.defaultTargetDir(srcDir); validatePaths(srcDir, target); if (cleanup) { cleanTargetDirectory(target, verbosity); } if (!Files.exists(target)) { Files.createDirectories(target); } if (Files.isRegularFile(sourceDir)) { System.out.println("Running a single Concord file: " + sourceDir); Files.copy(sourceDir, target.resolve("concord.yml"), StandardCopyOption.REPLACE_EXISTING); } else { copySourceToTarget(srcDir, target, verbosity); } return target; } private static Path resolveSourceDir(Path sourceDir) { if (Files.isRegularFile(sourceDir)) { var parent = sourceDir.getParent(); if (parent == null) { throw new IllegalArgumentException("Cannot determine parent directory of: " + sourceDir); } return parent; } if (Files.isDirectory(sourceDir)) { return sourceDir; } throw new IllegalArgumentException("Invalid source (not a file or directory): " + sourceDir); } private static void validatePaths(Path src, Path target) { if (target.equals(src)) { throw new IllegalArgumentException("Target directory cannot be the same as the source: " + target); } if (src.startsWith(target)) { throw new IllegalArgumentException("Target directory cannot be a parent of the source: " + target); } if (Files.isSymbolicLink(target)) { throw new IllegalArgumentException("Target directory cannot be a symbolic link: " + target); } } private static void cleanTargetDirectory(Path target, Verbosity verbosity) throws IOException { if (Files.exists(target)) { if (verbosity.verbose()) { System.out.println("Cleaning target directory: " + target); } PathUtils.deleteRecursively(target); } } private void copySourceToTarget(Path src, Path target, Verbosity verbosity) throws IOException { Path skipDir = target.startsWith(src) ? target : null; GitIgnoreFilter filter = noGitIgnore ? null : GitIgnoreFilter.load(src); String targetRelPath = skipDir != null ? src.relativize(target).toString() : null; String displayPath = (targetRelPath != null) ? "./" + targetRelPath : target.toString(); CopyNotifier notifier = new CopyNotifier(verbosity.verbose() ? 0 : 100, displayPath); copyWithGitIgnore(src, target, skipDir, filter, notifier, StandardCopyOption.REPLACE_EXISTING); } private static void dumpArguments(Map args) { ObjectMapper om = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)); try { System.out.print(ansi().fgYellow().a("\nProcess arguments:\n\t")); System.out.println(om.writerWithDefaultPrettyPrinter().writeValueAsString(args).replace("\n", "\n\t")); } catch (Exception e) { throw new RuntimeException(e); } } static void logException(Verbosity verbosity, Exception e) { if (verbosity.verbose()) { System.err.print(ansi().fgBrightRed().a("Error: ")); e.printStackTrace(System.err); } else { System.err.println("Error: " + e.getMessage()); // TODO } } private static class CopyNotifier implements FileVisitor { private final long notifyOnCount; private final String targetDirDisplay; private long currentCount = 0; public CopyNotifier(long notifyOnCount, String targetDirDisplay) { this.notifyOnCount = notifyOnCount; this.targetDirDisplay = targetDirDisplay; } @Override public void visit(Path sourceFile, Path dstFile) { if (currentCount == -1) { return; } if (currentCount == notifyOnCount) { System.out.println(ansi().fgBrightBlack().a("Copying files into " + targetDirDisplay + " directory...")); currentCount = -1; return; } currentCount++; } } private static void copyWithGitIgnore(Path src, Path dst, Path skipDir, GitIgnoreFilter filter, FileVisitor visitor, CopyOption... options) throws IOException { Files.walkFileTree(src, new SimpleFileVisitor<>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (dir.equals(src)) { return FileVisitResult.CONTINUE; } // Skip the target directory (if it's inside the source directory) if (skipDir != null && Files.isSameFile(dir, skipDir)) { return FileVisitResult.SKIP_SUBTREE; } Path rel = src.relativize(dir); // Check gitignore if (filter != null && filter.isIgnored(rel, true)) { return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Path rel = src.relativize(file); if (filter != null && filter.isIgnored(rel, false)) { return FileVisitResult.CONTINUE; } Path dstFile = dst.resolve(rel); Path parent = dstFile.getParent(); if (!Files.exists(parent)) { Files.createDirectories(parent); } if (Files.isSymbolicLink(file)) { Path link = Files.readSymbolicLink(file); Path target = file.getParent().resolve(link).normalize(); if (!target.startsWith(src)) { throw new IOException("Symlinks outside the base directory are not supported: " + file + " -> " + target); } if (Files.notExists(target)) { return FileVisitResult.CONTINUE; } Files.deleteIfExists(dstFile); Files.createSymbolicLink(dstFile, link); } else { Files.copy(file, dstFile, options); } if (visitor != null) { visitor.visit(file, dstFile); } return FileVisitResult.CONTINUE; } }); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/SelfUpdate.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.maven.artifact.versioning.ComparableVersion; import picocli.CommandLine; import picocli.CommandLine.Command; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse.BodyHandlers; import java.nio.file.*; import java.nio.file.attribute.PosixFilePermission; import java.time.Duration; import java.util.HashSet; import java.util.Optional; import java.util.concurrent.Callable; import static org.fusesource.jansi.Ansi.ansi; @Command(name = "self-update", description = "Update the CLI to the latest release version") public class SelfUpdate implements Callable { private static final Duration TIMEOUT = Duration.ofSeconds(10); private static final URI GITHUB_RELEASES_ENDPOINT = URI.create("https://api.github.com/repos/walmartlabs/concord/releases/latest"); private static final String DOWNLOAD_TEMPLATE = "https://repo.maven.apache.org/maven2/com/walmartlabs/concord/concord-cli/%1$s/concord-cli-%1$s.sh"; @CommandLine.Option(names = {"-h", "--help"}, usageHelp = true, description = "display the command's help message") boolean helpRequested = false; @Override public Integer call() { var selfLocation = SelfUpdate.class.getProtectionDomain().getCodeSource().getLocation(); Path dst; try { dst = Paths.get(selfLocation.getPath()); } catch (InvalidPathException e) { return unableToDetermineSelfLocation(); } if (Files.isDirectory(dst)) { return unableToDetermineSelfLocation(); } if (!Files.isWritable(dst)) { return selfLocationIsNotWritable(); } String latestVersion; try { System.out.println("Checking for updates..."); var maybeLatestVersion = getLatestVersion(); if (maybeLatestVersion.isEmpty()) { return unableToDetermineLatestReleaseVersion(); } latestVersion = maybeLatestVersion.get(); } catch (IOException | InterruptedException e) { return err(e.getMessage()); } var currentVersion = Version.getVersion(); var comparison = new ComparableVersion(latestVersion).compareTo(new ComparableVersion(currentVersion)); if (comparison == 0) { return currentVersionIsLatest(); } else if (comparison < 0) { return currentVersionIsMoreRecent(); } System.out.printf("Updating to %s...%n", latestVersion); try { var tmpFile = Files.createTempFile("concord-cli-" + latestVersion, ".sh"); var src = downloadArtifact(latestVersion, tmpFile); Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING); } catch (IOException | InterruptedException e) { return err(e.getMessage()); } System.out.println(ansi().fgBrightGreen().a("Done!")); try { var permissions = new HashSet<>(Files.getPosixFilePermissions(dst)); permissions.add(PosixFilePermission.OWNER_EXECUTE); Files.setPosixFilePermissions(dst, permissions); } catch (UnsupportedOperationException | IOException e) { warn("Unable to mark the binary as an executable. You might need to manually update the permissions of " + dst.toAbsolutePath()); } return 0; } private static Optional getLatestVersion() throws IOException, InterruptedException { var client = HttpClient.newBuilder() .connectTimeout(TIMEOUT) .build(); var request = HttpRequest.newBuilder() .uri(GITHUB_RELEASES_ENDPOINT) .timeout(TIMEOUT) .header("Accept", "application/vnd.github.v3+json") .header("User-Agent", "concord-cli " + Version.getVersion()) .GET() .build(); var response = client.send(request, BodyHandlers.ofInputStream()); if (response.statusCode() == 200) { var mapper = new ObjectMapper(); try (var body = response.body()) { var json = mapper.readTree(body); var tagName = json.path("tag_name").asText(); if (!tagName.isEmpty()) { return Optional.of(tagName); } } } else if (response.statusCode() == 404) { throw new IOException("Repository not found or no releases available."); } else { throw new IOException("GitHub API returned unexpected status: " + response.statusCode()); } return Optional.empty(); } private static Path downloadArtifact(String version, Path dst) throws IOException, InterruptedException { var client = HttpClient.newBuilder() .connectTimeout(TIMEOUT) .build(); var request = HttpRequest.newBuilder() .uri(URI.create(DOWNLOAD_TEMPLATE.formatted(version))) .timeout(TIMEOUT) .header("Accept", "application/octet-stream") .header("User-Agent", "concord-cli " + Version.getVersion()) .GET() .build(); var response = client.send(request, BodyHandlers.ofFile(dst)); if (response.statusCode() == 200) { return response.body(); } else if (response.statusCode() == 404) { throw new IOException("Release %s not found".formatted(version)); } else { throw new IOException("Maven Central returned unexpected status: " + response.statusCode()); } } private static int unableToDetermineSelfLocation() { return err("Unable to determine the location of the CLI binary, self-update is not possible."); } private static int selfLocationIsNotWritable() { return err("Unable to overwrite the CLI binary, self-update is not possible."); } private static int unableToDetermineLatestReleaseVersion() { return err("Cannot determine the latest release version."); } private static int currentVersionIsLatest() { System.out.println("The current version is the latest release version. Nothing to do."); return 0; } private static int currentVersionIsMoreRecent() { System.out.println("The current version is more recent than the latest available release version. Nothing to do."); return 0; } private static int err(String msg) { System.out.println(ansi().fgRed().a(msg)); return -1; } private static void warn(String msg) { System.out.println(ansi().fgYellow().a(msg)); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/Verbosity.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public class Verbosity { private final boolean[] verbosity; public Verbosity(boolean[] verbosity) { this.verbosity = verbosity; } public boolean logFlowSteps() { return verbosity.length > 0; } public boolean logTaskParams() { return verbosity.length > 1; } public boolean verbose() { return verbosity.length > 2; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/Version.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.IOException; import java.util.Properties; public class Version { private static final String VERSION; static { Properties props = new Properties(); try { props.load(Version.class.getClassLoader().getResourceAsStream("project.properties")); } catch (IOException e) { throw new RuntimeException(e); } VERSION = props.getProperty("project.version"); } public static String getVersion() { return VERSION; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/lint/DummyImportsNormalizer.java ================================================ package com.walmartlabs.concord.cli.lint; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.imports.Imports; import com.walmartlabs.concord.process.loader.ImportsNormalizer; public class DummyImportsNormalizer implements ImportsNormalizer { @Override public Imports normalize(Imports imports) { if (imports != null && !imports.isEmpty()) { System.out.println("WARN: Linting of 'imports' is not supported at the moment."); } return Imports.builder().build(); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/lint/ExpressionLinter.java ================================================ package com.walmartlabs.concord.cli.lint; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.model.ExpressionStep; import com.walmartlabs.concord.runtime.model.SourceMap; import com.walmartlabs.concord.runtime.model.Step; import javax.el.ELException; import java.util.Collections; import java.util.List; public class ExpressionLinter extends FlowElementLinter { public ExpressionLinter(boolean verbose) { super(verbose); } @Override protected List apply(Step element) { ExpressionStep task = (ExpressionStep) element; String expr = task.expression(); notify(" Validating expression: " + expr); if (expr == null || expr.trim().isEmpty()) { String msg = "Empty or null expression"; return Collections.singletonList(LintResult.error(element.location(), msg)); } LintResult r = validate(expr, element.location()); if (r != null) { return Collections.singletonList(r); } return null; } @Override protected boolean accepts(Step element) { return element instanceof ExpressionStep; } @Override protected String getStartMessage() { return "Validating expressions..."; } public static LintResult validate(String expr, SourceMap sourceMap) { try { Utils.compileExpression(expr); } catch (ELException e) { return Utils.toResult(e, sourceMap, null); } return null; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/lint/FlowElementLinter.java ================================================ package com.walmartlabs.concord.cli.lint; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.model.FlowDefinition; import com.walmartlabs.concord.runtime.model.ProcessDefinition; import com.walmartlabs.concord.runtime.model.Step; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; public abstract class FlowElementLinter implements Linter { private final boolean verbose; public FlowElementLinter(boolean verbose) { this.verbose = verbose; } @Override public List apply(ProcessDefinition pd) { notify(">> " + getStartMessage()); Map flows = pd.flows(); if (flows == null || flows.isEmpty()) { return Collections.emptyList(); } List results = new ArrayList<>(); flows.forEach((key, value) -> results.addAll(apply(value))); notify("<< ...done\n"); return results; } private List apply(FlowDefinition pd) { List results = new ArrayList<>(); for (Step e : pd.steps()) { if (!accepts(e)) { continue; } List r = apply(e); if (r != null) { results.addAll(r); } } return results; } protected abstract boolean accepts(Step element); protected abstract List apply(Step element); protected abstract String getStartMessage(); protected void notify(String s) { if (!verbose) { return; } System.out.println(s); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/lint/LintResult.java ================================================ package com.walmartlabs.concord.cli.lint; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.model.SourceMap; public class LintResult { public static LintResult error(SourceMap sourceMap, String message) { return new LintResult(Type.ERROR, sourceMap, message); } private final Type type; private final SourceMap sourceMap; private final String message; public LintResult(Type type, SourceMap sourceMap, String message) { this.type = type; this.sourceMap = sourceMap; this.message = message; } public Type getType() { return type; } public SourceMap getSourceMap() { return sourceMap; } public String getMessage() { return message; } public enum Type { WARNING, ERROR } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/lint/Linter.java ================================================ package com.walmartlabs.concord.cli.lint; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.model.ProcessDefinition; import java.util.List; public interface Linter { List apply(ProcessDefinition pd); } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/lint/TaskCallLinter.java ================================================ package com.walmartlabs.concord.cli.lint; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.model.SourceMap; import com.walmartlabs.concord.runtime.model.Step; import com.walmartlabs.concord.runtime.model.TaskCallStep; import javax.el.ELException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; public class TaskCallLinter extends FlowElementLinter { public TaskCallLinter(boolean verbose) { super(verbose); } @Override protected List apply(Step element) { List results = new ArrayList<>(); TaskCallStep task = (TaskCallStep) element; String expr = task.name(); notify(" Validating task call: " + expr); LintResult r = ExpressionLinter.validate(expr, element.location()); if (r != null) { results.add(r); } Map inVars = task.input(); for (Map.Entry e : inVars.entrySet()) { LintResult lr = validateArgument(e.getKey(), e.getValue(), element.location()); if (lr != null) { results.add(lr); } } return results; } private LintResult validateArgument(String paramName, Object value, SourceMap sourceMap) { if (value != null) { return validateValue(paramName, value, sourceMap); } return null; } private LintResult validateValue(String paramName, Object value, SourceMap sourceMap) { if (value instanceof String) { String s = (String) value; if (s.contains("${")) { return validateExpression(paramName, s, sourceMap); } } else if (value instanceof Collection) { Collection c = (Collection) value; for (Object vv : c) { LintResult r = validateValue(paramName, vv, sourceMap); if (r != null) { return r; } } } else if (value instanceof Map) { Map m = (Map) value; for (Object vv : m.values()) { LintResult r = validateValue(paramName, vv, sourceMap); if (r != null) { return r; } } } return null; } private LintResult validateExpression(String paramName, String expr, SourceMap sourceMap) { try { Utils.compileExpression(expr); } catch (ELException e) { return Utils.toResult(e, sourceMap, "Invalid expression in task arguments: \"" + expr + "\" in IN " + paramName); } return null; } @Override protected boolean accepts(Step element) { return element instanceof TaskCallStep; } @Override protected String getStartMessage() { return "Validating task calls..."; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/lint/Utils.java ================================================ package com.walmartlabs.concord.cli.lint; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.model.SourceMap; import javax.el.ELContext; import javax.el.ELException; import javax.el.ExpressionFactory; import javax.el.StandardELContext; public final class Utils { public static void compileExpression(String expr) { ExpressionFactory ef = ExpressionFactory.newInstance(); ELContext ctx = new StandardELContext(ef); ef.createValueExpression(ctx, expr, Object.class); } public static LintResult toResult(ELException e, SourceMap sm, String message) { // make EL exceptions a bit more compact String error = e.getCause().getMessage().replaceAll("\n", "").replaceAll(" {4}", " "); return LintResult.error(sm, (message != null ? message + " " + error : error)); } private Utils() { } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/ApiKey.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.cli.CliConfig; import com.walmartlabs.concord.cli.Verbosity; import com.walmartlabs.concord.cli.secrets.CliSecretService; import java.nio.file.Path; public record ApiKey(String value) { public static ApiKey create(CliConfig.CliConfigContext cliConfigContext, Path workDir, Verbosity verbosity) { CliConfig.RemoteRunConfiguration remoteRunConfig = cliConfigContext.remoteRun(); if (remoteRunConfig == null || remoteRunConfig.apiKeyRef() == null) { return new ApiKey(null); } if (verbosity.verbose()) { System.out.println("Using '" + remoteRunConfig.apiKeyRef() + "' as a secret for API key"); } CliSecretService secretService = CliSecretService.create(cliConfigContext, workDir, verbosity); try { return new ApiKey(secretService.exportAsString(remoteRunConfig.apiKeyRef().orgName(), remoteRunConfig.apiKeyRef().secretName(), null)); } catch (Exception e) { throw new RuntimeException("Unable to fetch the API key. " + e.getMessage()); } } @Override public String toString() { return "***"; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/CliApiClientProvider.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.client2.ApiClient; import com.walmartlabs.concord.client2.ApiClientConfiguration; import com.walmartlabs.concord.client2.ApiClientFactory; import com.walmartlabs.concord.runtime.v2.sdk.ProcessConfiguration; import javax.inject.Inject; import javax.inject.Provider; public class CliApiClientProvider implements Provider { private final ApiClientFactory clientFactory; private final ApiKey apiKey; private final ProcessConfiguration processCfg; @Inject public CliApiClientProvider(ApiClientFactory clientFactory, ApiKey apiKey, ProcessConfiguration processCfg) { this.clientFactory = clientFactory; this.apiKey = apiKey; this.processCfg = processCfg; } @Override public ApiClient get() { return clientFactory.create(ApiClientConfiguration.builder() .apiKey(apiKey.value()) .sessionToken(processCfg.processInfo().sessionToken()) .build()); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/CliCheckpointService.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.common.SerializationUtils; import com.walmartlabs.concord.runtime.v2.runner.ProcessSnapshot; import com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointService; import com.walmartlabs.concord.svm.Runtime; import com.walmartlabs.concord.svm.ThreadId; import java.io.ByteArrayOutputStream; import java.util.UUID; public class CliCheckpointService implements CheckpointService { @Override public void create(ThreadId threadId, UUID correlationId, String name, Runtime runtime, ProcessSnapshot snapshot) { try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { SerializationUtils.serialize(baos, snapshot.vmState()); } catch (Exception e) { throw new RuntimeException("Checkpoint create error", e); } System.out.println("Checkpoint '" + name + "' ignored"); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/CliDockerService.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.v2.sdk.DockerContainerSpec; import com.walmartlabs.concord.runtime.v2.sdk.DockerService; public class CliDockerService implements DockerService { @Override public int start(DockerContainerSpec spec, LogCallback outCallback, LogCallback errCallback) { throw new UnsupportedOperationException("Running Docker containers is not supported by the concord-cli yet"); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/CliImportsListener.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.imports.Import; import com.walmartlabs.concord.imports.ImportsListener; import java.util.List; public class CliImportsListener implements ImportsListener { private long startAt; @Override public void onStart(List items) { startAt = System.currentTimeMillis(); System.out.println("Resolving " + items.size() + " import(s)..."); } @Override public void onEnd(List items) { System.out.println("Imports resolution took " + (System.currentTimeMillis() - startAt) + "ms"); } @Override public void beforeImport(Import i) { System.out.println("Resolving import: " + i); } @Override public void afterImport(Import i) { System.out.println("Import resolved"); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/CliImportsNormalizer.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.imports.Import; import com.walmartlabs.concord.imports.Imports; import com.walmartlabs.concord.runtime.v2.ImportsNormalizer; import java.util.stream.Collectors; public class CliImportsNormalizer implements ImportsNormalizer { private static final String DEFAULT_DEST = "concord"; private final String defaultSource; private final boolean verbose; private final String defaultVersion; public CliImportsNormalizer(String defaultSource, boolean verbose, String defaultVersion) { this.defaultSource = defaultSource; this.verbose = verbose; this.defaultVersion = defaultVersion; } @Override public Imports normalize(Imports imports) { if (imports == null || imports.isEmpty()) { return Imports.builder().build(); } if (verbose) { System.out.println("Processing imports..."); } Imports result = Imports.of(imports.items().stream() .map(this::normalize) .collect(Collectors.toList())); if (verbose) { imports.items().forEach(i -> System.out.println("import: " + i)); } return result; } private Import normalize(Import i) { switch (i.type()) { case Import.MvnDefinition.TYPE: { Import.MvnDefinition src = (Import.MvnDefinition) i; return Import.MvnDefinition.builder() .from(src) .dest(src.dest() != null ? src.dest() : DEFAULT_DEST) .build(); } case Import.GitDefinition.TYPE: { Import.GitDefinition src = (Import.GitDefinition) i; return normalize(src); } case Import.DirectoryDefinition.TYPE: { return i; } default: { throw new IllegalArgumentException("Unsupported import type: '" + i.type() + "'"); } } } private Import.GitDefinition normalize(Import.GitDefinition e) { String url = e.url(); if (url == null) { String name = e.name(); url = normalizeUrl(defaultSource) + name; } return Import.GitDefinition.builder().from(e) .url(url) .version(e.version() != null ? e.version() : defaultVersion) .dest(e.dest() != null ? e.dest() : DEFAULT_DEST) .secret(e.secret()) .build(); } private static String normalizeUrl(String u) { if (u.endsWith("/")) { return u; } return u + "/"; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/CliLockService.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.v2.sdk.LockService; /** * Just a stub for the CLI. */ public class CliLockService implements LockService { @Override public void projectLock(String lockName) { // nothing to do } @Override public void projectUnlock(String lockName) { // nothing to do } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/CliRepositoryExporter.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.AuthTokenProvider; import com.walmartlabs.concord.common.ExternalAuthToken; import com.walmartlabs.concord.imports.Import; import com.walmartlabs.concord.imports.RepositoryExporter; import com.walmartlabs.concord.repository.*; import com.walmartlabs.concord.sdk.Secret; import javax.annotation.Nullable; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; import java.nio.file.Path; import java.time.Duration; import java.util.List; import java.util.Objects; import java.util.Optional; public class CliRepositoryExporter implements RepositoryExporter { // TODO: move to configuration private static final Duration DEFAULT_OPERATION_TIMEOUT = Duration.parse("PT10M"); private static final Duration FETCH_TIMEOUT = Duration.parse("PT10M"); private static final int HTTP_LOW_SPEED_LIMIT = 0; private static final Duration HTTP_LOW_SPEED_TIME = Duration.ofMinutes(10); private static final Duration SSH_TIMEOUT = Duration.ofMinutes(10); private static final int SSH_TIMEOUT_RETRY_COUNT = 1; private final Path repoCacheDir; private final RepositoryProviders providers; public CliRepositoryExporter(Path repoCacheDir) { this.repoCacheDir = repoCacheDir; GitClientConfiguration clientCfg = GitClientConfiguration.builder() .defaultOperationTimeout(DEFAULT_OPERATION_TIMEOUT) .fetchTimeout(FETCH_TIMEOUT) .httpLowSpeedLimit(HTTP_LOW_SPEED_LIMIT) .httpLowSpeedTime(HTTP_LOW_SPEED_TIME) .sshTimeout(SSH_TIMEOUT) .sshTimeoutRetryCount(SSH_TIMEOUT_RETRY_COUNT) .build(); AuthTokenProvider authProvider = new AuthTokenProvider() { @Override public boolean supports(URI repo, @Nullable Secret secret) { return false; } @Override public Optional getToken(URI repo, @Nullable Secret secret) throws RepositoryException { throw new UnsupportedOperationException("Not supported"); } }; this.providers = new RepositoryProviders(List.of(new GitCliRepositoryProvider(clientCfg, authProvider))); } @Override public Snapshot export(Import.GitDefinition entry, Path workDir) throws Exception { Path dest = workDir; if (entry.dest() != null) { dest = dest.resolve(Objects.requireNonNull(entry.dest())); } String url = Objects.requireNonNull(entry.url()); Path cacheDir = repoCacheDir.resolve(encodeUrl(url)); Secret secret = null; Repository repo = providers.fetch( FetchRequest.builder() .url(url) .version(FetchRequest.Version.from(entry.version())) .secret(secret) .destination(cacheDir) .build(), entry.path()); return repo.export(dest, entry.exclude()); } private static String encodeUrl(String url) { String encodedUrl; try { encodedUrl = URLEncoder.encode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RepositoryException("Url encoding error", e); } return encodedUrl; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/CliServicesModule.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.AbstractModule; import com.google.inject.Singleton; import com.google.inject.multibindings.Multibinder; import com.walmartlabs.concord.cli.CliConfig.CliConfigContext; import com.walmartlabs.concord.cli.Verbosity; import com.walmartlabs.concord.cli.secrets.CliSecretService; import com.walmartlabs.concord.client2.ApiClient; import com.walmartlabs.concord.dependencymanager.DependencyManager; import com.walmartlabs.concord.runtime.v2.runner.*; import com.walmartlabs.concord.runtime.v2.runner.checkpoints.CheckpointService; import com.walmartlabs.concord.runtime.v2.runner.guice.BaseRunnerModule; import com.walmartlabs.concord.runtime.v2.runner.logging.RunnerLogger; import com.walmartlabs.concord.runtime.v2.runner.logging.SimpleLogger; import com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallListener; import com.walmartlabs.concord.runtime.v2.sdk.DockerService; import com.walmartlabs.concord.runtime.v2.sdk.LockService; import com.walmartlabs.concord.runtime.v2.sdk.SecretService; import com.walmartlabs.concord.svm.ExecutionListener; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.function.Supplier; public class CliServicesModule extends AbstractModule { private final CliConfigContext cliConfigContext; private final Path workDir; private final Path defaultTaskVars; private final DependencyManager dependencyManager; private final Verbosity verbosity; public CliServicesModule(CliConfigContext cliConfigContext, Path workDir, Path defaultTaskVars, DependencyManager dependencyManager, Verbosity verbosity) { this.cliConfigContext = cliConfigContext; this.workDir = workDir; this.defaultTaskVars = defaultTaskVars; this.dependencyManager = dependencyManager; this.verbosity = verbosity; } @Override protected void configure() { install(new BaseRunnerModule()); bind(RunnerLogger.class).to(SimpleLogger.class); bind(SecretService.class).toInstance(CliSecretService.create(cliConfigContext, workDir, verbosity)); bind(DockerService.class).to(CliDockerService.class); bind(CheckpointService.class).to(CliCheckpointService.class); bind(PersistenceService.class).to(DefaultPersistenceService.class); bind(ProcessStatusCallback.class).toInstance(instanceId -> { }); bind(ApiKey.class).toInstance(ApiKey.create(cliConfigContext, workDir, verbosity)); bind(ApiClient.class).toProvider(CliApiClientProvider.class); bind(DefaultTaskVariablesService.class) .toInstance(new MapBackedDefaultTaskVariablesService(readDefaultVars(defaultTaskVars))); bind(LockService.class).to(CliLockService.class); bind(DependencyManager.class).toInstance(dependencyManager); bind(com.walmartlabs.concord.runtime.v2.sdk.DependencyManager.class).to(DefaultDependencyManager.class).in(Singleton.class); Multibinder executionListeners = Multibinder.newSetBinder(binder(), ExecutionListener.class); if (verbosity.logFlowSteps()) { executionListeners.addBinding().to(FlowStepLogger.class); } if (verbosity.logTaskParams()) { Multibinder taskCallListeners = Multibinder.newSetBinder(binder(), TaskCallListener.class); taskCallListeners.addBinding().toInstance(new TaskParamsLogger()); } } private static Map> readDefaultVars(Path defaultTaskVars) { if (Files.exists(defaultTaskVars)) { try (InputStream is = Files.newInputStream(defaultTaskVars)) { return parseDefaultVars(() -> is); } catch (Exception e) { System.out.println("Error parsing default variables in '" + defaultTaskVars + "': " + e.getMessage()); } } return parseDefaultVars(() -> CliServicesModule.class.getResourceAsStream("/default-vars.json")); } @SuppressWarnings("unchecked") private static Map> parseDefaultVars(Supplier isSupplier) { try (InputStream is = isSupplier.get()) { if (is == null) { throw new IllegalStateException("Default variables input stream is null."); } return new ObjectMapper().readValue(is, Map.class); } catch (Exception e) { throw new RuntimeException(e.getMessage()); } } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/DependencyResolver.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.dependencymanager.DependencyEntity; import com.walmartlabs.concord.dependencymanager.DependencyManager; import com.walmartlabs.concord.dependencymanager.ProgressListener; import java.io.IOException; import java.net.*; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import static com.walmartlabs.concord.dependencymanager.DependencyManager.MAVEN_SCHEME; public class DependencyResolver { private final DependencyManager dependencyManager; private final List defaultDependencies = Collections.emptyList(); private final boolean verbose; public DependencyResolver(DependencyManager dependencyManager, boolean verbose) { this.dependencyManager = dependencyManager; this.verbose = verbose; } public Collection resolveDeps(List dependencies) throws Exception { if (verbose) { System.out.println("Resolving process dependencies..."); } long t1 = System.currentTimeMillis(); // combine the default dependencies and the process' dependencies Collection uris = Stream.concat(defaultDependencies.stream(), normalizeUrls(dependencies).stream()) .collect(Collectors.toList()); Collection deps = dependencyManager.resolve(uris, new ProgressListener() { @Override public void onRetry(int retryCount, int maxRetry, long interval, String cause) { System.err.println("Error while downloading dependencies: " + cause); System.err.println("Retrying in " + interval + "ms"); } @Override public void onTransferFailed(String error) { // when we have more than one repo in mvn.json we can get transfer error for one repo // but artifact will be resolved with second repo... if (verbose) { System.err.println("Transfer failed: " + error); } } }); // sort dependencies to maintain consistency in runner configurations Collection paths = deps.stream() .map(DependencyEntity::getPath) .map(p -> p.toAbsolutePath().toString()) .sorted() .collect(Collectors.toList()); long t2 = System.currentTimeMillis(); if (verbose) { System.out.println("Dependency resolution took " + ((t2 - t1)) + "ms"); logDependencies(paths); } return paths; } private void logDependencies(Collection deps) { if (verbose && deps.isEmpty()) { System.out.println("No external dependencies."); return; } List l = deps.stream() .map(Object::toString) .collect(Collectors.toList()); StringBuilder b = new StringBuilder(); for (String s : l) { b.append("\n\t").append(s); } System.out.println("Dependencies: " + b); } private static Collection normalizeUrls(Collection urls) throws IOException, URISyntaxException { if (urls == null || urls.isEmpty()) { return Collections.emptySet(); } Collection result = new HashSet<>(); for (String s : urls) { URI u = new URI(s); String scheme = u.getScheme(); if (MAVEN_SCHEME.equalsIgnoreCase(scheme)) { result.add(u); continue; } if (scheme == null || scheme.trim().isEmpty()) { throw new IOException("Invalid dependency URL. Missing URL scheme: " + s); } if (s.endsWith(".jar")) { result.add(u); continue; } URL url = u.toURL(); while (true) { if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) { URLConnection conn = url.openConnection(); if (conn instanceof HttpURLConnection) { HttpURLConnection httpConn = (HttpURLConnection) conn; httpConn.setInstanceFollowRedirects(false); int code = httpConn.getResponseCode(); if (code == HttpURLConnection.HTTP_MOVED_TEMP || code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_SEE_OTHER || code == 307) { String location = httpConn.getHeaderField("Location"); url = new URL(location); System.out.println("normalizeUrls -> using: " + location); continue; } u = url.toURI(); } else { System.out.println("normalizeUrls -> unexpected connection type: " + conn.getClass() + " (for " + s + ")"); } } break; } result.add(u); } return result; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/FlowStepLogger.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.model.Location; import com.walmartlabs.concord.runtime.v2.model.*; import com.walmartlabs.concord.runtime.v2.runner.context.ContextFactory; import com.walmartlabs.concord.runtime.v2.runner.logging.SegmentedLogger; import com.walmartlabs.concord.runtime.v2.runner.vm.LogSegmentScopeCommand; import com.walmartlabs.concord.runtime.v2.runner.vm.StepCommand; import com.walmartlabs.concord.runtime.v2.sdk.Context; import com.walmartlabs.concord.svm.Runtime; import com.walmartlabs.concord.svm.*; import static org.fusesource.jansi.Ansi.ansi; public class FlowStepLogger implements ExecutionListener { @Override public Result beforeCommand(Runtime runtime, VM vm, State state, ThreadId threadId, Command cmd) { if (!(cmd instanceof StepCommand)) { return Result.CONTINUE; } if (cmd instanceof LogSegmentScopeCommand) { return Result.CONTINUE; } StepCommand s = (StepCommand) cmd; Location loc = s.getStep().getLocation(); System.out.println(ansi().fgBrightCyan().bold().a(">>> '").a(getDescription(runtime, state, threadId, s.getStep())).boldOff() .a("' @ ").a(loc.fileName()).a(":").a(loc.lineNum()).reset()); return Result.CONTINUE; } private static String getDescription(Runtime runtime, State state, ThreadId threadId, Step step) { if (step instanceof AbstractStep) { ContextFactory contextFactory = runtime.getService(ContextFactory.class); Context ctx = contextFactory.create(runtime, state, threadId, step); String rawSegmentName = SegmentedLogger.getSegmentName((AbstractStep) step); String segmentName = ctx.eval(rawSegmentName, String.class); if (segmentName != null) { return segmentName; } } return getDefaultDescription(step); } private static String getDefaultDescription(Step step) { if (step instanceof FlowCall) { return "Flow call: " + ((FlowCall) step).getFlowName(); } else if (step instanceof Expression) { return "Expression: " + ((Expression) step).getExpr(); } else if (step instanceof ScriptCall) { return "Script: " + ((ScriptCall) step).getLanguageOrRef(); } else if (step instanceof IfStep) { return "Check: " + ((IfStep) step).getExpression(); } else if (step instanceof SwitchStep) { return "Switch: " + ((SwitchStep) step).getExpression(); } else if (step instanceof SetVariablesStep) { return "Set variables"; } else if (step instanceof Checkpoint) { return "Checkpoint: " + ((Checkpoint) step).getName(); } else if (step instanceof FormCall) { return "Form call: " + ((FormCall) step).getName(); } else if (step instanceof GroupOfSteps) { return "Group of steps"; } else if (step instanceof ParallelBlock) { return "Parallel block"; } else if (step instanceof ExitStep) { return "Exit"; } else if (step instanceof ReturnStep) { return "Return"; } else if (step instanceof TaskCall) { return "Task: " + ((TaskCall) step).getName(); } return step.getClass().getName(); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/TaskParamsLogger.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallEvent; import com.walmartlabs.concord.runtime.v2.runner.tasks.TaskCallListener; import com.walmartlabs.concord.runtime.v2.sdk.Context; import com.walmartlabs.concord.runtime.v2.sdk.TaskResult; import com.walmartlabs.concord.runtime.v2.sdk.Variables; import org.fusesource.jansi.Ansi; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.fusesource.jansi.Ansi.ansi; public class TaskParamsLogger implements TaskCallListener { @Override public void onEvent(TaskCallEvent event) { Map inVars = convertInput(event.input()); Map outVars = asMapOrNull(event.result()); if (TaskCallEvent.Phase.PRE.equals(event.phase())) { System.out.println(ansi().fgCyan().a(" in: " + inVars).reset()); } else { System.out.println(ansi().fgCyan().a(" out: ").a(outVars).reset()); System.out.println(ansi().fgCyan().a(" duration: ").a(event.duration()).a("ms").reset()); if (event.error() != null) { System.out.println(ansi().fgBrightRed().a(" error: ").a(event.error()).reset()); } } } @SuppressWarnings("unchecked") private Map asMapOrNull(Object v) { if (v instanceof TaskResult.SimpleResult) { return ((TaskResult.SimpleResult) v).toMap(); } if (v instanceof Map) { return (Map) v; } return null; } private static Map convertInput(List input) { if (input.isEmpty()) { return Collections.emptyMap(); } if (input.size() == 1) { if (input.get(0) instanceof Variables) { return ((Variables) input.get(0)).toMap(); } } Map result = new HashMap<>(); for (int i = 0; i < input.size(); i++) { Object arg = input.get(i); if (arg instanceof Context) { arg = "context"; } result.put(String.valueOf(i), arg); } return result; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/runner/VaultProvider.java ================================================ package com.walmartlabs.concord.cli.runner; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Properties; import static org.fusesource.jansi.Ansi.ansi; /** * Implements a simple "vault" system. * When using {@code crypto.decryptString} in a flow executed by the CLI, * instead of decrypting the key, it performs a simple lookup. * E.g. *
 * {@code
 * # concord.yml
 * flows:
 *   default:
 *     - log: "${crypto.decryptString('abc')}" # prints out "the_actual_value"
 * }
 * 
*
 * {@code
 * # ~/.concord/vaults/default
 * abc = the_actual_value
 * }
 * 
*/ // TODO consider implementing support for encrypted Ansible vaults public class VaultProvider { private final String id; private final Map items; public VaultProvider(Path dir, String id) { this.id = id; this.items = load(vaultPath(dir, id)); } public String getValue(String key) { if (items == null) { System.err.println("Vault '" + id + "' not found. Can't get value for key '" + key + "'"); throw new RuntimeException("Vault not configured"); } if (!items.containsKey(key)) { System.out.println(ansi().fgRed().a("There are no key '").a(key).a("' in vault '").a(id).a("'").reset()); } return items.get(key); } private static Map load(Path file) { if (Files.notExists(file)) { return null; } Map result = new HashMap<>(); try (InputStream is = Files.newInputStream(file)) { Properties props = new Properties(); props.load(is); for (String name: props.stringPropertyNames()) { result.put(name, props.getProperty(name)); } return result; } catch (IOException e) { System.out.println(ansi().fgBrightRed().a("Error loading vault file '").a(file).a("': ").a(e.getMessage()).reset()); throw new RuntimeException(e.getMessage()); } } private static Path vaultPath(Path dir, String vaultId) { return dir.resolve(vaultId); } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/secrets/CliSecretService.java ================================================ package com.walmartlabs.concord.cli.secrets; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.cli.CliConfig.CliConfigContext; import com.walmartlabs.concord.cli.Verbosity; import com.walmartlabs.concord.cli.runner.VaultProvider; import com.walmartlabs.concord.runtime.v2.sdk.SecretService; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import static java.util.Objects.requireNonNull; import static org.fusesource.jansi.Ansi.ansi; public class CliSecretService implements SecretService { private final List secretsProviders; private final VaultProvider vaultProvider; private final Verbosity verbosity; public static CliSecretService create(CliConfigContext cliConfigContext, Path workDir, Verbosity verbosity) { var providers = new ArrayList(); var local = cliConfigContext.secrets().local(); if (local.enabled()) { var provider = new FileSecretsProvider(workDir, local.dir()); providers.add(new SecretsProviderRef("localFile", provider, local.writable())); } var remote = cliConfigContext.secrets().remote(); if (remote.enabled()) { var provider = new RemoteSecretsProvider(workDir, remote.baseUrl(), remote.apiKey(), remote.confirmAccess()); providers.add(new SecretsProviderRef("remote", provider, remote.writable())); } var vault = cliConfigContext.secrets().vault(); return new CliSecretService(providers, new VaultProvider(vault.dir(), vault.id()), verbosity); } public CliSecretService(List secretsProviders, VaultProvider vaultProvider, Verbosity verbosity) { this.secretsProviders = requireNonNull(secretsProviders); this.vaultProvider = requireNonNull(vaultProvider); this.verbosity = requireNonNull(verbosity); } @Override public SecretService.KeyPair exportKeyAsFile(String orgName, String secretName, String secretPassword) throws Exception { for (var ref : secretsProviders) { var result = ref.provider().exportKeyAsFile(orgName, secretName, secretPassword); if (result.isPresent()) { reportSecretFound(orgName, secretName, ref); return result.get(); } } throw new RuntimeException("Secret not found: %s/%s.".formatted(orgName, secretName)); } @Override public String exportAsString(String orgName, String secretName, String secretPassword) throws Exception { for (var ref : secretsProviders) { var result = ref.provider().exportAsString(orgName, secretName, secretPassword); if (result.isPresent()) { reportSecretFound(orgName, secretName, ref); return result.get(); } } throw new RuntimeException("Secret not found: %s/%s.".formatted(orgName, secretName)); } @Override public Path exportAsFile(String orgName, String secretName, String secretPassword) throws Exception { for (var ref : secretsProviders) { var result = ref.provider().exportAsFile(orgName, secretName, secretPassword); if (result.isPresent()) { reportSecretFound(orgName, secretName, ref); return result.get(); } } throw new RuntimeException("Secret not found: %s/%s.".formatted(orgName, secretName)); } @Override public UsernamePassword exportCredentials(String orgName, String secretName, String secretPassword) throws Exception { for (var ref : secretsProviders) { var result = ref.provider().exportCredentials(orgName, secretName, secretPassword); if (result.isPresent()) { reportSecretFound(orgName, secretName, ref); return result.get(); } } throw new RuntimeException("Secret not found: %s/%s.".formatted(orgName, secretName)); } @Override public SecretCreationResult createKeyPair(SecretParams secret, KeyPair keyPair) throws Exception { var ref = assertWritableProvider(); var result = ref.provider().createKeyPair(secret, keyPair); reportSecretCreated(secret, ref); return result; } @Override public SecretCreationResult createUsernamePassword(SecretParams secret, UsernamePassword usernamePassword) throws Exception { var ref = assertWritableProvider(); var result = ref.provider().createUsernamePassword(secret, usernamePassword); reportSecretCreated(secret, ref); return result; } @Override public SecretCreationResult createData(SecretParams secret, byte[] data) throws Exception { var ref = assertWritableProvider(); var result = ref.provider().createData(secret, data); reportSecretCreated(secret, ref); return result; } @Override public String decryptString(String encryptedValue) { return vaultProvider.getValue(encryptedValue); } @Override public String encryptString(String orgName, String projectName, String value) { throw new UnsupportedOperationException("Encrypting secrets is not supported by concord-cli yet"); } private SecretsProviderRef assertWritableProvider() { return secretsProviders.stream().filter(SecretsProviderRef::writable) .findFirst() .orElseThrow(() -> new RuntimeException("No writable secret providers configured")); } private void reportSecretFound(String orgName, String secretName, SecretsProviderRef ref) { if (verbosity.verbose()) { System.out.println(ansi().fgBlue().a("Fetched secret ").a(orgName).a("/").a(secretName).a(" from the '").a(ref.name()).a("' provider")); } } private void reportSecretCreated(SecretParams params, SecretsProviderRef ref) { if (verbosity.verbose()) { System.out.println(ansi().fgBlue().a("Created secret ").a(params.orgName()).a("/").a(params.secretName()).a(" in the '").a(ref.name()).a("' provider")); } } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/secrets/FileSecretsProvider.java ================================================ package com.walmartlabs.concord.cli.secrets; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.walmartlabs.concord.runtime.v2.sdk.SecretService; import javax.annotation.Nullable; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Map; import java.util.Optional; import java.util.UUID; import static com.walmartlabs.concord.cli.secrets.UncheckedIO.*; /** * Simple file-based secret provider. * Secrets stored unencrypted in ${dir}/${orgName}/${secretName}. */ public class FileSecretsProvider implements SecretsProvider { private final Path workDir; private final Path secretStoreDir; private final ObjectMapper objectMapper = new ObjectMapper(); public FileSecretsProvider(@Nullable Path workDir, Path secretStoreDir) { this.workDir = workDir; this.secretStoreDir = secretStoreDir; } @Override public Optional exportKeyAsFile(String orgName, String secretName, String password) throws Exception { var publicKey = toSecretPath(orgName, secretName + ".pub"); var privateKey = toSecretPath(orgName, secretName); if (Files.notExists(publicKey) || Files.notExists(privateKey)) { return Optional.empty(); } var tmpDir = UncheckedIO.assertTmpDir(assertWorkDir()); var tmpPublicKey = tmpDir.resolve(secretName + ".pub"); var tmpPrivateKey = tmpDir.resolve(secretName); Files.copy(publicKey, tmpPublicKey, StandardCopyOption.REPLACE_EXISTING); Files.copy(privateKey, tmpPrivateKey, StandardCopyOption.REPLACE_EXISTING); var result = SecretService.KeyPair.builder() .privateKey(tmpPrivateKey) .publicKey(tmpPublicKey) .build(); return Optional.of(result); } @Override public Optional exportAsString(String orgName, String secretName, String password) throws IOException { return getSecret(orgName, secretName) .map(UncheckedIO::readAllBytes) .map(String::new) .map(String::trim); } @Override public Optional exportAsFile(String orgName, String secretName, String password) throws IOException { return getSecret(orgName, secretName) .map(path -> { var tmpDir = assertTmpDir(assertWorkDir()); var dest = createTempFile(tmpDir, "file", ".bin"); copy(path, dest, StandardCopyOption.REPLACE_EXISTING); return dest; }); } @Override public Optional exportCredentials(String orgName, String secretName, String secretPassword) { return getSecret(orgName, secretName).map(path -> { try { var data = objectMapper.readTree(path.toFile()); var username = Optional.ofNullable(data.get("username")).map(JsonNode::asText) .orElseThrow(() -> new IllegalStateException("Secret %s/%s is missing the username field".formatted(orgName, secretName))); var password = Optional.ofNullable(data.get("password")).map(JsonNode::asText) .orElseThrow(() -> new IllegalStateException("Secret %s/%s is missing the password field".formatted(orgName, secretName))); return SecretService.UsernamePassword.of(username, password); } catch (IOException e) { throw new RuntimeException("Invalid secret '%s/%s' ('%s') format: %s".formatted(orgName, secretName, path, e.getMessage())); } }); } @Override public SecretService.SecretCreationResult createKeyPair(SecretService.SecretParams secret, SecretService.KeyPair keyPair) throws Exception { var publicKey = createSecretFile(secret.orgName(), secret.secretName() + ".pub"); var privateKey = createSecretFile(secret.orgName(), secret.secretName()); Files.copy(keyPair.publicKey(), publicKey); Files.copy(keyPair.privateKey(), privateKey); return SecretService.SecretCreationResult.builder() .id(UUID.randomUUID()) .build(); } @Override public SecretService.SecretCreationResult createUsernamePassword(SecretService.SecretParams secret, SecretService.UsernamePassword usernamePassword) throws Exception { var path = createSecretFile(secret.orgName(), secret.secretName()); objectMapper.writeValue(path.toFile(), Map.of( "username", usernamePassword.username(), "password", usernamePassword.password() )); return SecretService.SecretCreationResult.builder() .id(UUID.randomUUID()) .build(); } @Override public SecretService.SecretCreationResult createData(SecretService.SecretParams secret, byte[] data) throws Exception { var path = createSecretFile(secret.orgName(), secret.secretName()); Files.write(path, data); return SecretService.SecretCreationResult.builder() .id(UUID.randomUUID()) .build(); } private Optional getSecret(String orgName, String secretName) { var secretPath = toSecretPath(orgName, secretName); if (Files.notExists(secretPath)) { return Optional.empty(); } return Optional.of(secretPath); } private Path createSecretFile(String orgName, String secretName) throws IOException { var path = toSecretPath(orgName, secretName); if (Files.exists(path)) { throw new RuntimeException("Secret '%s/%s' ('%s') already exists".formatted(orgName, secretName, path)); } if (Files.notExists(path.getParent())) { Files.createDirectories(path.getParent()); } return path; } private Path toSecretPath(String orgName, String name) { var secretPath = secretStoreDir; if (orgName != null) { secretPath = secretStoreDir.resolve(orgName); } var result = secretPath.resolve(name); if (!result.normalize().startsWith(secretStoreDir)) { throw new IllegalArgumentException("Invalid secret name: %s/%s".formatted(orgName, name)); } return result; } private Path assertWorkDir() { if (this.workDir == null) { throw new RuntimeException("The workDir must be specified for the export functions to work"); } return this.workDir; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/secrets/RemoteSecretsProvider.java ================================================ package com.walmartlabs.concord.cli.secrets; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.cli.AbortException; import com.walmartlabs.concord.cli.Confirmation; import com.walmartlabs.concord.cli.Version; import com.walmartlabs.concord.client2.*; import com.walmartlabs.concord.common.secret.BinaryDataSecret; import com.walmartlabs.concord.common.secret.KeyPair; import com.walmartlabs.concord.common.secret.UsernamePassword; import com.walmartlabs.concord.runtime.v2.sdk.SecretService; import javax.annotation.Nullable; import java.io.IOException; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Objects; import java.util.Optional; import static java.util.Objects.requireNonNull; public class RemoteSecretsProvider implements SecretsProvider { private final Path workDir; private final SecretClient secretClient; private final boolean confirmAccess; public RemoteSecretsProvider(@Nullable Path workDir, String baseUrl, String apiKey, boolean confirmAccess) { this.workDir = workDir; this.confirmAccess = confirmAccess; var apiClient = new DefaultApiClientFactory(requireNonNull(baseUrl)) .create(ApiClientConfiguration.builder() .apiKey(requireNonNull(apiKey)) .build()); apiClient.setUserAgent("Concord-Cli (%s)".formatted(Version.getVersion())); this.secretClient = new SecretClient(apiClient); } @Override public Optional exportKeyAsFile(String orgName, String secretName, String secretPassword) throws Exception { return getKeyPair(orgName, secretName, secretPassword) .map(keyPair -> { var tmpDir = UncheckedIO.assertTmpDir(assertWorkDir()); var publicKeyPath = tmpDir.resolve(secretName + ".pub"); UncheckedIO.write(publicKeyPath, keyPair.getPublicKey(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); var privateKeyPath = tmpDir.resolve(secretName); UncheckedIO.write(privateKeyPath, keyPair.getPrivateKey(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); return SecretService.KeyPair.builder() .privateKey(privateKeyPath) .publicKey(publicKeyPath) .build(); }); } @Override public Optional exportAsString(String orgName, String secretName, String secretPassword) throws Exception { return getBinaryDataSecret(orgName, secretName, secretPassword) .map(BinaryDataSecret::getData) .map(String::new); } @Override public Optional exportAsFile(String orgName, String secretName, String secretPassword) throws Exception { return getBinaryDataSecret(orgName, secretName, secretPassword) .map(BinaryDataSecret::getData) .map(data -> { var tmpDir = UncheckedIO.assertTmpDir(assertWorkDir()); var secretPath = tmpDir.resolve(secretName + ".bin"); UncheckedIO.write(secretPath, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); return secretPath; }); } @Override public Optional exportCredentials(String orgName, String secretName, String secretPassword) throws Exception { return getUsernamePassword(orgName, secretName, secretPassword) .map(secret -> SecretService.UsernamePassword.of(secret.getUsername(), new String(secret.getPassword()))); } @Override public SecretService.SecretCreationResult createKeyPair(SecretService.SecretParams secret, SecretService.KeyPair keyPair) throws Exception { return toResult(secretClient.createSecret(secretRequest(secret) .keyPair(CreateSecretRequest.KeyPair.builder() .publicKey(keyPair.publicKey()) .privateKey(keyPair.privateKey()) .build()) .build())); } @Override public SecretService.SecretCreationResult createUsernamePassword(SecretService.SecretParams secret, SecretService.UsernamePassword usernamePassword) throws Exception { return toResult(secretClient.createSecret(secretRequest(secret) .usernamePassword(CreateSecretRequest.UsernamePassword.of(usernamePassword.username(), usernamePassword.password())) .build())); } @Override public SecretService.SecretCreationResult createData(SecretService.SecretParams secret, byte[] data) throws Exception { return toResult(secretClient.createSecret(secretRequest(secret) .data(data) .build())); } private Optional getKeyPair(String orgName, String secretName, String secretPassword) throws Exception { askForAccessConfirmation(orgName, secretName); try { return Optional.of(secretClient.getData(orgName, secretName, secretPassword, SecretEntryV2.TypeEnum.KEY_PAIR)); } catch (com.walmartlabs.concord.client2.SecretNotFoundException e) { return Optional.empty(); } } private Optional getBinaryDataSecret(String orgName, String secretName, String secretPassword) throws Exception { askForAccessConfirmation(orgName, secretName); try { return Optional.of(secretClient.getData(orgName, secretName, secretPassword, SecretEntryV2.TypeEnum.DATA)); } catch (com.walmartlabs.concord.client2.SecretNotFoundException e) { return Optional.empty(); } } private Optional getUsernamePassword(String orgName, String secretName, String secretPassword) throws Exception { askForAccessConfirmation(orgName, secretName); try { return Optional.of(secretClient.getData(orgName, secretName, secretPassword, SecretEntryV2.TypeEnum.USERNAME_PASSWORD)); } catch (com.walmartlabs.concord.client2.SecretNotFoundException e) { return Optional.empty(); } } private static SecretService.SecretCreationResult toResult(SecretOperationResponse response) { return SecretService.SecretCreationResult.builder() .id(response.getId()) .password(response.getPassword()) .build(); } private ImmutableCreateSecretRequest.Builder secretRequest(SecretService.SecretParams params) { var builder = CreateSecretRequest.builder() .org(params.orgName()) .name(params.secretName()) .generatePassword(params.generatePassword()) .storePassword(params.storePassword()); var visibility = params.visibility(); if (visibility != null) { builder.visibility(SecretEntryV2.VisibilityEnum.fromValue(visibility.name())); } if (params.project() != null) { builder.addProjectNames(Objects.requireNonNull(params.project())); } return builder; } private void askForAccessConfirmation(String orgName, String secretName) throws IOException { if (!confirmAccess) { return; } if (!Confirmation.confirm("Fetching remote secret %s/%s. Are you sure you want to proceed? (y/N)".formatted(orgName, secretName))) { throw new AbortException(); } } private Path assertWorkDir() { if (this.workDir == null) { throw new RuntimeException("The workDir must be specified for the export functions to work"); } return this.workDir; } } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/secrets/SecretsProvider.java ================================================ package com.walmartlabs.concord.cli.secrets; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.runtime.v2.sdk.SecretService; import com.walmartlabs.concord.runtime.v2.sdk.SecretService.KeyPair; import com.walmartlabs.concord.runtime.v2.sdk.SecretService.SecretCreationResult; import com.walmartlabs.concord.runtime.v2.sdk.SecretService.UsernamePassword; import java.nio.file.Path; import java.util.Optional; public interface SecretsProvider { Optional exportKeyAsFile(String orgName, String secretName, String password) throws Exception; Optional exportAsString(String orgName, String secretName, String password) throws Exception; Optional exportAsFile(String orgName, String secretName, String password) throws Exception; Optional exportCredentials(String orgName, String secretName, String secretPassword) throws Exception; SecretCreationResult createKeyPair(SecretService.SecretParams secret, KeyPair keyPair) throws Exception; SecretCreationResult createUsernamePassword(SecretService.SecretParams secret, UsernamePassword usernamePassword) throws Exception; SecretCreationResult createData(SecretService.SecretParams secret, byte[] data) throws Exception; } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/secrets/SecretsProviderRef.java ================================================ package com.walmartlabs.concord.cli.secrets; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public record SecretsProviderRef(String name, SecretsProvider provider, boolean writable) { } ================================================ FILE: cli/src/main/java/com/walmartlabs/concord/cli/secrets/UncheckedIO.java ================================================ package com.walmartlabs.concord.cli.secrets; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.cli.CliPaths; import com.walmartlabs.concord.sdk.Constants; import java.io.IOException; import java.nio.file.CopyOption; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; public final class UncheckedIO { public static Path assertTmpDir(Path workDir) { var dir = CliPaths.defaultTargetDir(workDir).resolve(Constants.Files.CONCORD_TMP_DIR_NAME); if (Files.notExists(dir)) { try { Files.createDirectories(dir); } catch (IOException e) { throw new RuntimeException(e.getMessage()); } } return dir; } public static Path write(Path path, byte[] bytes, OpenOption... options) { try { return Files.write(path, bytes, options); } catch (IOException e) { throw new RuntimeException(e.getMessage()); } } public static byte[] readAllBytes(Path path) { try { return Files.readAllBytes(path); } catch (IOException e) { throw new RuntimeException(e.getMessage()); } } public static Path createTempFile(Path dir, String prefix, String suffix) { try { return Files.createTempFile(dir, prefix, suffix); } catch (IOException e) { throw new RuntimeException(e.getMessage()); } } public static Path copy(Path source, Path target, CopyOption... options) { try { return Files.copy(source, target, options); } catch (IOException e) { throw new RuntimeException(e.getMessage()); } } private UncheckedIO() { } } ================================================ FILE: cli/src/main/resources/com/walmartlabs/concord/cli/defaultCliConfig.yaml ================================================ contexts: default: secrets: vault: dir: "${configDir}/vaults" id: "default" local: enabled: true writable: true dir: "${configDir}/secrets" remote: enabled: false writable: false confirmAccess: true ================================================ FILE: cli/src/main/resources/default-vars.json ================================================ { "ansible": { "enableEvents": false } } ================================================ FILE: cli/src/main/resources/logback.xml ================================================ true %d{HH:mm:ss.SSS} %green([%thread]) %highlight(%msg%n) ================================================ FILE: cli/src/test/filtered-resources/com/walmartlabs/concord/cli/defaultCfg/concord.yml ================================================ configuration: debug: true flows: default: - log: "isTrue = ${files.notExists('non-exist-dir')}" ================================================ FILE: cli/src/test/filtered-resources/com/walmartlabs/concord/cli/defaultCfg/defaults.yml ================================================ configuration: dependencies: - "mvn://com.walmartlabs.concord.plugins.basic:file-tasks:${project.version}" ================================================ FILE: cli/src/test/java/com/walmartlabs/concord/cli/AbstractTest.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2022 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; import java.util.concurrent.Callable; import static org.junit.jupiter.api.Assertions.fail; public abstract class AbstractTest { private final PrintStream originalOut = System.out; private final PrintStream originalErr = System.err; private final InputStream originalIn = System.in; private final ByteArrayOutputStream out = new ByteArrayOutputStream(); private final ByteArrayOutputStream err = new ByteArrayOutputStream(); @BeforeEach public void setUpStreams() { out.reset(); err.reset(); System.setOut(new PrintStream(out)); System.setErr(new PrintStream(err)); } @AfterEach public void restoreStreams() { System.setOut(originalOut); System.setErr(originalErr); System.setIn(originalIn); } protected void assertLog(String pattern) { String outStr = out.toString(); if (grep(outStr, pattern) != 1) { fail("Expected a single log entry: '" + pattern + "', got: \n" + outStr); } } protected void assertLog(String pattern, int times) { String outStr = out.toString(); int found = grep(outStr, pattern); if (found != times) { fail("Expected [" + times + "] log entries: '" + pattern + "', got [" + found + "]: \n" + outStr); } } protected String stdOut() { return out.toString(); } protected String stdErr() { return err.toString(); } protected void assertOutContainsRegex(String pattern) { assertContainsRegex(stdOut(), pattern, "stdout"); } protected void assertErrContainsRegex(String pattern) { assertContainsRegex(stdErr(), pattern, "stderr"); } protected T withInput(String value, Callable action) throws Exception { var previous = System.getProperty(PromptSupport.ALLOW_STDIN_PROMPTS_PROPERTY); System.setIn(new java.io.ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8))); System.setProperty(PromptSupport.ALLOW_STDIN_PROMPTS_PROPERTY, "true"); try { return action.call(); } finally { if (previous == null) { System.clearProperty(PromptSupport.ALLOW_STDIN_PROMPTS_PROPERTY); } else { System.setProperty(PromptSupport.ALLOW_STDIN_PROMPTS_PROPERTY, previous); } } } private static int grep(String str, String pattern) { int cnt = 0; String[] lines = str.split("\\r?\\n"); for (String line : lines) { if (line.matches(pattern)) { cnt++; } } return cnt; } private static void assertContainsRegex(String value, String pattern, String streamName) { if (!Pattern.compile(pattern, Pattern.DOTALL | Pattern.MULTILINE).matcher(value).find()) { fail("Expected " + streamName + " to match: '" + pattern + "', got:\n" + value); } } } ================================================ FILE: cli/src/test/java/com/walmartlabs/concord/cli/CliConfigTest.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.*; public class CliConfigTest { @TempDir private Path tempDir; @Test public void parse() throws Exception { var cfg = load("testConfig.yaml"); var defaultCtx = cfg.contexts().get("default"); assertNotNull(defaultCtx); assertEquals("foo", defaultCtx.secrets().vault().id()); } @Test public void checkDefaults() throws Exception { var cfg = load("configWithDefaults.yaml"); var defaultCtx = cfg.contexts().get("default"); assertNotNull(defaultCtx); assertNotNull(defaultCtx.secrets().vault().dir()); assertTrue(defaultCtx.secrets().vault().dir().toString().contains("/vaults")); } @Test public void withOverrides() throws Exception { var cfg = load("testConfig.yaml"); var defaultCtx = cfg.contexts().get("default"); assertNotNull(defaultCtx); var ctxWithOverrides = defaultCtx.withOverrides(new CliConfig.Overrides(Path.of("/barbaz"), Path.of("/foobar"), "qux")); assertEquals("/barbaz", ctxWithOverrides.secrets().local().dir().toString()); assertEquals("/foobar", ctxWithOverrides.secrets().vault().dir().toString()); assertEquals("qux", ctxWithOverrides.secrets().vault().id()); } @Test public void multiContexts() throws Exception { var cfg = load("multiContextConfig.yaml"); var anotherCtx = cfg.contexts().get("another"); assertNotNull(anotherCtx); assertEquals("bar", anotherCtx.secrets().vault().id()); assertEquals("qux", anotherCtx.secrets().vault().dir().toString()); } @Test public void missingContextWithoutUserConfig() throws Exception { var homeDir = tempDir.resolve("missing-context-home"); var e = withUserHome(homeDir, () -> assertThrows(CliConfig.MissingContextException.class, () -> CliConfig.loadOrThrow(new Verbosity(new boolean[0]), "another", new CliConfig.Overrides(null, null, null)))); assertTrue(e.getMessage().contains("Configuration context not found: another")); assertTrue(e.getMessage().contains("only the built-in 'default' context is available")); } @Test public void missingContextWithUserConfig() throws Exception { var homeDir = tempDir.resolve("configured-home"); var configDir = homeDir.resolve(".concord"); Files.createDirectories(configDir); Files.writeString(configDir.resolve("cli.yaml"), """ contexts: default: {} """); var e = withUserHome(homeDir, () -> assertThrows(CliConfig.MissingContextException.class, () -> CliConfig.loadOrThrow(new Verbosity(new boolean[0]), "another", new CliConfig.Overrides(null, null, null)))); assertEquals("Configuration context not found: another. Check the CLI configuration file.", e.getMessage()); } private static CliConfig load(String resource) throws IOException, URISyntaxException { var src = Paths.get(requireNonNull(CliConfigTest.class.getResource(resource)).toURI()); return CliConfig.loadConfigFile(src); } private static T withUserHome(Path userHome, java.util.concurrent.Callable action) throws Exception { var originalUserHome = System.getProperty("user.home"); System.setProperty("user.home", userHome.toString()); try { return action.call(); } finally { if (originalUserHome == null) { System.clearProperty("user.home"); } else { System.setProperty("user.home", originalUserHome); } } } } ================================================ FILE: cli/src/test/java/com/walmartlabs/concord/cli/GitIgnoreFilterTest.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.*; class GitIgnoreFilterTest { @TempDir Path tempDir; @Test void testNoGitignoreReturnsNull() throws IOException { var filter = GitIgnoreFilter.load(tempDir); assertNull(filter); } @Test void testEmptyGitignoreReturnsNull() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), ""); var filter = GitIgnoreFilter.load(tempDir); assertNull(filter); } @Test void testCommentsOnlyReturnsNull() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), "# This is a comment\n# Another comment\n"); var filter = GitIgnoreFilter.load(tempDir); assertNull(filter); } @Test void testBasicPatterns() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), "*.log\nnode_modules/\n"); var filter = GitIgnoreFilter.load(tempDir); assertNotNull(filter); assertTrue(filter.isIgnored(Path.of("debug.log"), false)); assertTrue(filter.isIgnored(Path.of("error.log"), false)); assertFalse(filter.isIgnored(Path.of("debug.txt"), false)); assertTrue(filter.isIgnored(Path.of("node_modules"), true)); assertFalse(filter.isIgnored(Path.of("other_modules"), true)); } @Test void testGlobPatterns() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), "**/*.tmp\nbuild/**/output\n"); var filter = GitIgnoreFilter.load(tempDir); assertNotNull(filter); assertTrue(filter.isIgnored(Path.of("test.tmp"), false)); assertTrue(filter.isIgnored(Path.of("sub/dir/test.tmp"), false)); assertFalse(filter.isIgnored(Path.of("test.txt"), false)); assertTrue(filter.isIgnored(Path.of("build/output"), true)); assertTrue(filter.isIgnored(Path.of("build/sub/output"), true)); } @Test void testNegationPatterns() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), "*.log\n!important.log\n"); var filter = GitIgnoreFilter.load(tempDir); assertNotNull(filter); assertTrue(filter.isIgnored(Path.of("debug.log"), false)); assertTrue(filter.isIgnored(Path.of("error.log"), false)); assertFalse(filter.isIgnored(Path.of("important.log"), false)); } @Test void testDirectoryOnlyPatterns() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), "build/\n"); var filter = GitIgnoreFilter.load(tempDir); assertNotNull(filter); assertTrue(filter.isIgnored(Path.of("build"), true)); // A file named "build" should not be ignored when pattern has trailing slash assertFalse(filter.isIgnored(Path.of("build"), false)); } @Test void testAnchoredPatterns() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), "/config.local\n"); var filter = GitIgnoreFilter.load(tempDir); assertNotNull(filter); assertTrue(filter.isIgnored(Path.of("config.local"), false)); // Anchored pattern should not match in subdirectories assertFalse(filter.isIgnored(Path.of("sub/config.local"), false)); } @Test void testNestedGitignore() throws IOException { // Root .gitignore Files.writeString(tempDir.resolve(".gitignore"), "*.log\n"); // Create subdirectory with its own .gitignore var subDir = tempDir.resolve("subdir"); Files.createDirectories(subDir); Files.writeString(subDir.resolve(".gitignore"), "*.txt\n!keep.txt\n"); var filter = GitIgnoreFilter.load(tempDir); assertNotNull(filter); // Root patterns apply everywhere assertTrue(filter.isIgnored(Path.of("debug.log"), false)); assertTrue(filter.isIgnored(Path.of("subdir/debug.log"), false)); // Subdir patterns only apply within subdir assertFalse(filter.isIgnored(Path.of("test.txt"), false)); assertTrue(filter.isIgnored(Path.of("subdir/test.txt"), false)); assertFalse(filter.isIgnored(Path.of("subdir/keep.txt"), false)); } @Test void testBlankLinesIgnored() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), "*.log\n\n\n*.tmp\n"); var filter = GitIgnoreFilter.load(tempDir); assertNotNull(filter); assertTrue(filter.isIgnored(Path.of("debug.log"), false)); assertTrue(filter.isIgnored(Path.of("test.tmp"), false)); } @Test void testMixedPatterns() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), """ # Dependencies node_modules/ # Build output build/ dist/ *.class # IDE .idea/ *.iml # Logs *.log !important.log # Temp files **/*.tmp """); var filter = GitIgnoreFilter.load(tempDir); assertNotNull(filter); assertTrue(filter.isIgnored(Path.of("node_modules"), true)); assertTrue(filter.isIgnored(Path.of("build"), true)); assertTrue(filter.isIgnored(Path.of("dist"), true)); assertTrue(filter.isIgnored(Path.of("Main.class"), false)); assertTrue(filter.isIgnored(Path.of(".idea"), true)); assertTrue(filter.isIgnored(Path.of("project.iml"), false)); assertTrue(filter.isIgnored(Path.of("app.log"), false)); assertFalse(filter.isIgnored(Path.of("important.log"), false)); assertTrue(filter.isIgnored(Path.of("deep/nested/file.tmp"), false)); } @Test void testSubdirectoryPaths() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), "target/\n"); var filter = GitIgnoreFilter.load(tempDir); assertNotNull(filter); assertTrue(filter.isIgnored(Path.of("target"), true)); assertTrue(filter.isIgnored(Path.of("sub/target"), true)); } @Test void testFilesInIgnoredDirectory() throws IOException { Files.writeString(tempDir.resolve(".gitignore"), "build/\n"); var filter = GitIgnoreFilter.load(tempDir); assertNotNull(filter); // The directory itself is ignored assertTrue(filter.isIgnored(Path.of("build"), true)); // Files inside an ignored directory are NOT directly matched by the pattern. // In practice, the walk would skip the directory entirely, so this case wouldn't occur. // The pattern `build/` only matches directories, not paths within them. assertFalse(filter.isIgnored(Path.of("build/output.jar"), false)); } } ================================================ FILE: cli/src/test/java/com/walmartlabs/concord/cli/LintTest.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2022 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.PathUtils; import com.walmartlabs.concord.common.TemporaryPath; import org.junit.jupiter.api.Test; import picocli.CommandLine; import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; class LintTest extends AbstractTest { @Test void lintV1Test() throws Exception { int exitCode = lint("lintV1"); assertEquals(0, exitCode); assertLog(".*flows: 2.*"); } @Test void lintV2Test() throws Exception { int exitCode = lint("lintV2"); assertEquals(0, exitCode); assertLog(".*flows: 2.*"); } private static int lint(String payload) throws Exception { URI uri = LintTest.class.getResource(payload).toURI(); Path source = Paths.get(uri); try (TemporaryPath dst = PathUtils.tempDir("cli-tests")) { PathUtils.copy(source, dst.path()); App app = new App(); CommandLine cmd = new CommandLine(app); List args = new ArrayList<>(); args.add("lint"); Path pwd = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); Path relative = pwd.relativize(dst.path()); args.add(relative.toString()); return cmd.execute(args.toArray(new String[0])); } } } ================================================ FILE: cli/src/test/java/com/walmartlabs/concord/cli/ResumeTest.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2026 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.PathUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import picocli.CommandLine; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class ResumeTest extends AbstractTest { @TempDir private Path tempDir; @Test void suspendedRunPersistsStateAndPrintsGuidance() throws Exception { var source = preparePayload("suspend"); var targetDir = CliPaths.defaultTargetDir(source); var exitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, exitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertLog(".*before suspend.*"); assertOutContainsRegex("Process suspended\\.\\R\\RResume dir: " + quoted(source) + "\\RCommands below assume you are in that directory\\.\\R\\RAdditional waiting events:\\R ev1\\R\\RContinue with:\\R Resume event:\\R concord resume --event ev1"); assertFalse(stdOut().contains("...done!"), stdOut()); assertTrue(Files.exists(targetDir.resolve("_attachments").resolve("_state").resolve("instance"))); assertTrue(Files.exists(targetDir.resolve("_attachments").resolve("_state").resolve("_suspend"))); assertTrue(Files.exists(targetDir.resolve("_attachments").resolve("_state").resolve("_cliResume.json"))); } @Test void resumeConsumesInputFileAndSaveAs() throws Exception { var source = preparePayload("suspend"); var targetDir = CliPaths.defaultTargetDir(source); var inputFile = tempDir.resolve("payload.yml"); Files.writeString(inputFile, "value: resumed-value\n"); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var afterRun = stdOut(); var resumeExitCode = executeIn(source, List.of("resume", "--input-file", inputFile.toString(), "--save-as", "myForm")); assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var resumeOutput = stdOut().substring(afterRun.length()); assertTrue(resumeOutput.contains("after resume: resumed-value"), resumeOutput); assertTrue(resumeOutput.contains("...done!"), resumeOutput); assertFalse(Files.exists(targetDir.resolve("_attachments").resolve("_state")), targetDir.toString()); } @Test void resumeConsumesInlineNestedInput() throws Exception { var source = preparePayload("suspend"); var targetDir = CliPaths.defaultTargetDir(source); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var afterRun = stdOut(); var resumeExitCode = executeIn(source, List.of("resume", "-e", "myForm.value=resumed-inline")); assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var resumeOutput = stdOut().substring(afterRun.length()); assertTrue(resumeOutput.contains("after resume: resumed-inline"), resumeOutput); assertTrue(resumeOutput.contains("...done!"), resumeOutput); assertFalse(Files.exists(targetDir.resolve("_attachments").resolve("_state")), targetDir.toString()); } @Test void resumePromptsForPendingStandardForms() throws Exception { var source = preparePayload("form"); var targetDir = CliPaths.defaultTargetDir(source); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertOutContainsRegex("Process suspended\\.\\R\\RResume dir: " + quoted(source) + "\\RCommands below assume you are in that directory\\.\\R\\RPending forms:\\R Form key\\s+Event ID\\R myForm\\s+[^\\n]+\\R\\RContinue with:\\R Describe input:\\R concord resume --event [^\\s]+ --describe-input\\R Submit input:\\R concord resume --event [^\\s]+ --input-file myForm\\.yml"); assertFalse(stdOut().contains("concord resume " + source), stdOut()); assertFalse(stdOut().contains("Fill pending form now?"), stdOut()); assertFalse(stdOut().contains("Fill interactively:"), stdOut()); assertTrue(Files.exists(targetDir.resolve("_attachments").resolve("_state").resolve("V2forms").resolve("myForm"))); var afterRun = stdOut(); var resumeExitCode = withInput("John Smith\n33\n", () -> executeIn(source, List.of("resume"))); assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var resumeOutput = stdOut().substring(afterRun.length()); assertOutContainsRegex("Pending form input:\\R myForm -> [^\\n]+"); assertTrue(resumeOutput.contains("after form: John Smith, 33"), resumeOutput); assertTrue(resumeOutput.contains("...done!"), resumeOutput); assertFalse(Files.exists(targetDir.resolve("_attachments").resolve("_state")), targetDir.toString()); } @Test void resumeFailsFastForFormsWithoutInteractiveInput() throws Exception { var source = preparePayload("form"); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var resumeExitCode = executeIn(source, List.of("resume")); assertEquals(CliExitCodes.INPUT_REQUIRED, resumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertErrContainsRegex("Resume dir: " + quoted(source) + "\\RCommands below assume you are in that directory\\.\\R\\RPending form requires input in non-interactive mode\\.\\R\\RPending forms:\\R Form key\\s+Event ID\\R myForm\\s+[^\\n]+\\R\\RContinue with:\\R Describe input:\\R concord resume --event [^\\s]+ --describe-input\\R Submit input:\\R concord resume --event [^\\s]+ --input-file myForm\\.yml"); assertFalse(stdErr().contains("Fill interactively:"), stdErr()); } @Test void describeInputShowsExpectedFormShape() throws Exception { var source = preparePayload("form"); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var afterRun = stdOut(); var describeExitCode = executeIn(source, List.of("resume", "--describe-input")); assertEquals(CliExitCodes.SUCCESS, describeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var describeOutput = stdOut().substring(afterRun.length()); assertOutContainsRegex("Resume dir: " + quoted(source) + "\\RPending form input:\\R myForm -> [^\\n]+\\RRequired fields:\\R name\\R age"); assertTrue(describeOutput.contains("Example input file:"), describeOutput); assertTrue(describeOutput.contains("myForm:"), describeOutput); assertTrue(describeOutput.contains("name: \"\""), describeOutput); assertTrue(describeOutput.contains("age: 0"), describeOutput); } @Test void runOffersImmediateFormFill() throws Exception { var source = preparePayload("form"); var targetDir = CliPaths.defaultTargetDir(source); var exitCode = withInput("\nJohn Smith\n33\n", () -> executeIn(source, runArgs())); assertEquals(CliExitCodes.SUCCESS, exitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertTrue(stdOut().contains("Fill pending form now? (Y/n)"), stdOut()); assertTrue(stdOut().contains("Name [string, required]:"), stdOut()); assertTrue(stdOut().contains("after form: John Smith, 33"), stdOut()); assertTrue(stdOut().contains("...done!"), stdOut()); assertFalse(stdOut().contains("Process suspended."), stdOut()); assertFalse(Files.exists(targetDir.resolve("_attachments").resolve("_state")), targetDir.toString()); } @Test void runNoPromptSkipsImmediateFormFill() throws Exception { var source = preparePayload("form"); var exitCode = withInput("\nJohn Smith\n33\n", () -> executeIn(source, List.of("run", "--no-default-cfg", "--no-prompt"))); assertEquals(CliExitCodes.SUSPENDED, exitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertFalse(stdOut().contains("Fill pending form now?"), stdOut()); assertOutContainsRegex("Process suspended\\.\\R\\RResume dir: " + quoted(source) + "\\RCommands below assume you are in that directory\\.\\R\\RPending forms:\\R Form key\\s+Event ID\\R myForm\\s+[^\\n]+"); } @Test void interruptedImmediateFormFillCanBeResumed() throws Exception { var source = preparePayload("parallelForms"); var targetDir = CliPaths.defaultTargetDir(source); var runExitCode = withInput("\nfirst-value\n", () -> executeIn(source, runArgs())); assertEquals(CliExitCodes.ERROR, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertTrue(Files.exists(targetDir.resolve("_attachments").resolve("_state").resolve("_cliResume.json"))); var afterRun = stdOut(); var resumeExitCode = withInput("second-value\n", () -> executeIn(source, List.of("resume"))); assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var resumeOutput = stdOut().substring(afterRun.length()); assertOutContainsRegex("Pending form input:\\R form2 -> [^\\n]+"); assertTrue(stdOut().contains("parallel forms: form1=first-value"), stdOut()); assertTrue(resumeOutput.contains("parallel forms: done one=first-value two=second-value"), resumeOutput); assertTrue(resumeOutput.contains("...done!"), resumeOutput); assertFalse(Files.exists(targetDir.resolve("_attachments").resolve("_state")), targetDir.toString()); } @Test void parallelFormsRunShowsMappingsAndAutomationHints() throws Exception { var source = preparePayload("parallelForms"); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertOutContainsRegex("Process suspended\\.\\R\\RResume dir: " + quoted(source) + "\\RCommands below assume you are in that directory\\.\\R\\RPending forms:\\R Form key\\s+Event ID\\R form1\\s+[^\\n]+\\R form2\\s+[^\\n]+\\R\\RContinue with:\\R Describe input:\\R concord resume --event [^\\s]+ --describe-input\\R concord resume --event [^\\s]+ --describe-input\\R Submit input:\\R concord resume --event [^\\s]+ --input-file form1\\.yml\\R concord resume --event [^\\s]+ --input-file form2\\.yml"); assertFalse(stdOut().contains("concord resume " + source), stdOut()); } @Test void resumeWithoutPayloadOnMultipleFormsRequiresExplicitEvent() throws Exception { var source = preparePayload("parallelForms"); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var resumeExitCode = executeIn(source, List.of("resume")); assertEquals(CliExitCodes.INPUT_REQUIRED, resumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertErrContainsRegex("Resume dir: " + quoted(source) + "\\RCommands below assume you are in that directory\\.\\R\\RPending forms require input or explicit event selection\\.\\R\\RPending forms:\\R Form key\\s+Event ID\\R form1\\s+[^\\n]+\\R form2\\s+[^\\n]+"); assertTrue(stdErr().contains("--describe-input"), stdErr()); assertTrue(stdErr().contains("--input-file form1.yml"), stdErr()); assertTrue(stdErr().contains("--input-file form2.yml"), stdErr()); } @Test void describeInputRequiresEventForMultipleForms() throws Exception { var source = preparePayload("parallelForms"); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var describeExitCode = executeIn(source, List.of("resume", "--describe-input")); assertEquals(CliExitCodes.INPUT_REQUIRED, describeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertErrContainsRegex("Resume dir: " + quoted(source) + "\\RCommands below assume you are in that directory\\.\\R\\RPending forms require explicit event selection before describing input\\.\\R\\RPending forms:\\R Form key\\s+Event ID\\R form1\\s+[^\\n]+\\R form2\\s+[^\\n]+\\R\\RContinue with:\\R Describe input:"); assertTrue(stdErr().contains("--describe-input"), stdErr()); } @Test void mixedFormAndEventGuidanceIncludesBothChoices() throws Exception { var source = preparePayload("mixedFormEvent"); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertOutContainsRegex("Process suspended\\.\\R\\RResume dir: " + quoted(source) + "\\RCommands below assume you are in that directory\\.\\R\\RPending forms:\\R Form key\\s+Event ID\\R approvalForm\\s+[^\\n]+\\R\\RAdditional waiting events:\\R ev_timeout\\R\\RContinue with:\\R Describe input:\\R concord resume --event [^\\s]+ --describe-input\\R Submit input:\\R concord resume --event [^\\s]+ --input-file approvalForm\\.yml\\R Resume event:\\R concord resume --event ev_timeout"); } @Test void fileUploadFormDescribeInputAndResumeFailure() throws Exception { var source = preparePayload("fileForm"); var inputFile = tempDir.resolve("uploadForm.yml"); Files.writeString(inputFile, "uploadForm:\n attachment: path/to/file\n"); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertTrue(stdOut().contains("not supported for file-upload fields"), stdOut()); var afterRun = stdOut(); var describeExitCode = executeIn(source, List.of("resume", "--describe-input")); assertEquals(CliExitCodes.SUCCESS, describeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var describeOutput = stdOut().substring(afterRun.length()); assertTrue(describeOutput.contains("File-upload fields:"), describeOutput); assertTrue(describeOutput.contains("attachment"), describeOutput); assertTrue(describeOutput.contains("not supported for file-upload fields"), describeOutput); var resumeExitCode = executeIn(source, List.of("resume", "--input-file", inputFile.toString())); assertEquals(CliExitCodes.NON_INTERACTIVE_UNSUPPORTED, resumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertErrContainsRegex("Resume dir: " + quoted(source) + "\\RCommands below assume you are in that directory\\.\\R\\RPending form cannot be submitted non-interactively because it contains file-upload fields\\.\\R\\RPending forms:\\R Form key\\s+Event ID\\R uploadForm\\s+[^\\n]+"); } @Test void nonInteractiveFormInputIsConvertedAndValidated() throws Exception { var source = preparePayload("validatedForm"); var targetDir = CliPaths.defaultTargetDir(source); var invalidInputFile = tempDir.resolve("invalidValidatedForm.yml"); var validInputFile = tempDir.resolve("validValidatedForm.yml"); Files.writeString(invalidInputFile, """ validatedForm: name: Alice age: 10 choice: green """); Files.writeString(validInputFile, """ name: Alice age: 33 choice: red """); var runExitCode = executeIn(source, runArgs()); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var invalidResumeExitCode = executeIn(source, List.of("resume", "--input-file", invalidInputFile.toString())); assertEquals(CliExitCodes.INPUT_REQUIRED, invalidResumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertTrue(stdErr().contains("Invalid form input:"), stdErr()); assertTrue(stdErr().contains("Age"), stdErr()); assertTrue(stdErr().contains("Choice"), stdErr()); assertTrue(Files.exists(targetDir.resolve("_attachments").resolve("_state").resolve("instance"))); var afterInvalidResume = stdOut(); var validResumeExitCode = executeIn(source, List.of("resume", "--input-file", validInputFile.toString())); assertEquals(CliExitCodes.SUCCESS, validResumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var resumeOutput = stdOut().substring(afterInvalidResume.length()); assertTrue(resumeOutput.contains("validated form: name=Alice, age=33, choice=red, note=default-note, readOnly=locked"), resumeOutput); assertTrue(resumeOutput.contains("...done!"), resumeOutput); assertFalse(Files.exists(targetDir.resolve("_attachments").resolve("_state")), targetDir.toString()); } @Test void fileUploadFormRetryCleansFailedAttemptTempFiles() throws Exception { var source = preparePayload("fileRetryForm"); var uploadFile = tempDir.resolve("upload.txt"); Files.writeString(uploadFile, "upload contents"); var beforeTmpFiles = listTmpFiles("attachment"); var exitCode = withInput("\n" + uploadFile + "\n10\n" + uploadFile + "\n33\n", () -> executeIn(source, runArgs())); assertEquals(CliExitCodes.SUCCESS, exitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertTrue(stdOut().contains("after upload retry: 33"), stdOut()); assertEquals(beforeTmpFiles, listTmpFiles("attachment")); } @Test void resumeMetadataStoresAbsolutePathsAndResolvesOldRelativePaths() { var metadata = LocalSuspendPersistence.ResumeMetadata.from(Path.of("work"), Path.of("resume"), Path.of("defaultTaskVars.json"), Path.of("deps"), "default", new CliConfig.Overrides(Path.of("secrets"), Path.of("vault"), "test"), List.of(), null, null); assertTrue(Path.of(metadata.workDir()).isAbsolute()); assertTrue(Path.of(metadata.resumeDir()).isAbsolute()); assertTrue(Path.of(metadata.defaultTaskVars()).isAbsolute()); assertTrue(Path.of(metadata.depsCacheDir()).isAbsolute()); assertTrue(Path.of(metadata.cliConfig().secretStoreDir()).isAbsolute()); assertTrue(Path.of(metadata.cliConfig().vaultDir()).isAbsolute()); var oldMetadata = new LocalSuspendPersistence.ResumeMetadata(null, null, List.of(), "resume", "resume/target", "defaultTaskVars.json", "deps", new LocalSuspendPersistence.CliConfigData("default", false, "secrets", "vault", "test")); assertEquals(Path.of("resume", "defaultTaskVars.json").normalize().toAbsolutePath(), oldMetadata.defaultTaskVarsPath()); assertEquals(Path.of("resume", "deps").normalize().toAbsolutePath(), oldMetadata.depsCacheDirPath()); } @Test void suspendedMetadataOmitsApiKeyAndResumeReloadsStoredContextAndOverrides() throws Exception { var source = preparePayload("secretResume"); var targetDir = CliPaths.defaultTargetDir(source); var homeDir = tempDir.resolve("home"); var overrideSecretDir = tempDir.resolve("override-secrets"); writeSecret(overrideSecretDir, "Default", "resumeSecret", "value-from-override"); writeCliConfig(homeDir, """ contexts: default: secrets: local: enabled: true writable: true dir: "%s" remote: enabled: true writable: false baseUrl: "http://localhost:8001" apiKey: "resume-api-key" another: secrets: local: dir: "%s" """.formatted(tempDir.resolve("wrong-default-secrets"), tempDir.resolve("wrong-another-secrets"))); var runExitCode = withUserHome(homeDir, () -> executeIn(source, runArgs("--context", "another", "--secret-dir", overrideSecretDir.toString()))); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var metadata = Files.readString(targetDir.resolve("_attachments").resolve("_state").resolve("_cliResume.json")); assertFalse(metadata.contains("resume-api-key"), metadata); assertTrue(metadata.contains("\"contextName\" : \"another\""), metadata); assertTrue(metadata.contains(overrideSecretDir.toString()), metadata); var afterRun = stdOut(); var resumeExitCode = withUserHome(homeDir, () -> executeIn(source, List.of("resume"))); assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var resumeOutput = stdOut().substring(afterRun.length()); assertTrue(resumeOutput.contains("after resume secret: value-from-override"), resumeOutput); } @Test void passwordRetryPromptsDoNotEchoSecretsAndRunCleanupRemovesSessionFiles() throws Exception { var source = preparePayload("passwordRetry"); var targetDir = CliPaths.defaultTargetDir(source); var exitCode = withInput("\nentered-secret\n10\nentered-secret\n33\n", () -> executeIn(source, runArgs())); assertEquals(CliExitCodes.SUCCESS, exitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); assertTrue(stdOut().contains("Fill pending form now? (Y/n)"), stdOut()); assertTrue(stdOut().contains("after password form: 33"), stdOut()); assertEquals(2, countMatches(stdOut(), "Password \\[string, required, default: \\]:"), stdOut()); assertFalse(stdOut().contains("seed-secret"), stdOut()); assertFalse(stdOut().contains("entered-secret"), stdOut()); assertFalse(stdErr().contains("seed-secret"), stdErr()); assertFalse(stdErr().contains("entered-secret"), stdErr()); assertFalse(Files.exists(targetDir.resolve("_attachments").resolve("_session_files")), targetDir.toString()); } @Test void resumeCleanupRemovesSessionFiles() throws Exception { var source = preparePayload("passwordSuspend"); var targetDir = CliPaths.defaultTargetDir(source); var runExitCode = withInput("\nresume-secret\n33\n", () -> executeIn(source, runArgs())); assertEquals(CliExitCodes.SUSPENDED, runExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var sessionFilesDir = targetDir.resolve("_attachments").resolve("_session_files"); Files.createDirectories(sessionFilesDir); Files.writeString(sessionFilesDir.resolve("sensitive.json"), "test"); var afterRun = stdOut(); var resumeExitCode = executeIn(source, List.of("resume")); assertEquals(CliExitCodes.SUCCESS, resumeExitCode, () -> "out:\n" + stdOut() + "\n\nerr:\n" + stdErr()); var resumeOutput = stdOut().substring(afterRun.length()); assertTrue(resumeOutput.contains("after resume: 33"), resumeOutput); assertFalse(Files.exists(targetDir.resolve("_attachments").resolve("_state")), targetDir.toString()); assertFalse(Files.exists(sessionFilesDir), targetDir.toString()); } @Test void cleanupRemovesStateAndSessionFiles() throws Exception { var workDir = tempDir.resolve("cleanup"); var stateDir = workDir.resolve("_attachments").resolve("_state"); var sessionFilesDir = workDir.resolve("_attachments").resolve("_session_files"); Files.createDirectories(stateDir); Files.createDirectories(sessionFilesDir); Files.writeString(stateDir.resolve("instance"), "state"); Files.writeString(sessionFilesDir.resolve("sensitive.json"), "session"); LocalSuspendPersistence.cleanup(workDir); assertFalse(Files.exists(stateDir), workDir.toString()); assertFalse(Files.exists(sessionFilesDir), workDir.toString()); } private int execute(List args) { var app = new App(); var cmd = new CommandLine(app); return cmd.execute(args.toArray(new String[0])); } private int executeIn(Path userDir, List args) { var originalUserDir = System.getProperty("user.dir"); System.setProperty("user.dir", userDir.toString()); try { return execute(args); } finally { if (originalUserDir == null) { System.clearProperty("user.dir"); } else { System.setProperty("user.dir", originalUserDir); } } } private static List runArgs(String... extraArgs) { var result = new java.util.ArrayList(); result.add("run"); result.add("--no-default-cfg"); result.addAll(List.of(extraArgs)); return result; } private Path preparePayload(String payload) throws Exception { var uri = ResumeTest.class.getResource(payload).toURI(); var source = Paths.get(uri); PathUtils.copy(source, tempDir); return tempDir; } private static String quoted(Path path) { return Pattern.quote(path.toString()); } private T withUserHome(Path userHome, java.util.concurrent.Callable action) throws Exception { var originalUserHome = System.getProperty("user.home"); System.setProperty("user.home", userHome.toString()); try { return action.call(); } finally { if (originalUserHome == null) { System.clearProperty("user.home"); } else { System.setProperty("user.home", originalUserHome); } } } private static void writeCliConfig(Path homeDir, String contents) throws IOException { var configDir = homeDir.resolve(".concord"); Files.createDirectories(configDir); Files.writeString(configDir.resolve("cli.yaml"), contents); } private static void writeSecret(Path secretDir, String orgName, String secretName, String value) throws IOException { var dir = secretDir.resolve(orgName); Files.createDirectories(dir); Files.writeString(dir.resolve(secretName), value); } private static int countMatches(String value, String regex) { var matcher = Pattern.compile(regex).matcher(value); var count = 0; while (matcher.find()) { count++; } return count; } private static Set listTmpFiles(String prefix) throws IOException { try (var files = Files.list(PathUtils.TMP_DIR)) { return files.filter(p -> { var fileName = p.getFileName().toString(); return fileName.startsWith(prefix) && fileName.endsWith(".tmp"); }) .collect(Collectors.toSet()); } } } ================================================ FILE: cli/src/test/java/com/walmartlabs/concord/cli/RunTest.java ================================================ package com.walmartlabs.concord.cli; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.PathUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import picocli.CommandLine; import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import static org.junit.jupiter.api.Assertions.assertEquals; class RunTest extends AbstractTest { @TempDir private Path tempDir; @Test void runTest() throws Exception { Map extraVars = Collections.singletonMap("name", "Concord"); List args = new ArrayList<>(); for (Map.Entry e : extraVars.entrySet()) { args.add("-e"); args.add(e.getKey() + "=" + e.getValue()); } int exitCode = run("simple", args); assertExitCode(0, exitCode); assertLog(".*Hello, Concord.*"); assertEquals(0, exitCode); // default dependencies should be added assertLog(".*concord-tasks-" + Version.getVersion() + ".jar.*"); assertLog(".*http-tasks-" + Version.getVersion() + ".jar.*"); assertLog(".*slack-tasks-" + Version.getVersion() + ".jar.*"); } @Test void testResourceTask() throws Exception { int exitCode = run("resourceTask", Collections.emptyList()); assertExitCode(0, exitCode); assertLog(".*\"k\" : \"v\".*"); } @Test void testDepsFromProfile() throws Exception { int exitCode = run("profileDeps", Arrays.asList("-p", "test")); assertExitCode(0, exitCode); assertLog(".*exists=true.*"); } @Test void testCliCheckpointService() throws Exception { int exitCode = run("cliCheckpointService", Collections.emptyList()); assertExitCode(0, exitCode); assertLog(".*Checkpoint.*ignored.*", 2); } @Test void testCustomDefaultConfig() throws Exception { int exitCode = run("defaultCfg", Collections.emptyList(), "defaults.yml"); assertExitCode(0, exitCode); assertLog(".*file-tasks-" + Version.getVersion() + ".jar.*"); } @Test void testCustomDefaultTaskVars() throws Exception { int exitCode = run("defaultTaskVars", List.of("--default-task-vars", tempDir.resolve("defaultTaskVars.json").toString())); assertExitCode(0, exitCode); assertLog(".*Unknown action: 'customInvalidAction'. Available actions.*"); } @Test void testProcessProjectInfo() throws Exception { Map extraVars = new HashMap<>(); extraVars.put("processInfo.sessionToken", "test-token"); extraVars.put("projectInfo.orgName", "test-org"); List args = new ArrayList<>(); for (Map.Entry e : extraVars.entrySet()) { args.add("-e"); args.add(e.getKey() + "=" + e.getValue()); } int exitCode = run("processProjectInfo", args); assertExitCode(0, exitCode); assertLog(".*processInfo: \\{sessionToken=test-token}.*"); assertLog(".*projectInfo: \\{orgName=test-org}.*"); } private void assertExitCode(int expected, int current) { assertEquals(expected, current, () -> "out:\n" + stdOut() + "\n\n" + "err:\n" + stdErr()); } private int run(String payload, List args) throws Exception { return run(payload, args, null); } private int run(String payload, List args, String defaultCfg) throws Exception { URI uri = RunTest.class.getResource(payload).toURI(); Path source = Paths.get(uri); PathUtils.copy(source, tempDir); App app = new App(); CommandLine cmd = new CommandLine(app); List effectiveArgs = new ArrayList<>(); effectiveArgs.add("run"); effectiveArgs.addAll(args); effectiveArgs.add(tempDir.toString()); if (defaultCfg != null) { effectiveArgs.add("--default-cfg"); effectiveArgs.add(tempDir.resolve(defaultCfg).toString()); } return cmd.execute(effectiveArgs.toArray(new String[0])); } } ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/cliCheckpointService/concord.yml ================================================ configuration: runtime: "concord-v2" flows: default: - checkpoint: my-first-checkpoint - checkpoint: my-second-checkpoint - log: "Checkpoint done!" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/configWithDefaults.yaml ================================================ contexts: default: secrets: vault: id: "foo" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/defaultTaskVars/concord.yml ================================================ flows: default: # execute some task that looks for default (policy-provide) variables # obviously this won't successfully send a message in an IT, however # the error messages are useful enough to make sure the vars work - task: slack in: # action is given via default variables text: mock-message error: - if: "${not lastError.message.contains('Unknown action')}" then: - throw: "Unexpected error: ${lastError}" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/defaultTaskVars/defaultTaskVars.json ================================================ { "slack": { "action": "customInvalidAction" } } ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/fileForm/concord.yml ================================================ flows: default: - log: "before file form" - form: uploadForm - log: "after upload: ${uploadForm.attachment}" forms: uploadForm: - attachment: label: Attachment type: file ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/fileRetryForm/concord.yml ================================================ flows: default: - form: uploadForm - log: "after upload retry: ${uploadForm.age}" forms: uploadForm: - attachment: label: Attachment type: file - age: label: Age type: int min: 21 ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/form/concord.yml ================================================ flows: default: - log: "before form" - form: myForm - log: "after form: ${myForm.name}, ${myForm.age}" forms: myForm: - name: label: Name type: string - age: label: Age type: int ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/lintV1/concord/extra.concord.yml ================================================ flows: extraFlow: - log: "extra" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/lintV1/concord.yml ================================================ flows: default: - log: "Hello, world!" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/lintV2/concord/extra.concord.yml ================================================ flows: extraFlow: - log: "extra" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/lintV2/concord.yml ================================================ configuration: runtime: concord-v2 flows: default: - log: "Hello, world!" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/mixedFormEvent/concord.yml ================================================ flows: default: - log: "before mixed suspend" - parallel: - block: - form: approvalForm - block: - suspend: ev_timeout out: - approvalForm - log: "after mixed suspend: ${approvalForm.decision}" forms: approvalForm: - decision: label: Decision type: string ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/multiContextConfig.yaml ================================================ contexts: default: secrets: vault: id: "foo" dir: "qux" another: secrets: vault: id: "bar" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/parallelForms/concord.yml ================================================ flows: default: - log: "parallel forms: before" - parallel: - block: - form: form1 - log: "parallel forms: form1=${form1.value}" - block: - form: form2 - log: "parallel forms: form2=${form2.value}" out: - form1 - form2 - log: "parallel forms: done one=${form1.value} two=${form2.value}" forms: form1: - value: { label: "Form One", type: "string" } form2: - value: { label: "Form Two", type: "string" } ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/passwordRetry/concord.yml ================================================ flows: default: - form: secureForm - log: "after password form: ${secureForm.age}" forms: secureForm: - password: { label: "Password", type: "string", inputType: "password", value: "seed-secret" } - age: { label: "Age", type: "int", min: 21 } ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/passwordSuspend/concord.yml ================================================ flows: default: - form: secureForm - suspend: ev1 - log: "after resume: ${secureForm.age}" forms: secureForm: - password: { label: "Password", type: "string", inputType: "password" } - age: { label: "Age", type: "int", min: 21 } ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/processProjectInfo/concord.yml ================================================ configuration: debug: true flows: default: - log: "processInfo: ${processInfo}" - log: "projectInfo: ${projectInfo}" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/profileDeps/concord.yml ================================================ configuration: runtime: "concord-v2" profiles: test: configuration: dependencies: - "mvn://com.walmartlabs.concord.plugins.basic:file-tasks:1.96.0" flows: default: - log: "exists=${files.exists('concord.yml')}" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/resourceTask/concord.yml ================================================ configuration: runtime: "concord-v2" arguments: msg: k: v flows: default: - log: ${resource.asString(resource.writeAsJson(msg))} ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/secretResume/concord.yml ================================================ flows: default: - suspend: ev1 - log: "after resume secret: ${crypto.exportAsString('Default', 'resumeSecret', null)}" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/simple/concord.yml ================================================ configuration: debug: true flows: default: - log: "Hello, ${name}" - expr: "${sleep.ms(10)}" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/suspend/concord.yml ================================================ flows: default: - log: "before suspend" - suspend: ev1 - log: "after resume: ${myForm.value}" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/testConfig.yaml ================================================ contexts: default: secrets: vault: dir: "/my-vault" id: "foo" local: dir: "/my-secrets" remote: enabled: true baseUrl: "http://localhost:8001" apiKey: "foobar" ================================================ FILE: cli/src/test/resources/com/walmartlabs/concord/cli/validatedForm/concord.yml ================================================ flows: default: - form: validatedForm - log: "validated form: name=${validatedForm.name}, age=${validatedForm.age}, choice=${validatedForm.choice}, note=${validatedForm.note}, readOnly=${validatedForm.readOnlyValue}" forms: validatedForm: - name: label: Name type: string - age: label: Age type: int min: 21 max: 65 - choice: label: Choice type: string allow: - "red" - "blue" - note: label: Note type: string value: default-note - readOnlyValue: label: Read Only type: string value: locked readOnly: true ================================================ FILE: cli/src/test/resources/logback-test.xml ================================================ false %d{HH:mm:ss.SSS} [%thread] %msg%n ================================================ FILE: client2/README.md ================================================ # concord-client A Concord API client based on Swagger codegen. Build `server/impl` first to generate Swagger spec. ================================================ FILE: client2/pom.xml ================================================ 4.0.0 com.walmartlabs.concord parent 2.40.1-SNAPSHOT ../pom.xml concord-client2 jar ${project.groupId}:${project.artifactId} com.walmartlabs.concord.server concord-server provided org.slf4j slf4j-api com.google.code.findbugs jsr305 com.fasterxml.jackson.core jackson-core com.fasterxml.jackson.core jackson-annotations com.fasterxml.jackson.core jackson-databind com.fasterxml.jackson.datatype jackson-datatype-jsr310 org.immutables value provided org.junit.jupiter junit-jupiter-api test org.wiremock wiremock-jetty12 test false ${project.basedir}/src/main/resources **/* true ${project.basedir}/src/main/filtered-resources **/* org.codehaus.mojo cobertura-maven-plugin org.openapitools openapi-generator-maven-plugin generate ${project.basedir}/../server/impl/target/classes/com/walmartlabs/concord/server/swagger/swagger.yaml java com.walmartlabs.concord.client2 com.walmartlabs.concord.client2 com.walmartlabs.concord.client2 com.walmartlabs.concord.client2 src/gen/java/main java8 true false false false native false false false false true ApiClient.java,ApiResponse.java,ApiException.java,Pair.java ${project.basedir}/src/main/template true string+binary=InputStream InputStream=java.io.InputStream org.revapi revapi-maven-plugin true ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/ApiClientConfiguration.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.immutables.value.Value; import javax.annotation.Nullable; @Value.Immutable @Value.Style(jdkOnly = true) public interface ApiClientConfiguration { /** * Base URL of the API, e.g. {@code http://localhost:8001} */ @Nullable String baseUrl(); /** * The process' session token. */ @Nullable String sessionToken(); /** * The user's API key. If set then the session token will be ignored. */ @Nullable String apiKey(); static ImmutableApiClientConfiguration.Builder builder() { return ImmutableApiClientConfiguration.builder(); } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/ApiClientFactory.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public interface ApiClientFactory { ApiClient create(ApiClientConfiguration cfg); } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/ClientUtils.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; public final class ClientUtils { private static final Logger log = LoggerFactory.getLogger(ClientUtils.class); public static T withRetry(int retryCount, long retryInterval, Callable c) throws ApiException { Exception exception = null; int tryCount = 0; while (!Thread.currentThread().isInterrupted() && tryCount < retryCount + 1) { try { return c.call(); } catch (ApiException e) { exception = e; if (e.getCode() >= 400 && e.getCode() < 500) { break; } log.warn("call error: '{}'", getErrorMessage(e)); } catch (Exception e) { exception = e; log.error("call error", e); } log.info("retry after {} sec", retryInterval / 1000); sleep(retryInterval); tryCount++; } if (exception instanceof ApiException) { throw (ApiException) exception; } throw new ApiException(exception); } /** * Returns a value of the specified header. * Only the first value is returned. * The header's {@code name} is case-insensitive. */ public static String getHeader(String name, ApiResponse resp) { Map> headers = resp.getHeaders(); if (headers == null) { return null; } for (Map.Entry> e : headers.entrySet()) { if (!e.getKey().equalsIgnoreCase(name)) { continue; } List values = e.getValue(); if (values == null || values.isEmpty()) { return null; } return values.get(0); } return null; } private static void sleep(long t) { try { Thread.sleep(t); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private static String getErrorMessage(ApiException e) { String error = e.getMessage(); if (e.getResponseBody() != null && !e.getResponseBody().isEmpty()) { error += ": " + e.getResponseBody(); } return error; } private ClientUtils() { } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/ConcordApiClient.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.net.http.HttpClient; public class ConcordApiClient extends ApiClient { public ConcordApiClient(HttpClient httpClient) { super(httpClient); } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/CreateSecretRequest.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.client2.ImmutableCreateSecretRequest; import com.walmartlabs.concord.client2.ImmutableKeyPair; import com.walmartlabs.concord.client2.ImmutableUsernamePassword; import org.immutables.value.Value; import javax.annotation.Nullable; import java.nio.file.Path; import java.util.List; import java.util.UUID; @Value.Immutable @Value.Style(jdkOnly = true) public interface CreateSecretRequest { String org(); String name(); @Value.Default default boolean generatePassword() { return false; } @Nullable String storePassword(); @Nullable SecretEntryV2.VisibilityEnum visibility(); @Nullable List projectNames(); @Nullable List projectIds(); @Nullable byte[] data(); @Nullable KeyPair keyPair(); @Nullable UsernamePassword usernamePassword(); static ImmutableCreateSecretRequest.Builder builder() { return ImmutableCreateSecretRequest.builder(); } @Value.Immutable @Value.Style(jdkOnly = true) interface KeyPair { long serialVersionUID = 1L; Path privateKey(); Path publicKey(); static ImmutableKeyPair.Builder builder() { return ImmutableKeyPair.builder(); } } @Value.Immutable @Value.Style(jdkOnly = true) interface UsernamePassword { long serialVersionUID = 1L; String username(); String password(); static UsernamePassword of(String username, String password) { return ImmutableUsernamePassword.builder() .username(username) .password(password) .build(); } } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/DefaultApiClientFactory.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.net.http.HttpClient; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.time.Duration; public class DefaultApiClientFactory { private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(30); private final HttpClient httpClient; private final String defaultApiUrl; public DefaultApiClientFactory(String defaultApiUrl) { this(defaultApiUrl, null, true); } public DefaultApiClientFactory(String defaultApiUrl, Duration connectTimeout) { this(defaultApiUrl, connectTimeout, true); } public DefaultApiClientFactory(String defaultApiUrl, Duration connectTimeout, boolean verifySsl) { this.defaultApiUrl = defaultApiUrl; this.httpClient = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .connectTimeout(connectTimeout != null ? connectTimeout : DEFAULT_CONNECT_TIMEOUT) .sslContext(sslContext(verifySsl)) .build(); } public ApiClient create() { return new ConcordApiClient(httpClient) .setBaseUrl(defaultApiUrl); } public ApiClient create(ApiClientConfiguration overrides) { String baseUrl = overrides.baseUrl() != null ? overrides.baseUrl() : defaultApiUrl; String sessionToken = null; if (overrides.apiKey() == null) { sessionToken = overrides.sessionToken(); } String apiKey = overrides.apiKey(); if (apiKey != null) { sessionToken = null; } if (sessionToken == null && apiKey == null) { throw new IllegalArgumentException("Session token or an API key is required"); } return new ConcordApiClient(httpClient) .setBaseUrl(baseUrl) .setSessionToken(sessionToken) .setApiKey(apiKey) .addDefaultHeader("Accept", "*/*"); } private static SSLContext sslContext(boolean verifySsl) { try { SSLContext sslContext = SSLContext.getInstance("TLS"); if (verifySsl) { sslContext.init(null, null, null); } else { TrustManager trustAll = new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { } @Override public X509Certificate[] getAcceptedIssuers() { return null; } }; TrustManager[] trustManagers = new TrustManager[]{trustAll}; System.getProperties().setProperty("jdk.internal.httpclient.disableHostnameVerification", Boolean.TRUE.toString()); sslContext.init(null, trustManagers, new SecureRandom()); } return sslContext; } catch (Exception e) { throw new RuntimeException(e); } } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/ProcessDataInclude.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public enum ProcessDataInclude { CHECKPOINTS ("checkpoints"), CHECKPOINTS_HISTORY ("checkpointsHistory"), CHILDREN_IDS ("childrenIds"), STATUS_HISTORY ("history"); private final String value; ProcessDataInclude(String value) { this.value = value; } public String getValue() { return value; } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/ProcessListFilter.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.client2.ImmutableProcessListFilter; import com.walmartlabs.concord.client2.ProcessDataInclude; import org.immutables.value.Value; import javax.annotation.Nullable; import java.time.OffsetDateTime; import java.util.Map; import java.util.Set; import java.util.UUID; @Value.Immutable @Value.Style(jdkOnly = true) public interface ProcessListFilter { @Nullable UUID orgId(); @Nullable String orgName(); @Nullable UUID projectId(); @Nullable String projectName(); @Nullable UUID repoId(); @Nullable String repoName(); @Nullable OffsetDateTimeParam afterCreatedAt(); @Nullable OffsetDateTimeParam beforeCreatedAt(); @Nullable Set tags(); @Nullable String status(); @Nullable String initiator(); @Nullable UUID parentInstanceId(); @Nullable Set include(); @Nullable Integer limit(); @Nullable Integer offset(); @Nullable Map meta(); class Builder extends ImmutableProcessListFilter.Builder { public Builder status(ProcessEntry.StatusEnum status) { return status(status.getValue()); } public Builder addInclude(ProcessDataInclude... elements) { for (ProcessDataInclude e : elements) { addInclude(e.getValue()); } return this; } public Builder afterCreatedAt(OffsetDateTime afterCreatedAt) { if (afterCreatedAt == null) { return this; } afterCreatedAt(new OffsetDateTimeParam().value(afterCreatedAt)); return this; } } static ImmutableProcessListFilter.Builder builder() { return new Builder(); } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/ProcessUtils.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.client2.ProcessEntry.StatusEnum; public class ProcessUtils { public static boolean isFinal(StatusEnum s) { return s == StatusEnum.FINISHED || s == StatusEnum.FAILED || s == StatusEnum.CANCELLED || s == StatusEnum.SUSPENDED || s == StatusEnum.TIMED_OUT; } private ProcessUtils() { } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/SecretClient.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.secret.BinaryDataSecret; import com.walmartlabs.concord.common.secret.KeyPair; import com.walmartlabs.concord.common.secret.UsernamePassword; import com.walmartlabs.concord.sdk.Constants; import com.walmartlabs.concord.sdk.Secret; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; public class SecretClient { private static final int DEFAULT_RETRY_COUNT = 3; private static final long DEFAULT_RETRY_INTERVAL = 5000; private final ApiClient apiClient; private final int retryCount; private final long retryInterval; public SecretClient(ApiClient apiClient) { this(apiClient, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_INTERVAL); } public SecretClient(ApiClient apiClient, int retryCount, long retryInterval) { this.apiClient = apiClient; this.retryCount = retryCount; this.retryInterval = retryInterval; } /** * Fetches a decrypted Concord secret from the server. */ public T getData(String orgName, String secretName, String password, SecretEntryV2.TypeEnum expectedType) throws Exception { SecretsApi.GetSecretDataRequest req = new SecretsApi.GetSecretDataRequest() .storePassword(password); SecretsApi api = new SecretsApi(apiClient); ApiResponse r = null; try { r = ClientUtils.withRetry(retryCount, retryInterval, () -> api.getSecretDataWithHttpInfo(orgName, secretName, req.asMap())); if (r.getData() == null) { throw new SecretNotFoundException(orgName, secretName); } String secretType = ClientUtils.getHeader(Constants.Headers.SECRET_TYPE, r); if (secretType == null) { throw new IllegalStateException("Can't determine the secret's expectedType. Server response: code=" + r.getStatusCode()); } SecretEntryV2.TypeEnum actualSecretType = SecretEntryV2.TypeEnum.valueOf(secretType); if (expectedType != null && expectedType != actualSecretType) { String msg = "Unexpected type of %s/%s. Expected %s, got %s. " + "Check the secret's expectedType and its usage - some secrets can only be used for specific purposes " + "(e.g. %s is typically used for key-based authentication)."; throw new IllegalArgumentException(String.format(msg, orgName, secretName, expectedType, actualSecretType, SecretEntryV2.TypeEnum.KEY_PAIR)); } try (InputStream is = r.getData()) { return readSecret(actualSecretType, is.readAllBytes()); } } catch (ApiException e) { if (e.getCode() == 404) { throw new SecretNotFoundException(orgName, secretName); } throw e; } finally { if (r != null && r.getData() != null) { r.getData().close(); } } } /** * Decrypt the provided string using the project's key. */ public byte[] decryptString(UUID instanceId, byte[] input) throws Exception { ProcessApi api = new ProcessApi(apiClient); return ClientUtils.withRetry(retryCount, retryInterval, () -> api.decryptString(instanceId, input)); } /** * Encrypts the provided string using the project's key. */ public String encryptString(String orgName, String projectName, String input) throws Exception { ProjectsApi api = new ProjectsApi(apiClient); EncryptValueResponse r = ClientUtils.withRetry(retryCount, retryInterval, () -> api.encrypt(orgName, projectName, input)); return r.getData(); } /** * Creates a new Concord secret. */ public SecretOperationResponse createSecret(CreateSecretRequest secretRequest) throws ApiException { String path = "/api/v1/org/" + secretRequest.org() + "/secret"; Map params = new HashMap<>(); params.put(Constants.Multipart.NAME, secretRequest.name()); params.put(Constants.Multipart.GENERATE_PASSWORD, secretRequest.generatePassword()); if (secretRequest.storePassword() != null) { params.put(Constants.Multipart.STORE_PASSWORD, secretRequest.storePassword()); } SecretEntryV2.VisibilityEnum visibility = secretRequest.visibility(); if (visibility != null) { params.put(Constants.Multipart.VISIBILITY, visibility.getValue()); } if (secretRequest.projectIds() != null) { params.put(Constants.Multipart.PROJECT_IDS, secretRequest.projectIds().stream().map(UUID::toString).collect(Collectors.joining(","))); } else if (secretRequest.projectNames() != null) { params.put(Constants.Multipart.PROJECT_NAMES, String.join(",", secretRequest.projectNames())); } byte[] data = secretRequest.data(); CreateSecretRequest.KeyPair keyPair = secretRequest.keyPair(); CreateSecretRequest.UsernamePassword usernamePassword = secretRequest.usernamePassword(); if (data != null) { params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.DATA.getValue()); params.put(Constants.Multipart.DATA, data); } else if (keyPair != null) { params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.KEY_PAIR.getValue()); params.put(Constants.Multipart.PUBLIC, readFile(keyPair.publicKey())); params.put(Constants.Multipart.PRIVATE, readFile(keyPair.privateKey())); } else if (usernamePassword != null) { params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.USERNAME_PASSWORD.getValue()); params.put(Constants.Multipart.USERNAME, usernamePassword.username()); params.put(Constants.Multipart.PASSWORD, usernamePassword.password()); } else { throw new IllegalArgumentException("Secret data, a key pair or username/password must be specified."); } SecretsApi api = new SecretsApi(apiClient); SecretOperationResponse response = ClientUtils.withRetry(retryCount, retryInterval, () -> api.createSecret(secretRequest.org(), params)); return response; } public void updateSecret(String orgName, String secretName, UpdateSecretRequest request) throws ApiException { String path = "/api/v2/org/" + orgName + "/secret/" + secretName; Map params = new HashMap<>(); params.put(Constants.Multipart.ORG_ID, request.newOrgId()); params.put(Constants.Multipart.ORG_NAME, request.newOrgName()); params.put("removeProjectLink", request.removeProjectLink()); params.put("ownerId", request.newOwnerId()); params.put(Constants.Multipart.STORE_PASSWORD, request.currentPassword()); params.put("newStorePassword", request.newPassword()); params.put(Constants.Multipart.NAME, request.newName()); params.put(Constants.Multipart.VISIBILITY, request.newVisibility()); if (request.newProjectIds() != null) { params.put(Constants.Multipart.PROJECT_IDS, request.newProjectIds().stream().map(UUID::toString).collect(Collectors.joining(","))); } else if (request.newProjectNames() != null) { params.put(Constants.Multipart.PROJECT_NAMES, String.join(",", request.newProjectNames())); } byte[] data = request.data(); CreateSecretRequest.KeyPair keyPair = request.keyPair(); CreateSecretRequest.UsernamePassword usernamePassword = request.usernamePassword(); if (data != null) { params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.DATA.getValue()); params.put(Constants.Multipart.DATA, data); } else if (keyPair != null) { params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.KEY_PAIR.getValue()); params.put(Constants.Multipart.PUBLIC, readFile(keyPair.publicKey())); params.put(Constants.Multipart.PRIVATE, readFile(keyPair.privateKey())); } else if (usernamePassword != null) { params.put(Constants.Multipart.TYPE, SecretEntryV2.TypeEnum.USERNAME_PASSWORD.getValue()); params.put(Constants.Multipart.USERNAME, usernamePassword.username()); params.put(Constants.Multipart.PASSWORD, usernamePassword.password()); } params.values().removeIf(Objects::isNull); SecretsV2Api api = new SecretsV2Api(apiClient); ClientUtils.withRetry(retryCount, retryInterval, () -> api.updateSecret(orgName, secretName, params)); } private static byte[] readFile(Path file) { if (file == null) { return null; } if (Files.notExists(file)) { throw new IllegalArgumentException("File '" + file + "' not found"); } try { return Files.readAllBytes(file); } catch (IOException e) { throw new RuntimeException("Error while reading " + file + ": " + e.getMessage()); } } @SuppressWarnings("unchecked") private static T readSecret(SecretEntryV2.TypeEnum type, byte[] bytes) { switch (type) { case DATA: return (T) new BinaryDataSecret(bytes); case KEY_PAIR: return (T) KeyPair.deserialize(bytes); case USERNAME_PASSWORD: return (T) UsernamePassword.deserialize(bytes); default: throw new IllegalArgumentException("unknown secret type: " + type); } } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/SecretNotFoundException.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2021 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public class SecretNotFoundException extends IllegalArgumentException { private static final long serialVersionUID = 1L; private final String orgName; private final String secretName; public SecretNotFoundException(String orgName, String secretName) { super("Secret not found: " + orgName + "/" + secretName); this.orgName = orgName; this.secretName = secretName; } public String getOrgName() { return orgName; } public String getSecretName() { return secretName; } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/UpdateSecretRequest.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.immutables.value.Value; import javax.annotation.Nullable; import java.util.List; import java.util.UUID; @Value.Immutable @Value.Style(jdkOnly = true) public interface UpdateSecretRequest { @Nullable UUID newOrgId(); @Nullable String newOrgName(); @Nullable List newProjectNames(); @Nullable List newProjectIds(); @Value.Default default boolean removeProjectLink() { return false; } @Nullable UUID newOwnerId(); @Nullable String currentPassword(); @Nullable String newPassword(); @Nullable String newName(); @Nullable SecretEntryV2.VisibilityEnum newVisibility(); @Nullable byte[] data(); @Nullable CreateSecretRequest.KeyPair keyPair(); @Nullable CreateSecretRequest.UsernamePassword usernamePassword(); static ImmutableUpdateSecretRequest.Builder builder() { return ImmutableUpdateSecretRequest.builder(); } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/ByteArrayBuffer.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public class ByteArrayBuffer { private byte[] array; private int len; public ByteArrayBuffer(int capacity) { super(); this.array = new byte[capacity]; } public void append(byte[] b) { append(b, 0, b.length); } public void append(byte[] b, int off, int len) { if (b == null) { return; } if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) < 0) || ((off + len) > b.length)) { throw new IndexOutOfBoundsException("off: "+off+" len: "+len+" b.length: "+b.length); } if (len == 0) { return; } int newlen = this.len + len; if (newlen > this.array.length) { expand(newlen); } System.arraycopy(b, off, this.array, this.len, len); this.len = newlen; } private void expand(int newlen) { byte[] newArray = new byte[Math.max(this.array.length << 1, newlen)]; System.arraycopy(this.array, 0, newArray, 0, this.len); this.array = newArray; } public byte[] array() { return this.array; } public byte[] toByteArray() { final byte[] b = new byte[this.len]; if (this.len > 0) { System.arraycopy(this.array, 0, b, 0, this.len); } return b; } public int length() { return this.len; } public void clear() { this.len = 0; } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/ContentType.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; public class ContentType { public static final ContentType APPLICATION_JSON = create( "application/json", StandardCharsets.UTF_8); public static final ContentType APPLICATION_OCTET_STREAM = create( "application/octet-stream"); public static final ContentType TEXT_PLAIN = create("text/plain"); public static final ContentType MULTIPART_FORM = create("multipart/form-data"); public static ContentType create(String mimeType) { return create(mimeType, null); } public static ContentType create(String mimeType, Charset charset) { String normalizedMimeType = mimeType.toLowerCase(); return new ContentType(normalizedMimeType, charset); } private final String mimeType; private final Charset charset; private final List params; public ContentType(String mimeType, Charset charset) { this(mimeType, charset, null); } public ContentType(String mimeType, Charset charset, List params) { this.mimeType = mimeType; this.charset = charset; this.params = params; } public ContentType withCharset(Charset charset) { return create(getMimeType(), charset); } public ContentType withParameters(List params) { return new ContentType(getMimeType(), charset, params); } public String getMimeType() { return mimeType; } public Charset getCharset() { return charset; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(this.mimeType); if (this.params != null) { buf.append("; "); formatParameters(buf, this.params); } else if (this.charset != null) { buf.append("; charset="); buf.append(this.charset.name().toLowerCase()); } return buf.toString(); } private static void formatParameters(StringBuilder buf, List params) { String s = params.stream() .map(nvp -> nvp.getName() + "=" + nvp.getValue()) .collect(Collectors.joining("; ")); buf.append(s); } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/Headers.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.Collections; import java.util.List; public class Headers { private final List items; public static Headers of(String name, String value) { return new Headers(Collections.singletonList(new NameValuePair(name, value))); } public Headers(List items) { this.items = items; } public String get(String name) { return items.stream() .filter(nvp -> nvp.getName().equalsIgnoreCase(name)) .map(NameValuePair::getValue) .findFirst() .orElse(null); } public int size() { return items.size(); } public String name(int index) { return items.get(index).getName(); } public String value(int index) { return items.get(index).getValue(); } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/HttpEntity.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.IOException; import java.io.InputStream; public interface HttpEntity { ContentType contentType(); long contentLength() throws IOException; InputStream getContent() throws IOException; } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/MultipartBuilder.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.SequenceInputStream; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.*; public class MultipartBuilder { private static final byte[] COLONSPACE = {':', ' '}; private static final byte[] CRLF = {'\r', '\n'}; private static final byte[] DASHDASH = {'-', '-'}; private final List partHeaders = new ArrayList<>(); private final List partBodies = new ArrayList<>(); private final ContentType type = ContentType.MULTIPART_FORM; private final String boundary; public MultipartBuilder() { this(UUID.randomUUID().toString()); } public MultipartBuilder(String boundary) { this.boundary = boundary; } public MultipartBuilder addFormDataPart(String name, String value) { return addFormDataPart(name, null, RequestBody.create(null, value)); } public MultipartBuilder addFormDataPart(String name, String filename, RequestBody value) { Objects.requireNonNull(name, "name"); StringBuilder disposition = new StringBuilder("form-data; name="); appendQuotedString(disposition, name); if (filename != null) { disposition.append("; filename="); appendQuotedString(disposition, filename); } return addPart(Headers.of("Content-Disposition", disposition.toString()), value); } public MultipartBuilder addPart(Headers headers, RequestBody body) { if (body == null) { throw new NullPointerException("body == null"); } if (headers != null && headers.get("Content-Type") != null) { throw new IllegalArgumentException("Unexpected header: Content-Type"); } if (headers != null && headers.get("Content-Length") != null) { throw new IllegalArgumentException("Unexpected header: Content-Length"); } partHeaders.add(headers); partBodies.add(body); return this; } public RequestBody build() { return new MultipartRequestBody(type, boundary, partHeaders, partBodies); } private static void appendQuotedString(StringBuilder target, String key) { target.append('"'); for (int i = 0, len = key.length(); i < len; i++) { char ch = key.charAt(i); switch (ch) { case '\n': target.append("%0A"); break; case '\r': target.append("%0D"); break; case '"': target.append("%22"); break; default: target.append(ch); break; } } target.append('"'); } private static final class MultipartRequestBody extends RequestBody { private final String boundary; private final ContentType contentType; private final List partHeaders; private final List partBodies; public MultipartRequestBody(ContentType type, String boundary, List partHeaders, List partBodies) { Objects.requireNonNull(type, "type"); this.boundary = boundary; this.contentType = type.withParameters(Collections.singletonList(new NameValuePair("boundary", boundary))); this.partHeaders = partHeaders; this.partBodies = partBodies; } @Override public ContentType contentType() { return contentType; } @Override public long contentLength() { return -1; } @Override public InputStream getContent() throws IOException { SequenceInputStreamBuilder result = new SequenceInputStreamBuilder(); try { write(result); return result.build(); } catch (Exception e) { result.close(); throw e; } } private void write(SequenceInputStreamBuilder result) throws IOException { ByteArrayBuffer boundaryEncoded = encode(StandardCharsets.US_ASCII, this.boundary); for (int p = 0, partCount = partHeaders.size(); p < partCount; p++) { Headers headers = partHeaders.get(p); RequestBody body = partBodies.get(p); result.write(DASHDASH); result.write(boundaryEncoded); result.write(CRLF); if (headers != null) { for (int h = 0, headerCount = headers.size(); h < headerCount; h++) { writeHeader(headers.name(h), headers.value(h), result); } } ContentType contentType = body.contentType(); if (contentType != null) { writeHeader("Content-Type", contentType.toString(), result); } long contentLength = body.contentLength(); if (contentLength != -1) { writeHeader("Content-Length", String.valueOf(contentLength), result); } result.write(CRLF); result.write(body.getContent()); result.write(CRLF); } result.write(DASHDASH); result.write(boundaryEncoded); result.write(DASHDASH); result.write(CRLF); } private void writeHeader(String name, String value, SequenceInputStreamBuilder out) throws IOException { out.write(encodeHeader(name)); out.write(COLONSPACE); out.write(encodeHeader(value)); out.write(CRLF); } private static ByteArrayBuffer encode(Charset charset, String string) { ByteBuffer encoded = charset.encode(CharBuffer.wrap(string)); ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining()); bab.append(encoded.array(), encoded.arrayOffset() + encoded.position(), encoded.remaining()); return bab; } private static ByteArrayBuffer encodeHeader(String value) { return encode(StandardCharsets.ISO_8859_1, value); } } static class SequenceInputStreamBuilder { private final Vector streams = new Vector<>(); private final ByteArrayBuffer currentBuffer = new ByteArrayBuffer(1024); public void write(byte[] buff) { currentBuffer.append(buff); } public void write(ByteArrayBuffer buff) { currentBuffer.append(buff.array(), 0, buff.length()); } public void write(InputStream stream) { flushCurrentBuffer(); streams.add(stream); } public void close() throws IOException { IOException ioe = null; for (InputStream in : streams) { try { in.close(); } catch (IOException e) { if (ioe == null) { ioe = e; } else { ioe.addSuppressed(e); } } } if (ioe != null) { throw ioe; } } public InputStream build() { flushCurrentBuffer(); return new SequenceInputStream(streams.elements()); } private void flushCurrentBuffer() { if (currentBuffer.length() > 0) { streams.add(new ByteArrayInputStream(currentBuffer.toByteArray(), 0, currentBuffer.length())); currentBuffer.clear(); } } } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/MultipartRequestBodyHandler.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; import java.util.Map; import java.util.UUID; public final class MultipartRequestBodyHandler { public static HttpEntity handle(ObjectMapper objectMapper, Map data) { return handle(new MultipartBuilder(), objectMapper, data); } @SuppressWarnings("unchecked") public static HttpEntity handle(MultipartBuilder b, ObjectMapper objectMapper, Map data) { for (Map.Entry e : data.entrySet()) { String k = e.getKey(); Object v = e.getValue(); if (v instanceof InputStream) { b.addFormDataPart(k, null, new InputStreamRequestBody((InputStream) v)); } else if (v instanceof byte[]) { b.addFormDataPart(k, null, RequestBody.create(ContentType.APPLICATION_OCTET_STREAM, (byte[]) v)); } else if (v instanceof String) { b.addFormDataPart(k, (String) v); } else if (v instanceof Path) { b.addFormDataPart(k, null, new PathRequestBody((Path) v)); } else if (v instanceof Map) { String json; try { json = objectMapper.writeValueAsString(v); } catch (JsonProcessingException ex) { throw new RuntimeException(ex); } b.addFormDataPart(k, null, RequestBody.create(ContentType.APPLICATION_JSON, json)); } else if (v instanceof Boolean) { b.addFormDataPart(k, null, RequestBody.create(ContentType.TEXT_PLAIN, v.toString())); } else if (v instanceof String[]) { b.addFormDataPart(k, null, RequestBody.create(ContentType.TEXT_PLAIN, String.join(",", (String[]) v))); } else if (v instanceof Collection) { b.addFormDataPart(k, null, RequestBody.create(ContentType.TEXT_PLAIN, String.join(",", (Collection) v))); } else if (v instanceof UUID) { b.addFormDataPart(k, v.toString()); } else if (v instanceof Enum) { b.addFormDataPart(k, ((Enum)v).name()); } else { throw new IllegalArgumentException("Unknown input type: " + k + "=" + v + (v != null ? " (" + v.getClass() + ")" : "")); } } return b.build(); } private MultipartRequestBodyHandler() { } public static final class InputStreamRequestBody extends RequestBody { private final InputStream in; public InputStreamRequestBody(InputStream in) { this.in = in; } @Override public ContentType contentType() { return ContentType.APPLICATION_OCTET_STREAM; } @Override public long contentLength() { return -1; } @Override public InputStream getContent() { return in; } } public static class PathRequestBody extends RequestBody { private final Path path; public PathRequestBody(Path path) { this.path = path; } @Override public ContentType contentType() { return ContentType.APPLICATION_OCTET_STREAM; } @Override public long contentLength() throws IOException { return Files.size(path); } @Override public InputStream getContent() throws IOException { return Files.newInputStream(this.path); } } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/NameValuePair.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.Serializable; import java.util.Objects; public class NameValuePair implements Serializable { private static final long serialVersionUID = 1L; private final String name; private final String value; public NameValuePair(final String name, final String value) { this.name = name; this.value = value; } public String getName() { return this.name; } public String getValue() { return this.value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; NameValuePair that = (NameValuePair) o; return Objects.equals(name, that.name) && Objects.equals(value, that.value); } @Override public int hashCode() { return Objects.hash(name, value); } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/OffsetDateTimeDeserializer.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer; import java.time.Instant; import java.time.OffsetDateTime; public class OffsetDateTimeDeserializer extends InstantDeserializer { public OffsetDateTimeDeserializer() { super( OffsetDateTime.class, OffsetDateTimeSerializer.FORMATTER, OffsetDateTime::from, a -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId), a -> OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId), (d, z) -> (d.isEqual(OffsetDateTime.MIN) || d.isEqual(OffsetDateTime.MAX) ? d : d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime()))), false ); } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/OffsetDateTimeSerializer.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; public class OffsetDateTimeSerializer extends JsonSerializer { public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"); @Override public void serialize(OffsetDateTime value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { if (value == null) { throw new IOException("OffsetDateTime argument is null."); } jsonGenerator.writeString(FORMATTER.format(value)); } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/RequestBody.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Objects; public abstract class RequestBody implements HttpEntity { public static RequestBody create(ContentType contentType, String content) { Charset charset = StandardCharsets.UTF_8; if (contentType != null) { charset = contentType.getCharset(); if (charset == null) { charset = StandardCharsets.UTF_8; contentType = contentType.withCharset(charset); } } byte[] bytes = content.getBytes(charset); return create(contentType, bytes); } public static RequestBody create(ContentType contentType, byte[] content) { return create(contentType, content, 0, content.length); } public static RequestBody create(ContentType contentType, byte[] content, int offset, int byteCount) { Objects.requireNonNull(content, "content"); return new RequestBody() { @Override public ContentType contentType() { return contentType; } @Override public long contentLength() { return byteCount; } @Override public InputStream getContent() { return new ByteArrayInputStream(content, offset, byteCount); } }; } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/RequestBodyHandler.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; import java.net.http.HttpRequest; public final class RequestBodyHandler { public static HttpRequest.BodyPublisher handle(ObjectMapper ignoredObjectMapper, byte[] param) throws IOException { return HttpRequest.BodyPublishers.ofByteArray(param); } public static HttpRequest.BodyPublisher handle(ObjectMapper ignoredObjectMapper, InputStream param) throws IOException { return HttpRequest.BodyPublishers.ofInputStream(() -> param); } public static HttpRequest.BodyPublisher handle(ObjectMapper objectMapper, Object param) throws IOException { if (param instanceof String) { return HttpRequest.BodyPublishers.ofString((String) param); } byte[] localVarPostBody = objectMapper.writeValueAsBytes(param); return HttpRequest.BodyPublishers.ofByteArray(localVarPostBody); } private RequestBodyHandler() { } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/ResponseBodyHandler.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.walmartlabs.concord.client2.ApiException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; import java.net.http.HttpResponse; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class ResponseBodyHandler { @SuppressWarnings("unchecked") public static T handle(ObjectMapper objectMapper, HttpResponse response, TypeReference returnTypeRef) throws IOException, ApiException { if (response == null) { return null; } Type returnType = returnTypeRef.getType(); InputStream is = response.body(); if (is == null) { return null; } try { if (returnType.equals(byte[].class)) { return (T)is.readAllBytes(); } else if (returnType.equals(InputStream.class)) { return (T)is; } String contentType = response.headers().firstValue("Content-Type").orElse("application/json"); if (isJsonMime(contentType)) { return objectMapper.readValue(is, returnTypeRef); } else if (returnType.equals(String.class)) { return (T) toString(is, charset(response)); } else { throw new ApiException( "Content type \"" + contentType + "\" is not supported for type: " + returnType, response.statusCode(), response.headers(), "skipped"); } } finally { if (!returnType.equals(InputStream.class)) { is.close(); } } } private static boolean isJsonMime(String mime) { String jsonMime = "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$"; return mime != null && (mime.matches(jsonMime) || mime.equals("*/*")); } private static String toString(InputStream input, Charset charset) throws IOException { return new String(input.readAllBytes(), charset); } private static Charset charset(HttpResponse response) { String contentType = response.headers().firstValue("Content-Type").orElse(null); if (contentType == null) { return StandardCharsets.UTF_8; } return parseCharset(contentType, StandardCharsets.UTF_8); } // TODO: super simple private static Charset parseCharset(String contentTypeHeader, Charset defaultCharset) { Pattern pattern = Pattern.compile("charset=([\\w-]+)", Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(contentTypeHeader); if (matcher.find()) { return Charset.forName(matcher.group(1)); } return defaultCharset; } private ResponseBodyHandler() { } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/auth/ApiKey.java ================================================ package com.walmartlabs.concord.client2.impl.auth; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.net.http.HttpRequest; public class ApiKey implements Authentication { private final String key; public ApiKey(String key) { this.key = key; } @Override public HttpRequest.Builder applyTo(HttpRequest.Builder requesBuilder) { return requesBuilder.setHeader("Authorization", key); } } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/auth/Authentication.java ================================================ package com.walmartlabs.concord.client2.impl.auth; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.net.http.HttpRequest; public interface Authentication { HttpRequest.Builder applyTo(HttpRequest.Builder requesBuilder); } ================================================ FILE: client2/src/main/java/com/walmartlabs/concord/client2/impl/auth/SessionToken.java ================================================ package com.walmartlabs.concord.client2.impl.auth; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.net.http.HttpRequest; public class SessionToken implements Authentication { private final String token; public SessionToken(String token) { this.token = token; } @Override public HttpRequest.Builder applyTo(HttpRequest.Builder requesBuilder) { return requesBuilder.setHeader("X-Concord-SessionToken", token); } } ================================================ FILE: client2/src/main/template/README.md ================================================ Check the diff between `*.mustache` and `*.orig` to see the introduced customizations. ================================================ FILE: client2/src/main/template/libraries/native/ApiClient.mustache ================================================ {{>licenseInfo}} package {{invokerPackage}}; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; {{#openApiNullable}} import org.openapitools.jackson.nullable.JsonNullableModule; {{/openApiNullable}} import java.io.InputStream; import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.HashMap; import java.util.StringJoiner; import java.util.function.Consumer; import java.util.stream.Collectors; import com.walmartlabs.concord.client2.impl.auth.Authentication; import com.walmartlabs.concord.client2.impl.auth.ApiKey; import com.walmartlabs.concord.client2.impl.auth.SessionToken; import com.walmartlabs.concord.client2.impl.OffsetDateTimeDeserializer; import com.walmartlabs.concord.client2.impl.OffsetDateTimeSerializer; import static java.nio.charset.StandardCharsets.UTF_8; /** * Configuration and utility class for API clients. * *

This class can be constructed and modified, then used to instantiate the * various API classes. The API classes use the settings in this class to * configure themselves, but otherwise do not store a link to this class.

* *

This class is mutable and not synchronized, so it is not thread-safe. * The API classes generated from this are immutable and thread-safe.

* *

The setter methods of this class return the current object to facilitate * a fluent style of configuration.

*/ {{>generatedAnnotation}} public class ApiClient { private final HttpClient httpClient; private ObjectMapper mapper; private String baseUri; private String scheme; private String host; private int port; private String basePath; private Consumer interceptor; private Consumer> responseInterceptor; private Consumer> asyncResponseInterceptor; private Duration readTimeout; private Duration connectTimeout; private Authentication auth; private final Map defaultHeaderMap = new HashMap(); public Authentication getAuth() { return auth; } public void setAuth(Authentication auth) { this.auth = auth; } public Map defaultHeaderMap() { return defaultHeaderMap; } private static String valueToString(Object value) { if (value == null) { return ""; } if (value instanceof OffsetDateTime) { return ((OffsetDateTime) value).format(OffsetDateTimeSerializer.FORMATTER); } return value.toString(); } /** * URL encode a string in the UTF-8 encoding. * * @param s String to encode. * @return URL-encoded representation of the input string. */ public static String urlEncode(String s) { return URLEncoder.encode(s, UTF_8).replaceAll("\\+", "%20"); } /** * Convert a URL query name/value parameter to a list of encoded {@link Pair} * objects. * *

The value can be null, in which case an empty list is returned.

* * @param name The query name parameter. * @param value The query value, which may not be a collection but may be * null. * @return A singleton list of the {@link Pair} objects representing the input * parameters, which is encoded for use in a URL. If the value is null, an * empty list is returned. */ public static List parameterToPairs(String name, Object value) { if (name == null || name.isEmpty() || value == null) { return Collections.emptyList(); } return Collections.singletonList(new Pair(urlEncode(name), urlEncode(valueToString(value)))); } /** * Convert a URL query name/collection parameter to a list of encoded * {@link Pair} objects. * * @param collectionFormat The swagger collectionFormat string (csv, tsv, etc). * @param name The query name parameter. * @param values A collection of values for the given query name, which may be * null. * @return A list of {@link Pair} objects representing the input parameters, * which is encoded for use in a URL. If the values collection is null, an * empty list is returned. */ public static List parameterToPairs( String collectionFormat, String name, Collection values) { if (name == null || name.isEmpty() || values == null || values.isEmpty()) { return Collections.emptyList(); } // get the collection format (default: csv) String format = collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat; // create the params based on the collection format if ("multi".equals(format)) { return values.stream() .map(value -> new Pair(urlEncode(name), urlEncode(valueToString(value)))) .collect(Collectors.toList()); } String delimiter; switch(format) { case "csv": delimiter = urlEncode(","); break; case "ssv": delimiter = urlEncode(" "); break; case "tsv": delimiter = urlEncode("\t"); break; case "pipes": delimiter = urlEncode("|"); break; default: throw new IllegalArgumentException("Illegal collection format: " + collectionFormat); } StringJoiner joiner = new StringJoiner(delimiter); for (Object value : values) { joiner.add(urlEncode(valueToString(value))); } return Collections.singletonList(new Pair(urlEncode(name), joiner.toString())); } /** * Create an instance of ApiClient. */ public ApiClient(HttpClient httpClient) { this.httpClient = httpClient; this.mapper = createDefaultObjectMapper(); updateBaseUri(getDefaultBaseUri()); interceptor = null; readTimeout = null; connectTimeout = null; responseInterceptor = null; asyncResponseInterceptor = null; } public static ObjectMapper createDefaultObjectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); mapper.configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, true); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING); mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE); mapper.registerModule(new JavaTimeModule() .addDeserializer(OffsetDateTime.class, new OffsetDateTimeDeserializer()) .addSerializer(OffsetDateTime.class, new OffsetDateTimeSerializer())); {{#openApiNullable}} mapper.registerModule(new JsonNullableModule()); {{/openApiNullable}} return mapper; } public ApiClient setUserAgent(String userAgent) { addDefaultHeader("User-Agent", userAgent); return this; } public ApiClient addDefaultHeader(String key, String value) { defaultHeaderMap.put(key, value); return this; } protected String getDefaultBaseUri() { return "{{{basePath}}}"; } public HttpRequest.Builder requestBuilder() { HttpRequest.Builder result = HttpRequest.newBuilder(); for (Map.Entry e : defaultHeaderMap.entrySet()) { result.header(e.getKey(), e.getValue()); } if (getAuth() != null) { result = getAuth().applyTo(result); } return result; } public void updateBaseUri(String baseUri) { this.baseUri = baseUri; URI uri = URI.create(baseUri); scheme = uri.getScheme(); host = uri.getHost(); port = uri.getPort(); basePath = uri.getRawPath(); } public ApiClient setBaseUrl(String baseUrl) { updateBaseUri(baseUrl); return this; } public String getBaseUrl() { return this.baseUri; } public ApiClient setSessionToken(String token) { if (token == null) { return this; } setAuth(new SessionToken(token)); return this; } public ApiClient setApiKey(String key) { if (key == null) { return this; } setAuth(new ApiKey(key)); return this; } /** * Get an {@link HttpClient} based on the current {@link HttpClient.Builder}. * *

The returned object is immutable and thread-safe.

* * @return The HTTP client. */ public HttpClient getHttpClient() { return httpClient; } /** * Set a custom {@link ObjectMapper} to serialize and deserialize the request * and response bodies. * * @param mapper Custom object mapper. * @return This object. */ public ApiClient setObjectMapper(ObjectMapper mapper) { this.mapper = mapper; return this; } /** * Get a copy of the current {@link ObjectMapper}. * * @return A copy of the current object mapper. */ public ObjectMapper getObjectMapper() { return mapper.copy(); } /** * Set a custom host name for the target service. * * @param host The host name of the target service. * @return This object. */ public ApiClient setHost(String host) { this.host = host; return this; } /** * Set a custom port number for the target service. * * @param port The port of the target service. Set this to -1 to reset the * value to the default for the scheme. * @return This object. */ public ApiClient setPort(int port) { this.port = port; return this; } /** * Set a custom base path for the target service, for example '/v2'. * * @param basePath The base path against which the rest of the path is * resolved. * @return This object. */ public ApiClient setBasePath(String basePath) { this.basePath = basePath; return this; } /** * Get the base URI to resolve the endpoint paths against. * * @return The complete base URI that the rest of the API parameters are * resolved against. */ public String getBaseUri() { return scheme + "://" + host + (port == -1 ? "" : ":" + port) + basePath; } /** * Set a custom scheme for the target service, for example 'https'. * * @param scheme The scheme of the target service * @return This object. */ public ApiClient setScheme(String scheme){ this.scheme = scheme; return this; } /** * Set a custom request interceptor. * *

A request interceptor is a mechanism for altering each request before it * is sent. After the request has been fully configured but not yet built, the * request builder is passed into this function for further modification, * after which it is sent out.

* *

This is useful for altering the requests in a custom manner, such as * adding headers. It could also be used for logging and monitoring.

* * @param interceptor A function invoked before creating each request. A value * of null resets the interceptor to a no-op. * @return This object. */ public ApiClient setRequestInterceptor(Consumer interceptor) { this.interceptor = interceptor; return this; } /** * Get the custom interceptor. * * @return The custom interceptor that was set, or null if there isn't any. */ public Consumer getRequestInterceptor() { return interceptor; } /** * Set a custom response interceptor. * *

This is useful for logging, monitoring or extraction of header variables

* * @param interceptor A function invoked before creating each request. A value * of null resets the interceptor to a no-op. * @return This object. */ public ApiClient setResponseInterceptor(Consumer> interceptor) { this.responseInterceptor = interceptor; return this; } /** * Get the custom response interceptor. * * @return The custom interceptor that was set, or null if there isn't any. */ public Consumer> getResponseInterceptor() { return responseInterceptor; } /** * Set a custom async response interceptor. Use this interceptor when asyncNative is set to 'true'. * *

This is useful for logging, monitoring or extraction of header variables

* * @param interceptor A function invoked before creating each request. A value * of null resets the interceptor to a no-op. * @return This object. */ public ApiClient setAsyncResponseInterceptor(Consumer> interceptor) { this.asyncResponseInterceptor = interceptor; return this; } /** * Get the custom async response interceptor. Use this interceptor when asyncNative is set to 'true'. * * @return The custom interceptor that was set, or null if there isn't any. */ public Consumer> getAsyncResponseInterceptor() { return asyncResponseInterceptor; } /** * Set the read timeout for the http client. * *

This is the value used by default for each request, though it can be * overridden on a per-request basis with a request interceptor.

* * @param readTimeout The read timeout used by default by the http client. * Setting this value to null resets the timeout to an * effectively infinite value. * @return This object. */ public ApiClient setReadTimeout(Duration readTimeout) { this.readTimeout = readTimeout; return this; } /** * Get the read timeout that was set. * * @return The read timeout, or null if no timeout was set. Null represents * an infinite wait time. */ public Duration getReadTimeout() { return readTimeout; } } ================================================ FILE: client2/src/main/template/libraries/native/api.mustache ================================================ {{>licenseInfo}} package {{package}}; import {{invokerPackage}}.ApiClient; import {{invokerPackage}}.ApiException; import {{invokerPackage}}.ApiResponse; import {{invokerPackage}}.Pair; {{#imports}} import {{import}}; {{/imports}} import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.walmartlabs.concord.client2.impl.*; {{#hasFormParamsInSpec}} {{/hasFormParamsInSpec}} import java.io.InputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.http.HttpRequest; import java.nio.channels.Channels; import java.nio.channels.Pipe; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; {{^fullJavaUtil}} import java.util.ArrayList; import java.util.StringJoiner; import java.util.List; import java.util.Map; import java.util.Set; import java.util.HashMap; import java.util.function.Consumer; {{/fullJavaUtil}} {{#asyncNative}} import java.util.concurrent.CompletableFuture; {{/asyncNative}} {{>generatedAnnotation}} {{#operations}} public class {{classname}} { private final HttpClient memberVarHttpClient; private final ObjectMapper memberVarObjectMapper; private final String memberVarBaseUri; private final {{#fullJavaUtil}}java.util.function.{{/fullJavaUtil}}Consumer memberVarInterceptor; private final Duration memberVarReadTimeout; private final {{#fullJavaUtil}}java.util.function.{{/fullJavaUtil}}Consumer> memberVarResponseInterceptor; private final {{#fullJavaUtil}}java.util.function.{{/fullJavaUtil}}Consumer> memberVarAsyncResponseInterceptor; private final ApiClient apiClient; public {{classname}}(ApiClient apiClient) { memberVarHttpClient = apiClient.getHttpClient(); memberVarObjectMapper = apiClient.getObjectMapper(); memberVarBaseUri = apiClient.getBaseUri(); memberVarInterceptor = apiClient.getRequestInterceptor(); memberVarReadTimeout = apiClient.getReadTimeout(); memberVarResponseInterceptor = apiClient.getResponseInterceptor(); memberVarAsyncResponseInterceptor = apiClient.getAsyncResponseInterceptor(); this.apiClient = apiClient; } public ApiClient getApiClient() { return this.apiClient; } {{#asyncNative}} private ApiException getApiException(String operationId, HttpResponse response) { String message = formatExceptionMessage(operationId, response.statusCode(), response.body()); return new ApiException(response.statusCode(), message, response.headers(), response.body()); } {{/asyncNative}} {{^asyncNative}} protected ApiException getApiException(String operationId, HttpResponse response) throws IOException { String body = response.body() == null ? null : new String(response.body().readAllBytes()); String message = formatExceptionMessage(operationId, response.statusCode(), body); return new ApiException(response.statusCode(), message, response.headers(), body); } {{/asyncNative}} private String formatExceptionMessage(String operationId, int statusCode, String body) { if (body == null || body.isEmpty()) { body = "[no body]"; } return operationId + " call failed with: " + statusCode + " - " + body; } {{#operation}} {{#vendorExtensions.x-group-parameters}} {{#hasParams}} {{#isDeprecated}} @Deprecated {{/isDeprecated}} public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}(API{{operationId}}Request apiRequest) throws ApiException { {{#allParams}} {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}(); {{/allParams}} {{#returnType}}return {{/returnType}}{{^returnType}}{{#asyncNative}}return {{/asyncNative}}{{/returnType}}{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); } {{#isDeprecated}} @Deprecated {{/isDeprecated}} public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo(API{{operationId}}Request apiRequest) throws ApiException { {{#allParams}} {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}(); {{/allParams}} return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); } {{/hasParams}} {{/vendorExtensions.x-group-parameters}} {{#isDeprecated}} @Deprecated {{/isDeprecated}} {{#vendorExtensions.x-concord.groupParams}} public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{vendorExtensions.x-concord.groupName}} in) throws ApiException { return {{operationId}}({{#isMultipart}}{{#allParams}}{{^isFormParam}}in.{{paramName}}(),{{/isFormParam}}{{/allParams}} multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}}in.{{paramName}}(){{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}}); } {{/vendorExtensions.x-concord.groupParams}} public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#isMultipart}}{{#allParams}}{{^isFormParam}}{{#vendorExtensions.x-concord.customQueryParams}}Map{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}},{{/isFormParam}}{{/allParams}} Map multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}} {{#vendorExtensions.x-concord.customQueryParams}}Map{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}}{{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}}) throws ApiException { {{^asyncNative}} {{#returnType}}ApiResponse<{{{.}}}> localVarResponse = {{/returnType}}{{operationId}}WithHttpInfo({{#isMultipart}}{{#allParams}}{{^isFormParam}}{{paramName}},{{/isFormParam}}{{/allParams}} multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}}{{paramName}}{{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}}); {{#returnType}} return localVarResponse.getData(); {{/returnType}} {{/asyncNative}} {{#asyncNative}} try { HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); return memberVarHttpClient.sendAsync( localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()).thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode()/ 100 != 2) { return CompletableFuture.failedFuture(getApiException("{{operationId}}", localVarResponse)); } {{#returnType}} try { String responseBody = localVarResponse.body(); return CompletableFuture.completedFuture( responseBody == null || responseBody.isBlank() ? null : memberVarObjectMapper.readValue(responseBody, new TypeReference<{{{returnType}}}>() {}) ); } catch (IOException e) { return CompletableFuture.failedFuture(new ApiException(e)); } {{/returnType}} {{^returnType}} return CompletableFuture.completedFuture(null); {{/returnType}} }); } catch (ApiException e) { return CompletableFuture.failedFuture(e); } {{/asyncNative}} } {{#isDeprecated}} @Deprecated {{/isDeprecated}} public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo({{#isMultipart}}{{#allParams}}{{^isFormParam}}{{#vendorExtensions.x-concord.customQueryParams}}Map{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}},{{/isFormParam}}{{/allParams}} Map multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}} {{#vendorExtensions.x-concord.customQueryParams}}Map{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}}{{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}}) throws ApiException { {{^asyncNative}} HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#isMultipart}}{{#allParams}}{{^isFormParam}}{{paramName}},{{/isFormParam}}{{/allParams}} multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}}{{paramName}}{{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}}); try { HttpResponse localVarResponse = memberVarHttpClient.send( localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofInputStream()); if (memberVarResponseInterceptor != null) { memberVarResponseInterceptor.accept(localVarResponse); } try { if (localVarResponse.statusCode()/ 100 != 2) { throw getApiException("{{operationId}}", localVarResponse); } if (localVarResponse.statusCode() == 204) { return new ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>( localVarResponse.statusCode(), localVarResponse.headers().map(), null ); } return new ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>( localVarResponse.statusCode(), localVarResponse.headers().map(), {{#returnType}} ResponseBodyHandler.handle(memberVarObjectMapper, localVarResponse, new TypeReference<{{{returnType}}}>() {}) {{/returnType}} {{^returnType}} null {{/returnType}} ); } finally { {{^returnType}} // Drain the InputStream while (localVarResponse.body().read() != -1) { // Ignore } localVarResponse.body().close(); {{/returnType}} } } catch (IOException e) { throw new ApiException(e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new ApiException(e); } {{/asyncNative}} {{#asyncNative}} try { HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); return memberVarHttpClient.sendAsync( localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()).thenComposeAsync(localVarResponse -> { if (memberVarAsyncResponseInterceptor != null) { memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode()/ 100 != 2) { return CompletableFuture.failedFuture(getApiException("{{operationId}}", localVarResponse)); } {{#returnType}} try { String responseBody = localVarResponse.body(); return CompletableFuture.completedFuture( new ApiResponse<{{{returnType}}}>( localVarResponse.statusCode(), localVarResponse.headers().map(), TODO:) ); } catch (IOException e) { return CompletableFuture.failedFuture(new ApiException(e)); } {{/returnType}} {{^returnType}} return CompletableFuture.completedFuture( new ApiResponse(localVarResponse.statusCode(), localVarResponse.headers().map(), null) ); {{/returnType}} } ); } catch (ApiException e) { return CompletableFuture.failedFuture(e); } {{/asyncNative}} } private HttpRequest.Builder {{operationId}}RequestBuilder({{#isMultipart}}{{#allParams}}{{^isFormParam}}{{#vendorExtensions.x-concord.customQueryParams}}Map{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}},{{/isFormParam}}{{/allParams}} Map multipartInput{{/isMultipart}}{{^isMultipart}}{{#allParams}} {{#vendorExtensions.x-concord.customQueryParams}}Map{{/vendorExtensions.x-concord.customQueryParams}}{{^vendorExtensions.x-concord.customQueryParams}}{{{dataType}}}{{/vendorExtensions.x-concord.customQueryParams}} {{paramName}}{{^-last}}, {{/-last}} {{/allParams}}{{/isMultipart}}) throws ApiException { {{#allParams}} {{#required}} // verify the required parameter '{{paramName}}' is set if ({{paramName}} == null) { throw new ApiException(400, "Missing the required parameter '{{paramName}}' when calling {{operationId}}"); } {{/required}} {{/allParams}} HttpRequest.Builder localVarRequestBuilder = apiClient.requestBuilder(); {{! Switch delimiters for baseName so we can write constants like "{query}" }} String localVarPath = "{{{path}}}"{{#pathParams}} .replace({{=<% %>=}}"{<%baseName%>}"<%={{ }}=%>, ApiClient.urlEncode({{{paramName}}}.toString())){{/pathParams}}; {{#hasQueryParams}} {{javaUtilPrefix}}List localVarQueryParams = new {{javaUtilPrefix}}ArrayList<>(); {{javaUtilPrefix}}StringJoiner localVarQueryStringJoiner = new {{javaUtilPrefix}}StringJoiner("&"); {{#queryParams}} {{#vendorExtensions.x-concord.customQueryParams}} if ({{paramName}} != null) { for (Map.Entry e : {{paramName}}.entrySet()) { localVarQueryParams.addAll(ApiClient.parameterToPairs(e.getKey(), e.getValue())); } } {{/vendorExtensions.x-concord.customQueryParams}} {{#collectionFormat}} localVarQueryParams.addAll(ApiClient.parameterToPairs("{{{collectionFormat}}}", "{{baseName}}", {{paramName}})); {{/collectionFormat}} {{^collectionFormat}} {{#isDeepObject}} if ({{paramName}} != null) { {{#isArray}} for (int i=0; i < {{paramName}}.size(); i++) { localVarQueryStringJoiner.add({{paramName}}.get(i).toUrlQueryString(String.format("{{baseName}}[%d]", i))); } {{/isArray}} {{^isArray}} localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString("{{baseName}}")); {{/isArray}} } {{/isDeepObject}} {{^isDeepObject}} {{#isExplode}} {{#hasVars}} {{#vars}} {{#isArray}} localVarQueryParams.addAll(ApiClient.parameterToPairs("multi", "{{baseName}}", {{paramName}}.{{getter}}())); {{/isArray}} {{^isArray}} if ({{paramName}} != null) { localVarQueryParams.addAll(ApiClient.parameterToPairs("{{paramName}}", {{paramName}}.{{getter}}())); } {{/isArray}} {{/vars}} {{/hasVars}} {{^hasVars}} {{#isModel}} localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString()); {{/isModel}} {{^isModel}} {{^vendorExtensions.x-concord.customQueryParams}} localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}})); {{/vendorExtensions.x-concord.customQueryParams}} {{/isModel}} {{/hasVars}} {{/isExplode}} {{^isExplode}} localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}})); {{/isExplode}} {{/isDeepObject}} {{/collectionFormat}} {{/queryParams}} if (!localVarQueryParams.isEmpty() || localVarQueryStringJoiner.length() != 0) { {{javaUtilPrefix}}StringJoiner queryJoiner = new {{javaUtilPrefix}}StringJoiner("&"); localVarQueryParams.forEach(p -> queryJoiner.add(p.getName() + '=' + p.getValue())); if (localVarQueryStringJoiner.length() != 0) { queryJoiner.add(localVarQueryStringJoiner.toString()); } localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath + '?' + queryJoiner.toString())); } else { localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); } {{/hasQueryParams}} {{^hasQueryParams}} localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); {{/hasQueryParams}} {{#headerParams}} if ({{paramName}} != null) { localVarRequestBuilder.header("{{baseName}}", {{paramName}}.toString()); } {{/headerParams}} {{#bodyParam}} localVarRequestBuilder.header("Content-Type", "{{#hasConsumes}}{{#consumes}}{{#-first}}{{mediaType}}{{/-first}}{{/consumes}}{{/hasConsumes}}{{#hasConsumes}}{{^consumes}}application/json{{/consumes}}{{/hasConsumes}}{{^hasConsumes}}application/json{{/hasConsumes}}"); {{/bodyParam}} String acceptHeaderValue = "{{#hasProduces}}{{#produces}}{{mediaType}}{{^-last}}, {{/-last}}{{/produces}}{{/hasProduces}}{{#hasProduces}}{{^produces}}application/json{{/produces}}{{/hasProduces}}{{^hasProduces}}application/json{{/hasProduces}}"; acceptHeaderValue += ",application/vnd.siesta-validation-errors-v1+json"; acceptHeaderValue += ",application/vnd.concord-validation-errors-v1+json"; localVarRequestBuilder.header("Accept", acceptHeaderValue); {{#bodyParam}} {{#isString}} localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.ofString({{paramName}})); {{/isString}} {{^isString}} try { localVarRequestBuilder.method("{{httpMethod}}", RequestBodyHandler.handle(memberVarObjectMapper, {{paramName}})); } catch (IOException e) { throw new ApiException(e); } {{/isString}} {{/bodyParam}} {{^bodyParam}} {{#isMultipart}} HttpEntity entity = MultipartRequestBodyHandler.handle(memberVarObjectMapper, multipartInput); localVarRequestBuilder .header("Content-Type", entity.contentType().toString()) .method("{{httpMethod}}", HttpRequest.BodyPublishers.ofInputStream(() -> { try { return entity.getContent(); } catch (IOException e) { throw new RuntimeException(e); } })); {{/isMultipart}} {{^isMultipart}} {{#hasFormParams}} List formValues = new ArrayList<>(); {{#formParams}} {{#isArray}} for (int i=0; i < {{paramName}}.size(); i++) { if ({{paramName}}.get(i) != null) { formValues.add(new NameValuePair("{{{baseName}}}", {{paramName}}.get(i).toString())); } } {{/isArray}} {{^isArray}} if ({{paramName}} != null) { formValues.add(new NameValuePair("{{{baseName}}}", {{paramName}}.toString())); } {{/isArray}} {{/formParams}} HttpEntity entity = new UrlEncodedFormEntity(formValues, java.nio.charset.StandardCharsets.UTF_8); ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream(); try { entity.writeTo(formOutputStream); } catch (IOException e) { throw new RuntimeException(e); } localVarRequestBuilder .header("Content-Type", entity.getContentType().toString()) .method("{{httpMethod}}", HttpRequest.BodyPublishers .ofInputStream(() -> new ByteArrayInputStream(formOutputStream.toByteArray()))); {{/hasFormParams}} {{^hasFormParams}} localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.noBody()); {{/hasFormParams}} {{/isMultipart}} {{/bodyParam}} if (memberVarReadTimeout != null) { localVarRequestBuilder.timeout(memberVarReadTimeout); } if (memberVarInterceptor != null) { memberVarInterceptor.accept(localVarRequestBuilder); } return localVarRequestBuilder; } {{#isMultipart}} {{#hasParams}} public static final class {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request { {{#allParams}} {{#isFormParam}} private {{{dataType}}} {{paramName}}; {{/isFormParam}} {{/allParams}} {{#allParams}} {{#isFormParam}} public {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}Request {{paramName}}({{{dataType}}} {{paramName}}) { this.{{paramName}} = {{paramName}}; return this; } {{/isFormParam}} {{/allParams}} public Map asMap() { Map result = new HashMap<>(); {{#allParams}} {{#isFormParam}} if ({{paramName}} != null) { result.put("{{baseName}}", {{paramName}}); } {{/isFormParam}} {{/allParams}} return result; } } {{/hasParams}} {{/isMultipart}} {{/operation}} } {{/operations}} ================================================ FILE: client2/src/main/template/libraries/native/api.mustache.orig ================================================ {{>licenseInfo}} package {{package}}; import {{invokerPackage}}.ApiClient; import {{invokerPackage}}.ApiException; import {{invokerPackage}}.ApiResponse; import {{invokerPackage}}.Pair; {{#imports}} import {{import}}; {{/imports}} import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; {{#hasFormParamsInSpec}} import org.apache.http.HttpEntity; import org.apache.http.NameValuePair; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.message.BasicNameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; {{/hasFormParamsInSpec}} import java.io.InputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.http.HttpRequest; import java.nio.channels.Channels; import java.nio.channels.Pipe; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.ArrayList; import java.util.StringJoiner; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; {{#asyncNative}} import java.util.concurrent.CompletableFuture; {{/asyncNative}} {{>generatedAnnotation}} {{#operations}} public class {{classname}} { private final HttpClient memberVarHttpClient; private final ObjectMapper memberVarObjectMapper; private final String memberVarBaseUri; private final Consumer memberVarInterceptor; private final Duration memberVarReadTimeout; private final Consumer> memberVarResponseInterceptor; private final Consumer> memberVarAsyncResponseInterceptor; public {{classname}}() { this(new ApiClient()); } public {{classname}}(ApiClient apiClient) { memberVarHttpClient = apiClient.getHttpClient(); memberVarObjectMapper = apiClient.getObjectMapper(); memberVarBaseUri = apiClient.getBaseUri(); memberVarInterceptor = apiClient.getRequestInterceptor(); memberVarReadTimeout = apiClient.getReadTimeout(); memberVarResponseInterceptor = apiClient.getResponseInterceptor(); memberVarAsyncResponseInterceptor = apiClient.getAsyncResponseInterceptor(); } {{#asyncNative}} private ApiException getApiException(String operationId, HttpResponse response) { String message = formatExceptionMessage(operationId, response.statusCode(), response.body()); return new ApiException(response.statusCode(), message, response.headers(), response.body()); } {{/asyncNative}} {{^asyncNative}} protected ApiException getApiException(String operationId, HttpResponse response) throws IOException { String body = response.body() == null ? null : new String(response.body().readAllBytes()); String message = formatExceptionMessage(operationId, response.statusCode(), body); return new ApiException(response.statusCode(), message, response.headers(), body); } {{/asyncNative}} private String formatExceptionMessage(String operationId, int statusCode, String body) { if (body == null || body.isEmpty()) { body = "[no body]"; } return operationId + " call failed with: " + statusCode + " - " + body; } {{#operation}} {{#vendorExtensions.x-group-parameters}} {{#hasParams}} /** * {{summary}} * {{notes}} * @param apiRequest {@link API{{operationId}}Request} {{#returnType}} * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}{{returnType}}{{#asyncNative}}>{{/asyncNative}} {{/returnType}} {{^returnType}} {{#asyncNative}} * @return CompletableFuture<Void> {{/asyncNative}} {{/returnType}} * @throws ApiException if fails to make API call {{#isDeprecated}} * @deprecated {{/isDeprecated}} {{#externalDocs}} * {{description}} * @see {{summary}} Documentation {{/externalDocs}} */ {{#isDeprecated}} @Deprecated {{/isDeprecated}} public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}(API{{operationId}}Request apiRequest) throws ApiException { {{#allParams}} {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}(); {{/allParams}} {{#returnType}}return {{/returnType}}{{^returnType}}{{#asyncNative}}return {{/asyncNative}}{{/returnType}}{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); } /** * {{summary}} * {{notes}} * @param apiRequest {@link API{{operationId}}Request} * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{returnType}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} * @throws ApiException if fails to make API call {{#isDeprecated}} * @deprecated {{/isDeprecated}} {{#externalDocs}} * {{description}} * @see {{summary}} Documentation {{/externalDocs}} */ {{#isDeprecated}} @Deprecated {{/isDeprecated}} public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo(API{{operationId}}Request apiRequest) throws ApiException { {{#allParams}} {{{dataType}}} {{paramName}} = apiRequest.{{paramName}}(); {{/allParams}} return {{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); } {{/hasParams}} {{/vendorExtensions.x-group-parameters}} /** * {{summary}} * {{notes}} {{#allParams}} * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}{{/required}} {{/allParams}} {{#returnType}} * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}{{returnType}}{{#asyncNative}}>{{/asyncNative}} {{/returnType}} {{^returnType}} {{#asyncNative}} * @return CompletableFuture<Void> {{/asyncNative}} {{/returnType}} * @throws ApiException if fails to make API call {{#isDeprecated}} * @deprecated {{/isDeprecated}} {{#externalDocs}} * {{description}} * @see {{summary}} Documentation {{/externalDocs}} */ {{#isDeprecated}} @Deprecated {{/isDeprecated}} public {{#returnType}}{{#asyncNative}}CompletableFuture<{{{returnType}}}>{{/asyncNative}}{{^asyncNative}}{{{returnType}}}{{/asyncNative}}{{/returnType}}{{^returnType}}{{#asyncNative}}CompletableFuture{{/asyncNative}}{{^asyncNative}}void{{/asyncNative}}{{/returnType}} {{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException { {{^asyncNative}} {{#returnType}}ApiResponse<{{{.}}}> localVarResponse = {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); {{#returnType}} return localVarResponse.getData(); {{/returnType}} {{/asyncNative}} {{#asyncNative}} try { HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); return memberVarHttpClient.sendAsync( localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()).thenComposeAsync(localVarResponse -> { if (localVarResponse.statusCode()/ 100 != 2) { return CompletableFuture.failedFuture(getApiException("{{operationId}}", localVarResponse)); } {{#returnType}} try { String responseBody = localVarResponse.body(); return CompletableFuture.completedFuture( responseBody == null || responseBody.isBlank() ? null : memberVarObjectMapper.readValue(responseBody, new TypeReference<{{{returnType}}}>() {}) ); } catch (IOException e) { return CompletableFuture.failedFuture(new ApiException(e)); } {{/returnType}} {{^returnType}} return CompletableFuture.completedFuture(null); {{/returnType}} }); } catch (ApiException e) { return CompletableFuture.failedFuture(e); } {{/asyncNative}} } /** * {{summary}} * {{notes}} {{#allParams}} * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}}{{/required}} {{/allParams}} * @return {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{returnType}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} * @throws ApiException if fails to make API call {{#isDeprecated}} * @deprecated {{/isDeprecated}} {{#externalDocs}} * {{description}} * @see {{summary}} Documentation {{/externalDocs}} */ {{#isDeprecated}} @Deprecated {{/isDeprecated}} public {{#asyncNative}}CompletableFuture<{{/asyncNative}}ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>{{#asyncNative}}>{{/asyncNative}} {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException { {{^asyncNative}} HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); try { HttpResponse localVarResponse = memberVarHttpClient.send( localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofInputStream()); if (memberVarResponseInterceptor != null) { memberVarResponseInterceptor.accept(localVarResponse); } try { if (localVarResponse.statusCode()/ 100 != 2) { throw getApiException("{{operationId}}", localVarResponse); } {{#vendorExtensions.x-java-text-plain-string}} // for plain text response if (localVarResponse.headers().map().containsKey("Content-Type") && "text/plain".equalsIgnoreCase(localVarResponse.headers().map().get("Content-Type").get(0).split(";")[0].trim())) { java.util.Scanner s = new java.util.Scanner(localVarResponse.body()).useDelimiter("\\A"); String responseBodyText = s.hasNext() ? s.next() : ""; return new ApiResponse( localVarResponse.statusCode(), localVarResponse.headers().map(), responseBodyText ); } else { throw new RuntimeException("Error! The response Content-Type is supposed to be `text/plain` but it's not: " + localVarResponse); } {{/vendorExtensions.x-java-text-plain-string}} {{^vendorExtensions.x-java-text-plain-string}} return new ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}>( localVarResponse.statusCode(), localVarResponse.headers().map(), {{#returnType}} localVarResponse.body() == null ? null : memberVarObjectMapper.readValue(localVarResponse.body(), new TypeReference<{{{returnType}}}>() {}) // closes the InputStream {{/returnType}} {{^returnType}} null {{/returnType}} ); {{/vendorExtensions.x-java-text-plain-string}} } finally { {{^returnType}} // Drain the InputStream while (localVarResponse.body().read() != -1) { // Ignore } localVarResponse.body().close(); {{/returnType}} } } catch (IOException e) { throw new ApiException(e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new ApiException(e); } {{/asyncNative}} {{#asyncNative}} try { HttpRequest.Builder localVarRequestBuilder = {{operationId}}RequestBuilder({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); return memberVarHttpClient.sendAsync( localVarRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()).thenComposeAsync(localVarResponse -> { if (memberVarAsyncResponseInterceptor != null) { memberVarAsyncResponseInterceptor.accept(localVarResponse); } if (localVarResponse.statusCode()/ 100 != 2) { return CompletableFuture.failedFuture(getApiException("{{operationId}}", localVarResponse)); } {{#returnType}} try { String responseBody = localVarResponse.body(); return CompletableFuture.completedFuture( new ApiResponse<{{{returnType}}}>( localVarResponse.statusCode(), localVarResponse.headers().map(), responseBody == null || responseBody.isBlank() ? null : memberVarObjectMapper.readValue(responseBody, new TypeReference<{{{returnType}}}>() {})) ); } catch (IOException e) { return CompletableFuture.failedFuture(new ApiException(e)); } {{/returnType}} {{^returnType}} return CompletableFuture.completedFuture( new ApiResponse(localVarResponse.statusCode(), localVarResponse.headers().map(), null) ); {{/returnType}} } ); } catch (ApiException e) { return CompletableFuture.failedFuture(e); } {{/asyncNative}} } private HttpRequest.Builder {{operationId}}RequestBuilder({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException { {{#allParams}} {{#required}} // verify the required parameter '{{paramName}}' is set if ({{paramName}} == null) { throw new ApiException(400, "Missing the required parameter '{{paramName}}' when calling {{operationId}}"); } {{/required}} {{/allParams}} HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder(); {{! Switch delimiters for baseName so we can write constants like "{query}" }} String localVarPath = "{{{path}}}"{{#pathParams}} .replace({{=<% %>=}}"{<%baseName%>}"<%={{ }}=%>, ApiClient.urlEncode({{{paramName}}}.toString())){{/pathParams}}; {{#hasQueryParams}} List localVarQueryParams = new ArrayList<>(); StringJoiner localVarQueryStringJoiner = new StringJoiner("&"); String localVarQueryParameterBaseName; {{#queryParams}} localVarQueryParameterBaseName = "{{{baseName}}}"; {{#collectionFormat}} localVarQueryParams.addAll(ApiClient.parameterToPairs("{{{collectionFormat}}}", "{{baseName}}", {{paramName}})); {{/collectionFormat}} {{^collectionFormat}} {{#isDeepObject}} if ({{paramName}} != null) { {{#isArray}} for (int i=0; i < {{paramName}}.size(); i++) { localVarQueryStringJoiner.add({{paramName}}.get(i).toUrlQueryString(String.format("{{baseName}}[%d]", i))); } {{/isArray}} {{^isArray}} localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString("{{baseName}}")); {{/isArray}} } {{/isDeepObject}} {{^isDeepObject}} {{#isExplode}} {{#hasVars}} {{#vars}} {{#isArray}} localVarQueryParams.addAll(ApiClient.parameterToPairs("multi", "{{baseName}}", {{paramName}}.{{getter}}())); {{/isArray}} {{^isArray}} localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}}.{{getter}}())); {{/isArray}} {{/vars}} {{/hasVars}} {{^hasVars}} {{#isModel}} localVarQueryStringJoiner.add({{paramName}}.toUrlQueryString()); {{/isModel}} {{^isModel}} localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}})); {{/isModel}} {{/hasVars}} {{/isExplode}} {{^isExplode}} localVarQueryParams.addAll(ApiClient.parameterToPairs("{{baseName}}", {{paramName}})); {{/isExplode}} {{/isDeepObject}} {{/collectionFormat}} {{/queryParams}} if (!localVarQueryParams.isEmpty() || localVarQueryStringJoiner.length() != 0) { StringJoiner queryJoiner = new StringJoiner("&"); localVarQueryParams.forEach(p -> queryJoiner.add(p.getName() + '=' + p.getValue())); if (localVarQueryStringJoiner.length() != 0) { queryJoiner.add(localVarQueryStringJoiner.toString()); } localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath + '?' + queryJoiner.toString())); } else { localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); } {{/hasQueryParams}} {{^hasQueryParams}} localVarRequestBuilder.uri(URI.create(memberVarBaseUri + localVarPath)); {{/hasQueryParams}} {{#headerParams}} if ({{paramName}} != null) { localVarRequestBuilder.header("{{baseName}}", {{paramName}}.toString()); } {{/headerParams}} {{#bodyParam}} localVarRequestBuilder.header("Content-Type", "{{#hasConsumes}}{{#consumes}}{{#-first}}{{mediaType}}{{/-first}}{{/consumes}}{{/hasConsumes}}{{#hasConsumes}}{{^consumes}}application/json{{/consumes}}{{/hasConsumes}}{{^hasConsumes}}application/json{{/hasConsumes}}"); {{/bodyParam}} localVarRequestBuilder.header("Accept", "{{#hasProduces}}{{#produces}}{{mediaType}}{{^-last}}, {{/-last}}{{/produces}}{{/hasProduces}}{{#hasProduces}}{{^produces}}application/json{{/produces}}{{/hasProduces}}{{^hasProduces}}application/json{{/hasProduces}}"); {{#bodyParam}} {{#isString}} localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.ofString({{paramName}})); {{/isString}} {{^isString}} try { byte[] localVarPostBody = memberVarObjectMapper.writeValueAsBytes({{paramName}}); localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.ofByteArray(localVarPostBody)); } catch (IOException e) { throw new ApiException(e); } {{/isString}} {{/bodyParam}} {{^bodyParam}} {{#hasFormParams}} {{#isMultipart}} MultipartEntityBuilder multiPartBuilder = MultipartEntityBuilder.create(); boolean hasFiles = false; {{#formParams}} {{#isArray}} for (int i=0; i < {{paramName}}.size(); i++) { {{#isFile}} multiPartBuilder.addBinaryBody("{{{baseName}}}", {{paramName}}.get(i)); hasFiles = true; {{/isFile}} {{^isFile}} multiPartBuilder.addTextBody("{{{baseName}}}", {{paramName}}.get(i).toString()); {{/isFile}} } {{/isArray}} {{^isArray}} {{#isFile}} multiPartBuilder.addBinaryBody("{{{baseName}}}", {{paramName}}); hasFiles = true; {{/isFile}} {{^isFile}} multiPartBuilder.addTextBody("{{{baseName}}}", {{paramName}}.toString()); {{/isFile}} {{/isArray}} {{/formParams}} HttpEntity entity = multiPartBuilder.build(); HttpRequest.BodyPublisher formDataPublisher; if (hasFiles) { Pipe pipe; try { pipe = Pipe.open(); } catch (IOException e) { throw new RuntimeException(e); } new Thread(() -> { try (OutputStream outputStream = Channels.newOutputStream(pipe.sink())) { entity.writeTo(outputStream); } catch (IOException e) { e.printStackTrace(); } }).start(); formDataPublisher = HttpRequest.BodyPublishers.ofInputStream(() -> Channels.newInputStream(pipe.source())); } else { ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream(); try { entity.writeTo(formOutputStream); } catch (IOException e) { throw new RuntimeException(e); } formDataPublisher = HttpRequest.BodyPublishers .ofInputStream(() -> new ByteArrayInputStream(formOutputStream.toByteArray())); } localVarRequestBuilder .header("Content-Type", entity.getContentType().getValue()) .method("{{httpMethod}}", formDataPublisher); {{/isMultipart}} {{^isMultipart}} List formValues = new ArrayList<>(); {{#formParams}} {{#isArray}} for (int i=0; i < {{paramName}}.size(); i++) { if ({{paramName}}.get(i) != null) { formValues.add(new BasicNameValuePair("{{{baseName}}}", {{paramName}}.get(i).toString())); } } {{/isArray}} {{^isArray}} if ({{paramName}} != null) { formValues.add(new BasicNameValuePair("{{{baseName}}}", {{paramName}}.toString())); } {{/isArray}} {{/formParams}} HttpEntity entity = new UrlEncodedFormEntity(formValues, java.nio.charset.StandardCharsets.UTF_8); ByteArrayOutputStream formOutputStream = new ByteArrayOutputStream(); try { entity.writeTo(formOutputStream); } catch (IOException e) { throw new RuntimeException(e); } localVarRequestBuilder .header("Content-Type", entity.getContentType().getValue()) .method("{{httpMethod}}", HttpRequest.BodyPublishers .ofInputStream(() -> new ByteArrayInputStream(formOutputStream.toByteArray()))); {{/isMultipart}} {{/hasFormParams}} {{^hasFormParams}} localVarRequestBuilder.method("{{httpMethod}}", HttpRequest.BodyPublishers.noBody()); {{/hasFormParams}} {{/bodyParam}} if (memberVarReadTimeout != null) { localVarRequestBuilder.timeout(memberVarReadTimeout); } if (memberVarInterceptor != null) { memberVarInterceptor.accept(localVarRequestBuilder); } return localVarRequestBuilder; } {{#vendorExtensions.x-group-parameters}} {{#hasParams}} public static final class API{{operationId}}Request { {{#requiredParams}} private {{{dataType}}} {{paramName}}; // {{description}} (required) {{/requiredParams}} {{#optionalParams}} private {{{dataType}}} {{paramName}}; // {{description}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/isContainer}} {{/optionalParams}} private API{{operationId}}Request(Builder builder) { {{#requiredParams}} this.{{paramName}} = builder.{{paramName}}; {{/requiredParams}} {{#optionalParams}} this.{{paramName}} = builder.{{paramName}}; {{/optionalParams}} } {{#allParams}} public {{{dataType}}} {{paramName}}() { return {{paramName}}; } {{/allParams}} public static Builder newBuilder() { return new Builder(); } public static class Builder { {{#requiredParams}} private {{{dataType}}} {{paramName}}; {{/requiredParams}} {{#optionalParams}} private {{{dataType}}} {{paramName}}; {{/optionalParams}} {{#allParams}} public Builder {{paramName}}({{{dataType}}} {{paramName}}) { this.{{paramName}} = {{paramName}}; return this; } {{/allParams}} public API{{operationId}}Request build() { return new API{{operationId}}Request(this); } } } {{/hasParams}} {{/vendorExtensions.x-group-parameters}} {{/operation}} } {{/operations}} ================================================ FILE: client2/src/main/template/libraries/native/pojo.mustache ================================================ {{#discriminator}} import {{invokerPackage}}.JSON; {{/discriminator}} /** * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}} * @deprecated{{/isDeprecated}} */{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{#swagger1AnnotationLibrary}} {{#description}} @ApiModel(description = "{{{.}}}") {{/description}} {{/swagger1AnnotationLibrary}} {{#swagger2AnnotationLibrary}} {{#description}} @Schema(description = "{{{.}}}") {{/description}} {{/swagger2AnnotationLibrary}} {{#jackson}} @JsonPropertyOrder({ {{#vars}} {{classname}}.JSON_PROPERTY_{{nameInSnakeCase}}{{^-last}},{{/-last}} {{/vars}} }) {{/jackson}} {{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} {{#vendorExtensions.x-class-extra-annotation}} {{{vendorExtensions.x-class-extra-annotation}}} {{/vendorExtensions.x-class-extra-annotation}} public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{ {{#serializableModel}} private static final long serialVersionUID = 1L; {{/serializableModel}} {{#vars}} {{#isEnum}} {{^isContainer}} {{^vendorExtensions.x-enum-as-string}} {{>modelInnerEnum}} {{/vendorExtensions.x-enum-as-string}} {{/isContainer}} {{#isContainer}} {{#mostInnerItems}} {{>modelInnerEnum}} {{/mostInnerItems}} {{/isContainer}} {{/isEnum}} {{#gson}} public static final String SERIALIZED_NAME_{{nameInSnakeCase}} = "{{baseName}}"; {{/gson}} {{#jackson}} public static final String JSON_PROPERTY_{{nameInSnakeCase}} = "{{baseName}}"; {{/jackson}} {{#withXml}} {{#isXmlAttribute}} @XmlAttribute(name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") {{/isXmlAttribute}} {{^isXmlAttribute}} {{^isContainer}} @XmlElement({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") {{/isContainer}} {{#isContainer}} // Is a container wrapped={{isXmlWrapped}} {{#items}} // items.name={{name}} items.baseName={{baseName}} items.xmlName={{xmlName}} items.xmlNamespace={{xmlNamespace}} // items.example={{example}} items.type={{dataType}} @XmlElement({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") {{/items}} {{#isXmlWrapped}} @XmlElementWrapper({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") {{/isXmlWrapped}} {{/isContainer}} {{/isXmlAttribute}} {{/withXml}} {{#gson}} @SerializedName(SERIALIZED_NAME_{{nameInSnakeCase}}) {{/gson}} {{#vendorExtensions.x-field-extra-annotation}} {{{vendorExtensions.x-field-extra-annotation}}} {{/vendorExtensions.x-field-extra-annotation}} {{#vendorExtensions.x-is-jackson-optional-nullable}} {{#isContainer}} private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined(); {{/isContainer}} {{^isContainer}} private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; {{/isContainer}} {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} private {{{datatypeWithEnum}}} {{name}}; {{/vendorExtensions.x-is-jackson-optional-nullable}} {{/vars}} public {{classname}}() { {{#parent}}{{#parcelableModel}} super();{{/parcelableModel}}{{/parent}}{{#gson}}{{#discriminator}} this.{{{discriminatorName}}} = this.getClass().getSimpleName();{{/discriminator}}{{/gson}} }{{#vendorExtensions.x-has-readonly-properties}}{{^withXml}} {{#jackson}}@JsonCreator{{/jackson}} public {{classname}}( {{#readOnlyVars}} @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}} {{/readOnlyVars}} ) { this(); {{#readOnlyVars}} this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}}; {{/readOnlyVars}} }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}} {{#vars}} {{^isReadOnly}} {{#vendorExtensions.x-enum-as-string}} public static final Set {{{nameInSnakeCase}}}_VALUES = new HashSet<>(Arrays.asList( {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}} )); {{/vendorExtensions.x-enum-as-string}} public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{#vendorExtensions.x-enum-as-string}} if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) { throw new IllegalArgumentException({{name}} + " is invalid. Possible values for {{name}}: " + String.join(", ", {{{nameInSnakeCase}}}_VALUES)); } {{/vendorExtensions.x-enum-as-string}} {{#vendorExtensions.x-is-jackson-optional-nullable}} this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} this.{{name}} = {{name}}; {{/vendorExtensions.x-is-jackson-optional-nullable}} return this; } {{#isArray}} public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { {{#vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null || !this.{{name}}.isPresent()) { this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}); } try { this.{{name}}.get().add({{name}}Item); } catch (java.util.NoSuchElementException e) { // this can never happen, as we make sure above that the value is present } return this; {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null) { this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; } this.{{name}}.add({{name}}Item); return this; {{/vendorExtensions.x-is-jackson-optional-nullable}} } {{/isArray}} {{#isMap}} public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { {{#vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null || !this.{{name}}.isPresent()) { this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}); } try { this.{{name}}.get().put(key, {{name}}Item); } catch (java.util.NoSuchElementException e) { // this can never happen, as we make sure above that the value is present } return this; {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null) { this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; } this.{{name}}.put(key, {{name}}Item); return this; {{/vendorExtensions.x-is-jackson-optional-nullable}} } {{/isMap}} {{/isReadOnly}} /** {{#description}} * {{.}} {{/description}} {{^description}} * Get {{name}} {{/description}} {{#minimum}} * minimum: {{.}} {{/minimum}} {{#maximum}} * maximum: {{.}} {{/maximum}} * @return {{name}} {{#deprecated}} * @deprecated {{/deprecated}} **/ {{#deprecated}} @Deprecated {{/deprecated}} // {{#required}} // {{#isNullable}} // @{{javaxPackage}}.annotation.Nullable // {{/isNullable}} // {{^isNullable}} // @{{javaxPackage}}.annotation.Nonnull // {{/isNullable}} // {{/required}} // {{^required}} // @{{javaxPackage}}.annotation.Nullable // {{/required}} {{#useBeanValidation}} {{>beanValidation}} {{/useBeanValidation}} {{#swagger1AnnotationLibrary}} @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}") {{/swagger1AnnotationLibrary}} {{#swagger2AnnotationLibrary}} @Schema({{#example}}example = "{{{.}}}", {{/example}}requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}, description = "{{{description}}}") {{/swagger2AnnotationLibrary}} {{#vendorExtensions.x-extra-annotation}} {{{vendorExtensions.x-extra-annotation}}} {{/vendorExtensions.x-extra-annotation}} {{#vendorExtensions.x-is-jackson-optional-nullable}} {{!unannotated, Jackson would pick this up automatically and add it *in addition* to the _JsonNullable getter field}} @JsonIgnore {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}}{{#jackson}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}} public {{{datatypeWithEnum}}} {{getter}}() { {{#vendorExtensions.x-is-jackson-optional-nullable}} {{#isReadOnly}}{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}} if ({{name}} == null) { {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; } {{/isReadOnly}} return {{name}}.orElse(null); {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} return {{name}}; {{/vendorExtensions.x-is-jackson-optional-nullable}} } {{#vendorExtensions.x-is-jackson-optional-nullable}} {{> jackson_annotations}} public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}_JsonNullable() { return {{name}}; } {{/vendorExtensions.x-is-jackson-optional-nullable}}{{#vendorExtensions.x-is-jackson-optional-nullable}} @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) {{#isReadOnly}}private{{/isReadOnly}}{{^isReadOnly}}public{{/isReadOnly}} void {{setter}}_JsonNullable(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) { {{! For getters/setters that have name differing from attribute name, we must include setter (albeit private) for jackson to be able to set the attribute}} this.{{name}} = {{name}}; } {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^isReadOnly}} {{#vendorExtensions.x-setter-extra-annotation}} {{{vendorExtensions.x-setter-extra-annotation}}} {{/vendorExtensions.x-setter-extra-annotation}}{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{> jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { {{#vendorExtensions.x-enum-as-string}} if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) { throw new IllegalArgumentException({{name}} + " is invalid. Possible values for {{name}}: " + String.join(", ", {{{nameInSnakeCase}}}_VALUES)); } {{/vendorExtensions.x-enum-as-string}} {{#vendorExtensions.x-is-jackson-optional-nullable}} this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} this.{{name}} = {{name}}; {{/vendorExtensions.x-is-jackson-optional-nullable}} } {{/isReadOnly}} {{/vars}} {{>libraries/native/additional_properties}} {{#parent}} {{#allVars}} {{#isOverridden}} @Override public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{#vendorExtensions.x-is-jackson-optional-nullable}} this.{{setter}}(JsonNullable.<{{{datatypeWithEnum}}}>of({{name}})); {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} this.{{setter}}({{name}}); {{/vendorExtensions.x-is-jackson-optional-nullable}} return this; } {{/isOverridden}} {{/allVars}} {{/parent}} /** * Return true if this {{name}} object is equal to o. */ @Override public boolean equals(Object o) { {{#useReflectionEqualsHashCode}} return EqualsBuilder.reflectionEquals(this, o, false, null, true); {{/useReflectionEqualsHashCode}} {{^useReflectionEqualsHashCode}} if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; }{{#hasVars}} {{classname}} {{classVarName}} = ({{classname}}) o; return {{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}equalsNullable(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}} && {{/-last}}{{/vars}}{{#additionalPropertiesType}}&& Objects.equals(this.additionalProperties, {{classVarName}}.additionalProperties){{/additionalPropertiesType}}{{#parent}} && super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}} return {{#parent}}super.equals(o){{/parent}}{{^parent}}true{{/parent}};{{/hasVars}} {{/useReflectionEqualsHashCode}} }{{#vendorExtensions.x-jackson-optional-nullable-helpers}} private static boolean equalsNullable(JsonNullable a, JsonNullable b) { return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get())); }{{/vendorExtensions.x-jackson-optional-nullable-helpers}} @Override public int hashCode() { {{#useReflectionEqualsHashCode}} return HashCodeBuilder.reflectionHashCode(this); {{/useReflectionEqualsHashCode}} {{^useReflectionEqualsHashCode}} return Objects.hash({{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}hashCodeNullable({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}{{#additionalPropertiesType}}, additionalProperties{{/additionalPropertiesType}}); {{/useReflectionEqualsHashCode}} }{{#vendorExtensions.x-jackson-optional-nullable-helpers}} private static int hashCodeNullable(JsonNullable a) { if (a == null) { return 1; } return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31; }{{/vendorExtensions.x-jackson-optional-nullable-helpers}} @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class {{classname}} {\n"); {{#parent}} sb.append(" ").append(toIndentedString(super.toString())).append("\n"); {{/parent}} {{#vars}} sb.append(" {{name}}: ").append(toIndentedString({{name}})).append("\n"); {{/vars}} {{#additionalPropertiesType}} sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n"); {{/additionalPropertiesType}} sb.append("}"); return sb.toString(); } /** * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ private String toIndentedString(Object o) { if (o == null) { return "null"; } return o.toString().replace("\n", "\n "); } {{#supportUrlQuery}} /** * Convert the instance into URL query string. * * @return URL query string */ public String toUrlQueryString() { return toUrlQueryString(null); } /** * Convert the instance into URL query string. * * @param prefix prefix of the query string * @return URL query string */ public String toUrlQueryString(String prefix) { String suffix = ""; String containerSuffix = ""; String containerPrefix = ""; if (prefix == null) { // style=form, explode=true, e.g. /pet?name=cat&type=manx prefix = ""; } else { // deepObject style e.g. /pet?id[name]=cat&id[type]=manx prefix = prefix + "["; suffix = "]"; containerSuffix = "]"; containerPrefix = "["; } StringJoiner joiner = new StringJoiner("&"); {{#allVars}} // add `{{baseName}}` to the URL query string {{#isArray}} {{#items.isPrimitiveType}} {{#uniqueItems}} if ({{getter}}() != null) { int i = 0; for ({{{items.dataType}}} _item : {{getter}}()) { joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } i++; } {{/uniqueItems}} {{^uniqueItems}} if ({{getter}}() != null) { for (int i = 0; i < {{getter}}().size(); i++) { joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), URLEncoder.encode(String.valueOf({{getter}}().get(i)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } } {{/uniqueItems}} {{/items.isPrimitiveType}} {{^items.isPrimitiveType}} {{#items.isModel}} {{#uniqueItems}} if ({{getter}}() != null) { int i = 0; for ({{{items.dataType}}} _item : {{getter}}()) { if (_item != null) { joiner.add(_item.toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); } } i++; } {{/uniqueItems}} {{^uniqueItems}} if ({{getter}}() != null) { for (int i = 0; i < {{getter}}().size(); i++) { if ({{getter}}().get(i) != null) { joiner.add({{getter}}().get(i).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); } } } {{/uniqueItems}} {{/items.isModel}} {{^items.isModel}} {{#uniqueItems}} if ({{getter}}() != null) { int i = 0; for ({{{items.dataType}}} _item : {{getter}}()) { if (_item != null) { joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } i++; } } {{/uniqueItems}} {{^uniqueItems}} if ({{getter}}() != null) { for (int i = 0; i < {{getter}}().size(); i++) { if ({{getter}}().get(i) != null) { joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), URLEncoder.encode(String.valueOf({{getter}}().get(i)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } } } {{/uniqueItems}} {{/items.isModel}} {{/items.isPrimitiveType}} {{/isArray}} {{^isArray}} {{#isMap}} {{^items.isModel}} if ({{getter}}() != null) { for (String _key : {{getter}}().keySet()) { joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix), {{getter}}().get(_key), URLEncoder.encode(String.valueOf({{getter}}().get(_key)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } } {{/items.isModel}} {{#items.isModel}} if ({{getter}}() != null) { for (String _key : {{getter}}().keySet()) { if ({{getter}}().get(_key) != null) { joiner.add({{getter}}().get(_key).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix)))); } } } {{/items.isModel}} {{/isMap}} {{^isMap}} {{#isPrimitiveType}} if ({{getter}}() != null) { joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } {{/isPrimitiveType}} {{^isPrimitiveType}} {{#isModel}} if ({{getter}}() != null) { joiner.add({{getter}}().toUrlQueryString(prefix + "{{{baseName}}}" + suffix)); } {{/isModel}} {{^isModel}} if ({{getter}}() != null) { joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } {{/isModel}} {{/isPrimitiveType}} {{/isMap}} {{/isArray}} {{/allVars}} return joiner.toString(); } {{/supportUrlQuery}} {{#parcelableModel}} public void writeToParcel(Parcel out, int flags) { {{#model}} {{#isArray}} out.writeList(this); {{/isArray}} {{^isArray}} {{#parent}} super.writeToParcel(out, flags); {{/parent}} {{#vars}} out.writeValue({{name}}); {{/vars}} {{/isArray}} {{/model}} } {{classname}}(Parcel in) { {{#isArray}} in.readTypedList(this, {{arrayModelType}}.CREATOR); {{/isArray}} {{^isArray}} {{#parent}} super(in); {{/parent}} {{#vars}} {{#isPrimitiveType}} {{name}} = ({{{datatypeWithEnum}}})in.readValue(null); {{/isPrimitiveType}} {{^isPrimitiveType}} {{name}} = ({{{datatypeWithEnum}}})in.readValue({{complexType}}.class.getClassLoader()); {{/isPrimitiveType}} {{/vars}} {{/isArray}} } public int describeContents() { return 0; } public static final Parcelable.Creator<{{classname}}> CREATOR = new Parcelable.Creator<{{classname}}>() { public {{classname}} createFromParcel(Parcel in) { {{#model}} {{#isArray}} {{classname}} result = new {{classname}}(); result.addAll(in.readArrayList({{arrayModelType}}.class.getClassLoader())); return result; {{/isArray}} {{^isArray}} return new {{classname}}(in); {{/isArray}} {{/model}} } public {{classname}}[] newArray(int size) { return new {{classname}}[size]; } }; {{/parcelableModel}} {{#discriminator}} static { // Initialize and register the discriminator mappings. Map> mappings = new HashMap>(); {{#mappedModels}} mappings.put("{{mappingName}}", {{modelName}}.class); {{/mappedModels}} mappings.put("{{name}}", {{classname}}.class); JSON.registerDiscriminator({{classname}}.class, "{{propertyBaseName}}", mappings); } {{/discriminator}} } ================================================ FILE: client2/src/main/template/libraries/native/pojo.mustache.orig ================================================ {{#discriminator}} import {{invokerPackage}}.JSON; {{/discriminator}} /** * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}} * @deprecated{{/isDeprecated}} */{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{#swagger1AnnotationLibrary}} {{#description}} @ApiModel(description = "{{{.}}}") {{/description}} {{/swagger1AnnotationLibrary}} {{#swagger2AnnotationLibrary}} {{#description}} @Schema(description = "{{{.}}}") {{/description}} {{/swagger2AnnotationLibrary}} {{#jackson}} @JsonPropertyOrder({ {{#vars}} {{classname}}.JSON_PROPERTY_{{nameInSnakeCase}}{{^-last}},{{/-last}} {{/vars}} }) {{/jackson}} {{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} {{#vendorExtensions.x-class-extra-annotation}} {{{vendorExtensions.x-class-extra-annotation}}} {{/vendorExtensions.x-class-extra-annotation}} public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{ {{#serializableModel}} private static final long serialVersionUID = 1L; {{/serializableModel}} {{#vars}} {{#isEnum}} {{^isContainer}} {{^vendorExtensions.x-enum-as-string}} {{>modelInnerEnum}} {{/vendorExtensions.x-enum-as-string}} {{/isContainer}} {{#isContainer}} {{#mostInnerItems}} {{>modelInnerEnum}} {{/mostInnerItems}} {{/isContainer}} {{/isEnum}} {{#gson}} public static final String SERIALIZED_NAME_{{nameInSnakeCase}} = "{{baseName}}"; {{/gson}} {{#jackson}} public static final String JSON_PROPERTY_{{nameInSnakeCase}} = "{{baseName}}"; {{/jackson}} {{#withXml}} {{#isXmlAttribute}} @XmlAttribute(name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") {{/isXmlAttribute}} {{^isXmlAttribute}} {{^isContainer}} @XmlElement({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") {{/isContainer}} {{#isContainer}} // Is a container wrapped={{isXmlWrapped}} {{#items}} // items.name={{name}} items.baseName={{baseName}} items.xmlName={{xmlName}} items.xmlNamespace={{xmlNamespace}} // items.example={{example}} items.type={{dataType}} @XmlElement({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") {{/items}} {{#isXmlWrapped}} @XmlElementWrapper({{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") {{/isXmlWrapped}} {{/isContainer}} {{/isXmlAttribute}} {{/withXml}} {{#gson}} @SerializedName(SERIALIZED_NAME_{{nameInSnakeCase}}) {{/gson}} {{#vendorExtensions.x-field-extra-annotation}} {{{vendorExtensions.x-field-extra-annotation}}} {{/vendorExtensions.x-field-extra-annotation}} {{#vendorExtensions.x-is-jackson-optional-nullable}} {{#isContainer}} private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined(); {{/isContainer}} {{^isContainer}} private JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; {{/isContainer}} {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} private {{{datatypeWithEnum}}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; {{/vendorExtensions.x-is-jackson-optional-nullable}} {{/vars}} public {{classname}}() { {{#parent}}{{#parcelableModel}} super();{{/parcelableModel}}{{/parent}}{{#gson}}{{#discriminator}} this.{{{discriminatorName}}} = this.getClass().getSimpleName();{{/discriminator}}{{/gson}} }{{#vendorExtensions.x-has-readonly-properties}}{{^withXml}} {{#jackson}}@JsonCreator{{/jackson}} public {{classname}}( {{#readOnlyVars}} @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}} {{/readOnlyVars}} ) { this(); {{#readOnlyVars}} this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}}; {{/readOnlyVars}} }{{/withXml}}{{/vendorExtensions.x-has-readonly-properties}} {{#vars}} {{^isReadOnly}} {{#vendorExtensions.x-enum-as-string}} public static final Set {{{nameInSnakeCase}}}_VALUES = new HashSet<>(Arrays.asList( {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}} )); {{/vendorExtensions.x-enum-as-string}} public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{#vendorExtensions.x-enum-as-string}} if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) { throw new IllegalArgumentException({{name}} + " is invalid. Possible values for {{name}}: " + String.join(", ", {{{nameInSnakeCase}}}_VALUES)); } {{/vendorExtensions.x-enum-as-string}} {{#vendorExtensions.x-is-jackson-optional-nullable}} this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} this.{{name}} = {{name}}; {{/vendorExtensions.x-is-jackson-optional-nullable}} return this; } {{#isArray}} public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { {{#vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null || !this.{{name}}.isPresent()) { this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}); } try { this.{{name}}.get().add({{name}}Item); } catch (java.util.NoSuchElementException e) { // this can never happen, as we make sure above that the value is present } return this; {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null) { this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; } this.{{name}}.add({{name}}Item); return this; {{/vendorExtensions.x-is-jackson-optional-nullable}} } {{/isArray}} {{#isMap}} public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { {{#vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null || !this.{{name}}.isPresent()) { this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}); } try { this.{{name}}.get().put(key, {{name}}Item); } catch (java.util.NoSuchElementException e) { // this can never happen, as we make sure above that the value is present } return this; {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null) { this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; } this.{{name}}.put(key, {{name}}Item); return this; {{/vendorExtensions.x-is-jackson-optional-nullable}} } {{/isMap}} {{/isReadOnly}} /** {{#description}} * {{.}} {{/description}} {{^description}} * Get {{name}} {{/description}} {{#minimum}} * minimum: {{.}} {{/minimum}} {{#maximum}} * maximum: {{.}} {{/maximum}} * @return {{name}} {{#deprecated}} * @deprecated {{/deprecated}} **/ {{#deprecated}} @Deprecated {{/deprecated}} {{#required}} {{#isNullable}} @{{javaxPackage}}.annotation.Nullable {{/isNullable}} {{^isNullable}} @{{javaxPackage}}.annotation.Nonnull {{/isNullable}} {{/required}} {{^required}} @{{javaxPackage}}.annotation.Nullable {{/required}} {{#useBeanValidation}} {{>beanValidation}} {{/useBeanValidation}} {{#swagger1AnnotationLibrary}} @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}") {{/swagger1AnnotationLibrary}} {{#swagger2AnnotationLibrary}} @Schema({{#example}}example = "{{{.}}}", {{/example}}requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}, description = "{{{description}}}") {{/swagger2AnnotationLibrary}} {{#vendorExtensions.x-extra-annotation}} {{{vendorExtensions.x-extra-annotation}}} {{/vendorExtensions.x-extra-annotation}} {{#vendorExtensions.x-is-jackson-optional-nullable}} {{!unannotated, Jackson would pick this up automatically and add it *in addition* to the _JsonNullable getter field}} @JsonIgnore {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}}{{#jackson}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}} public {{{datatypeWithEnum}}} {{getter}}() { {{#vendorExtensions.x-is-jackson-optional-nullable}} {{#isReadOnly}}{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}} if ({{name}} == null) { {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; } {{/isReadOnly}} return {{name}}.orElse(null); {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} return {{name}}; {{/vendorExtensions.x-is-jackson-optional-nullable}} } {{#vendorExtensions.x-is-jackson-optional-nullable}} {{> jackson_annotations}} public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}_JsonNullable() { return {{name}}; } {{/vendorExtensions.x-is-jackson-optional-nullable}}{{#vendorExtensions.x-is-jackson-optional-nullable}} @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) {{#isReadOnly}}private{{/isReadOnly}}{{^isReadOnly}}public{{/isReadOnly}} void {{setter}}_JsonNullable(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) { {{! For getters/setters that have name differing from attribute name, we must include setter (albeit private) for jackson to be able to set the attribute}} this.{{name}} = {{name}}; } {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^isReadOnly}} {{#vendorExtensions.x-setter-extra-annotation}} {{{vendorExtensions.x-setter-extra-annotation}}} {{/vendorExtensions.x-setter-extra-annotation}}{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{> jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { {{#vendorExtensions.x-enum-as-string}} if (!{{{nameInSnakeCase}}}_VALUES.contains({{name}})) { throw new IllegalArgumentException({{name}} + " is invalid. Possible values for {{name}}: " + String.join(", ", {{{nameInSnakeCase}}}_VALUES)); } {{/vendorExtensions.x-enum-as-string}} {{#vendorExtensions.x-is-jackson-optional-nullable}} this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} this.{{name}} = {{name}}; {{/vendorExtensions.x-is-jackson-optional-nullable}} } {{/isReadOnly}} {{/vars}} {{>libraries/native/additional_properties}} {{#parent}} {{#allVars}} {{#isOverridden}} @Override public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{#vendorExtensions.x-is-jackson-optional-nullable}} this.{{setter}}(JsonNullable.<{{{datatypeWithEnum}}}>of({{name}})); {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} this.{{setter}}({{name}}); {{/vendorExtensions.x-is-jackson-optional-nullable}} return this; } {{/isOverridden}} {{/allVars}} {{/parent}} /** * Return true if this {{name}} object is equal to o. */ @Override public boolean equals(Object o) { {{#useReflectionEqualsHashCode}} return EqualsBuilder.reflectionEquals(this, o, false, null, true); {{/useReflectionEqualsHashCode}} {{^useReflectionEqualsHashCode}} if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; }{{#hasVars}} {{classname}} {{classVarName}} = ({{classname}}) o; return {{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}equalsNullable(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}} && {{/-last}}{{/vars}}{{#additionalPropertiesType}}&& Objects.equals(this.additionalProperties, {{classVarName}}.additionalProperties){{/additionalPropertiesType}}{{#parent}} && super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}} return {{#parent}}super.equals(o){{/parent}}{{^parent}}true{{/parent}};{{/hasVars}} {{/useReflectionEqualsHashCode}} }{{#vendorExtensions.x-jackson-optional-nullable-helpers}} private static boolean equalsNullable(JsonNullable a, JsonNullable b) { return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get())); }{{/vendorExtensions.x-jackson-optional-nullable-helpers}} @Override public int hashCode() { {{#useReflectionEqualsHashCode}} return HashCodeBuilder.reflectionHashCode(this); {{/useReflectionEqualsHashCode}} {{^useReflectionEqualsHashCode}} return Objects.hash({{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}hashCodeNullable({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}{{#additionalPropertiesType}}, additionalProperties{{/additionalPropertiesType}}); {{/useReflectionEqualsHashCode}} }{{#vendorExtensions.x-jackson-optional-nullable-helpers}} private static int hashCodeNullable(JsonNullable a) { if (a == null) { return 1; } return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31; }{{/vendorExtensions.x-jackson-optional-nullable-helpers}} @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class {{classname}} {\n"); {{#parent}} sb.append(" ").append(toIndentedString(super.toString())).append("\n"); {{/parent}} {{#vars}} sb.append(" {{name}}: ").append(toIndentedString({{name}})).append("\n"); {{/vars}} {{#additionalPropertiesType}} sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n"); {{/additionalPropertiesType}} sb.append("}"); return sb.toString(); } /** * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ private String toIndentedString(Object o) { if (o == null) { return "null"; } return o.toString().replace("\n", "\n "); } {{#supportUrlQuery}} /** * Convert the instance into URL query string. * * @return URL query string */ public String toUrlQueryString() { return toUrlQueryString(null); } /** * Convert the instance into URL query string. * * @param prefix prefix of the query string * @return URL query string */ public String toUrlQueryString(String prefix) { String suffix = ""; String containerSuffix = ""; String containerPrefix = ""; if (prefix == null) { // style=form, explode=true, e.g. /pet?name=cat&type=manx prefix = ""; } else { // deepObject style e.g. /pet?id[name]=cat&id[type]=manx prefix = prefix + "["; suffix = "]"; containerSuffix = "]"; containerPrefix = "["; } StringJoiner joiner = new StringJoiner("&"); {{#allVars}} // add `{{baseName}}` to the URL query string {{#isArray}} {{#items.isPrimitiveType}} {{#uniqueItems}} if ({{getter}}() != null) { int i = 0; for ({{{items.dataType}}} _item : {{getter}}()) { joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } i++; } {{/uniqueItems}} {{^uniqueItems}} if ({{getter}}() != null) { for (int i = 0; i < {{getter}}().size(); i++) { joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), URLEncoder.encode(String.valueOf({{getter}}().get(i)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } } {{/uniqueItems}} {{/items.isPrimitiveType}} {{^items.isPrimitiveType}} {{#items.isModel}} {{#uniqueItems}} if ({{getter}}() != null) { int i = 0; for ({{{items.dataType}}} _item : {{getter}}()) { if (_item != null) { joiner.add(_item.toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); } } i++; } {{/uniqueItems}} {{^uniqueItems}} if ({{getter}}() != null) { for (int i = 0; i < {{getter}}().size(); i++) { if ({{getter}}().get(i) != null) { joiner.add({{getter}}().get(i).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); } } } {{/uniqueItems}} {{/items.isModel}} {{^items.isModel}} {{#uniqueItems}} if ({{getter}}() != null) { int i = 0; for ({{{items.dataType}}} _item : {{getter}}()) { if (_item != null) { joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), URLEncoder.encode(String.valueOf(_item), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } i++; } } {{/uniqueItems}} {{^uniqueItems}} if ({{getter}}() != null) { for (int i = 0; i < {{getter}}().size(); i++) { if ({{getter}}().get(i) != null) { joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), URLEncoder.encode(String.valueOf({{getter}}().get(i)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } } } {{/uniqueItems}} {{/items.isModel}} {{/items.isPrimitiveType}} {{/isArray}} {{^isArray}} {{#isMap}} {{^items.isModel}} if ({{getter}}() != null) { for (String _key : {{getter}}().keySet()) { joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix), {{getter}}().get(_key), URLEncoder.encode(String.valueOf({{getter}}().get(_key)), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } } {{/items.isModel}} {{#items.isModel}} if ({{getter}}() != null) { for (String _key : {{getter}}().keySet()) { if ({{getter}}().get(_key) != null) { joiner.add({{getter}}().get(_key).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix)))); } } } {{/items.isModel}} {{/isMap}} {{^isMap}} {{#isPrimitiveType}} if ({{getter}}() != null) { joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } {{/isPrimitiveType}} {{^isPrimitiveType}} {{#isModel}} if ({{getter}}() != null) { joiner.add({{getter}}().toUrlQueryString(prefix + "{{{baseName}}}" + suffix)); } {{/isModel}} {{^isModel}} if ({{getter}}() != null) { joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), StandardCharsets.UTF_8).replaceAll("\\+", "%20"))); } {{/isModel}} {{/isPrimitiveType}} {{/isMap}} {{/isArray}} {{/allVars}} return joiner.toString(); } {{/supportUrlQuery}} {{#parcelableModel}} public void writeToParcel(Parcel out, int flags) { {{#model}} {{#isArray}} out.writeList(this); {{/isArray}} {{^isArray}} {{#parent}} super.writeToParcel(out, flags); {{/parent}} {{#vars}} out.writeValue({{name}}); {{/vars}} {{/isArray}} {{/model}} } {{classname}}(Parcel in) { {{#isArray}} in.readTypedList(this, {{arrayModelType}}.CREATOR); {{/isArray}} {{^isArray}} {{#parent}} super(in); {{/parent}} {{#vars}} {{#isPrimitiveType}} {{name}} = ({{{datatypeWithEnum}}})in.readValue(null); {{/isPrimitiveType}} {{^isPrimitiveType}} {{name}} = ({{{datatypeWithEnum}}})in.readValue({{complexType}}.class.getClassLoader()); {{/isPrimitiveType}} {{/vars}} {{/isArray}} } public int describeContents() { return 0; } public static final Parcelable.Creator<{{classname}}> CREATOR = new Parcelable.Creator<{{classname}}>() { public {{classname}} createFromParcel(Parcel in) { {{#model}} {{#isArray}} {{classname}} result = new {{classname}}(); result.addAll(in.readArrayList({{arrayModelType}}.class.getClassLoader())); return result; {{/isArray}} {{^isArray}} return new {{classname}}(in); {{/isArray}} {{/model}} } public {{classname}}[] newArray(int size) { return new {{classname}}[size]; } }; {{/parcelableModel}} {{#discriminator}} static { // Initialize and register the discriminator mappings. Map> mappings = new HashMap>(); {{#mappedModels}} mappings.put("{{mappingName}}", {{modelName}}.class); {{/mappedModels}} mappings.put("{{name}}", {{classname}}.class); JSON.registerDiscriminator({{classname}}.class, "{{propertyBaseName}}", mappings); } {{/discriminator}} } ================================================ FILE: client2/src/test/java/com/walmartlabs/concord/client2/ApiClientJsonTest.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import java.text.SimpleDateFormat; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Collections; import java.util.Date; import static org.junit.jupiter.api.Assertions.assertEquals; public class ApiClientJsonTest { @Test public void testParseDate() throws Exception { ObjectMapper om = ApiClient.createDefaultObjectMapper(); Date date = new Date(1587500112000L); OffsetDateTime offsetDateTime = date.toInstant().atOffset(ZoneOffset.UTC); String toParse = om.writeValueAsString(offsetDateTime); toParse = toParse.substring(1, toParse.length() - 1); // format like a sever entries Date parsed = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"). parse(toParse); assertEquals(date, parsed); } @Test public void testObjectSerialize() throws Exception { ProjectEntry project = new ProjectEntry() .name(""); String str = ApiClient.createDefaultObjectMapper().writeValueAsString(project); assertEquals("{\"name\":\"\"}", str); } @Test public void testEmptyCollectionSerialize() throws Exception { CreateUserRequest user = new CreateUserRequest() .username("test") .roles(Collections.emptySet()); String str = ApiClient.createDefaultObjectMapper().writeValueAsString(user); assertEquals("{\"username\":\"test\",\"roles\":[]}", str); } } ================================================ FILE: client2/src/test/java/com/walmartlabs/concord/client2/ProcessApiTest.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.client2.impl.auth.ApiKey; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import javax.xml.bind.DatatypeConverter; import java.util.UUID; public class ProcessApiTest { @Test @Disabled public void testDecrypt() throws Exception { ApiClient apiClient = new DefaultApiClientFactory("http://localhost:8001").create(); apiClient.setAuth(new ApiKey("cTFxMXExcTE=")); ProjectsApi projectsApi = new ProjectsApi(apiClient); EncryptValueResponse encrypted = projectsApi.encrypt("org_1692633472807_3d32f7", "project_1692633472833_a1a531", "123qwe"); String encryptedValue = encrypted.getData(); System.out.println(">>>" + encryptedValue); byte[] input; try { input = DatatypeConverter.parseBase64Binary(encryptedValue); } catch (Exception e) { throw new IllegalArgumentException("Invalid encrypted string value, please verify that it was specified/copied correctly: " + e.getMessage()); } ProcessApi api = new ProcessApi(apiClient); UUID pid = UUID.fromString("f891d797-d97e-4724-b0ba-91d48efce6d8"); System.out.println(">>>'" + new String(api.decryptString(pid, input)) + "'"); } } ================================================ FILE: client2/src/test/java/com/walmartlabs/concord/client2/SecretClientTest.java ================================================ package com.walmartlabs.concord.client2; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.walmartlabs.concord.client2.impl.auth.ApiKey; import com.walmartlabs.concord.common.secret.BinaryDataSecret; import com.walmartlabs.concord.sdk.Constants; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static org.junit.jupiter.api.Assertions.assertTrue; @WireMockTest public class SecretClientTest { @Test public void testInvalidSecretType(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { String orgName = "org_" + System.currentTimeMillis(); String secretName = "secret_" + System.currentTimeMillis(); stubFor(post(urlEqualTo("/api/v1/org/" + orgName + "/secret/" + secretName + "/data")) .willReturn(aResponse() .withStatus(200) .withHeader(Constants.Headers.SECRET_TYPE, SecretEntryV2.TypeEnum.DATA.name()) .withBody("Hello!"))); ApiClient apiClient = new DefaultApiClientFactory("http://localhost:" + wmRuntimeInfo.getHttpPort()).create(); SecretClient secretClient = new SecretClient(apiClient); try { secretClient.getData(orgName, secretName, null, SecretEntryV2.TypeEnum.KEY_PAIR); } catch (IllegalArgumentException e) { assertTrue(e.getMessage().contains("Unexpected type of " + orgName + "/" + secretName)); } } @Test @Disabled public void testGetSecret() throws Exception { ApiClient apiClient = new DefaultApiClientFactory("http://localhost:8001").create(); apiClient.setAuth(new ApiKey("cTFxMXExcTE")); SecretClient secretClient = new SecretClient(apiClient); BinaryDataSecret result = secretClient.getData("Default", "test", null, SecretEntryV2.TypeEnum.DATA); System.out.println(">>> '" + new String(result.getData()) + "'"); } } ================================================ FILE: client2/src/test/java/com/walmartlabs/concord/client2/impl/MultipartRequestBodyHandlerTest.java ================================================ package com.walmartlabs.concord.client2.impl; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; public class MultipartRequestBodyHandlerTest { @Test public void test2() throws Exception { Map data = new LinkedHashMap<>(); data.put("string-field", "this stuff"); data.put("byte[]-field", "byte array".getBytes()); data.put("inputstream-field", new ByteArrayInputStream("my input stream".getBytes())); data.put("boolean-field", true); data.put("json-field", Collections.singletonMap("k", "v")); data.put("string[]-field", new String[] {"one", "two"}); data.put("uuid-field", UUID.fromString("f8d30c37-4c84-4817-9cb8-23b27a54c459")); MultipartBuilder mpb = new MultipartBuilder("e572b648-941a-4648-97ed-0e3c5350f0ad"); HttpEntity entity = MultipartRequestBodyHandler.handle(mpb, new ObjectMapper(), data); try (InputStream is = entity.getContent()) { String str = new String(is.readAllBytes(), StandardCharsets.UTF_8); assertEquals(body, str); assertEquals("multipart/form-data; boundary=e572b648-941a-4648-97ed-0e3c5350f0ad", entity.contentType().toString()); } } private static final String body = "--e572b648-941a-4648-97ed-0e3c5350f0ad\r\n" + "Content-Disposition: form-data; name=\"string-field\"\r\n" + "Content-Length: 10\r\n" + "\r\n" + "this stuff\r\n" + "--e572b648-941a-4648-97ed-0e3c5350f0ad\r\n" + "Content-Disposition: form-data; name=\"byte[]-field\"\r\n" + "Content-Type: application/octet-stream\r\n" + "Content-Length: 10\r\n" + "\r\n" + "byte array\r\n" + "--e572b648-941a-4648-97ed-0e3c5350f0ad\r\n" + "Content-Disposition: form-data; name=\"inputstream-field\"\r\n" + "Content-Type: application/octet-stream\r\n" + "\r\n" + "my input stream\r\n" + "--e572b648-941a-4648-97ed-0e3c5350f0ad\r\n" + "Content-Disposition: form-data; name=\"boolean-field\"\r\n" + "Content-Type: text/plain; charset=utf-8\r\n" + "Content-Length: 4\r\n" + "\r\n" + "true\r\n" + "--e572b648-941a-4648-97ed-0e3c5350f0ad\r\n" + "Content-Disposition: form-data; name=\"json-field\"\r\n" + "Content-Type: application/json; charset=utf-8\r\n" + "Content-Length: 9\r\n" + "\r\n" + "{\"k\":\"v\"}\r\n" + "--e572b648-941a-4648-97ed-0e3c5350f0ad\r\n" + "Content-Disposition: form-data; name=\"string[]-field\"\r\n" + "Content-Type: text/plain; charset=utf-8\r\n" + "Content-Length: 7\r\n" + "\r\n" + "one,two\r\n" + "--e572b648-941a-4648-97ed-0e3c5350f0ad\r\n" + "Content-Disposition: form-data; name=\"uuid-field\"\r\n" + "Content-Length: 36\r\n" + "\r\n" + "f8d30c37-4c84-4817-9cb8-23b27a54c459\r\n" + "--e572b648-941a-4648-97ed-0e3c5350f0ad--" + "\r\n"; } ================================================ FILE: common/pom.xml ================================================ 4.0.0 com.walmartlabs.concord parent 2.40.1-SNAPSHOT ../pom.xml concord-common jar ${project.groupId}:${project.artifactId} com.walmartlabs.concord concord-sdk io.takari.bpm bpm-engine-api provided io.takari.bpm bpm-engine-impl javax.validation validation-api provided org.slf4j slf4j-api provided org.apache.commons commons-compress provided commons-validator commons-validator provided org.immutables value provided com.fasterxml.jackson.core jackson-databind provided com.fasterxml.jackson.datatype jackson-datatype-jdk8 provided javax.inject javax.inject provided com.fasterxml.jackson.datatype jackson-datatype-guava provided com.fasterxml.jackson.datatype jackson-datatype-jsr310 provided javax.xml.bind jaxb-api provided com.sun.xml.bind jaxb-impl provided org.junit.jupiter junit-jupiter-api test org.mockito mockito-core test org.mockito mockito-junit-jupiter test org.eclipse.sisu sisu-maven-plugin org.apache.maven.plugins maven-surefire-plugin ${java.io.tmpdir} ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/AllowNulls.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.lang.annotation.ElementType; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE}) public @interface AllowNulls {} ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/AuthTokenProvider.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.cfg.MappingAuthConfig; import com.walmartlabs.concord.common.cfg.OauthTokenConfig; import com.walmartlabs.concord.common.secret.BinaryDataSecret; import com.walmartlabs.concord.sdk.Secret; import javax.annotation.Nullable; import javax.inject.Inject; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; import static com.walmartlabs.concord.common.cfg.MappingAuthConfig.assertBaseUrlPattern; public interface AuthTokenProvider { /** * @return {@code true} if this the given repo URI and secret are compatible * with this provider's {@link #getToken(URI, Secret)} method, * {@code false} otherwise. */ boolean supports(URI repo, @Nullable Secret secret); Optional getToken(URI repo, @Nullable Secret secret); default URI addUserInfoToUri(URI repo, @Nullable Secret secret) { if (!supports(repo, secret)) { // not compatible with auth provider(s) return repo; } return getToken(repo, secret) .map(expiringToken -> { var token = expiringToken.token(); var userInfo = expiringToken.username() != null ? expiringToken.username() + ":" + token : token; try { return new URI(repo.getScheme(), userInfo, repo.getHost(), repo.getPort(), repo.getPath(), repo.getQuery(), repo.getFragment()); } catch (URISyntaxException e) { return null; } }) .orElse(repo); } @SuppressWarnings("ClassCanBeRecord") class OauthTokenProvider implements AuthTokenProvider { // >0 length, printable ascii (no newlines, etc) private static final Pattern BASIC_STRING_PTN = Pattern.compile("[ -~]+"); private final List authConfigs; @Inject public OauthTokenProvider(OauthTokenConfig config) { this.authConfigs = toConfigList(config); } @Override public boolean supports(URI repo, @Nullable Secret secret) { return validateSecret(secret) || systemSupports(repo); } @Override public Optional getToken(URI repo, @Nullable Secret secret) { if (secret != null) { if (secret instanceof BinaryDataSecret bds) { return Optional.of(ExternalAuthToken.StaticToken.builder() .token(new String(bds.getData())) .build()); } else { return Optional.empty(); } } return authConfigs.stream() .filter(auth -> auth.canHandle(repo)) .filter(MappingAuthConfig.OauthAuthConfig.class::isInstance) .map(MappingAuthConfig.OauthAuthConfig.class::cast) .findFirst() .map(auth -> ExternalAuthToken.StaticToken.builder() .authId(auth.id()) .token(auth.token()) .username(auth.username()) .build()); } private boolean validateSecret(Secret secret) { if (secret == null) { return false; } if (!(secret instanceof BinaryDataSecret bds)) { // this class is not the place for handling key pairs or username/password return false; } else { var data = new String(bds.getData()); return BASIC_STRING_PTN.matcher(data).matches(); } } private boolean systemSupports(URI repoUri) { return authConfigs.stream().anyMatch(auth -> auth.canHandle(repoUri)); } private static List toConfigList(OauthTokenConfig config) { var token = config.getOauthToken().orElse(null); if (token == null || token.isBlank() && config.getSystemAuth().isEmpty()) { return config.getSystemAuth(); } return List.of(MappingAuthConfig.OauthAuthConfig.builder() .id("static-token") .token(token) .username(config.getOauthUsername().orElse(null)) .urlPattern(assertBaseUrlPattern(config.getOauthUrlPattern().orElse(".*")))// for backwards compat with git.oauth .build()); } } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/ConfigurationUtils.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.*; public final class ConfigurationUtils { @SuppressWarnings("unchecked") public static boolean has(Map m, String[] path) { if (m == null) { return false; } if (path.length == 0) { return false; } for (int i = 0; i < path.length - 1; i++) { Object v = m.get(path[i]); if (!(v instanceof Map)) { return false; } m = (Map) v; } return m.containsKey(path[path.length - 1]); } public static Object get(Map m, String... path) { int depth = path != null ? path.length : 0; return get(m, depth, path); } @SuppressWarnings("unchecked") public static Object get(Map m, int depth, String... path) { if (m == null) { return null; } if (depth == 0) { return m; } for (int i = 0; i < depth - 1; i++) { Object v = m.get(path[i]); if (v == null) { return null; } if (!(v instanceof Map)) { throw new IllegalArgumentException("Invalid data type, expected JSON object, got: " + v.getClass()); } m = (Map) v; } return m.get(path[depth - 1]); } @SuppressWarnings("unchecked") public static void set(Map a, Object b, String... path) { Object holder = get(a, path.length - 1, path); if (holder != null && !(holder instanceof Map)) { throw new IllegalArgumentException("Value should be contained in a JSON object: " + String.join("/", path)); } Map m = (Map) holder; // TODO automatically create the value holder? assert m != null; m.put(path[path.length - 1], b); } @SuppressWarnings("unchecked") public static void delete(Map a, String... path) { Object holder = get(a, path.length - 1, path); if (holder == null) { return; } if (!(holder instanceof Map)) { throw new IllegalArgumentException("Value should be contained in a JSON object: " + String.join("/", path)); } Map m = (Map) holder; m.remove(path[path.length - 1]); } @SuppressWarnings("unchecked") public static void merge(Map a, Map b, String... path) { Object holder = get(a, path); if (holder != null && !(holder instanceof Map)) { throw new IllegalArgumentException("Existing value is not a JSON object: " + holder + " @ " + String.join("/", path)); } Map m = (Map) holder; // TODO automatically create the value holder? assert m != null; m.putAll(b); } @SuppressWarnings("unchecked") public static Map deepMerge(Map a, Map b) { Map result = new LinkedHashMap<>(a != null ? a : Collections.emptyMap()); for (String k : b.keySet()) { Object av = result.get(k); Object bv = b.get(k); Object o = bv; if (av instanceof Map && bv instanceof Map) { o = deepMerge((Map) av, (Map) bv); } // preserve the order of the keys if (result.containsKey(k)) { result.replace(k, o); } else { result.put(k, o); } } return result; } @SafeVarargs public static Map deepMerge(Map... maps) { if (maps == null || maps.length == 0) { return Collections.emptyMap(); } Map result = new LinkedHashMap<>(maps[0]); for (int i = 1; i < maps.length; i++) { result = deepMerge(result, maps[i]); } return result; } public static Map toNested(String k, Object v) { String[] as = k.split("\\."); if (as.length == 1) { return Collections.singletonMap(k, v); } Map m = new LinkedHashMap<>(); Map root = m; for (int i = 0; i < as.length; i++) { if (i + 1 >= as.length) { m.put(as[i], v); } else { Map mm = new LinkedHashMap<>(); m.put(as[i], mm); m = mm; } } return root; } @SuppressWarnings("rawtypes") public static boolean deepEquals(Object a, Object b) { if (!Objects.deepEquals(a, b)) { return false; } if (a instanceof Map && b instanceof Map) { return equals((Map) a, (Map) b); } else if (a instanceof Collection && b instanceof Collection) { return equals((Collection) a, (Collection) b); } return true; } @SafeVarargs public static Set distinct(Collection... collections) { Set result = new HashSet<>(); if (collections != null) { for (Collection coll : collections) { if (coll != null) { result.addAll(coll); } } } return result; } private static boolean equals(Map a, Map b) { if (a.keySet().size() != b.keySet().size()) { return false; } for (Map.Entry aEntry : a.entrySet()) { Object aValue = aEntry.getValue(); Object bValue = b.get(aEntry.getKey()); if (!deepEquals(aValue, bValue)) { return false; } } return true; } private static boolean equals(Collection a, Collection b) { if (a.size() != b.size()) { return false; } Iterator aIterator = a.iterator(); Iterator bIterator = b.iterator(); while (aIterator.hasNext()) { Object aValue = aIterator.next(); Object bValue = bIterator.next(); if (!deepEquals(aValue, bValue)) { return false; } } return true; } public static boolean isNestedKey(String key) { return key.contains("."); } private ConfigurationUtils() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/CycleChecker.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.*; public final class CycleChecker { public static class CheckResult { private final boolean hasCycle; private final String node1; private final String node2; private CheckResult(boolean hasCycle, String node1, String node2) { this.hasCycle = hasCycle; this.node1 = node1; this.node2 = node2; } public static CheckResult noCycle() { return new CheckResult(false, null, null); } public static CheckResult cycle(String node1, String node2) { return new CheckResult(true, node1, node2); } public boolean isHasCycle() { return hasCycle; } public String getNode1() { return node1; } public String getNode2() { return node2; } @Override public String toString() { return hasCycle ? getNode1() + " <-> " + getNode2() : "no cycle"; } } public static CheckResult check(Map m) { return check("root", m); } public static CheckResult check(String rootName, Map m) { Deque visited = new ArrayDeque<>(); return hasCycle(new N(rootName, m), visited); } private static CheckResult hasCycle(N node, Deque visited) { if (node.getObject() == null) { return CheckResult.noCycle(); } N n = find(visited, node); if (n != null) { return CheckResult.cycle(node.path, n.path); } visited.push(node); for (N nextNode : getNeighbours(node)) { CheckResult result = hasCycle(nextNode, visited); if (result.hasCycle) { return result; } } visited.pop(); return CheckResult.noCycle(); } private static N find(Collection s, N n) { for (N v : s) { if (v.equals(n)) { return v; } } return null; } @SuppressWarnings("unchecked") private static List getNeighbours(N n) { Object element = n.getObject(); if (element instanceof Map) { Map m = (Map) element; List result = new ArrayList<>(m.size()); m.forEach((key, value) -> result.add(new N(n.getPath() + "." + key, value))); return result; } else if (element instanceof Collection) { Collection c = (Collection) element; List result = new ArrayList<>(c.size()); c.forEach(v -> result.add(new N(n.getPath(), v))); return result; } return Collections.emptyList(); } private static class N { private final String path; private final Object object; public N(String path, Object object) { this.path = path; this.object = object; } public String getPath() { return path; } public Object getObject() { return object; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; N n = (N) o; return object == n.object; } @Override public int hashCode() { return Objects.hash(object); } @Override public String toString() { return getPath(); } } private CycleChecker() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/DateTimeUtils.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; public final class DateTimeUtils { private static final DateTimeFormatter FORMAT = DateTimeFormatter.ISO_OFFSET_DATE_TIME; /** * Formats the supplied {@link OffsetDateTime} to an ISO-8601 string * ({@code yyyy-MM-ddTHH:mm:ss.SSSX}) */ public static String toIsoString(OffsetDateTime t) { return FORMAT.format(t); } /** * Parses the supplied {@code text} as an ISO-8601 date/time value * ({@code yyyy-MM-ddTHH:mm:ss.SSSX}). */ public static OffsetDateTime fromIsoString(CharSequence text) { return OffsetDateTime.parse(text, FORMAT); } private DateTimeUtils() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/DockerProcessBuilder.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.sdk.Constants; import com.walmartlabs.concord.sdk.Context; import com.walmartlabs.concord.sdk.DockerContainerSpec; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; public class DockerProcessBuilder { public static DockerProcessBuilder from(UUID txId, DockerContainerSpec spec) { DockerProcessBuilder b = new DockerProcessBuilder(spec.image()) .name(spec.name()) .user(Optional.ofNullable(spec.user()).orElse(DEFAULT_USER)) .workdir(spec.workdir()) .entryPoint(spec.entryPoint()) .cpu(spec.cpu()) .memory(spec.memory()) .args(spec.args()) .env(spec.env()) .envFile(spec.envFile()) .labels(spec.labels()) .debug(spec.debug()) .forcePull(spec.forcePull()) .stdOutFilePath(spec.stdOutFilePath()) .redirectErrorStream(spec.redirectErrorStream()); DockerContainerSpec.Options options = spec.options(); if (options != null) { DockerOptionsBuilder ob = new DockerOptionsBuilder(); List hosts = options.hosts(); if (hosts != null) { hosts.forEach(ob::etcHost); } b.options(ob.build()); } // system stuff b.addLabel(CONCORD_TX_ID_LABEL, txId.toString()); return b; } public static DockerProcessBuilder from(Context ctx, DockerContainerSpec spec) { String txId = (String) ctx.getVariable(Constants.Context.TX_ID_KEY); return from(UUID.fromString(txId), spec); } private static final Logger log = LoggerFactory.getLogger(DockerProcessBuilder.class); public static final String CONCORD_DOCKER_LOCAL_MODE_KEY = "CONCORD_DOCKER_LOCAL_MODE"; public static final String CONCORD_DOCKER_DEFAULT_USER_KEY = "CONCORD_DOCKER_DEFAULT_USER"; public static final String CONCORD_DOCKER_USE_CONTAINER_USER_KEY = "CONCORD_DOCKER_USE_CONTAINER_USER"; public static final String CONCORD_TX_ID_LABEL = "concordTxId"; private static final String DEFAULT_USER; static { String s = System.getenv(CONCORD_DOCKER_DEFAULT_USER_KEY); if (s != null) { DEFAULT_USER = s; } else { DEFAULT_USER = "456"; // as in dockerPasswd } } private final String image; private String name; private String user = DEFAULT_USER; private String workdir; private String entryPoint; private String cpu; private String memory; private String stdOutFilePath; private final List args = new ArrayList<>(); private Map env; private String envFile; private Map labels; private Collection volumes = new ArrayList<>(); private List> options = new ArrayList<>(); private boolean cleanup = true; private boolean debug = false; private boolean forcePull = true; private boolean generateUsers = false; private boolean exposeHostUsers = false; private boolean useHostUser = false; private boolean useHostNetwork = true; private boolean useContainerUser; private boolean redirectErrorStream = true; private final List tmpPaths = new ArrayList<>(); public DockerProcessBuilder(String image) { this.image = image; if (Boolean.parseBoolean(env(CONCORD_DOCKER_LOCAL_MODE_KEY, "true"))) { // in the "local docker mode" we run all Docker processes using the current OS user's UID/GID // in order to do that, we need to mount the local /etc/passwd inside of the container log.warn("Running in the local Docker mode. Consider setting {}=false in the production environment.", CONCORD_DOCKER_LOCAL_MODE_KEY); this.exposeHostUsers = true; this.useHostUser = true; } else { this.generateUsers = true; } this.useContainerUser = Boolean.parseBoolean(env(CONCORD_DOCKER_USE_CONTAINER_USER_KEY, "false")); } public DockerProcess build() throws IOException { String[] cmd = buildCmd(); if (debug) { log.info("CMD: {}", (Object) cmd); } return new DockerProcess(cmd, redirectErrorStream, tmpPaths); } private String[] buildCmd() throws IOException { if (forcePull) { return new String[]{"/bin/bash", "-c", "docker pull " + q(image) + " && " + buildDockerCmd()}; } else { return new String[]{"/bin/bash", "-c", buildDockerCmd()}; } } private String buildDockerCmd() throws IOException { List c = new ArrayList<>(); c.add("docker"); c.add("run"); if (name != null) { c.add("--name"); c.add(q(name)); } if (user != null && !useHostUser && !useContainerUser) { c.add("-u"); c.add(user); } if (cleanup) { c.add("--rm"); } c.add("-i"); if (volumes != null) { volumes.forEach(v -> { c.add("-v"); c.add(q(v)); }); } if (env != null) { env.forEach((k, v) -> { c.add("-e"); c.add(q(k + "=" + v)); }); } if (envFile != null) { c.add("--env-file"); c.add(q(envFile)); } if (workdir != null) { c.add("-w"); c.add(q(workdir)); } if (labels != null) { for (Map.Entry l : labels.entrySet()) { String k = l.getKey(); String v = l.getValue(); c.add("--label"); c.add(q(k + (v != null ? "=" + v : ""))); } } if (entryPoint != null) { c.add("--entrypoint"); c.add(entryPoint); } if (generateUsers) { Path tmp = PathUtils.createTempFile("passwd", ".docker"); // NOSONAR tmpPaths.add(tmp); try (InputStream src = Objects.requireNonNull(DockerProcessBuilder.class.getResourceAsStream("dockerPasswd")); OutputStream dst = Files.newOutputStream(tmp)) { src.transferTo(dst); } c.add("-v"); c.add(tmp.toAbsolutePath() + ":/etc/passwd:ro"); } if (exposeHostUsers) { c.add("-v"); c.add("/etc/passwd:/etc/passwd:ro"); } if (useHostUser && !useContainerUser) { c.add("-u"); c.add("`id -u`:`id -g`"); c.add("-e"); c.add("HOME=/tmp"); } if (useHostNetwork) { c.add("--net=host"); } if (cpu != null) { c.add("--cpus"); c.add(cpu); } if (memory != null) { c.add("-m"); c.add(memory); } options.forEach(o -> { c.add(o.getKey()); if (o.getValue() != null) { c.add(o.getValue()); } }); c.add(q(image)); args.forEach(a -> c.add(q(a))); if (stdOutFilePath != null) { c.add(0, "set -o pipefail && "); c.add("| tee "); c.add(stdOutFilePath); } return String.join(" ", c); } public DockerProcessBuilder cpu(String cpu) { this.cpu = cpu; return this; } public DockerProcessBuilder memory(String memory) { this.memory = memory; return this; } public DockerProcessBuilder stdOutFilePath(String stdOutFilePath) { this.stdOutFilePath = stdOutFilePath; return this; } public DockerProcessBuilder name(String name) { this.name = name; return this; } public DockerProcessBuilder user(String user) { this.user = user; return this; } public DockerProcessBuilder labels(Map labels) { this.labels = labels; return this; } public DockerProcessBuilder addLabel(String k, String v) { if (labels == null) { labels = new HashMap<>(); } labels.put(k, v); return this; } public DockerProcessBuilder debug(boolean debug) { this.debug = debug; return this; } public DockerProcessBuilder workdir(String workdir) { this.workdir = workdir; return this; } public DockerProcessBuilder volumes(Collection volumes) { this.volumes = volumes; return this; } public DockerProcessBuilder volume(String spec) { volumes.add(spec); return this; } public DockerProcessBuilder volume(String hostSrc, String containerDest) { volumes.add(hostSrc + ":" + containerDest); return this; } public DockerProcessBuilder volume(String hostSrc, String containerDest, boolean readOnly) { volumes.add(hostSrc + ":" + containerDest + (readOnly ? ":ro" : ":rw")); return this; } public DockerProcessBuilder cleanup(boolean cleanup) { this.cleanup = cleanup; return this; } public DockerProcessBuilder args(List args) { if (args == null) { return this; } this.args.addAll(args); return this; } public DockerProcessBuilder arg(String v) { this.args.add(v); return this; } public DockerProcessBuilder arg(String k, String v) { this.args.add(k); this.args.add(v); return this; } public DockerProcessBuilder env(Map env) { this.env = env; return this; } public DockerProcessBuilder envFile(String envFile) { this.envFile = envFile; return this; } public DockerProcessBuilder entryPoint(String entryPoint) { this.entryPoint = entryPoint; return this; } public DockerProcessBuilder forcePull(boolean forcePull) { this.forcePull = forcePull; return this; } public DockerProcessBuilder useHostNetwork(boolean useHostNetwork) { this.useHostNetwork = useHostNetwork; return this; } public DockerProcessBuilder options(List> options) { this.options = options; return this; } public DockerProcessBuilder option(String k, String v) { this.options.add(new AbstractMap.SimpleEntry<>(k, v)); return this; } public DockerProcessBuilder redirectErrorStream(boolean redirectErrorStream) { this.redirectErrorStream = redirectErrorStream; return this; } public DockerProcessBuilder useContainerUser(boolean useContainerUser) { this.useContainerUser = useContainerUser; return this; } private static String q(String s) { if (s == null) { return null; } return "'" + s + "'"; } private static String env(String k, String defaultValue) { String s = System.getenv(k); return s != null ? s : defaultValue; } public static class DockerOptionsBuilder { private final List> options = new ArrayList<>(); public DockerOptionsBuilder etcHost(String host) { this.options.add(new AbstractMap.SimpleEntry<>("--add-host", host)); return this; } public List> build() { return options; } } public static class DockerProcess implements AutoCloseable { private final String[] cmd; private final boolean redirectErrorStream; private final List tmpPaths; public DockerProcess(String[] cmd, boolean redirectErrorStream, List tmpPaths) { this.cmd = cmd; this.redirectErrorStream = redirectErrorStream; this.tmpPaths = tmpPaths; } public Process start() throws IOException { return PrivilegedAction.perform("docker", () -> new ProcessBuilder(cmd) .redirectErrorStream(redirectErrorStream) .start()); } public String[] cmd() { return cmd; } @Override public void close() { for (Path p : tmpPaths) { try { PathUtils.deleteRecursively(p); } catch (IOException e) { log.warn("delete '{}' -> error: {}", p, e.getMessage()); } } } } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/DynamicTaskRegistry.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.sdk.Task; public interface DynamicTaskRegistry { Task getByKey(String key); void register(Class c); } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/ExceptionUtils.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.ArrayList; import java.util.List; import java.util.Optional; public final class ExceptionUtils { public static List getExceptionList(Throwable e) { List list = new ArrayList<>(); while (e != null && !list.contains(e)) { list.add(e); e = e.getCause(); } return list; } @SuppressWarnings("unchecked") public static T findLastException(T e, Class clazz) { var exceptions = getExceptionList(e); for (int i = exceptions.size() - 1; i >= 0; i--) { var ex = exceptions.get(i); if (clazz.isInstance(ex)) { return (T) ex; } } return e; } private ExceptionUtils() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/ExternalAuthToken.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import org.immutables.value.Value; import javax.annotation.Nullable; import java.time.Duration; import java.time.OffsetDateTime; @JsonDeserialize(as = ImmutableSimpleToken.class) public interface ExternalAuthToken { @Nullable @JsonProperty("auth_id") String authId(); @JsonProperty("token") String token(); @Nullable @JsonProperty("username") String username(); @Nullable @JsonProperty("expires_at") // GitHub gives time in seconds, but most parsers (e.g. jackson) expect milliseconds @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]X") OffsetDateTime expiresAt(); @Value.Default @JsonIgnore default long secondsUntilExpiration() { if (expiresAt() == null) { return Long.MAX_VALUE; } var d = Duration.between(OffsetDateTime.now(), expiresAt()); return d.getSeconds(); } /** * Basic implementation of an expiring token. */ @Value.Immutable @Value.Style(jdkOnly = true) interface SimpleToken extends ExternalAuthToken { static ImmutableSimpleToken.Builder builder() { return ImmutableSimpleToken.builder(); } } /** * A token that effectively never expires. */ @Value.Immutable @Value.Style(jdkOnly = true) interface StaticToken extends ExternalAuthToken { @Value.Default @Nullable @Override default OffsetDateTime expiresAt() { return null; } static ImmutableStaticToken.Builder builder() { return ImmutableStaticToken.builder(); } } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/FileVisitor.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.IOException; import java.nio.file.Path; public interface FileVisitor { void visit(Path sourceFile, Path dstFile) throws IOException; } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/GrepUtils.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.*; import java.util.ArrayList; import java.util.List; public final class GrepUtils { public static List grep(String pattern, byte[] ab) throws IOException { return grep(pattern, new ByteArrayInputStream(ab)); } public static List grep(String pattern, InputStream in) throws IOException { List result = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { String line; while ((line = reader.readLine()) != null) { if (line.matches(pattern)) { result.add(line); } } } return result; } private GrepUtils() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/IOUtils.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import javax.validation.constraints.NotNull; import java.io.*; import java.nio.file.CopyOption; import java.nio.file.Path; import java.util.List; /** * @deprecated use the alternatives in {@link PathUtils}, {@link ZipUtils}, {@link GrepUtils}, etc. */ @Deprecated public final class IOUtils { /** * @deprecated use {@link PathUtils#TMP_DIR_KEY} */ @Deprecated public static final String TMP_DIR_KEY = PathUtils.TMP_DIR_KEY; /** * @deprecated use {@link PathUtils#TMP_DIR} */ @Deprecated public static final Path TMP_DIR = PathUtils.TMP_DIR; /** * @deprecated use {@link PathUtils#tempFile(String, String)} */ @Deprecated public static TemporaryPath tempFile(String prefix, String suffix) throws IOException { return PathUtils.tempFile(prefix, suffix); } /** * @deprecated use {@link PathUtils#createTempFile(String, String)} */ @Deprecated public static Path createTempFile(String prefix, String suffix) throws IOException { return PathUtils.createTempFile(prefix, suffix); } /** * @deprecated use {@link PathUtils#createTempDir(Path, String)} */ @Deprecated public static Path createTempDir(Path dir, String prefix) throws IOException { return PathUtils.createTempDir(dir, prefix); } /** * @deprecated use {@link PathUtils#tempDir(String)} */ @Deprecated public static TemporaryPath tempDir(String prefix) throws IOException { return PathUtils.tempDir(prefix); } /** * @deprecated use {@link PathUtils#createTempDir(String)} */ @Deprecated public static Path createTempDir(String prefix) throws IOException { return PathUtils.createTempDir(prefix); } /** * @deprecated use {@link ZipUtils#zipFile(ZipArchiveOutputStream, Path, String)} */ @Deprecated public static void zipFile(ZipArchiveOutputStream zip, Path src, String name) throws IOException { ZipUtils.zipFile(zip, src, name); } /** * @deprecated use {@link ZipUtils#zip(ZipArchiveOutputStream, Path, String...)} */ @Deprecated public static void zip(ZipArchiveOutputStream zip, Path srcDir, String... filters) throws IOException { ZipUtils.zip(zip, srcDir, filters); } /** * @deprecated use {@link ZipUtils#zip(ZipArchiveOutputStream, String, Path, String...)} */ @Deprecated public static void zip(ZipArchiveOutputStream zip, String dstPrefix, Path srcDir, String... filters) throws IOException { ZipUtils.zip(zip, dstPrefix, srcDir, filters); } /** * @deprecated use {@link ZipUtils#unzip(InputStream, Path, CopyOption...)} */ @Deprecated public static void unzip(InputStream in, Path targetDir, CopyOption... options) throws IOException { ZipUtils.unzip(in, targetDir, options); } /** * @deprecated use {@link ZipUtils#unzip(Path, Path, CopyOption...)} */ @Deprecated public static void unzip(Path in, Path targetDir, CopyOption... options) throws IOException { ZipUtils.unzip(in, targetDir, options); } /** * @deprecated use {@link ZipUtils#unzip(Path, Path, boolean, CopyOption...)} */ @Deprecated public static void unzip(Path in, Path targetDir, boolean skipExisting, CopyOption... options) throws IOException { ZipUtils.unzip(in, targetDir, skipExisting, options); } /** * @deprecated use {@link ZipUtils#unzip(InputStream, Path, boolean, FileVisitor, CopyOption...)} */ @Deprecated public static void unzip(InputStream in, Path targetDir, boolean skipExisting, FileVisitor visitor, CopyOption... options) throws IOException { ZipUtils.unzip(in, targetDir, skipExisting, visitor, options); } /** * @deprecated use {@link ZipUtils#unzip(Path, Path, boolean, FileVisitor, CopyOption...)} */ @Deprecated public static void unzip(Path in, Path targetDir, boolean skipExisting, FileVisitor visitor, CopyOption... options) throws IOException { ZipUtils.unzip(in, targetDir, skipExisting, visitor, options); } /** * @deprecated use {@link InputStream#transferTo(OutputStream)} */ @Deprecated public static void copy(InputStream in, OutputStream out) throws IOException { byte[] ab = new byte[4096]; int read; while ((read = in.read(ab)) > 0) { out.write(ab, 0, read); } } /** * @deprecated use {@link PathUtils#copy(Path, Path)} */ @Deprecated public static void copy(Path src, Path dst) throws IOException { PathUtils.copy(src, dst); } /** * @deprecated use {@link PathUtils#copy(Path, Path, CopyOption...)} */ @Deprecated public static void copy(Path src, Path dst, CopyOption... options) throws IOException { PathUtils.copy(src, dst, options); } /** * @deprecated use {@link PathUtils#copy(Path, Path, String, CopyOption...)} */ @Deprecated public static void copy(Path src, Path dst, String ignorePattern, CopyOption... options) throws IOException { PathUtils.copy(src, dst, ignorePattern, options); } /** * @deprecated use {@link PathUtils#copy(Path, Path, String, FileVisitor, CopyOption...)} */ @Deprecated public static void copy(Path src, Path dst, String skipContents, FileVisitor visitor, CopyOption... options) throws IOException { PathUtils.copy(src, dst, skipContents, visitor, options); } /** * @deprecated use {@link PathUtils#copy(Path, Path, List, FileVisitor, CopyOption...)} */ @Deprecated public static void copy(Path src, Path dst, List skipContents, FileVisitor visitor, CopyOption... options) throws IOException { PathUtils.copy(src, dst, skipContents, visitor, options); } /** * @deprecated use {@link GrepUtils#grep(String, byte[])} */ @Deprecated public static List grep(String pattern, byte[] ab) throws IOException { return GrepUtils.grep(pattern, ab); } /** * @deprecated use {@link GrepUtils#grep(String, InputStream)} */ @Deprecated public static List grep(String pattern, InputStream in) throws IOException { return GrepUtils.grep(pattern, in); } /** * @deprecated use {@link PathUtils#deleteRecursively(Path)} */ @Deprecated public static boolean deleteRecursively(Path p) throws IOException { return PathUtils.deleteRecursively(p); } /** * @deprecated use {@link InputStream#readAllBytes()} */ @Deprecated public static byte[] toByteArray(InputStream src) throws IOException { ByteArrayOutputStream dst = new ByteArrayOutputStream(); copy(src, dst); return dst.toByteArray(); } /** * @deprecated use {@link PathUtils#delete(File)} */ @Deprecated public static void delete(File f) { PathUtils.delete(f); } /** * @deprecated use {@link PathUtils#assertInPath(Path, String)} */ @Deprecated public static Path assertInPath(@NotNull Path parent, @NotNull String child) throws IOException { return PathUtils.assertInPath(parent, child); } private IOUtils() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/LogUtils.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.slf4j.MDC; import org.slf4j.helpers.FormattingTuple; import org.slf4j.helpers.MessageFormatter; import java.io.PrintWriter; import java.io.StringWriter; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Map; import java.util.concurrent.Callable; public final class LogUtils { // the UI expects log timestamps in a specific format to be able to convert it to the local time // see also runner/src/main/resources/logback.xml and console2/src/components/molecules/ProcessLogViewer/datetime.tsx private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US); private static final ZoneId DEFAULT_TIMESTAMP_TZ = ZoneId.of("UTC"); public enum LogLevel { DEBUG, INFO, WARN, ERROR } public static String formatMessage(LogLevel level, String log, Object... args) { String timestamp = ZonedDateTime.now(DEFAULT_TIMESTAMP_TZ).format(TIMESTAMP_FORMAT); FormattingTuple m = MessageFormatter.arrayFormat(log, args); if (m.getThrowable() != null) { return String.format("%s [%-5s] %s%n%s%n", timestamp, level.name(), m.getMessage(), formatException(m.getThrowable())); } return String.format("%s [%-5s] %s%n", timestamp, level.name(), m.getMessage()); } public static Runnable withMdc(Runnable runnable) { Map mdc = MDC.getCopyOfContextMap(); return () -> { if (mdc != null) { MDC.setContextMap(mdc); } try { runnable.run(); } finally { if (mdc != null) { MDC.clear(); } } }; } public static Callable withMdc(Callable callable) { Map mdc = MDC.getCopyOfContextMap(); return () -> { if (mdc != null) { MDC.setContextMap(mdc); } try { return callable.call(); } finally { if (mdc != null) { MDC.clear(); } } }; } private static String formatException(Throwable t) { StringWriter sw = new StringWriter(); t.printStackTrace(new PrintWriter(sw)); return sw.toString(); } private LogUtils() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/Matcher.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.Collection; import java.util.Map; import java.util.UUID; import java.util.regex.Pattern; public final class Matcher { public static boolean matches(Object data, Object conditions) { return compareNodes(data, conditions); } public static boolean matchAny(Object condition, Collection nodes) { for (Object n : nodes) { boolean result = compareNodes(n, condition); if (result) { return true; } } return false; } public static boolean matchAny(Collection conditions, T data) { for (T c : conditions) { boolean result = compareNodes(data, c); if (result) { return true; } } return false; } @SuppressWarnings({"unchecked", "rawtypes"}) private static boolean compareNodes(Object data, Object conditions) { data = normalizeNull(data, conditions); if (data == null && conditions == null) { return true; } else if ((data == null && !(conditions instanceof Collection)) || conditions == null) { return false; } if (conditions instanceof Map && data instanceof Map) { return compareObjectNodes((Map) data, (Map) conditions); } else if (conditions instanceof String && data instanceof UUID) { return compareStringValues(data.toString(), (String)conditions); } else if (conditions instanceof String && data instanceof String) { return compareStringValues((String) data, (String) conditions); } else if (conditions instanceof Collection && data instanceof Collection) { return compareArrayNodes((Collection) data, (Collection) conditions); } else if (conditions instanceof Collection) { return matchAny((Collection) conditions, data); } else if (data instanceof Collection) { return matchAny(conditions, (Collection)data); } else { return compareValues(data, conditions); } } private static Object normalizeNull(Object data, Object conditions) { if (data != null) { return data; } if (conditions instanceof String) { return ""; } return null; } private static boolean compareObjectNodes(Map data, Map conditions) { if (conditions.isEmpty() && !data.isEmpty()) { return false; } for (Map.Entry e : conditions.entrySet()) { Object dataItem = data.get(e.getKey()); if (!compareNodes(dataItem, e.getValue())) { return false; } } return true; } private static boolean compareStringValues(String value, String condition) { return Pattern.compile(condition, Pattern.CASE_INSENSITIVE).matcher(value).matches(); } private static boolean compareArrayNodes(Collection dataElements, Collection conditionElements) { if (conditionElements.size() > dataElements.size()) { return false; } if (conditionElements.isEmpty() && !dataElements.isEmpty()) { return false; } for (Object c : conditionElements) { boolean matched = matchAny(c, dataElements); if (!matched) { return false; } } return true; } private static boolean compareValues(Object dataValue, Object conditionValue) { return dataValue.equals(conditionValue); } private Matcher() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/MemoSupplier.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.function.Supplier; /** * A memoizing {@link Supplier}. Not thread-safe. */ public class MemoSupplier implements Supplier { public static Supplier memo(Supplier delegate) { return new MemoSupplier<>(delegate); } private final Supplier delegate; private volatile boolean initialized; T value; public MemoSupplier(Supplier delegate) { this.delegate = delegate; } @Override public synchronized T get() { if (!initialized) { T t = delegate.get(); value = t; initialized = true; return t; } return value; } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/ObjectInputStreamWithClassLoader.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import java.lang.reflect.Proxy; /** * Almost a carbon copy of ClassLoaderObjectInputStream * from commons-io. */ public class ObjectInputStreamWithClassLoader extends ObjectInputStream { private final ClassLoader classLoader; public ObjectInputStreamWithClassLoader(InputStream in, ClassLoader classLoader) throws IOException { super(in); this.classLoader = classLoader; } @Override protected Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { try { return Class.forName(desc.getName(), false, classLoader); } catch (ClassNotFoundException e) { // delegate to super class loader which can resolve primitives return super.resolveClass(desc); } } @Override protected Class resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { Class[] interfaceClasses = new Class[interfaces.length]; for (int i = 0; i < interfaces.length; i++) { interfaceClasses[i] = Class.forName(interfaces[i], false, classLoader); } try { return Proxy.getProxyClass(classLoader, interfaceClasses); } catch (IllegalArgumentException e) { return super.resolveProxyClass(interfaces); } } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/ObjectMapperProvider.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.guava.GuavaModule; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Provider; public class ObjectMapperProvider implements Provider { private static final Logger log = LoggerFactory.getLogger(ObjectMapperProvider.class); @Override public ObjectMapper get() { log.debug("Using concord-common's ObjectMapper..."); ObjectMapper mapper = new ObjectMapper() .registerModule(new Jdk8Module()) .registerModule(new GuavaModule()) .registerModule(new JavaTimeModule()); // Write dates as ISO-8601 mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // Ignore unknown properties mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // Ignore nulls mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper; } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/PathUtils.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.validation.constraints.NotNull; import java.io.File; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Collections; import java.util.List; public final class PathUtils { public static final String TMP_DIR_KEY = "CONCORD_TMP_DIR"; public static final Path TMP_DIR = Paths.get(getEnv(TMP_DIR_KEY, System.getProperty("java.io.tmpdir"))); private static final Logger log = LoggerFactory.getLogger(PathUtils.class); static { try { if (!Files.exists(TMP_DIR)) { Files.createDirectories(TMP_DIR); } log.debug("Using {} as CONCORD_TMP_DIR", TMP_DIR); } catch (IOException e) { throw new RuntimeException(e); } } public static TemporaryPath tempFile(String prefix, String suffix) throws IOException { return new TemporaryPath(createTempFile(prefix, suffix)); } public static Path createTempFile(String prefix, String suffix) throws IOException { return Files.createTempFile(TMP_DIR, prefix, suffix); } public static Path createTempDir(Path dir, String prefix) throws IOException { return Files.createTempDirectory(dir, prefix); } public static TemporaryPath tempDir(String prefix) throws IOException { return new TemporaryPath(createTempDir(prefix)); } public static Path createTempDir(String prefix) throws IOException { return Files.createTempDirectory(TMP_DIR, prefix); } public static boolean deleteRecursively(Path p) throws IOException { if (!Files.exists(p)) { return false; } if (!Files.isDirectory(p)) { Files.delete(p); return true; } Files.walkFileTree(p, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); return true; } public static void delete(File f) { if (f == null || !f.exists()) { return; } if (!f.delete()) { log.warn("delete ['{}'] -> failed", f.getAbsolutePath()); } } /** * Resolves a child path within a parent, asserting the normalized child * starts with the parent path to avoid relative path escaping (e.g. * {@code "../../not/in/parent"}). * @param parent parent path within which child must exist when resolved * @param child filename or path to resolve as a child of {@code parent} * @return normalized child path * @throws IOException when the child does not resolve to an absolute path within the parent path */ public static Path assertInPath(@NotNull Path parent, @NotNull String child) throws IOException { Path normalizedParent = parent.normalize().toAbsolutePath(); Path normalizedChild = normalizedParent.resolve(child).normalize().toAbsolutePath(); if (!normalizedChild.startsWith(normalizedParent)) { throw new IOException("Child path resolves outside of parent path: " + child); } return normalizedChild; } private static String getEnv(String key, String defaultValue) { String s = System.getenv(key); if (s == null) { return defaultValue; } return s; } private PathUtils() { } public static void copy(Path src, Path dst) throws IOException { copy(src, dst, (String) null, null, new CopyOption[0]); } public static void copy(Path src, Path dst, CopyOption... options) throws IOException { copy(src, dst, (String) null, null, options); } public static void copy(Path src, Path dst, String ignorePattern, CopyOption... options) throws IOException { _copy(src, src, dst, toList(ignorePattern), null, options); } public static void copy(Path src, Path dst, String skipContents, FileVisitor visitor, CopyOption... options) throws IOException { _copy(src, src, dst, toList(skipContents), visitor, options); } public static void copy(Path src, Path dst, List skipContents, FileVisitor visitor, CopyOption... options) throws IOException { _copy(src, src, dst, skipContents, visitor, options); } private static void _copy(Path root, Path src, Path dst, List ignorePattern, FileVisitor visitor, CopyOption... options) throws IOException { Files.walkFileTree(src, new SimpleFileVisitor() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { if (dir != src && anyMatch(src.relativize(dir).toString(), ignorePattern)) { return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (file != src && anyMatch(src.relativize(file).toString(), ignorePattern)) { return FileVisitResult.CONTINUE; } Path a = file; Path b = dst.resolve(src.relativize(file)); Path parent = b.getParent(); if (!Files.exists(parent)) { Files.createDirectories(parent); } if (Files.isSymbolicLink(file)) { Path link = Files.readSymbolicLink(file); Path target = file.getParent().resolve(link).normalize(); if (!target.startsWith(root)) { throw new IOException("Symlinks outside the base directory are not supported: " + file + " -> " + target); } if (Files.notExists(target)) { // missing target return FileVisitResult.CONTINUE; } Files.createSymbolicLink(b, link); return FileVisitResult.CONTINUE; } Files.copy(a, b, options); if (visitor != null) { visitor.visit(a, b); } return FileVisitResult.CONTINUE; } }); } private static List toList(String entry) { if (entry == null) { return Collections.emptyList(); } return Collections.singletonList(entry); } private static boolean anyMatch(String what, List patterns) { if (patterns == null) { return false; } return patterns.stream().anyMatch(what::matches); } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/Posix.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.nio.file.attribute.PosixFilePermission; import java.util.Collections; import java.util.HashSet; import java.util.Set; public final class Posix { public static final int DEFAULT_UNIX_MODE = 420; // 0644 public static int unixMode(Set s) { if (s == null || s.isEmpty()) { return DEFAULT_UNIX_MODE; } int i = 0; for (PosixFilePermission p : s) { switch (p) { case OWNER_EXECUTE: i += 64; // 0100 break; case OWNER_WRITE: i += 128; // 0200 break; case OWNER_READ: i += 256; // 0400 break; case GROUP_EXECUTE: i += 8; // 0010 break; case GROUP_WRITE: i += 16; // 0020 break; case GROUP_READ: i += 32; // 0040 break; case OTHERS_EXECUTE: i += 1; // 0001 break; case OTHERS_WRITE: i += 2; // 0002 break; case OTHERS_READ: i += 4; // 0004 break; } } return i; } public static Set posix(int unixMode) { if (unixMode <= 0) { return Collections.emptySet(); } Set s = new HashSet<>(); if ((unixMode & 64) == 64) { // 0100 s.add(PosixFilePermission.OWNER_EXECUTE); } if ((unixMode & 128) == 128) { // 0200 s.add(PosixFilePermission.OWNER_WRITE); } if ((unixMode & 256) == 256) { // 0400 s.add(PosixFilePermission.OWNER_READ); } if ((unixMode & 8) == 8) { // 0010 s.add(PosixFilePermission.GROUP_EXECUTE); } if ((unixMode & 16) == 16) { // 0020 s.add(PosixFilePermission.GROUP_WRITE); } if ((unixMode & 32) == 32) { // 0040 s.add(PosixFilePermission.GROUP_READ); } if ((unixMode & 1) == 1) { // 0001 s.add(PosixFilePermission.OTHERS_EXECUTE); } if ((unixMode & 2) == 2) { // 0002 s.add(PosixFilePermission.OTHERS_WRITE); } if ((unixMode & 4) == 4) { // 0004 s.add(PosixFilePermission.OTHERS_READ); } return s; } private Posix() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/PrivilegedAction.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.IOException; public final class PrivilegedAction { private static final ThreadLocal currentDomain = new ThreadLocal<>(); public static String getCurrentDomain() { return currentDomain.get(); } public static T perform(String domain, IOAction f) throws IOException { String prevDomain = currentDomain.get(); try { currentDomain.set(domain); return f.call(); } finally { if (prevDomain == null) { currentDomain.remove(); } else { currentDomain.set(prevDomain); } } } public interface IOAction { T call() throws IOException; } private PrivilegedAction() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/ReflectionUtils.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.lang.annotation.Annotation; public final class ReflectionUtils { public static A findAnnotation(Class clazz, Class annotationType) { A annotation = clazz.getAnnotation(annotationType); if (annotation != null) { return annotation; } for (Class ifc : clazz.getInterfaces()) { annotation = findAnnotation(ifc, annotationType); if (annotation != null) { return annotation; } } Class superClass = clazz.getSuperclass(); if (superClass == null || superClass == Object.class) { return null; } return findAnnotation(superClass, annotationType); } private ReflectionUtils() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/StringUtils.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public final class StringUtils { public static String abbreviate(String str, int maxWidth) { if (str == null) { return null; } if (maxWidth < 4) { throw new IllegalArgumentException("Minimum abbreviation width is 4"); } if (str.length() <= maxWidth) { return str; } return str.substring(0, maxWidth - 3) + "..."; } private StringUtils() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/TemporaryPath.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; public class TemporaryPath implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(TemporaryPath.class); private final Path path; public TemporaryPath(Path path) { this.path = path; } public Path path() { return path; } @Override public void close() { if (path == null) { return; } try { if (Files.isDirectory(path)) { PathUtils.deleteRecursively(path); } else { Files.deleteIfExists(path); } } catch (IOException e) { log.warn("cleanup ['{}'] -> error: {}", path, e.getMessage()); } } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/ThreadLocalStack.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.LinkedList; import java.util.List; public class ThreadLocalStack { private final ThreadLocal> localStack = ThreadLocal.withInitial(LinkedList::new); public void push(T value) { List stack = localStack.get(); if (stack == null) { stack = new LinkedList<>(); localStack.set(stack); } stack.add(0, value); } public T pop() { List stack = localStack.get(); if (stack == null || stack.isEmpty()) { throw new IllegalStateException("Stack is empty. This is most likely a bug."); } T result = stack.remove(0); if (stack.isEmpty()) { localStack.remove(); } return result; } public T peek() { List stack = localStack.get(); if (stack == null || stack.isEmpty()) { return null; } return stack.get(0); } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/ToStringHelper.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2021 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.Collection; public class ToStringHelper { public static ToStringHelper prefix(String prefix) { return new ToStringHelper(prefix); } private final String prefix; private final StringBuilder builder = new StringBuilder(); private boolean appendSeparator = false; public ToStringHelper(String prefix) { this.prefix = prefix; } public ToStringHelper add(String name, Object value) { return addNameValue(name, value); } public ToStringHelper add(String name, Number value) { return addNameValue(name, value); } public ToStringHelper add(String name, String value) { if (value == null) { return this; } return addNameValue(name, "\"" + value + "\""); } public ToStringHelper add(String name, Collection value) { if (value == null || value.isEmpty()) { return this; } return addNameValue(name, value); } public String toString() { builder.insert(0, '{'); if (prefix != null) { builder.insert(0, prefix); } builder.append('}'); return builder.toString(); } private ToStringHelper addNameValue(String name, Object value) { if (value == null) { return this; } if (appendSeparator) { builder.append(", "); } builder.append(name).append('=').append(value); appendSeparator = true; return this; } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/TruncBufferedReader.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; public class TruncBufferedReader extends BufferedReader { public static final int DEFAULT_MAX_LINE_LENGTH = 4*1024; private static final int CR = '\r'; private static final int LF = '\n'; private final int readerMaxLineLen; private final char[] data; public TruncBufferedReader(Reader reader) { this(reader, DEFAULT_MAX_LINE_LENGTH); } public TruncBufferedReader(Reader reader, int maxLineLen) { super(reader); if (maxLineLen <= 0) { throw new IllegalArgumentException("maxLineLen must be greater than 0"); } this.readerMaxLineLen = maxLineLen; this.data = new char[readerMaxLineLen]; } @Override public String readLine() throws IOException { int currentPos = 0; int currentCharVal = super.read(); while ((currentCharVal != CR) && (currentCharVal != LF) && (currentCharVal >= 0)) { data[currentPos++] = (char) currentCharVal; if (currentPos < readerMaxLineLen) { currentCharVal = super.read(); } else { break; } } if (currentCharVal < 0) { if (currentPos > 0) { return (new String(data, 0, currentPos)); } else { return null; } } else { int skipped = skipTillEndOfLine(currentCharVal); String result = new String(data, 0, currentPos); if (skipped > 0) { result += "...[skipped " + skipped + " bytes]"; } return result; } } private int skipTillEndOfLine(int currentCharVal) throws IOException { int skippedCount = 0; while ((currentCharVal != CR) && (currentCharVal != LF) && (currentCharVal >= 0)) { currentCharVal = super.read(); skippedCount++; } if (currentCharVal < 0) { return skippedCount - 1; } if (currentCharVal == CR) { super.mark(1); if (super.read() != LF) { super.reset(); } } return skippedCount - 1; } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/ZipUtils.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.apache.commons.compress.archivers.zip.ZipFile; import java.io.IOException; import java.io.InputStream; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.PosixFilePermission; import java.util.Enumeration; import java.util.Set; public final class ZipUtils { public static void zipFile(ZipArchiveOutputStream zip, Path src, String name) throws IOException { ZipArchiveEntry e = new ZipArchiveEntry(name) { @Override public int getPlatform() { return PLATFORM_UNIX; } }; Set permissions = Files.getPosixFilePermissions(src); e.setUnixMode(Posix.unixMode(permissions)); e.setSize(Files.size(src)); zip.putArchiveEntry(e); Files.copy(src, zip); zip.closeArchiveEntry(); } public static void zip(ZipArchiveOutputStream zip, Path srcDir, String... filters) throws IOException { zip(zip, null, srcDir, filters); } public static void zip(ZipArchiveOutputStream zip, String dstPrefix, Path srcDir, String... filters) throws IOException { Files.walkFileTree(srcDir, new SimpleFileVisitor() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { if (dir.toAbsolutePath().equals(srcDir)) { return FileVisitResult.CONTINUE; } if (matches(dir, filters)) { return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (matches(file, filters)) { return FileVisitResult.SKIP_SUBTREE; } String n = srcDir.relativize(file).toString(); if (dstPrefix != null) { n = dstPrefix + n; } zipFile(zip, file, n); return FileVisitResult.CONTINUE; } }); } public static void unzip(InputStream in, Path targetDir, CopyOption... options) throws IOException { try (TemporaryPath tmpZip = new TemporaryPath(PathUtils.createTempFile("unzip", "zip"))) { Files.copy(in, tmpZip.path(), StandardCopyOption.REPLACE_EXISTING); unzip(tmpZip.path(), targetDir, options); } } public static void unzip(Path in, Path targetDir, CopyOption... options) throws IOException { unzip(in, targetDir, false, null, options); } public static void unzip(Path in, Path targetDir, boolean skipExisting, CopyOption... options) throws IOException { unzip(in, targetDir, skipExisting, null, options); } public static void unzip(InputStream in, Path targetDir, boolean skipExisting, FileVisitor visitor, CopyOption... options) throws IOException { try (TemporaryPath tmpZip = new TemporaryPath(PathUtils.createTempFile("unzip", "zip"))) { Files.copy(in, tmpZip.path(), StandardCopyOption.REPLACE_EXISTING); unzip(tmpZip.path(), targetDir, skipExisting, visitor, options); } } public static void unzip(Path in, Path targetDir, boolean skipExisting, FileVisitor visitor, CopyOption... options) throws IOException { targetDir = targetDir.normalize().toAbsolutePath(); try (ZipFile zip = new ZipFile(in.toFile())) { Enumeration entries = zip.getEntries(); while (entries.hasMoreElements()) { ZipArchiveEntry e = entries.nextElement(); Path p = targetDir.resolve(e.getName()); // skip paths outside of targetDir // (don't log anything to avoid "log bombing") if (!p.normalize().toAbsolutePath().startsWith(targetDir)) { continue; } if (skipExisting && Files.exists(p)) { continue; } if (e.isDirectory()) { Files.createDirectories(p); } else { Path parent = p.getParent(); if (!Files.exists(parent)) { Files.createDirectories(parent); } try (InputStream src = zip.getInputStream(e)) { Files.copy(src, p, options); } int unixMode = e.getUnixMode(); if (unixMode <= 0) { unixMode = Posix.DEFAULT_UNIX_MODE; } Files.setPosixFilePermissions(p, Posix.posix(unixMode)); if (visitor != null) { visitor.visit(p, p); } } } } } private static boolean matches(Path p, String... filters) { String n = p.getName(p.getNameCount() - 1).toString(); for (String f : filters) { if (n.matches(f)) { return true; } } return false; } private ZipUtils() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/cfg/MappingAuthConfig.java ================================================ package com.walmartlabs.concord.common.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.immutables.value.Value; import javax.annotation.Nullable; import java.net.URI; import java.util.regex.Pattern; /** * Configuration for mapping Git repository URLs to an authentication method. * Mapping is based on regex matching (see {@link #urlPattern()}) against the * repository URL. */ public interface MappingAuthConfig { /** * Identification for the auth config. Should be unique within the * application config. May be used for identifying source configs in metrics */ String id(); /** Regex matching the host, optional port and path of a Git repository URL. */ Pattern urlPattern(); /** * Username to use for authentication with a provided token. Some services * (e.g. GitHub API for app installation) require a specific username. Others * (e.g. GitHub API for personal access tokens) accept just the token and no username */ @Nullable String username(); /** * For compatibility with a {@link MappingAuthConfig} instance, a URI must match the * {@link #urlPattern()} regex. The regex may match against the path to support * either a Git host behind a reverse proxy or restricting the auth to specific * org/repo patterns. * @return {@code true} if this provider can handle the given repo URI, {@code false} otherwise. */ default boolean canHandle(URI repo) { var port = (repo.getPort() == -1 ? "" : (":" + repo.getPort())); var path = (repo.getPath() == null ? "" : repo.getPath()); var repoHostPortAndPath = repo.getHost() + port + path; return repoHostPortAndPath.matches(urlPattern() + ".*"); } static Pattern assertBaseUrlPattern(String pattern) { return pattern.endsWith(".*") ? Pattern.compile(pattern) : Pattern.compile(pattern + ".*"); } @Value.Immutable @Value.Style(jdkOnly = true) abstract class OauthAuthConfig implements MappingAuthConfig { public abstract String token(); public static ImmutableOauthAuthConfig.Builder builder() { return ImmutableOauthAuthConfig.builder(); } } @Value.Immutable @Value.Style(jdkOnly = true) interface ConcordServerAuthConfig extends MappingAuthConfig { static ImmutableConcordServerAuthConfig.Builder builder() { return ImmutableConcordServerAuthConfig.builder(); } } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/cfg/OauthTokenConfig.java ================================================ package com.walmartlabs.concord.common.cfg; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.List; import java.util.Optional; public interface OauthTokenConfig { Optional getOauthToken(); Optional getOauthUsername(); Optional getOauthUrlPattern(); List getSystemAuth(); } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/form/ConcordFormFields.java ================================================ package com.walmartlabs.concord.common.form; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import static io.takari.bpm.model.form.FormField.Option; public final class ConcordFormFields { public static final class FileField { public static final String TYPE = "file"; private FileField() { } } public static final class DateField { public static final String TYPE = "date"; private DateField() { } } public static final class DateTimeField { public static final String TYPE = "dateTime"; private DateTimeField() { } } public static final class DateFieldOptions { public static final Option POPUP_POSITION = new Option<>("popupPosition", String.class); private DateFieldOptions() { } } public static final class FieldOptions { public static final Option INPUT_TYPE = new Option<>("inputType", String.class); public static final Option PLACEHOLDER = new Option<>("placeholder", String.class); public static final Option READ_ONLY = new Option<>("readOnly", Boolean.class); public static final Option SEARCH = new Option<>("search", Boolean.class); private FieldOptions() { } } private ConcordFormFields() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/form/ConcordFormValidator.java ================================================ package com.walmartlabs.concord.common.form; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import io.takari.bpm.api.ExecutionException; import io.takari.bpm.form.DefaultFormValidator; import io.takari.bpm.form.FormSubmitResult; import io.takari.bpm.form.FormValidatorLocale; import io.takari.bpm.model.form.FormField; import org.apache.commons.validator.routines.EmailValidator; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class ConcordFormValidator extends DefaultFormValidator { public ConcordFormValidator() { this(new DefaultConcordFormValidatorLocale()); } public ConcordFormValidator(FormValidatorLocale locale) { super(createValidators(locale), locale); } private static Collection createValidators(FormValidatorLocale locale) { List vs = new ArrayList<>(); vs.add(new StringFieldValidator(locale)); vs.add(new DefaultFormValidator.IntegerFieldValidator(locale)); vs.add(new DefaultFormValidator.DecimalFieldValidator(locale)); vs.add(new DefaultFormValidator.BooleanFieldValidator(locale)); vs.add(new FileFieldValidator()); vs.add(new DateFieldValidator()); return vs; } public static final class FileFieldValidator implements DefaultFormValidator.FieldValidator { private static final String[] TYPES = {ConcordFormFields.FileField.TYPE}; @Override public String[] allowedTypes() { return TYPES; } @Override public FormSubmitResult.ValidationError validate(String formId, FormField f, Integer idx, Object v) { String fieldName = f.getName(); if (!(v instanceof String)) { throw new IllegalArgumentException("Expected a file value: " + fieldName); } return null; } } public static final class StringFieldValidator implements DefaultFormValidator.FieldValidator { private final DefaultFormValidator.StringFieldValidator delegate; public StringFieldValidator(FormValidatorLocale locale) { this.delegate = new DefaultFormValidator.StringFieldValidator(locale); } @Override public String[] allowedTypes() { return delegate.allowedTypes(); } @Override public FormSubmitResult.ValidationError validate(String formId, FormField f, Integer idx, Object v) throws ExecutionException { FormSubmitResult.ValidationError error = delegate.validate(formId, f, idx, v); if (error != null) { return error; } String inputType = f.getOption(new FormField.Option<>("inputType", String.class)); if ("email".equalsIgnoreCase(inputType)) { boolean valid = EmailValidator.getInstance().isValid((String)v); if (!valid) { return new FormSubmitResult.ValidationError(f.getName(), "Invalid email address"); } } return null; } } public static final class DateFieldValidator implements DefaultFormValidator.FieldValidator { private static final String[] TYPES = {ConcordFormFields.DateField.TYPE, ConcordFormFields.DateTimeField.TYPE}; @Override public String[] allowedTypes() { return TYPES; } @Override public FormSubmitResult.ValidationError validate(String formId, FormField f, Integer idx, Object v) { String fieldName = f.getName(); if (!(v instanceof String)) { throw new IllegalArgumentException("Expected a date value: " + fieldName); } return null; } } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/form/ConcordFormValidatorLocale.java ================================================ package com.walmartlabs.concord.common.form; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import io.takari.bpm.form.FormValidatorLocale; import io.takari.bpm.model.form.FormField; public interface ConcordFormValidatorLocale extends FormValidatorLocale { /** * Expected a date value. * * @param formId * @param field * @param idx * @param value * @return */ String expectedDate(String formId, FormField field, Integer idx, Object value); } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/form/DefaultConcordFormValidatorLocale.java ================================================ package com.walmartlabs.concord.common.form; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import io.takari.bpm.form.DefaultFormValidatorLocale; import io.takari.bpm.model.form.FormField; public class DefaultConcordFormValidatorLocale extends DefaultFormValidatorLocale implements ConcordFormValidatorLocale { @Override public String expectedDate(String formId, FormField field, Integer idx, Object value) { return String.format("%s: expected a date value, got %s", fieldName(field, idx), value); } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/secret/BinaryDataSecret.java ================================================ package com.walmartlabs.concord.common.secret; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.sdk.Secret; public class BinaryDataSecret implements Secret { private static final long serialVersionUID = 1L; private final byte[] data; public BinaryDataSecret(byte[] data) { // NOSONAR this.data = data; } public byte[] getData() { return data; } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/secret/HashAlgorithm.java ================================================ package com.walmartlabs.concord.common.secret; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2022 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.util.Arrays; public enum HashAlgorithm { @Deprecated LEGACY_MD5("md5"), SHA256("SHA-256"); private String name; HashAlgorithm(String name) { this.name = name; } public String getName() { return name; } public static HashAlgorithm getByName(String name) { return Arrays.stream(HashAlgorithm.values()).filter(hashAlgorithm -> hashAlgorithm.getName().equals(name)).findFirst().orElse(LEGACY_MD5); } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/secret/KeyPair.java ================================================ package com.walmartlabs.concord.common.secret; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.sdk.Secret; import java.io.*; public class KeyPair implements Secret { private static final long serialVersionUID = 1L; public static KeyPair deserialize(byte[] input) { try { DataInput in = new DataInputStream(new ByteArrayInputStream(input)); int n1 = assertKeyLength(in.readInt()); byte[] ab1 = new byte[n1]; in.readFully(ab1); int n2 = assertKeyLength(in.readInt()); byte[] ab2 = new byte[n2]; in.readFully(ab2); return new KeyPair(ab1, ab2); } catch (IOException e) { throw new RuntimeException(e); } } public static byte[] serialize(KeyPair k) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutput out = new DataOutputStream(baos); out.writeInt(k.getPublicKey().length); out.write(k.getPublicKey()); out.writeInt(k.getPrivateKey().length); out.write(k.getPrivateKey()); return baos.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); } } private static int assertKeyLength(int n) { if (n < 0 || n > 8192) { throw new IllegalArgumentException("Invalid key length: " + n); } return n; } private final byte[] publicKey; private final byte[] privateKey; public KeyPair(byte[] publicKey, byte[] privateKey) { // NOSONAR this.publicKey = publicKey; this.privateKey = privateKey; } public byte[] getPublicKey() { return publicKey; } public byte[] getPrivateKey() { return privateKey; } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/secret/SecretEncryptedByType.java ================================================ package com.walmartlabs.concord.common.secret; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ public enum SecretEncryptedByType { SERVER_KEY, PASSWORD } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/secret/SecretUtils.java ================================================ package com.walmartlabs.concord.common.secret; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.spec.SecretKeySpec; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; public final class SecretUtils { public static byte[] encrypt(byte[] input, byte[] password, byte[] salt) { return encrypt(input, password, salt, HashAlgorithm.LEGACY_MD5); } public static byte[] encrypt(byte[] input, byte[] password, byte[] salt, HashAlgorithm hashAlgorithm) { try { return encrypt(new ByteArrayInputStream(input), password, salt, hashAlgorithm).readAllBytes(); } catch (IOException e) { throw new SecurityException("Error encrypting a secret: " + e); } } public static InputStream encrypt(InputStream input, byte[] password, byte[] salt) { return encrypt(input, password, salt, HashAlgorithm.LEGACY_MD5); } public static InputStream encrypt(InputStream input, byte[] password, byte[] salt, HashAlgorithm hashAlgorithm) { try { Cipher c = init(password, salt, Cipher.ENCRYPT_MODE, hashAlgorithm); return new CipherInputStream(input, c); } catch (GeneralSecurityException e) { throw new SecurityException("Error encrypting a secret: " + e); } } public static byte[] decrypt(byte[] input, byte[] password, byte[] salt) { return decrypt(input, password, salt, HashAlgorithm.LEGACY_MD5); } public static byte[] decrypt(byte[] input, byte[] password, byte[] salt, HashAlgorithm hashAlgorithm) { try { InputStream out = decrypt(new ByteArrayInputStream(input), password, salt, hashAlgorithm); return out.readAllBytes(); } catch (IOException e) { Throwable t = e.getCause() == null ? e : e.getCause(); if (t instanceof BadPaddingException) { throw new SecurityException("Error decrypting a secret: " + t.getMessage() + ". Invalid input data and/or a password."); } throw new SecurityException("Error decrypting a secret: " + e.getMessage(), t); } } public static InputStream decrypt(InputStream input, byte[] password, byte[] salt) { return decrypt(input, password, salt, HashAlgorithm.LEGACY_MD5); } public static InputStream decrypt(InputStream input, byte[] password, byte[] salt, HashAlgorithm hashAlgorithm) { try { Cipher c = init(password, salt, Cipher.DECRYPT_MODE, hashAlgorithm); return new CipherInputStream(input, c); } catch (BadPaddingException e) { throw new SecurityException("Error decrypting a secret: " + e.getMessage() + ". Invalid input data and/or a password."); } catch (GeneralSecurityException e) { throw new SecurityException("Error decrypting a secret: " + e.getMessage()); } } public static byte[] hash(byte[] in, byte[] salt, HashAlgorithm hashAlgorithm) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance(hashAlgorithm.getName()); digest.update(salt); return in != null ? digest.digest(in) : digest.digest(); } private static Cipher init(byte[] password, byte[] salt, int mode, HashAlgorithm hashAlgorithm) throws GeneralSecurityException { Cipher c = Cipher.getInstance("AES"); byte[] key = hash(password, salt, hashAlgorithm); SecretKeySpec k = new SecretKeySpec(key, "AES"); c.init(mode, k); return c; } public static byte[] generateSalt(int size) { SecureRandom sr = new SecureRandom(); byte[] bytes = new byte[size]; sr.nextBytes(bytes); return bytes; } private SecretUtils() { } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/secret/UsernamePassword.java ================================================ package com.walmartlabs.concord.common.secret; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.sdk.Secret; import java.io.*; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; public class UsernamePassword implements Secret { private static final long serialVersionUID = 1L; public static byte[] serialize(UsernamePassword input) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutput out = new DataOutputStream(baos); out.writeUTF(input.getUsername()); ByteBuffer bb = StandardCharsets.UTF_8.encode(CharBuffer.wrap(input.getPassword())); byte[] ab = new byte[bb.remaining()]; bb.get(ab); out.writeInt(ab.length); out.write(ab); return baos.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); } } public static UsernamePassword deserialize(byte[] input) { try { DataInput in = new DataInputStream(new ByteArrayInputStream(input)); String username = in.readUTF(); int len = in.readInt(); byte[] ab = new byte[len]; in.readFully(ab); char[] password = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(ab)).array(); return new UsernamePassword(username, password); } catch (IOException e) { throw new RuntimeException(e); } } private final String username; private final char[] password; public UsernamePassword(String username, char[] password) { // NOSONAR this.username = username; this.password = password; } public String getUsername() { return username; } public char[] getPassword() { return password; } } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/validation/ConcordId.java ================================================ package com.walmartlabs.concord.common.validation; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Pattern; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({FIELD, PARAMETER, ANNOTATION_TYPE}) @Retention(RUNTIME) @Pattern(regexp = "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") @Constraint(validatedBy = {}) public @interface ConcordId { String message() default "{concord.validation.constraints.ConcordKey.message}"; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: common/src/main/java/com/walmartlabs/concord/common/validation/ConcordKey.java ================================================ package com.walmartlabs.concord.common.validation; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.Pattern; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE}) @Retention(RUNTIME) @Pattern(regexp = ConcordKey.PATTERN) @Constraint(validatedBy = {}) public @interface ConcordKey { String PATTERN = "^[0-9a-zA-Z][0-9a-zA-Z_@.\\-~]{2,127}$"; String MESSAGE = "Must contain only alphanumeric characters, digits, underscore, @, dot (.) or a minus (-). " + "Must start with an alphanumeric character or a digit. " + "Must be between 2 and 128 characters in length."; String message() default MESSAGE; Class[] groups() default {}; Class[] payload() default {}; } ================================================ FILE: common/src/main/resources/com/walmartlabs/concord/common/dockerPasswd ================================================ root:x:0:0:root:/root:/bin/bash concord:x:456:456::/tmp:/sbin/nologin ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/AuthTokenProviderTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.walmartlabs.concord.common.cfg.MappingAuthConfig; import com.walmartlabs.concord.common.cfg.OauthTokenConfig; import com.walmartlabs.concord.common.secret.BinaryDataSecret; import com.walmartlabs.concord.common.secret.UsernamePassword; import org.immutables.value.Value; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class AuthTokenProviderTest { private static final byte[] SECRET_BYTES = "abc123".getBytes(StandardCharsets.UTF_8); private static final String MOCK_TOKEN = "mock-token"; private static final String MOCK_USERNAME = "mock-username"; private static final String VALID_REPO = "https://github.local/owner/repo.git"; @Mock BinaryDataSecret binaryDataSecret; @Mock UsernamePassword usernamePassword; @Mock MappingAuthConfig.OauthAuthConfig oauth; @Mock TestOauthTokenConfig oauthTokenConfig; @Test void testSingleOauth() { // the "old" config approach when(oauthTokenConfig.getOauthToken()).thenReturn(Optional.of(MOCK_TOKEN)); when(oauthTokenConfig.getOauthUrlPattern()).thenReturn(Optional.of("github\\.local")); when(oauthTokenConfig.getOauthUsername()).thenReturn(Optional.of(MOCK_USERNAME)); executeWithoutSecret(oauthTokenConfig); verify(oauthTokenConfig, times(1)).getOauthUrlPattern(); // retrieved once and stored } @Test void testSystemAuth() { when(oauth.canHandle(any())).thenCallRealMethod(); when(oauth.urlPattern()).thenReturn(Pattern.compile("github\\.local")); when(oauth.token()).thenReturn(MOCK_TOKEN); when(oauth.username()).thenReturn(MOCK_USERNAME); var cfg = TestOauthTokenConfig.builder() .addSystemAuth(oauth) .build(); executeWithoutSecret(cfg); verify(oauth, times(12)).canHandle(any()); } void executeWithoutSecret(OauthTokenConfig cfg) { var provider = new AuthTokenProvider.OauthTokenProvider(cfg); assertTrue(provider.supports(URI.create("https://github.local/owner/repo.git"), null)); assertTrue(provider.supports(URI.create("https://github.local/owner/repo"), null)); assertTrue(provider.supports(URI.create("https://github.local/owner/repo/"), null)); assertFalse(provider.supports(URI.create("https://elsewhere.local/owner/repo.git"), null)); assertFalse(provider.supports(URI.create("https://elsewhere.local/owner/repo"), null)); assertEquals(MOCK_TOKEN, provider.getToken(URI.create("https://github.local/owner/repo.git"), null).map(ExternalAuthToken::token).orElse(null)); assertEquals(MOCK_TOKEN, provider.getToken(URI.create("https://github.local/owner/repo"), null).map(ExternalAuthToken::token).orElse(null)); assertEquals(MOCK_TOKEN, provider.getToken(URI.create("https://github.local/owner/repo/"), null).map(ExternalAuthToken::token).orElse(null)); assertFalse(provider.getToken(URI.create("https://elsewhere.local/owner/repo.git"), null).isPresent()); assertFalse(provider.getToken(URI.create("https://elsewhere.local/owner/repo"), null).isPresent()); var enriched = provider.addUserInfoToUri(URI.create("https://github.local/owner/repo.git"), null); assertEquals(MOCK_USERNAME + ":" + MOCK_TOKEN, enriched.getUserInfo()); assertEquals("https://" + MOCK_USERNAME + ":" + MOCK_TOKEN + "@github.local/owner/repo.git", enriched.toString()); } @Test void testUsernamePassword() { var cfg = TestOauthTokenConfig.builder().build(); var provider = new AuthTokenProvider.OauthTokenProvider(cfg); assertFalse(provider.supports(URI.create(VALID_REPO), usernamePassword)); } @Test void testWithSecret() { var cfg = TestOauthTokenConfig.builder() .addSystemAuth(oauth) // won't be used .build(); executeWithSecret(cfg); } @Test void testWithSecretNoDefault() { var cfg = TestOauthTokenConfig.builder().build(); executeWithSecret(cfg); } private void executeWithSecret(TestOauthTokenConfig cfg) { var provider = new AuthTokenProvider.OauthTokenProvider(cfg); when(binaryDataSecret.getData()).thenReturn(SECRET_BYTES); assertTrue(provider.supports(URI.create("https://github.local/owner/repo.git"), binaryDataSecret)); verify(oauth, never()).token(); // prove it wasn't used verify(binaryDataSecret, times(1)).getData(); } @Value.Immutable interface TestOauthTokenConfig extends OauthTokenConfig { static ImmutableTestOauthTokenConfig.Builder builder() { return ImmutableTestOauthTokenConfig.builder(); } } } ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/ConfigurationUtilsTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; public class ConfigurationUtilsTest { @Test public void deepMergeTest() { Map m1 = new HashMap<>(); m1.put("a", "a-value1"); m1.put("b", "b-value1"); Map m2 = new HashMap<>(); m2.put("a", "a-value2"); m2.put("c", "b-value2"); Map result = ConfigurationUtils.deepMerge(m1, m2); assertEquals("a-value2", result.get("a")); assertEquals("b-value1", result.get("b")); assertEquals("b-value2", result.get("c")); } @Test public void deepEqualsTest() { Object a = Collections.singletonMap("x", Collections.singletonList("test1")); Object b = Collections.singletonMap("x", Collections.singletonList("test2")); assertFalse(ConfigurationUtils.deepEquals(a, b)); a = Collections.singletonMap("x", Collections.singletonList("test")); b = Collections.singletonMap("x", Collections.singletonList("test")); assertTrue(ConfigurationUtils.deepEquals(a, b)); } } ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/CycleCheckerTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class CycleCheckerTest { /** * m -(m1)- m1 -(mm1)- m2 * \(m2)- m2 -(mm2)- m1 */ @Test public void test1() throws Exception { Map m = new HashMap<>(); Map m1 = new HashMap<>(); Map m2 = new HashMap<>(); m1.put("mm1", m2); m2.put("mm2", m1); m.put("m1", m1); m.put("m2", m2); System.out.println(CycleChecker.check(m)); assertTrue(CycleChecker.check(m).isHasCycle()); } /** * m -(m2)- "a1" * \(m2)- m */ @Test public void test2() throws Exception { Map m = new HashMap<>(); m.put("m2", Arrays.asList("a1", m)); System.out.println(CycleChecker.check(m)); assertTrue(CycleChecker.check(m).isHasCycle()); } /** * m -(k)-- "v" * \(k2)- * -(kk2)- "value" */ @Test public void test3() throws Exception { Map m = new HashMap<>(); m.put("k", "v"); m.put("k2", Collections.singletonMap("kk2", "value")); System.out.println(CycleChecker.check(m)); assertFalse(CycleChecker.check(m).isHasCycle()); System.out.println(new ObjectMapper().writeValueAsString(m)); } /** * m -(m2)- "a1" * \(m2)- * -(kk2)- "value" */ @Test public void test4() throws Exception { Map m = new HashMap<>(); m.put("m2", Arrays.asList("a1", Collections.singletonMap("kk2", "value"))); System.out.println(new ObjectMapper().writeValueAsString(m)); System.out.println(CycleChecker.check(m)); assertFalse(CycleChecker.check(m).isHasCycle()); } /** * m -(m1)- m1 -(k1)- "v1" * \(m2)- m2 -(k2)- "v2" * \(m2)- m2 -(k3)- m1 */ @Test public void test5() throws Exception { Map m = new HashMap<>(); Map m1 = new HashMap<>(); Map m2 = new HashMap<>(); m1.put("k1", "v1"); m2.put("k2", "v2"); m2.put("k3", m1); m.put("m1", m1); m.put("m2", m2); System.out.println(new ObjectMapper().writeValueAsString(m)); System.out.println(CycleChecker.check(m)); assertFalse(CycleChecker.check(m).isHasCycle()); } } ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/DateTimeUtilsTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.Test; import javax.xml.bind.DatatypeConverter; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.temporal.ChronoField; import java.util.Calendar; import static org.junit.jupiter.api.Assertions.assertEquals; public class DateTimeUtilsTest { @Test public void test() throws Exception { Calendar now1 = Calendar.getInstance(); now1.set(Calendar.MILLISECOND, 123); String a = DatatypeConverter.printDateTime(now1); OffsetDateTime now2 = OffsetDateTime.ofInstant(now1.toInstant(), ZoneId.of(now1.getTimeZone().getID())); String b = DateTimeUtils.toIsoString(now2); assertEquals(a, b); String src = "2020-07-16T13:47:51.085-04:00"; OffsetDateTime x = DateTimeUtils.fromIsoString(src); String dst = DateTimeUtils.toIsoString(x); assertEquals(src, dst); src = "2020-07-16T17:13:27.912Z"; x = DateTimeUtils.fromIsoString(src); assertEquals(x.toZonedDateTime().getZone().normalized(), ZoneId.of("UTC").normalized()); assertEquals(x.get(ChronoField.YEAR), 2020); assertEquals(x.get(ChronoField.MILLI_OF_SECOND), 912); } } ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/ExternalAuthTokenTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import java.time.OffsetDateTime; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; class ExternalAuthTokenTest { static final String MOCK_TOKEN = "mock-token"; static final ObjectMapper MAPPER = new ObjectMapperProvider().get(); @Test void testExpiration() { var externalToken = ExternalAuthToken.SimpleToken.builder() .token(MOCK_TOKEN) .expiresAt(OffsetDateTime.now().minusSeconds((100))) .build(); assertTrue(externalToken.secondsUntilExpiration() < 0); } @Test void testStaticExpiration() { var externalToken = ExternalAuthToken.StaticToken.builder() .token(MOCK_TOKEN) .build(); assertEquals(MOCK_TOKEN, externalToken.token()); assertEquals(Long.MAX_VALUE, externalToken.secondsUntilExpiration()); } @Test void testMinimalDeserialization() throws JsonProcessingException { var minimalFromJson = MAPPER.readValue(""" { "token": "mock-token" } """, ExternalAuthToken.class); assertEquals(MOCK_TOKEN, minimalFromJson.token()); assertEquals(Long.MAX_VALUE, minimalFromJson.secondsUntilExpiration()); } @Test void testFullDeserialization() throws JsonProcessingException { var fullFromJson = MAPPER.readValue(""" { "token": "mock-token", "expires_at": "2099-12-31T23:59:59Z", "username": "mock-username" } """, ExternalAuthToken.class); assertEquals(MOCK_TOKEN, fullFromJson.token()); assertEquals("mock-username", fullFromJson.username()); var dt = fullFromJson.expiresAt(); assertNotNull(dt); assertEquals(2099, dt.getYear()); } @Test void testFullDeserializationMillis() throws JsonProcessingException { var fullFromJson = MAPPER.readValue(""" { "token": "mock-token", "expires_at": "2099-12-31T23:59:59.123Z", "username": "mock-username" } """, ExternalAuthToken.class); assertEquals(MOCK_TOKEN, fullFromJson.token()); var dt = fullFromJson.expiresAt(); assertNotNull(dt); assertEquals(2099, dt.getYear()); assertEquals(123, dt.getNano() / 1_000_000); } @Test void testDateSerializationSecondsToMillis() throws JsonProcessingException { var json = MAPPER.writeValueAsString(ExternalAuthToken.SimpleToken.builder() .token(MOCK_TOKEN) .expiresAt(OffsetDateTime.parse("2099-12-31T23:59:59Z")) .build()); assertTrue(json.contains("23:59:59.000Z")); } @Test void testDateSerializationMillis() throws JsonProcessingException { var json = MAPPER.writeValueAsString(ExternalAuthToken.SimpleToken.builder() .token(MOCK_TOKEN) .expiresAt(OffsetDateTime.parse("2099-12-31T23:59:59.123Z")) .build()); assertTrue(json.contains("23:59:59.123Z")); } } ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/LogUtilsTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.Test; public class LogUtilsTest { @Test public void test() throws Exception { System.out.println(LogUtils.formatMessage(LogUtils.LogLevel.INFO, "Hello, {}!", "there")); } } ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/MatcherTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.Test; import java.util.*; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class MatcherTest { @Test public void testAllJsonTypes() { Map event = new HashMap<>(); event.put("a", "a-value"); event.put("b", "b-value"); event.put("c", 123); event.put("d", null); event.put("e", true); event.put("f", asList("3", "1", "4", "2")); Map conditions = new HashMap<>(); conditions.put("a", "a-v.*"); conditions.put("b", "b-value"); conditions.put("c", 123); conditions.put("d", null); conditions.put("e", true); conditions.put("f", asList("1", "2")); boolean result = Matcher.matches(event, conditions); assertTrue(result); } @Test public void testNoConditions() { Map event = new HashMap<>(); event.put("a", "a-value"); Map conditions = new HashMap<>(); boolean result = Matcher.matches(event, conditions); assertFalse(result); } @Test public void testNotMatched() { Map event = new HashMap<>(); event.put("a", "a-value"); event.put("b", "b-value"); event.put("c", 123); event.put("d", null); event.put("e", true); event.put("f", asList("3", "1", "4", "2")); Map conditions = new HashMap<>(); conditions.put("b", "XXXX"); boolean result = Matcher.matches(event, conditions); assertFalse(result); } @Test public void testTypesMismatch() { Map event = new HashMap<>(); event.put("a", 100); Map conditions = new HashMap<>(); conditions.put("a", "123"); boolean result = Matcher.matches(event, conditions); assertFalse(result); } @Test public void testObjectsOfObjects() { Map m = new HashMap<>(); m.put("o1", "o1v1"); m.put("o2", "o2v2"); Map event = new HashMap<>(); event.put("a", 100); event.put("obj", m); Map conditions = new HashMap<>(); conditions.put("a", 100); conditions.put("obj", Collections.singletonMap("o1", "o1v1")); boolean result = Matcher.matches(event, conditions); assertTrue(result); } @Test public void testPartialMatch() { Map event = new HashMap<>(); event.put("unknownRepo", true); Map conditions = new HashMap<>(); conditions.put("unknownRepo", asList(true, false)); boolean result = Matcher.matches(event, conditions); assertTrue(result); } @Test public void testPartialNotMatch() { Map event = new HashMap<>(); event.put("unknownRepo", true); Map conditions = new HashMap<>(); conditions.put("unknownRepo", Collections.singletonList(false)); boolean result = Matcher.matches(event, conditions); assertFalse(result); } @Test public void testMatchEmptyCondition() { Map conditions = new HashMap<>(); conditions.put("params", emptyMap()); // --- empty param Map event1 = new HashMap<>(); event1.put("k", "v"); event1.put("params", emptyMap()); boolean result = Matcher.matches(event1, conditions); assertTrue(result); // --- param not present Map event2 = new HashMap<>(); event2.put("k", "v"); boolean result2 = Matcher.matches(event2, conditions); assertFalse(result2); // --- param present Map event3 = new HashMap<>(); event3.put("k", "v"); event3.put("params", Collections.singletonMap("a", "a-value")); boolean result3 = Matcher.matches(event3, conditions); assertFalse(result3); } @Test public void testMatchNullCondition() { Map conditions = new HashMap<>(); conditions.put("params", null); // --- empty param Map event1 = new HashMap<>(); event1.put("k", "v"); event1.put("params", emptyMap()); boolean result = Matcher.matches(event1, conditions); assertFalse(result); // --- param not present Map event2 = new HashMap<>(); event2.put("k", "v"); boolean result2 = Matcher.matches(event2, conditions); assertTrue(result2); // --- param is null Map event3 = new HashMap<>(); event3.put("k", "v"); event3.put("params", null); boolean result3 = Matcher.matches(event3, conditions); assertTrue(result3); // --- param present Map event4 = new HashMap<>(); event4.put("k", "v"); event4.put("params", Collections.singletonMap("a", "a-value")); boolean result4 = Matcher.matches(event4, conditions); assertFalse(result4); } @Test public void testNulls() { // data ? condition // null == null assertTrue(Matcher.matches(null, null)); // null == ".*" assertTrue(Matcher.matches(null, ".*")); // null == "" assertTrue(Matcher.matches(null, "")); // null != {} assertFalse(Matcher.matches(null, emptyMap())); // null != [] assertFalse(Matcher.matches(null, emptyList())); // {} == {} assertTrue(Matcher.matches(emptyMap(), emptyMap())); // [] == [] assertTrue(Matcher.matches(emptyList(), emptyList())); // null != 1 assertFalse(Matcher.matches(null, 1)); // "" != null assertFalse(Matcher.matches("", null)); // {} != null assertFalse(Matcher.matches(emptyMap(), null)); // [] != null assertFalse(Matcher.matches(emptyList(), null)); } @Test public void testOr() { // null == [null, []] assertTrue(Matcher.matches(null, asList(null, emptyList()))); // {} == [null, [], {}] assertTrue(Matcher.matches(emptyMap(), asList(null, emptyList(), emptyMap()))); //! [] == [null, []] assertFalse(Matcher.matches(emptyList(), asList(null, emptyList()))); } @Test public void testArrayMatch() { List data = Arrays.asList("one", "two"); assertTrue(Matcher.matches(data, "on.*")); assertFalse(Matcher.matches(data, "ono")); } // null == null, "", ".*", [null] // [] == [] // {} == {}, [{}] } ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/PathUtilsTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import static org.junit.jupiter.api.Assertions.*; public class PathUtilsTest { @Test public void testResolveNonChild() { Path parent = Paths.get("/parent/path"); Exception e = assertThrows(IOException.class, () -> PathUtils.assertInPath(parent, "../child")); assertTrue(e.getMessage().contains("Child path resolves outside of parent path")); } @Test public void testResolveValidChild() { Path parent = Paths.get("/parent/path"); assertDoesNotThrow(() -> PathUtils.assertInPath(parent, "child")); assertDoesNotThrow(() -> PathUtils.assertInPath(parent, "another/child")); Path p = assertDoesNotThrow(() -> PathUtils.assertInPath(parent, "odd/../but/valid")); assertEquals("/parent/path/but/valid", p.toString()); } @Test public void testCopy() throws Exception { Path src = Files.createTempDirectory("test"); Path dst = Files.createTempDirectory("test"); // --- Path nestedDir = src.resolve("a/b"); Files.createDirectories(nestedDir); Path srcFile = nestedDir.resolve("c.txt"); Files.createFile(srcFile); // --- PathUtils.copy(src, dst); assertTrue(Files.exists(dst.resolve("a/b/c.txt"))); } @Test public void testSymlinks() throws Exception { Path src = Files.createTempDirectory("test"); Path aFile = src.resolve("a"); Files.write(aFile, "hello".getBytes(), StandardOpenOption.CREATE); Path xDir = src.resolve("x"); Files.createDirectories(xDir); Path bLink = xDir.resolve("b"); Files.createSymbolicLink(bLink, aFile); // --- Path dst = Files.createTempDirectory("test"); // --- PathUtils.copy(src, dst); // --- assertTrue(Files.isSymbolicLink(dst.resolve("x").resolve("b"))); assertTrue(Files.isRegularFile(dst.resolve("x").resolve("b"))); } @Test public void testExternalSymlinks() throws Exception { Path src = Files.createTempDirectory("test"); Path link = src.resolve("a"); Path target = Paths.get("../../../etc/passwd"); Files.createSymbolicLink(link, target); // --- Path dst = Files.createTempDirectory("test"); // --- Exception e = assertThrows(IOException.class, () -> PathUtils.copy(src, dst)); assertTrue(e.getMessage().contains("Symlinks outside the base directory are not supported")); } } ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/StringUtilsTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; public class StringUtilsTest { @Test public void test() { try { assertEquals("123", StringUtils.abbreviate("123", 3)); fail("exception expected"); } catch (Exception e) { // expected } assertNull(StringUtils.abbreviate(null, 5)); assertEquals("1234", StringUtils.abbreviate("1234", 5)); assertEquals("12345", StringUtils.abbreviate("12345", 5)); assertEquals("12...", StringUtils.abbreviate("123456", 5)); } } ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/TruncBufferedReaderTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.junit.jupiter.api.Test; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; public class TruncBufferedReaderTest { @Test public void testEmpty() throws Exception { String str = ""; List r = readLines(str); assertEquals(0, r.size()); } @Test public void test1() throws Exception { String str = "1"; List r = readLines(str); assertEquals(1, r.size()); assertEquals("1", r.get(0)); } @Test public void test2() throws Exception { String str = "12"; List r = readLines(str, 2); assertEquals(1, r.size()); assertEquals("12", r.get(0)); } @Test public void test3() throws Exception { String str = "123456789"; List r = readLines(str, 2); assertEquals(1, r.size()); assertEquals("12...[skipped 7 bytes]", r.get(0)); } @Test public void test4() throws Exception { String str = "12\n3"; List r = readLines(str, 2); assertEquals(2, r.size()); assertEquals("12", r.get(0)); assertEquals("3", r.get(1)); } @Test public void test5() throws Exception { String str = "123\n456"; List r = readLines(str, 2); assertEquals(2, r.size()); assertEquals("12...[skipped 1 bytes]", r.get(0)); assertEquals("45...[skipped 1 bytes]", r.get(1)); } @Test public void test6() throws Exception { String str = "1\r\n23\n45\r6"; List r = readLines(str, 2); assertEquals(4, r.size()); assertEquals("1", r.get(0)); assertEquals("23", r.get(1)); assertEquals("45", r.get(2)); assertEquals("6", r.get(3)); } @Test public void test7() throws Exception { String str = "1\r\n23\n45\r6\n"; List r = readLines(str, 2); assertEquals(4, r.size()); assertEquals("1", r.get(0)); assertEquals("23", r.get(1)); assertEquals("45", r.get(2)); assertEquals("6", r.get(3)); } private List readLines(String str) throws IOException { return readLines(str, TruncBufferedReader.DEFAULT_MAX_LINE_LENGTH); } private List readLines(String str, int maxLineLength) throws IOException { List result = new ArrayList<>(); BufferedReader reader = new TruncBufferedReader(new InputStreamReader(new ByteArrayInputStream(str.getBytes())), maxLineLength); String line; while ((line = reader.readLine()) != null) { result.add(line); } return result; } } ================================================ FILE: common/src/test/java/com/walmartlabs/concord/common/ZipUtilsTest.java ================================================ package com.walmartlabs.concord.common; /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2025 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.junit.jupiter.api.Test; import java.nio.file.Files; import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.assertTrue; public class ZipUtilsTest { @Test public void testZipUnzip() throws Exception { Path src = Files.createTempDirectory("test-zip"); Files.createFile(src.resolve("a.txt")); Files.createFile(src.resolve("b\\c.txt")); Files.createDirectory(src.resolve("b")); Files.createFile(src.resolve("b").resolve("c.txt")); Path archive = Files.createTempFile("archive", "zip"); try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(Files.newOutputStream(archive))) { ZipUtils.zip(zip, src); } PathUtils.deleteRecursively(src); Path dst = Files.createTempDirectory("test"); ZipUtils.unzip(archive, dst); assertTrue(Files.exists(dst.resolve("a.txt"))); assertTrue(Files.exists(dst.resolve("b\\c.txt"))); assertTrue(Files.exists(dst.resolve("b").resolve("c.txt"))); } } ================================================ FILE: config/README.md ================================================ # Concord Config Based on [ollie-config](https://github.com/takari/ollie/tree/master/ollie-config), minus the environment handling. ================================================ FILE: config/pom.xml ================================================ 4.0.0 com.walmartlabs.concord parent 2.40.1-SNAPSHOT ../pom.xml concord-config jar ${project.groupId}:${project.artifactId} com.typesafe config com.google.inject guice org.reflections reflections org.eclipse.sisu sisu-maven-plugin ================================================ FILE: config/src/main/java/com/walmartlabs/concord/config/Config.java ================================================ package com.walmartlabs.concord.config; /*- * ***** * Concord * ----- * Copyright (C) 2018 Takari * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.google.inject.BindingAnnotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @BindingAnnotation @Target({FIELD, PARAMETER, METHOD}) @Retention(RUNTIME) public @interface Config { String value(); } ================================================ FILE: config/src/main/java/com/walmartlabs/concord/config/ConfigExtractor.java ================================================ package com.walmartlabs.concord.config; /*- * ***** * Concord * ----- * Copyright (C) 2018 Takari * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ interface ConfigExtractor { Object extractValue(com.typesafe.config.Config config, String path); Class[] getMatchingClasses(); } ================================================ FILE: config/src/main/java/com/walmartlabs/concord/config/ConfigExtractors.java ================================================ package com.walmartlabs.concord.config; /*- * ***** * Concord * ----- * Copyright (C) 2018 Takari * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import com.typesafe.config.*; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.Optional; enum ConfigExtractors implements ConfigExtractor { BOOLEAN(boolean.class, Boolean.class) { @Override public Object extractValue(Config config, String path) { return config.getBoolean(path); } }, BYTE(byte.class, Byte.class) { @Override public Object extractValue(Config config, String path) { return (byte) config.getInt(path); } }, SHORT(short.class, Short.class) { @Override public Object extractValue(Config config, String path) { return (short) config.getInt(path); } }, INTEGER(int.class, Integer.class) { @Override public Object extractValue(Config config, String path) { return config.getInt(path); } }, LONG(long.class, Long.class) { @Override public Object extractValue(Config config, String path) { return config.getLong(path); } }, FLOAT(float.class, Float.class) { @Override public Object extractValue(Config config, String path) { return (float) config.getDouble(path); } }, DOUBLE(double.class, Double.class) { @Override public Object extractValue(Config config, String path) { return config.getDouble(path); } }, STRING(String.class) { @Override public Object extractValue(Config config, String path) { return config.getString(path); } }, PATH(Path.class) { @Override public Object extractValue(Config config, String path) { return Paths.get(config.getString(path)); } }, ANY_REF(Object.class) { @Override public Object extractValue(Config config, String path) { return config.getAnyRef(path); } }, CONFIG(Config.class) { @Override public Object extractValue(Config config, String path) { return config.getConfig(path); } }, CONFIG_OBJECT(ConfigObject.class) { @Override public Object extractValue(Config config, String path) { return config.getObject(path); } }, CONFIG_VALUE(ConfigValue.class) { @Override public Object extractValue(Config config, String path) { return config.getValue(path); } }, CONFIG_LIST(ConfigList.class) { @Override public Object extractValue(Config config, String path) { return config.getList(path); } }, DURATION(Duration.class) { @Override public Object extractValue(Config config, String path) { return config.getDuration(path); } }, MEMORY_SIZE(ConfigMemorySize.class) { @Override public Object extractValue(Config config, String path) { return config.getMemorySize(path); } }, BYTE_ARRAY(byte[].class) { @Override public Object extractValue(Config config, String path) { return Base64.getDecoder().decode(config.getString(path)); } }; private final Class[] matchingClasses; private static final Map, ConfigExtractor> EXTRACTOR_MAP = new HashMap<>(); static { for (var extractor : ConfigExtractors.values()) { for (var clazz : extractor.getMatchingClasses()) { EXTRACTOR_MAP.put(clazz, extractor); } } } ConfigExtractors(Class... matchingClasses) { this.matchingClasses = matchingClasses; } @Override public Class[] getMatchingClasses() { return matchingClasses; } static Optional extractConfigValue(Config config, Class paramClass, String path) { if (config.hasPath(path) && EXTRACTOR_MAP.containsKey(paramClass)) { return Optional.of(EXTRACTOR_MAP.get(paramClass).extractValue(config, path)); } else { return Optional.empty(); } } } ================================================ FILE: config/src/main/java/com/walmartlabs/concord/config/ConfigModule.java ================================================ package com.walmartlabs.concord.config; /*- * ***** * Concord * ----- * Copyright (C) 2018 Takari * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.google.inject.AbstractModule; import com.google.inject.Key; import com.google.inject.Provider; import com.typesafe.config.*; import org.reflections.Reflections; import org.reflections.scanners.FieldAnnotationsScanner; import org.reflections.scanners.MethodAnnotationsScanner; import org.reflections.scanners.MethodParameterScanner; import org.reflections.scanners.TypeAnnotationsScanner; import org.reflections.util.ClasspathHelper; import org.reflections.util.ConfigurationBuilder; import org.reflections.util.FilterBuilder; import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.*; import java.util.stream.Collectors; public class ConfigModule extends AbstractModule { private static final String CONFIG_FILE = "concord.conf"; private static final String LEGACY_CONFIG_FILE = "ollie.conf"; private static final Provider NULL_PROVIDER = () -> null; private final com.typesafe.config.Config config; private final Reflections reflections; private final Set boundAnnotations; public ConfigModule(String packageToScan, com.typesafe.config.Config config) { var configBuilder = new ConfigurationBuilder() .filterInputsBy(new FilterBuilder().includePackage(packageToScan)) .setUrls(ClasspathHelper.forPackage(packageToScan)) .setScanners( new TypeAnnotationsScanner(), new MethodParameterScanner(), new MethodAnnotationsScanner(), new FieldAnnotationsScanner()); this.config = config; this.reflections = new Reflections(configBuilder); this.boundAnnotations = new HashSet<>(); } public static com.typesafe.config.Config load(String name) { var options = ConfigResolveOptions.defaults().setAllowUnresolved(true); var defaultConfig = ConfigFactory.load(name + ".conf", ConfigParseOptions.defaults(), options); var result = defaultConfig.getConfig(name); if (System.getProperty(LEGACY_CONFIG_FILE) != null) { var p = System.getProperty(LEGACY_CONFIG_FILE); var externalConfig = ConfigFactory.parseFile(new File(p)).getConfig(name); result = externalConfig.withFallback(result); } if (System.getProperty(CONFIG_FILE) != null) { var p = System.getProperty(CONFIG_FILE); var externalConfig = ConfigFactory.parseFile(new File(p)).getConfig(name); result = externalConfig.withFallback(result); } return result.resolve(); } @Override public void configure() { var annotatedConstructors = reflections.getConstructorsWithAnyParamAnnotated(Config.class); for (var c : annotatedConstructors) { var params = c.getParameters(); bindParameters(params); } var annotatedMethods = reflections.getMethodsWithAnyParamAnnotated(Config.class); for (var m : annotatedMethods) { var params = m.getParameters(); bindParameters(params); } var annotatedFields = reflections.getFieldsAnnotatedWith(Config.class); for (var f : annotatedFields) { var annotation = f.getAnnotation(Config.class); bindValue(f.getType(), f.getAnnotatedType().getType(), annotation, isNullable(f.getAnnotations())); } } private void bindParameters(Parameter[] params) { for (var p : params) { if (!p.isAnnotationPresent(Config.class)) { continue; } var annotation = p.getAnnotation(Config.class); bindValue(p.getType(), p.getAnnotatedType().getType(), annotation, isNullable(p.getAnnotations())); } } private void bindValue(Class paramClass, Type paramType, Config annotation, boolean nullable) { if (boundAnnotations.contains(annotation)) { return; } @SuppressWarnings("unchecked") var key = (Key) Key.get(paramType, annotation); var path = annotation.value(); var value = getConfigValue(paramClass, paramType, path, nullable); if (value == null) { if (nullable) { bind(key).toProvider(NULL_PROVIDER); } else { throw new ConfigException.Missing(path); } } else { bind(key).toInstance(value); } boundAnnotations.add(annotation); } private Object getConfigValue(Class paramClass, Type paramType, String path, boolean nullable) { var extractedValue = ConfigExtractors.extractConfigValue(config, paramClass, path); if (extractedValue.isPresent()) { return extractedValue.get(); } if (nullable && !config.hasPath(path)) { return null; } var value = config.getValue(path); var type = value.valueType(); if (type.equals(ConfigValueType.OBJECT) && Map.class.isAssignableFrom(paramClass)) { var object = config.getObject(path); return object.unwrapped(); } else if (type.equals(ConfigValueType.OBJECT)) { return ConfigBeanFactory.create(config.getConfig(path), paramClass); } else if (type.equals(ConfigValueType.LIST) && List.class.isAssignableFrom(paramClass)) { var listType = ((ParameterizedType) paramType).getActualTypeArguments()[0]; var extractedListValue = ListExtractors.extractConfigListValue(config, listType, path); if (extractedListValue.isPresent()) { return extractedListValue.get(); } else { var configList = config.getConfigList(path); return configList.stream() .map(cfg -> ConfigBeanFactory.create(cfg, (Class) listType)) .collect(Collectors.toList()); } } throw new RuntimeException("Cannot obtain config value for " + paramType + " at path: " + path); } private static boolean isNullable(Annotation[] annotations) { if (annotations == null || annotations.length == 0) { return false; } return Arrays.stream(annotations) .anyMatch(a -> "Nullable".equals(a.annotationType().getSimpleName())); } } ================================================ FILE: config/src/main/java/com/walmartlabs/concord/config/ListExtractor.java ================================================ package com.walmartlabs.concord.config; /*- * ***** * Concord * ----- * Copyright (C) 2018 Takari * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import java.lang.reflect.Type; import java.util.List; public interface ListExtractor { List extractListValue(com.typesafe.config.Config config, String path); Type getMatchingParameterizedType(); } ================================================ FILE: config/src/main/java/com/walmartlabs/concord/config/ListExtractors.java ================================================ package com.walmartlabs.concord.config; /*- * ***** * Concord * ----- * Copyright (C) 2018 Takari * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import com.typesafe.config.Config; import com.typesafe.config.ConfigMemorySize; import com.typesafe.config.ConfigObject; import java.lang.reflect.Type; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; public enum ListExtractors implements ListExtractor { BOOLEAN(Boolean.class) { @Override public List extractListValue(Config config, String path) { return config.getBooleanList(path); } }, INTEGER(Integer.class) { @Override public List extractListValue(Config config, String path) { return config.getIntList(path); } }, DOUBLE(Double.class) { @Override public List extractListValue(Config config, String path) { return config.getDoubleList(path); } }, LONG(Long.class) { @Override public List extractListValue(Config config, String path) { return config.getLongList(path); } }, STRING(String.class) { @Override public List extractListValue(Config config, String path) { return config.getStringList(path); } }, DURATION(Duration.class) { @Override public List extractListValue(Config config, String path) { return config.getDurationList(path); } }, MEMORY_SIZE(ConfigMemorySize.class) { @Override public List extractListValue(Config config, String path) { return config.getMemorySizeList(path); } }, OBJECT(Object.class) { @Override public List extractListValue(Config config, String path) { return config.getAnyRefList(path); } }, CONFIG(Config.class) { @Override public List extractListValue(Config config, String path) { return config.getConfigList(path); } }, CONFIG_OBJECT(ConfigObject.class) { @Override public List extractListValue(Config config, String path) { return config.getObjectList(path); } }, CONFIG_VALUE(ConfigObject.class) { @Override public List extractListValue(Config config, String path) { return config.getList(path); } }; private final Class parameterizedTypeClass; private static final Map EXTRACTOR_MAP = new HashMap<>(); static { for (var extractor : ListExtractors.values()) { EXTRACTOR_MAP.put(extractor.getMatchingParameterizedType(), extractor); } } ListExtractors(Class parameterizedTypeClass) { this.parameterizedTypeClass = parameterizedTypeClass; } @Override public Type getMatchingParameterizedType() { return parameterizedTypeClass; } static Optional> extractConfigListValue(Config config, Type listType, String path) { if (EXTRACTOR_MAP.containsKey(listType)) { return Optional.of(EXTRACTOR_MAP.get(listType).extractListValue(config, path)); } else { return Optional.empty(); } } } ================================================ FILE: console2/.gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build /dist # Vite *.local # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* .vscode .xit /storybook-static .docz doczrc.js *.mdx ================================================ FILE: console2/.npmrc ================================================ registry=https://registry.npmjs.org ================================================ FILE: console2/README.md ================================================ # Concord UI ## Prerequisites - Node 24 LTS, available in `$PATH`; - Java 17, available in `$PATH`. Necessary only to build the package. ## Dependencies To install the necessary dependencies for the first time: ```bash $ npm ci ``` To update `package-lock.json` run ```bash $ ../mvnw clean package # only for the first run $ ../mvnw com.github.eirslett:frontend-maven-plugin:npm -Darguments=install ``` ## Running in Dev Mode In the dev mode the UI is served by running `npm start`. First time: ```bash $ npm ci $ npm run dev ``` Open http://localhost:3000. The `ci` step can be skipped for subsequent runs. The dev mode has the following limitations: - file download (e.g. downloading raw logs) doesn't work; - [custom forms](https://concord.walmartlabs.com/docs/getting-started/forms.html#custom) don't work. In order to use those features, you need to run the UI in production mode (see below). ## Running in Production Mode In the production mode the UI is served by concord-server from the JAR file created during concord-console2 [build](./pom.xml). When running locally, it is available at http://localhost:8001. ## Configuration Specify the path to the `cfg.js` file when you start [the Server](../server/dist): ``` CONSOLE_CFG_FILE=/path/to/cfg.js ``` or using concord-server.conf: ``` concord-server { console { cfgFile = "/path/to/cfg.js" } } ``` Use [./public/cfg.js](./public/cfg.js) as an example. ================================================ FILE: console2/cfg.d.ts ================================================ import {ColumnDefinition} from "./src/api/org"; export {}; export interface ConcordEnvironment { topBar?: TopBarMeta; loginUrl?: string; logoutUrl?: string; login?: LoginConfiguration; extraProcessMenuLinks?: ExtraProcessMenuLinks; lastUpdated?: string; customResources?: CustomResources; processListColumns?: ColumnDefinition[]; } export interface TopBarMeta { systemLinks?: SystemLinks; } export type SystemLinks = LinkMeta[]; export type ExtraProcessMenuLinks = ExtraProcessMenuLink[]; export interface LinkMeta { text: string; url: string; icon?: string; } export interface LoginConfiguration { usernameValidator?: (username: string) => string | undefined; usernameHint?: string; } export interface ExtraProcessMenuLink { url: string; label: string; color: string; icon: string; } export interface CustomResources { [key: string]: CustomResource; } export interface CustomResource { title?: string; description?: string; icon?: string; url: string; width?: string; height?: string; } declare global { interface Window { concord: ConcordEnvironment; } } ================================================ FILE: console2/index.html ================================================ Concord
================================================ FILE: console2/npm.sh ================================================ #!/usr/bin/env bash # run a local version of node installed by Maven export SET NODE_OPTIONS=--openssl-legacy-provider ./target/node/node ./target/node/node_modules/npm/bin/npm-cli.js "$@" ================================================ FILE: console2/package/META-INF/concord/webapp.properties ================================================ path=/ checksumsFileResourcePath=META-INF/console2.checksums.cvs resourceRoot=META-INF/console2/ indexHtmlRelativePath=index.html ================================================ FILE: console2/package.json ================================================ { "name": "concord-console", "version": "1.0.0", "private": true, "type": "module", "devDependencies": { "@datasert/cronjs-matcher": "^1.4.0", "@testing-library/react": "12.1.2", "@types/jest": "27.0.3", "@types/lodash": "4.14.178", "@types/react": "^17.0.91", "@types/react-dom": "^17.0.26", "@types/sinon": "10.0.6", "@types/styled-components": "5.1.17", "@typescript-eslint/typescript-estree": "5.35.1", "@vitejs/plugin-react": "^6.0.1", "esbuild": "^0.27.4", "eslint": "7.32.0", "prettier": "2.5.1", "react-hooks-testing-library": "0.6.0", "shx": "0.3.3", "ts-node": "10.4.0", "typescript": "5.3.3", "vite": "^8.0.5" }, "dependencies": { "@monaco-editor/react": "4.3.1", "ansi_up": "6.0.6", "constate": "3.3.0", "copy-to-clipboard": "3.3.1", "date-fns": "2.27.0", "formik": "2.2.9", "lodash": "4.18.1", "parse-domain": "4.1.0", "query-string": "7.0.1", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "7.21.2", "react-idle-timer": "5.4.1", "react-json-view": "1.21.3", "react-router": "7.14.1", "react-spring": "9.3.2", "reakit": "1.3.11", "semantic-ui-calendar-react": "0.15.3", "semantic-ui-css": "2.4.1", "semantic-ui-react": "2.0.4", "styled-components": "5.3.3", "styled-tools": "1.7.2", "typeface-lato": "1.1.13", "url-search-params-polyfill": "8.1.1" }, "scripts": { "start": "vite", "dev": "vite", "build": "vite build", "build:check": "tsc && vite build", "preview": "vite preview", "test": "vitest", "pretty": "prettier --tab-width 4 --print-width 100 --single-quote --jsx-bracket-same-line --arrow-parens 'always' parser 'typescript' --write 'src/**/*.{ts,tsx}'" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ], "rules": { "import/no-anonymous-default-export": [ 2, { "allowArrowFunction": true, "allowAnonymousFunction": true, "allowAnonymousClass": true } ] } }, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ], "proxy": "http://localhost:8001" } ================================================ FILE: console2/pom.xml ================================================ 4.0.0 com.walmartlabs.concord parent 2.40.1-SNAPSHOT ../pom.xml concord-console2 jar ${project.groupId}:${project.artifactId} false https://nodejs.org/dist/ ci org.codehaus.mojo license-maven-plugin src com.github.eirslett frontend-maven-plugin install node and npm generate-resources install-node-and-npm ${skipNpm} npm ci npm ${skipNpm} ${npm.installCmd} --legacy-peer-deps build generate-resources npm ${project.version} ${skipNpm} run build true target ${node.downloadRoot} v${node.version} ${basedir} maven-resources-plugin copy-package-descriptor prepare-package copy-resources ${basedir}/target/classes package **/* net.nicoulaj.maven.plugins checksum-maven-plugin 1.11 create-checksum-file prepare-package files ${project.build.directory}/classes/META-INF/console2 **/*.* SHA-1 classes/META-INF/console2.checksums.cvs true ================================================ FILE: console2/public/cfg.js ================================================ // environment specific data window.concord = { documentationSite: 'https://concord.walmartlabs.com', topBar: { systemLinks: [ { text: 'GitHub', url: 'https://github.com/walmartlabs/concord', icon: 'github' } ] } }; ================================================ FILE: console2/public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "192x192", "type": "image/png" } ], "start_url": "./index.html", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: console2/react-json-view.d.ts ================================================ // adapted from the original react-json-view/index.d.ts file import * as React from 'react'; export interface ReactJsonViewProps { /** * This property contains your input JSON. * * Required. */ src: object; /** * Contains the name of your root node. Use null or false for no name. * * Default: "root" */ name?: string | null | false; /** * RJV supports base-16 themes. Check out the list of supported themes in the demo. * A custom "rjv-default" theme applies by default. * * Default: "rjv-default" */ theme?: ThemeKeys | ThemeObject; /** * Style attributes for react-json-view container. * Explicit style attributes will override attributes provided by a theme. * * Default: "rjv-default" */ style?: React.CSSProperties; /** * Style of expand/collapse icons. Accepted values are "circle", triangle" or "square". * * Default: {} */ iconStyle?: 'circle' | 'triangle' | 'square'; /** * Set the indent-width for nested objects. * * Default: 4 */ indentWidth?: number; /** * When set to true, all nodes will be collapsed by default. * Use an integer value to collapse at a particular depth. * * Default: false */ collapsed?: boolean | number; /** * When an integer value is assigned, strings will be cut off at that length. * Collapsed strings are followed by an ellipsis. * String content can be expanded and collapsed by clicking on the string value. * * Default: false */ collapseStringsAfterLength?: number | false; /** * Callback function to provide control over what objects and arrays should be collapsed by default. * An object is passed to the callback containing name, src, type ("array" or "object") and namespace. * * Default: false */ shouldCollapse?: false | ((field: CollapsedFieldProps) => boolean); /** * When an integer value is assigned, arrays will be displayed in groups by count of the value. * Groups are displayed with brakcet notation and can be expanded and collapsed by clickong on the brackets. * * Default: 100 */ groupArraysAfterLength?: number; /** * When prop is not false, the user can copy objects and arrays to clipboard by clicking on the clipboard icon. * Copy callbacks are supported. * * Default: true */ enableClipboard?: boolean | ((copy: OnCopyProps) => void); /** * When set to true, objects and arrays are labeled with size. * * Default: true */ displayObjectSize?: boolean; /** * When set to true, data type labels prefix values. * * Default: true */ displayDataTypes?: boolean; /** * When a callback function is passed in, edit functionality is enabled. * The callback is invoked before edits are completed. Returning false * from onEdit will prevent the change from being made. see: onEdit docs. * * Default: false */ onEdit?: ((edit: InteractionProps) => false | any) | false; /** * When a callback function is passed in, add functionality is enabled. * The callback is invoked before additions are completed. * Returning false from onAdd will prevent the change from being made. see: onAdd docs * * Default: false */ onAdd?: ((add: InteractionProps) => false | any) | false; /** * When a callback function is passed in, delete functionality is enabled. * The callback is invoked before deletions are completed. * Returning false from onDelete will prevent the change from being made. see: onDelete docs * * Default: false */ onDelete?: ((del: InteractionProps) => false | any) | false; /** * When a function is passed in, clicking a value triggers the onSelect method to be called. * * Default: false */ onSelect?: ((select: OnSelectProps) => void) | false; /** * Custom message for validation failures to onEdit, onAdd, or onDelete callbacks. * * Default: "Validation Error" */ validationMessage?: string; } export interface OnCopyProps { /** * The JSON tree source object */ src: object; /** * List of keys. */ namespace: Array; /** * The last key in the namespace array. */ name: string | null; } export interface CollapsedFieldProps { /** * The name of the entry. */ name: string | null; /** * The corresponding JSON subtree. */ src: object; /** * The type of src. Can only be "array" or "object". */ type: 'array' | 'object'; /** * The scopes above the current entry. */ namespace: Array; } export interface InteractionProps { /** * The updated subtree of the JSON tree. */ updated_src: object; /** * The existing subtree of the JSON tree. */ existing_src: object; /** * The key of the entry that is interacted with. */ name: string | null; /** * List of keys. */ namespace: Array; /** * The original value of the entry that is interacted with. */ existing_value: object | string | number | boolean | null; /** * The updated value of the entry that is interacted with. */ new_value?: object | string | number | boolean | null; } export interface OnSelectProps { /** * The name of the currently selected entry. */ name: string | null; /** * The value of the currently selected entry. */ value: object | string | number | boolean | null; /** * The type of the value. For "number" type, it will be replaced with the more * accurate types: "float", "integer", or "nan". */ type: string; /** * List of keys representing the scopes above the selected entry. */ namespace: Array; } export interface ThemeObject { base00: string; base01: string; base02: string; base03: string; base04: string; base05: string; base06: string; base07: string; base08: string; base09: string; base0A: string; base0B: string; base0C: string; base0D: string; base0E: string; base0F: string; } export type ThemeKeys = | 'apathy' | 'apathy:inverted' | 'ashes' | 'bespin' | 'brewer' | 'bright:inverted' | 'bright' | 'chalk' | 'codeschool' | 'colors' | 'eighties' | 'embers' | 'flat' | 'google' | 'grayscale' | 'grayscale:inverted' | 'greenscreen' | 'harmonic' | 'hopscotch' | 'isotope' | 'marrakesh' | 'mocha' | 'monokai' | 'ocean' | 'paraiso' | 'pop' | 'railscasts' | 'rjv-default' | 'shapeshifter' | 'shapeshifter:inverted' | 'solarized' | 'summerfruit' | 'summerfruit:inverted' | 'threezerotwofour' | 'tomorrow' | 'tube' | 'twilight'; declare const ReactJson: React.ComponentType; export default ReactJson; ================================================ FILE: console2/src/App.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Dispatch, useEffect, useReducer, useState } from 'react'; import { HashRouter, Navigate, Route, Routes } from 'react-router'; import { ProtectedRoute } from './components/organisms'; import { AboutPage, AddRepositoryPage, CustomResourcePage, JsonStorePage, LoginPage, LogoutPage, NewProjectPage, NewSecretPage, NewTeamPage, NotFoundPage, OrganizationListPage, OrganizationPage, ProcessFormPage, ProcessListPage, ProcessCardFormPage, ProcessPage, ProcessWizardPage, ProfilePage, ProjectPage, RepositoryPage, SecretPage, TeamPage, UnauthorizedPage, UserActivityPage, } from './components/pages'; import { Layout } from './components/templates'; import NewStorageQueryPage from './components/pages/JsonStorePage/NewStorageQueryPage'; import EditStoreQueryPage from './components/pages/JsonStorePage/EditStoreQueryPage'; import { initialState, LoadingAction, reducer } from './reducers/loading'; import NewStorePage from './components/pages/JsonStorePage/NewStorePage'; import NodeRosterPage from './components/pages/NodeRoster/NodeRosterPage'; import HostPage from './components/pages/NodeRoster/HostPage'; import { UserSessionContext, checkSession, UserInfo } from './session'; export const LoadingDispatch = React.createContext>( {} as Dispatch ); export const LoadingState = React.createContext(false); const App = () => { const [state, dispatch] = useReducer(reducer, initialState); const [userInfo, setUserInfo] = useState(); const [loggingIn, setLoggingIn] = useState(true); useEffect(() => { checkSession({ userInfo, setUserInfo, loggingIn: false, setLoggingIn }); }, [userInfo]); return ( } /> {/* pages with no decorations */} } /> } /> } /> } /> {/* pages with standard decorations (provided by Layout) */} }> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); // } }; export default App; ================================================ FILE: console2/src/api/__tests__/common.deepMerge.test.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { deepMerge } from '../common'; test('deepMerge works', () => { const actual = deepMerge({ x: { y: 123 } }, { x: { z: 234 } }); const expected = { x: { y: 123, z: 234 } }; expect(actual).toEqual(expected); }); ================================================ FILE: console2/src/api/__tests__/common.parseNestedQueryParams.test.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { parseNestedQueryParams } from '../common'; test('parseNestedQueryParams works with a single value', () => { const actual = parseNestedQueryParams({ 'x.y.z': '123' }, ['x']); const expected = { x: { y: { z: '123' } } }; expect(actual).toEqual(expected); }); test('parseNestedQueryParams works with multiple nested values', () => { const actual = parseNestedQueryParams({ normalOne: 'true', 'x.y.a': '123', 'x.y.b': '234' }, [ 'x' ]); const expected = { normalOne: 'true', x: { y: { b: '234', a: '123' } } }; expect(actual).toEqual(expected); }); ================================================ FILE: console2/src/api/__tests__/common.parseQueryParams.test.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { parseQueryParams } from '../common'; test('parseQueryParams handles one query parameter', () => { const actual = parseQueryParams('http://localhost:3000/#/org/?param1=123'); const expected = { param1: '123' }; expect(actual).toEqual(expected); }); test('parseQueryParams handles more than one query parameter', () => { const actual = parseQueryParams('http://localhost:3000/#/org/?param1=123¶m2=abc'); const expected = { param1: '123', param2: 'abc' }; expect(actual).toEqual(expected); }); test('parseQueryParams handles periods in a query parameter', () => { const result = parseQueryParams('http://localhost:3000/#/org/?param.1=123¶m.2=abc'); const expected = { 'param.1': '123', 'param.2': 'abc' }; expect(result).toEqual(expected); }); test('parseQueryParams handles url with no question mark "?"', () => { const actual = parseQueryParams('http://localhost:3000/#/org/param.1=123'); const expected = {}; expect(actual).toEqual(expected); }); test('parseQueryParams handles url with no params', () => { const result = parseQueryParams('http://localhost:3000/#/org/param.1=123'); const expected = {}; expect(result).toEqual(expected); }); test('parseQueryParams handles multi-value params', () => { const result = parseQueryParams('http://localhost:3000/#/org/?param.1=123¶m.1=234'); const expected = { 'param.1': ['123', '234'] }; expect(result).toEqual(expected); }); ================================================ FILE: console2/src/api/__tests__/common.queryParams.test.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { queryParams } from '../common'; import { AnsibleStatus, SearchFilter, SortField, SortOrder } from '../process/ansible'; test('queryParams accepts an object of key value pairs. e.g. { key: value, ... }', () => { const actual = queryParams({ param1: '123', param2: 'abc' }); const expected = 'param1=123¶m2=abc'; expect(actual).toEqual(expected); }); test('queryParams handles parameters with period characters in key name', () => { const actual = queryParams({ 'param.1': '123', 'param.2': 'abc' }); const expected = 'param.1=123¶m.2=abc'; expect(actual).toEqual(expected); }); test('queryParams accepts numbers as values', () => { const actual = queryParams({ param: 1, param2: 999 }); const expected = 'param=1¶m2=999'; expect(actual).toEqual(expected); }); test('queryParams handles multiple boolean values', () => { const actual = queryParams({ param: true, param2: false }); const expected = 'param=true¶m2=false'; expect(actual).toEqual(expected); }); test('queryParams handles undefined values', () => { const actual = queryParams({ param: undefined, param2: 'works' }); const expected = 'param2=works'; expect(actual).toEqual(expected); }); test('queryParams handles SearchFilter type', () => { const filters: SearchFilter = { host: 'host', hostGroup: 'host-group', limit: 1, offset: 10, status: AnsibleStatus.CHANGED, sortField: SortField.DURATION, sortBy: SortOrder.DESC }; const actual = queryParams({ ...filters }); const expected = 'host=host&hostGroup=host-group&limit=1&offset=10&status=CHANGED&sortField=DURATION&sortBy=DESC'; expect(actual).toEqual(expected); }); ================================================ FILE: console2/src/api/audit/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, ConcordKey, EntityOwner, fetchJson, queryParams } from '../common'; // must match the keys of com.walmartlabs.concord.server.audit.AuditObject enum export enum AuditObject { EXTERNAL_EVENT = 'EXTERNAL_EVENT', JSON_STORE = 'JSON_STORE', JSON_STORE_DATA = 'JSON_STORE_DATA', JSON_STORE_QUERY = 'JSON_STORE_QUERY', ORGANIZATION = 'ORGANIZATION', PROJECT = 'PROJECT', PROCESS = 'PROCESS', SECRET = 'SECRET', TEAM = 'TEAM' } // must match the keys of com.walmartlabs.concord.server.audit.AuditAction enum export enum AuditAction { CREATE = 'CREATE', UPDATE = 'UPDATE', DELETE = 'DELETE', ACCESS = 'ACCESS' } // must match the allowed keys in AuditLogResource#ALLOWED_DETAILS_KEYS export interface AuditLogFilter { object?: AuditObject; action?: AuditAction; userId?: ConcordId; username?: string; after?: string; before?: string; details?: { eventId?: string; fullRepoName?: string; githubEvent?: string; source?: string; orgName?: ConcordKey; projectName?: ConcordKey; secretName?: ConcordKey; jsonStoreName?: ConcordKey; teamName?: ConcordKey; }; offset?: number; // TODO rename to "page"? limit?: number; } export interface AuditLogEntry { entryDate: string; action: string; object: string; details: {}; user?: EntityOwner; } export interface PaginatedAuditLogEntries { items: AuditLogEntry[]; next: boolean; } export const list = async (filter: AuditLogFilter): Promise => { const { offset = 0, limit = 50 } = filter; const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset; const limitParam = limit > 0 ? limit + 1 : limit; // convert the `details` object into a set of `details.key=value` query parameters const details = filter.details || {}; const detailsParam = {}; Object.keys(details).forEach((k) => (detailsParam[`details.${k}`] = details[k])); const data: AuditLogEntry[] = await fetchJson( `/api/v1/audit?${queryParams({ ...detailsParam, object: filter.object, action: filter.action, userId: filter.userId, username: filter.username, after: filter.after, before: filter.before, offset: offsetParam, limit: limitParam })}` ); const hasMoreElements: boolean = !!limit && data.length > limit; if (limit > 0 && hasMoreElements) { // TODO we should trim the list to the size instead of removing a single element data.pop(); } return { items: data, next: hasMoreElements }; }; ================================================ FILE: console2/src/api/common.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ export type ConcordId = string; export type ConcordKey = string; export interface Owner { username: string; userDomain?: string; } export interface RequestErrorData { instanceId?: ConcordId; message?: string; details?: string; status: number; level?: string; } export type RequestError = RequestErrorData | null; export const parseSiestaError = async (resp: Response) => { const json = await resp.json(); let message; if (resp.status < 400 || resp.status >= 500) { message = `ERROR: ${resp.statusText} (${resp.status})`; } return { message, instanceId: json.instanceId, details: json[0].message, status: resp.status }; }; export const parseJsonError = async (resp: Response) => { const json = await resp.json(); let message; if (resp.status < 400 || resp.status >= 500) { message = json.message; } return { message, instanceId: json.instanceId, details: json.details, level: json.level ? json.level : 'ERROR', status: resp.status }; }; export const parseTextError = async (resp: Response) => { const text = await resp.text(); let message; if (resp.status < 400 && resp.status >= 500) { message = `ERROR: ${resp.statusText} (${resp.status})`; } return { message, details: text, status: resp.status }; }; export const makeError = async (resp: Response): Promise => { const contentLength = resp.headers.get('Content-Length'); if (contentLength !== '0') { const contentType = resp.headers.get('Content-Type') || ''; try { if (contentType.indexOf('vnd.concord-validation-errors-v1+json') >= 0) { return parseSiestaError(resp); } else if (contentType.indexOf('json') >= 0) { return parseJsonError(resp); } else if (contentType.indexOf('text/plain') >= 0) { return parseTextError(resp); } } catch (e) { console.warn('makeError -> error while parsing the response: %o', e); // fall back to the default error handling } } return { message: `ERROR: ${resp.statusText} (${resp.status})`, status: resp.status }; }; export const managedFetch = async (input: RequestInfo, init?: RequestInit): Promise => { if (!init) { init = {}; } init.credentials = 'same-origin'; if (!init.headers) { init.headers = new Headers(); } // send a special header with each request to indicate that this is, in fact, a UI request if (init.headers instanceof Headers) { init.headers.set('X-Concord-UI-Request', 'true'); } else { init.headers = { ...init.headers, 'X-Concord-UI-Request': 'true' }; } let response; try { response = await fetch(input, init); } catch (err) { console.warn( "managedFetch ['%o', '%o'] -> error while performing a request: %o", input, init, response, err ); return Promise.reject({ message: 'Error while performing a request', cause: err }); } if (!response.ok) { throw await makeError(response); } return response; }; /** * Generates a query parameter string from an object of key/value pairs * @param params the key/value object accepted * * @return a query parameter string e.g. "foo=123&bar=abc" */ export const queryParams = (params: any, allowEmpty?: boolean): string => { const esc = encodeURIComponent; const result: string[] = []; Object.keys(params) .filter((k) => { const v = params[k]; if (v === undefined || v === null || (!allowEmpty && v === '')) { return false; } if (Array.isArray(v) && v.length === 0) { return false; } return true; }) .forEach((k) => { const v = params[k]; if (Array.isArray(v)) { v.forEach((vv) => { result.push(esc(k) + '=' + esc(vv)); }); } else { result.push(esc(k) + '=' + esc(v)); } }); return result.join('&'); }; export type QueryParams = { [key: string]: string }; /** * Parse url parameters from a url string * @param url a http url string * * @return an object e.g. { "param": "value", ... } */ export const parseQueryParams = (url: string): QueryParams => { // Remove all non-valid characters const validString = url.replace(/[^a-z0-9\s-]/, ''); // Split url and take the right hand side // ! assumption there is only one question mark let queryParams: string; if (validString.includes('?')) { queryParams = validString.split('?')[1]; } else { // No Query Params exist in the string return {}; } // Split find params by splitting on & characters let kvs: string[] = []; if (queryParams.includes('&')) { kvs = queryParams.split('&'); } else { // There is only one param to return, so return it const [k, v] = queryParams.split('='); return { [k]: v }; } // initialize an object for iteration let result = {}; // inject params as key value pairs for (const kv of kvs) { const [k, v] = kv.split('='); // handle multi-value params const prev = result[k]; if (prev) { if (prev instanceof Array) { prev.push(v); } else { result[k] = [prev, v]; } } else { result[k] = v; } } return result; }; export const deepMerge = (a: any, b: any): any => { const result = { ...a }; Object.keys(b).forEach((k) => { const av = a[k]; const bv = b[k]; let o = bv; if (typeof av === 'object' && typeof bv === 'object') { o = deepMerge(av, bv); } result[k] = o; }); return result; }; export type QueryMultiParams = { [key: string]: any }; export const parseNestedQueryParams = (params: QueryParams, keys: string[]): QueryMultiParams => { let result: QueryMultiParams = { ...params }; Object.keys(params) .filter((p) => keys.some((k) => p.startsWith(k) && p.includes('.'))) .forEach((p) => { let as = p.split('.').reverse(); let obj: QueryMultiParams = { [as[0]]: params[p] }; for (let i = 1; i < as.length; i++) { obj = { [as[i]]: obj }; } delete result[p]; result = deepMerge(result, obj); }); return result; }; export const fetchJson = async (uri: string, init?: RequestInit): Promise => { const response = await managedFetch(uri, init); return response.json(); }; const wait = (delayMs: number) => new Promise((resolve) => { setTimeout(resolve, delayMs); }); export const retryRequest = async ( request: () => Promise, shouldRetry: (error: RequestError) => boolean, attempts = 5, delayMs = 250 ): Promise => { let lastError: RequestError; for (let attempt = 1; attempt <= attempts; attempt++) { try { return await request(); } catch (e) { lastError = e as RequestError; if (attempt === attempts || !shouldRetry(lastError)) { throw e; } await wait(delayMs); } } throw lastError; }; export interface EntityOwner { id: ConcordId; username: string; userDomain?: string; displayName?: string; } export enum OperationResult { CREATED = 'CREATED', UPDATED = 'UPDATED', DELETED = 'DELETED', ALREADY_EXISTS = 'ALREADY_EXISTS', NOT_FOUND = 'NOT_FOUND' } export interface GenericOperationResult { ok: boolean; result: OperationResult; } export enum EntityType { PROJECT = 'PROJECT', SECRET = 'SECRET' } ================================================ FILE: console2/src/api/noderoster/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { fetchJson, ConcordId, queryParams, ConcordKey } from '../common'; import { ProcessCheckpointEntry, ProcessHistoryEntry, ProcessKind, ProcessMeta, ProcessStatus, TriggeredByEntry } from '../process'; export interface HostEntry { id: ConcordId; name: string; createdAt: string; artifactUrl?: string; } export interface PaginatedHostEntry { items: HostEntry[]; next: boolean; } export interface HostFilter { host?: string; processInstanceId?: ConcordId; artifact?: string; } export interface HostArtifact { url: string; processInstanceId: ConcordId; } export interface PaginatedHostArtifacts { items: HostArtifact[]; next: boolean; } export interface HostProcessEntry { instanceId: ConcordId; parentInstanceId?: ConcordId; status: ProcessStatus; kind: ProcessKind; orgName?: ConcordKey; projectName?: ConcordKey; repoName?: ConcordKey; repoUrl?: string; repoPath?: string; commitId?: string; initiator: string; createdAt: string; startAt?: string; lastUpdatedAt: string; handlers?: string[]; meta?: ProcessMeta; tags?: string[]; checkpoints?: ProcessCheckpointEntry[]; statusHistory?: ProcessHistoryEntry[]; disabled: boolean; triggeredBy?: TriggeredByEntry; timeout?: number; } export interface PaginatedHostProcessEntry { items: HostProcessEntry[]; next: boolean; } export const getHost = async (id: ConcordId): Promise => { return fetchJson(`/api/v1/noderoster/hosts/${id}`); }; export type HostsInclude = 'artifacts'; export const listHosts = async ( page: number, limit: number, includes: HostsInclude[], filter?: HostFilter ): Promise => { const offsetParam = page > 0 && limit > 0 ? page * limit : page; const limitParam = limit > 0 ? limit + 1 : limit; const data: HostEntry[] = await fetchJson( `/api/v1/noderoster/hosts?${queryParams({ offset: offsetParam, limit: limitParam, host: filter?.host, processInstanceId: filter?.processInstanceId, artifact: filter?.artifact, include: includes })}` ); const hasMoreElements: boolean = limit > 0 && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; export const getLatestHostFacts = async (id: ConcordId): Promise => { return fetchJson(`/api/v1/noderoster/facts/last?hostId=${id}`); }; export const listHostArtifacts = async ( hostId: ConcordId, page: number, limit: number, filter?: string ): Promise => { const offsetParam = page > 0 && limit > 0 ? page * limit : page; const limitParam = limit > 0 ? limit + 1 : limit; const data: HostArtifact[] = await fetchJson( `/api/v1/noderoster/artifacts?${queryParams({ hostId, offset: offsetParam, limit: limitParam, filter })}` ); const hasMoreElements: boolean = limit > 0 && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; export const listHostProcesses = async ( hostId: ConcordId, page: number, limit: number ): Promise => { const offsetParam = page > 0 && limit > 0 ? page * limit : page; const limitParam = limit > 0 ? limit + 1 : limit; const data: HostProcessEntry[] = await fetchJson( `/api/v1/noderoster/processes?${queryParams({ hostId, offset: offsetParam, limit: limitParam })}` ); const hasMoreElements: boolean = limit > 0 && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; ================================================ FILE: console2/src/api/org/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { fetchJson, ConcordKey, ConcordId, OperationResult, EntityOwner, EntityType, queryParams } from '../common'; export enum OrganizationVisibility { PUBLIC = 'PUBLIC', PRIVATE = 'PRIVATE' } export type SearchType = 'substring' | 'equals'; export type SearchValueType = 'string' | 'boolean'; export interface SearchOption { value: string; text: string; } export enum RenderType { /** * Render a link to the process using the value as the link's caption. */ PROCESS_LINK = 'process-link', /** * Render a link to the process' project using the value as the link's caption. */ PROJECT_LINK = 'project-link', /** * Render a link to the process' repository using the value as the link's caption. */ REPO_LINK = 'repo-link', /** * Render the current process' status. */ PROCESS_STATUS = 'process-status', /** * Render as a timestamp. */ TIMESTAMP = 'timestamp', /** * Render as an array of strings. */ STRING_ARRAY = 'string-array', /** * Render as a duration (current timestamp - value) */ DURATION = 'duration', /** * Render as link */ LINK = 'link' } export interface ColumnDefinition { builtin?: string; caption: string; source: string; textAlign?: 'center' | 'left' | 'right'; collapsing?: boolean; singleLine?: boolean; render?: RenderType; searchValueType?: SearchValueType; searchType?: SearchType; searchOptions?: SearchOption[]; } export interface OrganizationEntryMetaUI { processList?: ColumnDefinition[]; } export interface CheckResult { result: boolean; } export interface OrganizationEntryMeta { ui?: OrganizationEntryMetaUI; } export interface OrganizationEntry { id: string; name: string; owner?: EntityOwner; visibility: OrganizationVisibility; meta?: OrganizationEntryMeta; } export enum ResourceAccessLevel { OWNER = 'OWNER', WRITER = 'WRITER', READER = 'READER' } export interface ResourceAccessEntry { teamId: ConcordId; teamName: ConcordKey; level: ResourceAccessLevel; } export interface OrganizationOperationResult { ok: boolean; id: ConcordId; result: OperationResult; } export interface PaginatedOrganizationEntries { items: OrganizationEntry[]; next: boolean; } export const list = async ( onlyCurrent: boolean, page: number, limit: number, filter?: string ): Promise => { const offsetParam = page > 0 && limit > 0 ? page * limit : page; const limitParam = limit > 0 ? limit + 1 : limit; const data: OrganizationEntry[] = await fetchJson( `/api/v1/org?${queryParams({ onlyCurrent: onlyCurrent, offset: offsetParam, limit: limitParam, filter })}` ); const hasMoreElements: boolean = limit > 0 && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; export const get = (orgName: ConcordKey): Promise => fetchJson(`/api/v1/org/${orgName}`); export const checkResult = (entity: EntityType, orgName: ConcordKey): Promise => fetchJson( `api/v1/${entity}/canCreate?${queryParams({ orgName })}` ); export const changeOwner = ( orgId: ConcordId, ownerId: ConcordId ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: orgId, owner: { id: ownerId } }) }; return fetchJson(`/api/v1/org`, opts); }; ================================================ FILE: console2/src/api/org/jsonstore/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, ConcordKey, fetchJson, GenericOperationResult, EntityOwner, queryParams, OperationResult } from '../../common'; import { ResourceAccessEntry } from '../index'; export enum StorageVisibility { PUBLIC = 'PUBLIC', PRIVATE = 'PRIVATE' } export interface StorageEntry { id: ConcordId; name: ConcordKey; orgId: ConcordId; orgName: ConcordKey; projectId: ConcordId; projectName: ConcordKey; visibility: StorageVisibility; owner?: EntityOwner; } export interface StorageCapacity { size?: number; maxSize?: number; } export interface StorageOperationResult { ok: boolean; id: ConcordId; result: OperationResult; } export interface PaginatedStorageEntries { items: StorageEntry[]; next: boolean; } export interface Pagination { limit: number; offset: number; } export type StorageDataEntry = string; export interface PaginatedStorageDataEntries { items: StorageDataEntry[]; next: boolean; } export interface StorageQueryEntry { name: ConcordKey; text: string; } export interface PaginatedStorageQueryEntries { items: StorageQueryEntry[]; next: boolean; } export const get = (orgName: ConcordKey, storeName: ConcordKey): Promise => { return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}`); }; export const getCapacity = ( orgName: ConcordKey, storeName: ConcordKey ): Promise => { return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/capacity`); }; export const list = async ( orgName: ConcordKey, offset: number, limit: number, filter?: string ): Promise => { const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset; const limitParam = limit > 0 ? limit + 1 : limit; const data: StorageEntry[] = await fetchJson( `/api/v1/org/${orgName}/jsonstore?${queryParams({ offset: offsetParam, limit: limitParam, filter })}` ); const hasMoreElements: boolean = !!limit && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; export const createOrUpdate = ( orgName: ConcordKey, storeName: ConcordKey, visibility?: StorageVisibility, newOrgName?: ConcordKey ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: storeName, orgName: newOrgName, visibility }) }; return fetchJson(`/api/v1/org/${orgName}/jsonstore`, opts); }; export const deleteStorage = ( orgName: ConcordKey, storeName: ConcordKey ): Promise => { const opts = { method: 'DELETE' }; return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}`, opts); }; export const updateVisibility = ( orgName: ConcordKey, storeName: ConcordKey, visibility: StorageVisibility ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: storeName, visibility }) }; return fetchJson(`/api/v1/org/${orgName}/jsonstore`, opts); }; export const changeOwner = ( orgName: ConcordKey, storeName: ConcordKey, ownerId: ConcordId ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: storeName, owner: { id: ownerId } }) }; return fetchJson(`/api/v1/org/${orgName}/jsonstore`, opts); }; export const getAccess = ( orgName: ConcordKey, storeName: ConcordKey ): Promise => { return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/access`); }; export const updateAccess = ( orgName: ConcordKey, storeName: ConcordKey, entries: ResourceAccessEntry[] ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entries) }; return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/access/bulk`, opts); }; export const listStorageData = async ( orgName: ConcordKey, storeName: ConcordKey, offset: number, limit: number, filter?: string ): Promise => { const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset; const limitParam = limit > 0 ? limit + 1 : limit; const data: StorageDataEntry[] = await fetchJson( `/api/v1/org/${orgName}/jsonstore/${storeName}/item?${queryParams({ offset: offsetParam, limit: limitParam, filter })}` ); const hasMoreElements: boolean = !!limit && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; export const deleteStorageData = ( orgName: ConcordKey, storeName: ConcordKey, storagePath: string ): Promise => { const opts = { method: 'DELETE' }; const escapedPath = escapeStoragePath(storagePath); return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/item/${escapedPath}`, opts); }; export const escapeStoragePath = (s: string): string => s.replace(/\//g, '%2F'); export const listStorageQuery = async ( orgName: ConcordKey, storeName: ConcordKey, offset: number, limit: number, filter?: string ): Promise => { const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset; const limitParam = limit > 0 ? limit + 1 : limit; const data: StorageQueryEntry[] = await fetchJson( `/api/v1/org/${orgName}/jsonstore/${storeName}/query?${queryParams({ offset: offsetParam, limit: limitParam, filter })}` ); const hasMoreElements: boolean = !!limit && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; export const deleteStorageQuery = ( orgName: ConcordKey, storeName: ConcordKey, storageQueryName: string ): Promise => { const opts = { method: 'DELETE' }; return fetchJson( `/api/v1/org/${orgName}/jsonstore/${storeName}/query/${storageQueryName}`, opts ); }; export const createOrUpdateStorageQuery = ( orgName: ConcordKey, storeName: ConcordKey, queryName: ConcordKey, query: string ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: queryName, text: query }) }; return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/query`, opts); }; export const getStorageQuery = ( orgName: ConcordKey, storeName: ConcordKey, queryName: ConcordKey ): Promise => { return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/query/${queryName}`); }; export const executeQuery = ( orgName: ConcordKey, storeName: ConcordKey, query: string ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: query }; return fetchJson(`/api/v1/org/${orgName}/jsonstore/${storeName}/execQuery?maxLimit=50`, opts); }; ================================================ FILE: console2/src/api/org/project/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ColumnDefinition, ResourceAccessEntry } from '../'; import { ConcordId, ConcordKey, EntityOwner, fetchJson, GenericOperationResult, OperationResult, queryParams, RequestError, retryRequest } from '../../common'; export enum ProjectVisibility { PUBLIC = 'PUBLIC', PRIVATE = 'PRIVATE' } export interface ProjectEntryMetaUI { processList?: ColumnDefinition[]; childrenProcessList?: ColumnDefinition[]; } export interface ProjectEntryMeta { ui?: ProjectEntryMetaUI; } export enum RawPayloadMode { DISABLED = 'DISABLED', OWNERS = 'OWNERS', TEAM_MEMBERS = 'TEAM_MEMBERS', ORG_MEMBERS = 'ORG_MEMBERS', EVERYONE = 'EVERYONE' } export enum OutVariablesMode { DISABLED = 'DISABLED', OWNERS = 'OWNERS', TEAM_MEMBERS = 'TEAM_MEMBERS', ORG_MEMBERS = 'ORG_MEMBERS', EVERYONE = 'EVERYONE' } export enum ProcessExecMode { DISABLED = 'DISABLED', READERS = 'READERS', WRITERS = 'WRITERS' } const shouldRetryProjectRequest = (error: RequestError) => { if (!error || (error.status !== 400 && error.status !== 404)) { return false; } return /(?:Organization|Project) not found:/i.test(error.details || ''); }; export interface ProjectEntry { id: ConcordId; name: ConcordKey; owner: EntityOwner; orgId: ConcordId; orgName: ConcordKey; description?: string; visibility: ProjectVisibility; rawPayloadMode: RawPayloadMode; meta?: ProjectEntryMeta; outVariablesMode: OutVariablesMode; processExecMode: ProcessExecMode; } export interface NewProjectEntry { name: ConcordKey; description?: string; visibility: ProjectVisibility; } export interface UpdateProjectEntry { id?: ConcordId; name?: ConcordKey; orgId?: ConcordId; orgName?: ConcordKey; description?: string; visibility?: ProjectVisibility; rawPayloadMode?: RawPayloadMode; outVariablesMode?: OutVariablesMode; processExecMode?: ProcessExecMode; } export interface PaginatedProjectEntries { items: ProjectEntry[]; next: boolean; } export interface KVCapacity { size: number; maxSize?: number; } export const get = (orgName: ConcordKey, projectName: ConcordKey): Promise => { return fetchJson(`/api/v2/org/${orgName}/project/${projectName}`); }; export const getCapacity = ( orgName: ConcordKey, projectName: ConcordKey ): Promise => { return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/kv/capacity`); }; export const list = async ( orgName: ConcordKey, offset: number, limit: number, filter?: string ): Promise => { const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset; const limitParam = limit > 0 ? limit + 1 : limit; const data: ProjectEntry[] = await fetchJson( `/api/v1/org/${orgName}/project?${queryParams({ offset: offsetParam, limit: limitParam, filter })}` ); const hasMoreElements: boolean = !!limit && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; export interface ProjectOperationResult { ok: boolean; id: ConcordId; result: OperationResult; } // TODO response type export const createOrUpdate = ( orgName: ConcordKey, entry: NewProjectEntry | UpdateProjectEntry ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry) }; return fetchJson(`/api/v1/org/${orgName}/project`, opts); }; // TODO should we just use createOrUpdate instead? export const rename = ( orgName: ConcordKey, projectId: ConcordId, projectName: ConcordKey ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: projectId, name: projectName }) }; return fetchJson(`/api/v1/org/${orgName}/project`, opts); }; // TODO should we just use createOrUpdate instead? export const changeOwner = ( orgName: ConcordKey, projectName: ConcordKey, ownerId: ConcordId ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: projectName, owner: { id: ownerId } }) }; return fetchJson(`/api/v1/org/${orgName}/project`, opts); }; export const deleteProject = ( orgName: ConcordKey, projectName: ConcordKey ): Promise => fetchJson(`/api/v1/org/${orgName}/project/${projectName}`, { method: 'DELETE' }); export const encrypt = ( orgName: ConcordKey, projectName: ConcordKey, value: string ): Promise => { const opts = { method: 'POST', body: value }; return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/encrypt`, opts); }; export const getProjectAccess = ( orgName: ConcordKey, projectName: ConcordKey ): Promise> => { return retryRequest( () => fetchJson(`/api/v1/org/${orgName}/project/${projectName}/access`), shouldRetryProjectRequest ); }; export const updateProjectAccess = ( orgName: ConcordKey, projectName: ConcordKey, entries: ResourceAccessEntry[] ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entries) }; return retryRequest( () => fetchJson(`/api/v1/org/${orgName}/project/${projectName}/access/bulk`, opts), shouldRetryProjectRequest ); }; export const getProjectConfiguration = ( orgName: ConcordKey, projectName: ConcordKey ): Promise => { return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/cfg`); }; export const updateProjectConfiguration = ( orgName: ConcordKey, projectName: ConcordKey, config: Object ): Promise => { const opts = { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }; return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/cfg`, opts); }; ================================================ FILE: console2/src/api/org/project/repository/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, ConcordKey, fetchJson, queryParams, GenericOperationResult, OperationResult } from '../../../common'; export interface RepositoryMeta { profiles?: string[]; entryPoints?: string[]; } export interface RepositoryEntry { id: ConcordId; name: ConcordKey; url: string; branch?: string; commitId?: string; path?: string; secretStoreType?: string; secretId: string; secretName: string; meta?: RepositoryMeta; disabled: boolean; triggersDisabled: boolean; } export interface PaginatedRepositoryEntries { items: RepositoryEntry[]; next: boolean; } export interface EditRepositoryEntry { id?: ConcordId; name: ConcordKey; url: string; branch?: string; commitId?: string; path?: string; secretId: string; disabled: boolean; triggers?: TriggerEntry; triggersDisabled: boolean; } export interface TriggerCfg { entryPoint: string; name?: string; } export interface TriggerConditions { spec?: string; version?: string; } export interface TriggerEntry { id: ConcordId; repositoryId: ConcordId; eventSource: ConcordKey; arguments?: object; conditions?: TriggerConditions; activeProfiles?: string[]; cfg: TriggerCfg; } export interface RepositoryValidationResponse { ok: boolean; result: OperationResult; errors?: string[]; warnings?: string[]; } export const get = async ( orgName: ConcordKey, projectName: ConcordKey, repoName: ConcordKey ): Promise => { return fetchJson( `/api/v1/org/${orgName}/project/${projectName}/repository/${repoName}` ); }; export const list = async ( orgName: ConcordKey, projectName: ConcordKey, offset: number, limit: number, filter?: string ): Promise => { const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset; const limitParam = limit > 0 ? limit + 1 : limit; const data: RepositoryEntry[] = await fetchJson( `/api/v1/org/${orgName}/project/${projectName}/repository?${queryParams({ offset: offsetParam, limit: limitParam, filter, })}` ); const hasMoreElements: boolean = !!limit && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements, }; }; export const createOrUpdate = ( orgName: ConcordKey, projectName: ConcordKey, entry: EditRepositoryEntry ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry) }; return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/repository`, opts); }; export const deleteRepository = ( orgName: ConcordKey, projectName: ConcordKey, repoName: ConcordKey ): Promise => { const opts = { method: 'DELETE' }; return fetchJson(`/api/v1/org/${orgName}/project/${projectName}/repository/${repoName}`, opts); }; export const refreshRepository = ( orgName: ConcordKey, projectName: ConcordKey, repoName: ConcordKey, sync: boolean ): Promise => { const opts = { method: 'POST' }; return fetchJson( `/api/v1/org/${orgName}/project/${projectName}/repository/${repoName}/refresh?sync=${sync}`, opts ); }; export const validateRepository = ( orgName: ConcordKey, projectName: ConcordKey, repoName: ConcordKey ): Promise => { const opts = { method: 'POST' }; return fetchJson( `/api/v1/org/${orgName}/project/${projectName}/repository/${repoName}/validate`, opts ); }; export const listTriggers = ( orgName: ConcordKey, projectName: ConcordKey, repoName: ConcordKey ): Promise => fetchJson(`/api/v1/org/${orgName}/project/${projectName}/repo/${repoName}/trigger`); export interface TriggerFilter { type?: ConcordKey; orgName?: ConcordKey; projectName?: ConcordKey; repoName?: ConcordKey; } export const listTriggersV2 = (filter: TriggerFilter): Promise => fetchJson(`/api/v2/trigger?${queryParams({ ...filter })}`); ================================================ FILE: console2/src/api/org/secret/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, ConcordKey, fetchJson, GenericOperationResult, EntityOwner, queryParams } from '../../common'; import { ResourceAccessEntry } from '../'; import { ProjectEntry } from '../project'; export enum SecretVisibility { PUBLIC = 'PUBLIC', PRIVATE = 'PRIVATE' } export enum SecretType { KEY_PAIR = 'KEY_PAIR', USERNAME_PASSWORD = 'USERNAME_PASSWORD', DATA = 'DATA' } export enum SecretTypeExt { NEW_KEY_PAIR, EXISTING_KEY_PAIR, USERNAME_PASSWORD, VALUE_STRING, VALUE_FILE } export enum SecretEncryptedByType { SERVER_KEY = 'SERVER_KEY', PASSWORD = 'PASSWORD' } export enum SecretStoreType { CONCORD = 'CONCORD', KEYWHIZ = 'KEYWHIZ' } export interface SecretEntry { id: ConcordId; name: ConcordKey; createdAt: string; lastUpdatedAt?: string; orgId: ConcordId; orgName: ConcordKey; projects: ProjectEntry[]; visibility: SecretVisibility; type: SecretType; encryptedBy: SecretEncryptedByType; storeType: SecretStoreType; owner?: EntityOwner; } export interface CreateSecretResponse extends GenericOperationResult { id: ConcordId; password?: string; publicKey?: string; } export interface NewSecretEntry { name: string; visibility: SecretVisibility; projects?: ProjectEntry[]; type: SecretTypeExt; publicFile?: File; privateFile?: File; username?: string; password?: string; valueString?: string; valueFile?: File; generatePassword?: boolean; storePassword?: string; storeType?: SecretStoreType; } export interface PaginatedSecretEntries { items: SecretEntry[]; next?: boolean; } export interface Pagination { limit: number; offset: number; } export interface PublicKeyResponse { publicKey: string; } export const get = (orgName: ConcordKey, secretName: ConcordKey): Promise => { return fetchJson(`/api/v2/org/${orgName}/secret/${secretName}`); }; export const list = async ( orgName: ConcordKey, offset: number, limit: number, filter?: string ): Promise => { const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset; const limitParam = limit > 0 ? limit + 1 : limit; const data: SecretEntry[] = await fetchJson( `/api/v2/org/${orgName}/secret?${queryParams({ offset: offsetParam, limit: limitParam, filter })}` ); const hasMoreElements: boolean = !!limit && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; export const deleteSecret = ( orgName: ConcordKey, secretName: ConcordKey ): Promise => { const opts = { method: 'DELETE' }; return fetchJson(`/api/v1/org/${orgName}/secret/${secretName}`, opts); }; export const renameSecret = ( orgName: ConcordKey, secretName: ConcordKey, newSecretName: ConcordKey ): Promise => { const data = new FormData(); data.append('name', newSecretName); const opts = { method: 'POST', body: data }; return fetchJson(`/api/v2/org/${orgName}/secret/${secretName}`, opts); }; export const updateSecretVisibility = ( orgName: ConcordKey, secretName: ConcordKey, visibility: SecretVisibility ): Promise => { const data = new FormData(); data.append('visibility', visibility); const opts = { method: 'POST', body: data }; return fetchJson(`/api/v2/org/${orgName}/secret/${secretName}`, opts); }; export const updateSecretProject = ( orgName: ConcordKey, secretName: ConcordKey, projectIds: String[] ): Promise => { const data = new FormData(); if (projectIds.length > 0) { data.append('projectIds', projectIds.join(',')); } else { data.append('removeProjectLink', 'true'); } const opts = { method: 'POST', body: data }; return fetchJson(`/api/v2/org/${orgName}/secret/${secretName}`, opts); }; // TODO response type export const create = ( orgName: ConcordKey, entry: NewSecretEntry ): Promise => { const data = new FormData(); data.append('name', entry.name); data.append('visibility', entry.visibility); switch (entry.type) { case SecretTypeExt.NEW_KEY_PAIR: { data.append('type', SecretType.KEY_PAIR); break; } case SecretTypeExt.EXISTING_KEY_PAIR: { data.append('type', SecretType.KEY_PAIR); data.append('public', entry.publicFile!); data.append('private', entry.privateFile!); break; } case SecretTypeExt.USERNAME_PASSWORD: { data.append('type', SecretType.USERNAME_PASSWORD); data.append('username', entry.username!); data.append('password', entry.password!); break; } case SecretTypeExt.VALUE_STRING: { data.append('type', SecretType.DATA); data.append('data', entry.valueString!); break; } case SecretTypeExt.VALUE_FILE: { data.append('type', SecretType.DATA); data.append('data', entry.valueFile!); break; } default: { return Promise.reject(`Unsupported secret type: ${entry.type}`); } } if (entry.generatePassword) { data.append('generatePassword', 'true'); } else if (entry.storePassword) { data.append('storePassword', entry.storePassword); } if (entry.storeType) { data.append('storeType', entry.storeType); } if (entry.projects) { data.append('projectIds', entry.projects.map((project) => project.id).join(',')); } const opts = { method: 'POST', body: data }; return fetchJson(`/api/v1/org/${orgName}/secret`, opts); }; export const changeOrganization = ( orgName: ConcordKey, secretName: ConcordKey, newOrgName: ConcordKey ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orgName: newOrgName }) }; return fetchJson(`/api/v2/org/${orgName}/secret/${secretName}`, opts); }; export const getPublicKey = (orgName: string, secretName: string): Promise => fetchJson(`/api/v1/org/${orgName}/secret/${secretName}/public`); export const getSecretAccess = ( orgName: ConcordKey, secretName: ConcordKey ): Promise => fetchJson(`/api/v1/org/${orgName}/secret/${secretName}/access`); export const updateSecretAccess = ( orgName: ConcordKey, secretName: ConcordKey, entries: ResourceAccessEntry[] ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entries) }; return fetchJson(`/api/v1/org/${orgName}/secret/${secretName}/access/bulk`, opts); }; export const changeOwner = ( orgName: ConcordKey, secretName: ConcordKey, ownerId: ConcordId ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ owner: { id: ownerId } }) }; return fetchJson(`/api/v1/org/${orgName}/secret/${secretName}`, opts); }; export const typeToText = (t: SecretType) => { switch (t) { case SecretType.KEY_PAIR: { return 'Key pair'; } case SecretType.USERNAME_PASSWORD: { return 'Username/password'; } case SecretType.DATA: { return 'Single value'; } default: { throw new Error(`Unexpected value: ${t}`); } } }; ================================================ FILE: console2/src/api/org/team/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, ConcordKey, fetchJson, GenericOperationResult, OperationResult, queryParams, RequestError, retryRequest } from '../../common'; import { UserType } from '../../user'; export interface TeamEntry { id: ConcordId; orgId: ConcordId; orgName: ConcordKey; name: ConcordKey; description?: string; } export interface NewTeamEntry { name: string; description?: string; } export interface CreateTeamResponse { ok: boolean; result: OperationResult; id: ConcordId; } const shouldRetryTeamRequest = (error: RequestError) => { if (!error || (error.status !== 400 && error.status !== 404)) { return false; } return /(?:Organization|Team) not found:/i.test(error.details || ''); }; export const get = (orgName: ConcordKey, teamName: ConcordKey): Promise => retryRequest( () => fetchJson(`/api/v1/org/${orgName}/team/${teamName}`), shouldRetryTeamRequest ); export const list = (orgName: ConcordKey): Promise => retryRequest(() => fetchJson(`/api/v1/org/${orgName}/team`), shouldRetryTeamRequest); export const createOrUpdate = ( orgName: ConcordKey, entry: NewTeamEntry ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry) }; return retryRequest(() => fetchJson(`/api/v1/org/${orgName}/team`, opts), shouldRetryTeamRequest); }; // TODO should we just use createOrUpdate instead? export const rename = ( orgName: ConcordKey, teamId: ConcordId, teamName: ConcordKey ): Promise => { const opts = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: teamId, name: teamName }) }; return retryRequest(() => fetchJson(`/api/v1/org/${orgName}/team`, opts), shouldRetryTeamRequest); }; export const deleteTeam = ( orgName: ConcordKey, teamName: ConcordKey ): Promise => retryRequest( () => fetchJson(`/api/v1/org/${orgName}/team/${teamName}`, { method: 'DELETE' }), shouldRetryTeamRequest ); export enum TeamRole { OWNER = 'OWNER', MAINTAINER = 'MAINTAINER', MEMBER = 'MEMBER' } export interface TeamUserEntry { userId: ConcordId; username: string; userDomain?: string; displayName?: string; userType?: UserType; role: TeamRole; memberType: MemberType; ldapGroupSource?: string; } export interface TeamLdapGroupEntry { group: string; role: TeamRole; } export enum MemberType { SINGLE = 'SINGLE', LDAP_GROUP = 'LDAP_GROUP' } export interface NewTeamUserEntry { userId?: ConcordId; username?: string; userDomain?: string; displayName?: string; userType?: UserType; role: TeamRole; } export interface NewTeamLdapGroupEntry { group: string; role: TeamRole; } export const listUsers = (orgName: ConcordKey, teamName: ConcordKey): Promise => retryRequest( () => fetchJson(`/api/v1/org/${orgName}/team/${teamName}/users`), shouldRetryTeamRequest ); export const listLdapGroups = ( orgName: ConcordKey, teamName: ConcordKey ): Promise => retryRequest( () => fetchJson(`/api/v1/org/${orgName}/team/${teamName}/ldapGroups`), shouldRetryTeamRequest ); export const addUsers = ( orgName: ConcordKey, teamName: ConcordKey, replace: boolean, users: NewTeamUserEntry[] ): Promise<{}> => { const opts = { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(users) }; return retryRequest( () => fetchJson( `/api/v1/org/${orgName}/team/${teamName}/users?${queryParams({ replace })}`, opts ), shouldRetryTeamRequest ); }; export const addLdapGroups = ( orgName: ConcordKey, teamName: ConcordKey, replace: boolean, groups: NewTeamLdapGroupEntry[] ): Promise<{}> => { const opts = { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(groups) }; return retryRequest( () => fetchJson( `/api/v1/org/${orgName}/team/${teamName}/ldapGroups?${queryParams({ replace })}`, opts ), shouldRetryTeamRequest ); }; ================================================ FILE: console2/src/api/process/ansible/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, fetchJson, queryParams } from '../../common'; import { ProcessEventEntry } from '../event'; export interface SearchFilter { limit?: number; offset?: number; host?: string; hostGroup?: string; status?: AnsibleStatus; statuses?: AnsibleStatus[]; playbookId?: ConcordId; sortField?: SortField; sortBy?: SortOrder; } export enum AnsibleStatus { CHANGED = 'CHANGED', FAILED = 'FAILED', OK = 'OK', RUNNING = 'RUNNING', SKIPPED = 'SKIPPED', UNREACHABLE = 'UNREACHABLE' } export enum SortOrder { ASC = 'ASC', DESC = 'DESC' } export enum SortField { HOST = 'HOST', DURATION = 'DURATION', STATUS = 'STATUS', HOST_GROUP = 'HOST_GROUP' } export const getStatusColor = (status: AnsibleStatus) => { switch (status) { case AnsibleStatus.OK: return '#5DB571'; case AnsibleStatus.CHANGED: return '#00A4D3'; case AnsibleStatus.FAILED: return '#EC6357'; case AnsibleStatus.UNREACHABLE: return '#BDB9B9'; case AnsibleStatus.SKIPPED: return '#F6BC32'; case AnsibleStatus.RUNNING: return '#BDBABD'; default: return '#3F3F3D'; } }; export interface AnsibleHost { host: string; hostGroup: string; status: AnsibleStatus; duration: number; } export interface AnsibleHostListResponse { hostGroups: string[]; items: AnsibleHost[]; } export interface PaginatedAnsibleHostEntries { items: AnsibleHost[]; hostGroups: string[]; next?: number; prev?: number; } export interface AnsibleEvent { host: string; hostGroup: string; playbook: string; status?: AnsibleStatus; task: string; action?: string; result?: object; ignore_errors?: boolean; duration?: number; phase: 'pre' | 'post'; correlationId: string; } export interface PlaybookInfo { id: ConcordId; name: string; startedAt: string; hostsCount: number; failedHostsCount: number; playsCount: number; failedTasksCount: number; progress: number; status: string; retryNum?: number; } export interface PlayInfo { playId: ConcordId; playName: string; playOrder: number; hostCount: number; taskCount: number; taskStats: TaskStats; finishedTaskCount: number; flowEventCorrelationId: ConcordId; } export interface TaskInfo { taskName: string; type: string; taskOrder: number; okCount: number; failedCount: number; unreachableCount: number; skippedCount: number; runningCount: number; } export interface TaskStats { ok: number; failed: number; unreachable: number; skipped: number; running: number; } export const listAnsibleHosts = ( instanceId: ConcordId, filters?: SearchFilter ): Promise => { const limit = filters && filters.limit ? filters.limit : 50; if (filters && filters.limit) { filters.limit = parseInt(filters.limit.toString(), 10) + 1; } const qp = filters ? '?' + queryParams(filters) : ''; const data: Promise = fetchJson( `/api/v1/process/${instanceId}/ansible/hosts${qp}` ); return data.then((resp: AnsibleHostListResponse) => { const hosts = resp.items; const hasMoreElements = limit && hosts.length > limit; const offset: number = filters && filters.offset ? filters.offset : 0; if (hasMoreElements) { hosts.pop(); } const nextOffset = offset + parseInt(limit.toString(), 10); const prevOffset = offset - limit; const onFirstPage = offset === 0; const nextPage = !!hasMoreElements ? nextOffset : undefined; const prevPage = !onFirstPage ? prevOffset : undefined; return { items: hosts, hostGroups: resp.hostGroups, next: nextPage, prev: prevPage }; }); }; export const listAnsibleEvents = ( instanceId: ConcordId, host?: string, hostGroup?: string, status?: string, playbookId?: ConcordId ): Promise[]> => fetchJson( `/api/v1/process/${instanceId}/ansible/events?${queryParams({ host, hostGroup, status, playbookId })}` ); export const listAnsiblePlaybooks = (instanceId: ConcordId): Promise => fetchJson(`/api/v1/process/${instanceId}/ansible/playbooks`); export const listAnsiblePlays = ( instanceId: ConcordId, playbookId: ConcordId ): Promise => fetchJson(`/api/v1/process/${instanceId}/ansible/${playbookId}/plays`); export const listAnsibleTaskStats = ( instanceId: ConcordId, playId: ConcordId ): Promise => fetchJson( `/api/v1/process/${instanceId}/ansible/tasks?${queryParams({ playId })}` ); export const listAnsibleTasks = ( instanceId: ConcordId, playbookId: ConcordId, host?: string, hostGroup?: string, status?: string ): Promise[]> => fetchJson( `/api/v1/process/${instanceId}/ansible/events?${queryParams({ host, hostGroup, status, playbookId })}` ); ================================================ FILE: console2/src/api/process/attachment/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, fetchJson } from '../../common'; export const list = (instanceId: ConcordId): Promise => fetchJson(`/api/v1/process/${instanceId}/attachment`); ================================================ FILE: console2/src/api/process/checkpoint/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, fetchJson } from '../../common'; import { ProcessEntry } from '../index'; export const restoreProcess = ( instanceId: ConcordId, checkpointId: ConcordId ): Promise => fetchJson(`/api/v1/process/${instanceId}/checkpoint/restore`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: checkpointId }) }); ================================================ FILE: console2/src/api/process/event/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, fetchJson, queryParams } from '../../common'; import { AnsibleEvent } from '../ansible'; export enum ProcessEventType { ELEMENT = 'ELEMENT', ANSIBLE = 'ANSIBLE' } export interface VariableMapping { source?: string; sourceExpression?: string; sourceValue: any; target: string; resolved: any; } // TODO find which properties are always defined export interface ProcessElementEvent { processDefinitionId: string; threadId?: number; fileName?: string; elementId: string; line: number; column: number; description?: string; phase?: 'pre' | 'post'; in?: VariableMapping[] | {}; out?: VariableMapping[] | {}; correlationId?: string; duration?: number; error?: string; } export type ProcessEventData = ProcessElementEvent | AnsibleEvent | {}; export interface ProcessEventFilter { instanceId: ConcordId; type?: string; fromId?: number; eventCorrelationId?: string; eventPhase?: 'PRE' | 'POST'; includeAll?: boolean; limit?: number; } export interface ProcessEventEntry { id: ConcordId; seqId: number; eventType: ProcessEventType; eventDate: string; data: T; } export const listEvents = ( filter: ProcessEventFilter ): Promise[]> => fetchJson(`/api/v1/process/${filter.instanceId}/event?${queryParams({ ...filter })}`); ================================================ FILE: console2/src/api/process/form/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, fetchJson } from '../../common'; export interface FormRunAs { username?: string; ldap?: { group: string[] | string } | [{ group: string }]; keep?: boolean; } export interface FormListEntry { name: string; custom: boolean; yield: boolean; runAs?: FormRunAs; } export enum Cardinality { ONE_OR_NONE = 'ONE_OR_NONE', ONE_AND_ONLY_ONE = 'ONE_AND_ONLY_ONE', AT_LEAST_ONE = 'AT_LEAST_ONE', ANY = 'ANY' } export enum FormFieldType { STRING = 'string', INT = 'int', DECIMAL = 'decimal', BOOLEAN = 'boolean', FILE = 'file', DATE = 'date', DATE_TIME = 'dateTime' } export interface FormField { name: string; label: string; type: FormFieldType; cardinality?: Cardinality; value?: any; allowedValue?: any; options?: { inputType?: string; popupPosition?: | 'top left' | 'top right' | 'bottom left' | 'bottom right' | 'right center' | 'left center' | 'top center' | 'bottom center'; }; } export interface FormInstanceEntry { processInstanceId: ConcordId; name: string; fields: FormField[]; custom: boolean; yield: boolean; } export interface FormSubmitErrors { [name: string]: string; } export interface FormDataType { [name: string]: any; } export interface FormSubmitResponse { ok: boolean; processInstanceId: ConcordId; errors?: FormSubmitErrors; } export const list = (processInstanceId: ConcordId): Promise => fetchJson(`/api/v1/process/${processInstanceId}/form`); export const get = (processInstanceId: ConcordId, formName: string): Promise => fetchJson(`/api/v1/process/${processInstanceId}/form/${formName}`); export const submit = ( processInstanceId: ConcordId, formName: string, values: {} ): Promise => { const body = new FormData(); Object.keys(values).forEach((name) => { let k = name; let v = values[k]; // special case: a JSON object encoded as a string in a multipart/form-data field if (v instanceof Array) { v = JSON.stringify(v); k = k + '/jsonField'; } if (v !== undefined) { body.append(k, v); } }); const opts = { method: 'POST', body }; return fetchJson(`/api/v1/process/${processInstanceId}/form/${formName}/multipart`, opts); }; ================================================ FILE: console2/src/api/process/history/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, fetchJson } from '../../common'; import { ProcessHistoryEntry } from '../'; export const get = (instanceId: ConcordId): Promise => fetchJson(`/api/v1/process/${instanceId}/history`); ================================================ FILE: console2/src/api/process/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { SemanticCOLORS, SemanticICONS } from 'semantic-ui-react'; import { ConcordId, ConcordKey, fetchJson, managedFetch, queryParams } from '../common'; import { ColumnDefinition } from '../org'; import 'url-search-params-polyfill'; export enum ProcessStatus { NEW = 'NEW', PREPARING = 'PREPARING', ENQUEUED = 'ENQUEUED', WAITING = 'WAITING', STARTING = 'STARTING', RUNNING = 'RUNNING', SUSPENDED = 'SUSPENDED', RESUMING = 'RESUMING', FINISHED = 'FINISHED', FAILED = 'FAILED', CANCELLED = 'CANCELLED', TIMED_OUT = 'TIMED_OUT' } export const getStatusSemanticColor = (status: ProcessStatus): SemanticCOLORS => { switch (status) { case ProcessStatus.NEW: case ProcessStatus.PREPARING: case ProcessStatus.RUNNING: case ProcessStatus.STARTING: case ProcessStatus.SUSPENDED: return 'blue'; case ProcessStatus.FINISHED: return 'green'; case ProcessStatus.CANCELLED: case ProcessStatus.FAILED: case ProcessStatus.TIMED_OUT: return 'red'; case ProcessStatus.ENQUEUED: case ProcessStatus.RESUMING: case ProcessStatus.WAITING: default: return 'grey'; } }; export const getStatusSemanticIcon = (status: ProcessStatus): SemanticICONS => { switch (status) { case ProcessStatus.NEW: case ProcessStatus.PREPARING: case ProcessStatus.RUNNING: case ProcessStatus.STARTING: return 'spinner'; case ProcessStatus.SUSPENDED: return 'hourglass half'; case ProcessStatus.FINISHED: return 'circle'; case ProcessStatus.CANCELLED: case ProcessStatus.FAILED: case ProcessStatus.TIMED_OUT: return 'exclamation circle'; case ProcessStatus.ENQUEUED: case ProcessStatus.RESUMING: case ProcessStatus.WAITING: default: return 'circle notch'; } }; export const isFinal = (s?: ProcessStatus) => s === ProcessStatus.FINISHED || s === ProcessStatus.FAILED || s === ProcessStatus.CANCELLED || s === ProcessStatus.TIMED_OUT; export const hasState = (s: ProcessStatus) => s !== ProcessStatus.PREPARING; export const canBeCancelled = (s: ProcessStatus) => s === ProcessStatus.ENQUEUED || s === ProcessStatus.RUNNING || s === ProcessStatus.WAITING || s === ProcessStatus.SUSPENDED; export const canBeRestarted = (s: ProcessStatus) => s === ProcessStatus.CANCELLED || s === ProcessStatus.FAILED || s === ProcessStatus.FINISHED || s === ProcessStatus.TIMED_OUT; export interface ProcessCheckpointEntry { id: string; name: string; createdAt: string; } export interface CheckpointRestoreHistoryEntry { id: number; checkpointId: string; processStatus: ProcessStatus; changeDate: string; } export interface ProcessHistoryEntry { id: ConcordId; status: ProcessStatus; changeDate: string; } export enum WaitType { NONE = 'NONE', PROCESS_COMPLETION = 'PROCESS_COMPLETION', PROCESS_LOCK = 'PROCESS_LOCK', PROCESS_SLEEP = 'PROCESS_SLEEP' } export interface AbstractWaitCondition { type: WaitType; reason: string; } export interface ProcessWaitCondition extends AbstractWaitCondition { processes?: ConcordId[]; } export interface ProcessLockCondition extends AbstractWaitCondition { instanceId: ConcordId; name: string; scope: string; } export interface ProcessSleepCondition extends AbstractWaitCondition { until: string; } export type WaitCondition = ProcessWaitCondition | ProcessLockCondition | ProcessSleepCondition; export interface ProcessWaitEntry { isWaiting: boolean; waits?: WaitCondition[]; } export interface ProcessMeta { out?: { lastError?: {}; [x: string]: any; }; [x: string]: any; } export enum ProcessKind { DEFAULT = 'DEFAULT', FAILURE_HANDLER = 'FAILURE_HANDLER', CANCEL_HANDLER = 'CANCEL_HANDLER', TIMEOUT_HANDLER = 'TIMEOUT_HANDLER' } export type ProcessRuntime = 'concord-v1' | 'concord-v2' | string; export interface TriggeredByEntry { externalEventId?: string; trigger: TriggerEntry; } export interface TriggerEntry { eventSource: string; } export interface ProcessEntry { instanceId: ConcordId; parentInstanceId?: ConcordId; status: ProcessStatus; kind: ProcessKind; orgName?: ConcordKey; projectName?: ConcordKey; repoName?: ConcordKey; repoUrl?: string; repoPath?: string; commitId?: string; initiator: string; createdAt: string; startAt?: string; lastUpdatedAt: string; lastRunAt?: string; totalRuntimeMs?: number; handlers?: string[]; meta?: ProcessMeta; tags?: string[]; checkpoints?: ProcessCheckpointEntry[]; checkpointRestoreHistory?: CheckpointRestoreHistoryEntry[]; statusHistory?: ProcessHistoryEntry[]; disabled: boolean; triggeredBy?: TriggeredByEntry; timeout?: number; suspendTimeout?: number; runtime?: ProcessRuntime; requirements?: {}; } export interface StartProcessResponse { ok: boolean; instanceId: string; } export interface RestoreProcessResponse { ok: boolean; } export const start = ( orgName: ConcordKey, projectName: ConcordKey, repoName: ConcordKey, entryPoint?: string, profiles?: string[], args?: object ): Promise => { const data = new FormData(); data.append('org', orgName); data.append('project', projectName); data.append('repo', repoName); if (entryPoint) { data.append('entryPoint', entryPoint); } if (profiles) { data.append('activeProfiles', profiles.join(',')); } if (args) { const argsBlob = [JSON.stringify({ arguments: args })]; data.append('request', new Blob(argsBlob, { type: 'application/octet-stream' })); } const opts = { method: 'POST', body: data }; return fetchJson('/api/v1/process', opts); }; export type ProcessDataInclude = 'checkpoints' | 'history' | 'childrenIds' | 'checkpointsHistory'; export const get = ( instanceId: ConcordId, includes: ProcessDataInclude[] ): Promise => { const params = new URLSearchParams(); includes.forEach((i) => params.append('include', i)); return fetchJson(`/api/v2/process/${instanceId}?${params.toString()}`); }; export const getRoot = ( instanceId: ConcordId ): Promise => { return fetchJson(`/api/v1/process/${instanceId}/root`); }; export const disable = (instanceId: ConcordId, disabled: boolean): Promise<{}> => managedFetch(`/api/v1/process/${instanceId}/disable/${disabled}`, { method: 'POST' }); export const kill = (instanceId: ConcordId): Promise<{}> => managedFetch(`/api/v1/process/${instanceId}`, { method: 'DELETE' }); export const restart = (instanceId: ConcordId): Promise<{}> => managedFetch(`/api/v1/process/${instanceId}/restart`, { method: 'POST' }); export const killBulk = (instanceIds: ConcordId[]): Promise<{}> => { const opts = { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(instanceIds) }; return managedFetch('/api/v1/process/bulk', opts); }; export interface ColumnFilter { column: ColumnDefinition; filter: string; } // TODO remove, use ProcessListQuery everywhere export interface ProcessFilters { [source: string]: string; } export interface PaginatedProcessEntries { items: ProcessEntry[]; next?: number; prev?: number; } export interface DateParam { value: string | null; compareType: 'ge' | 'len'; } export interface ProcessListQuery { [meta: string]: any; orgId?: ConcordId; orgName?: ConcordKey; projectId?: ConcordId; projectName?: ConcordKey; afterCreatedAt?: string; beforeCreatedAt?: string; startAt?: DateParam; tags?: string[]; status?: ProcessStatus; initiator?: string; parentInstanceId?: ConcordId; include?: ProcessDataInclude[]; limit?: number; offset?: number; } const filterToQueryParams = (params: object): string => { let keyValues = {}; Object.keys(params).forEach((k) => { const v = params[k]; const dp = v as DateParam; if (dp !== undefined && v.compareType !== undefined) { let value = ''; if (v.value !== null && v.value !== undefined) { value = v.value; } keyValues[k + '.' + v.compareType] = value; } else { keyValues[k] = v; } }); return queryParams(keyValues, true); }; export const list = async (q: ProcessListQuery): Promise => { let { limit = 50 } = q; // TODO fix the CheckpointView data instead limit = parseInt(limit.toString()); const requestLimit = limit + 1; const filters = { ...q, limit: requestLimit }; const qp = filters ? filterToQueryParams(filters) : ''; const data: ProcessEntry[] = await fetchJson(`/api/v2/process?${qp}`); const hasMoreElements: boolean = !!limit && data.length > limit; const offset: number = q.offset ? q.offset : 0; if (hasMoreElements) { data.pop(); } const nextOffset = offset + limit; const prevOffset = offset - limit; const onFirstPage = offset === 0; const nextPage = hasMoreElements ? nextOffset : undefined; const prevPage = !onFirstPage ? prevOffset : undefined; return { items: data, next: nextPage, prev: prevPage }; }; ================================================ FILE: console2/src/api/process/log/fetchLogAsBlobURL.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { managedFetch } from '../../common'; export default (processId: string) => managedFetch(`/api/v1/process/${processId}/log`) .then((response) => response.body) .then((rs) => { if (rs) { const reader = rs.getReader(); // ! Typescript currently does not support types for // ! readable streams, thus @ts-ignore // @ts-ignore return new ReadableStream({ async start(controller: any) { while (true) { const { done, value } = await reader.read(); // When no more data needs to be consumed, break the reading if (done) { break; } // Enqueue the next data chunk into our target stream controller.enqueue(value); } // Close the stream controller.close(); reader.releaseLock(); } }); } else { throw new Error(`Process: ${processId} body missing`); } }) // Create a new response out of the stream .then((rs) => new Response(rs)) // Create an object URL for the response .then((response) => response.blob()) .then((blob) => URL.createObjectURL(blob)); ================================================ FILE: console2/src/api/process/log/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, fetchJson, managedFetch, queryParams } from '../../common'; export interface LogRange { unit?: string; length?: number; low?: number; high?: number; } export interface LogChunk { data: string; range: LogRange; } const str = (s?: {}): string => (s === undefined ? '' : String(s)); const formatRangeHeader = (range: LogRange) => ({ Range: `bytes=${str(range.low)}-${str(range.high)}` }); const parseRange = (s: string): LogRange => { const regex = /^bytes (\d*)-(\d*)\/(\d*)$/; const m = regex.exec(s); if (!m) { throw Object({ error: true, message: `Invalid Content-Range header: ${s}` }); } return { unit: 'bytes', length: parseInt(m[3], 10), low: parseInt(m[1], 10), high: parseInt(m[2], 10) }; }; const toChunk = (data: string, range: LogRange): LogChunk => { // we assume that the data is aligned by \n // this will work only with our current implementation of the API return { data, range }; }; export const getLog = async (instanceId: ConcordId, range: LogRange): Promise => { const opts = { headers: formatRangeHeader(range) }; const resp = await managedFetch(`/api/v1/process/${instanceId}/log`, opts); const headers = resp.headers.get('Content-Range'); if (!headers) { return Promise.reject({ error: true, message: `Range header is missing: ${instanceId}` }); } const data = await resp.text(); return toChunk(data, parseRange(headers)); }; export enum SegmentStatus { OK = 'OK', FAILED = 'FAILED', RUNNING = 'RUNNING', SUSPENDED = 'SUSPENDED' } export interface LogSegmentEntry { id: number; correlationId?: string; name: string; createdAt: string; status?: SegmentStatus; statusUpdatedAt?: string; warnings?: number; errors?: number; } export interface PaginatedLogSegmentEntry { items: LogSegmentEntry[]; next: boolean; } export const listLogSegments = async ( instanceId: ConcordId, offset: number, limit: number ): Promise => { const limitParam = limit > 0 ? limit + 1 : limit; const data: LogSegmentEntry[] = await fetchJson( `/api/v2/process/${instanceId}/log/segment?${queryParams({ offset, limit: limitParam })}` ); const hasMoreElements: boolean = limit > 0 && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; export const getSegmentLog = async ( instanceId: ConcordId, segmentId: number, range: LogRange ): Promise => { const opts = { headers: formatRangeHeader(range) }; const resp = await managedFetch( `/api/v2/process/${instanceId}/log/segment/${segmentId}/data`, opts ); const headers = resp.headers.get('Content-Range'); if (!headers) { return Promise.reject({ error: true, message: `Range header is missing: ${instanceId}` }); } const data = await resp.text(); return toChunk(data, parseRange(headers)); }; ================================================ FILE: console2/src/api/process/wait/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ProcessWaitEntry } from '../'; import { ConcordId, managedFetch } from '../../common'; export const get = async (instanceId: ConcordId): Promise => { const response = await managedFetch(`/api/v1/process/${instanceId}/waits`); if (response.status === 204) { return undefined; } return response.json(); }; ================================================ FILE: console2/src/api/profile/api_token/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { fetchJson, GenericOperationResult, ConcordId, ConcordKey } from '../../common'; export interface TokenEntry { id: ConcordId; name: ConcordKey; expiredAt: string; } export interface NewTokenEntry { name: ConcordKey; } export interface CreateApiKeyResult { ok: boolean; id: string; key: string; expiredAt: string; } export const list = (): Promise => fetchJson(`/api/v1/apikey`); export const create = (entry: NewTokenEntry): Promise => { const obj: RequestInit = { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry) }; return fetchJson('/api/v1/apikey', obj); }; export const deleteToken = (id: ConcordId): Promise => fetchJson(`/api/v1/apikey/${id}`, { method: 'DELETE' }); ================================================ FILE: console2/src/api/profile/user/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2024 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import {ConcordId, fetchJson} from '../../common'; import {TeamRole} from "../../org/team"; export interface UserInfoTeam { orgName: string; teamName: string; role: TeamRole; } export interface UserInfoEntry { id: ConcordId; displayName: string; teams?: UserInfoTeam[]; roles?: string[]; ldapGroups?: string[]; } export const get = (): Promise => fetchJson(`/api/service/console/userInfo`); ================================================ FILE: console2/src/api/secret/store/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { fetchJson } from '../../common'; import { SecretStoreType } from '../../org/secret'; export interface SecretStoreEntry { storeType: SecretStoreType; description: string; } export const listActiveStores = (): Promise => fetchJson('/api/v1/secret/store'); ================================================ FILE: console2/src/api/server/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { fetchJson } from '../common'; export interface VersionResponse { version: string; } export const version = (): Promise => fetchJson('/api/v1/server/version'); ================================================ FILE: console2/src/api/service/console/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { throttle } from 'lodash'; import { ConcordKey, fetchJson, managedFetch, queryParams } from '../../common'; import { Organizations } from '../../../state/data/orgs/types'; export interface UserResponse { username: string; displayName: string; orgs: Organizations; } export const whoami = async ( username?: string, password?: string, rememberMe?: boolean, apiKey?: string ): Promise => { const h = new Headers(); if (apiKey) { h.set('Authorization', apiKey); h.set('X-Concord-EnableSession', 'true'); } else if (username && password) { h.set( 'Authorization', `Basic ${btoa(unescape(encodeURIComponent(username + ':' + password)))}` ); } if (rememberMe) { h.set('X-Concord-RememberMe', 'true'); } const json = await fetchJson('/api/service/console/whoami', { headers: h }); return json as UserResponse; }; export const logout = async () => { await managedFetch('/api/service/console/logout', { method: 'POST' }); return true; }; // TODO throttle in sagas? export const isProjectExists = throttle(async (orgName: ConcordKey, name: string): Promise< boolean > => { try { const json = await fetchJson(`/api/service/console/org/${orgName}/project/${name}/exists`); return json as boolean; } catch (e) { return false; } }, 1000); // TODO throttle in sagas? export const isSecretExists = throttle(async (orgName: ConcordKey, name: string): Promise< boolean > => { try { const json = await fetchJson(`/api/service/console/org/${orgName}/secret/${name}/exists`); return json as boolean; } catch (e) { return false; } }, 1000); export const isStorageExists = throttle(async (orgName: ConcordKey, name: string): Promise< boolean > => { try { const json = await fetchJson( `/api/service/console/org/${orgName}/jsonstore/${name}/exists` ); return json as boolean; } catch (exception) { return false; } }, 1000); export const isStorageQueryExists = throttle( async (orgName: ConcordKey, storageName: string, queryName: string): Promise => { try { const json = await fetchJson( `/api/service/console/org/${orgName}/jsonstore/${storageName}/query/${queryName}/exists` ); return json as boolean; } catch (exception) { return false; } }, 1000 ); export const isRepositoryExists = throttle( async (orgName: ConcordKey, projectName: ConcordKey, name: string): Promise => { try { const json = await fetchJson( `/api/service/console/org/${orgName}/project/${projectName}/repo/${name}/exists` ); return json as boolean; } catch (e) { return false; } }, 1000 ); // TODO throttle in sagas? export const isTeamExists = throttle(async (orgName: ConcordKey, name: string): Promise< boolean > => { try { const json = await fetchJson(`/api/service/console/org/${orgName}/team/${name}/exists`); return json as boolean; } catch (e) { return false; } }, 1000); // TODO throttle in sagas? export const isApiTokenExists = throttle(async (name: string): Promise => { try { const json = await fetchJson(`/api/service/console/apikey/${name}/exists`); return json as boolean; } catch (e) { return false; } }, 1000); export interface RepositoryTestRequest { orgName: ConcordKey; projectName: ConcordKey; url: string; branch?: string; commitId?: string; path?: string; withSecret?: boolean; secretId?: string; secretName?: string; } export const testRepository = async (req: RepositoryTestRequest): Promise => { const opts: RequestInit = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req) }; const success = await fetchJson('/api/service/console/repository/test', opts); if (!success) { throw new Error('Unknown error'); } }; export interface LdapGroupSearchResult { groupName: string; displayName: string; } export const findLdapGroups = (filter: string): Promise => fetchJson(`/api/service/console/search/ldapGroups?${queryParams({ filter })}`); export const validatePassword = throttle( async (pwd: string): Promise => { const opts: RequestInit = { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: pwd }; const json = await fetchJson('/api/service/console/validate-password', opts); return json as boolean; } ); ================================================ FILE: console2/src/api/service/console/user/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2023 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import {ConcordId, ConcordKey, fetchJson, queryParams} from '../../../common'; import { ProcessEntry } from '../../../process'; export interface UserActivity { processes: ProcessEntry[]; } export interface ProcessCardEntry { id: ConcordId; orgName: ConcordKey; projectName: ConcordKey; repoName: ConcordKey; entryPoint: string; name: string; description?: string; icon?: string; isCustomForm: boolean; } export const getActivity = ( maxOwnProcesses: number ): Promise => fetchJson( `/api/v2/service/console/user/activity?${queryParams({ maxOwnProcesses })}` ); export const listProcessCards = ( ): Promise => fetchJson( `/api/v1/processcard` ); export const getProcessCard = ( cardId: ConcordId ): Promise => fetchJson( `/api/v1/processcard/${cardId}` ); ================================================ FILE: console2/src/api/service/custom_form/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, fetchJson } from '../../common'; export interface FormSessionResponse { uri: string; } export const startSession = ( processInstanceId: ConcordId, formName: string ): Promise => fetchJson(`/api/service/custom_form/${processInstanceId}/${formName}/start`, { method: 'POST' }); ================================================ FILE: console2/src/api/usePolling.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { useEffect, useRef, useState } from 'react'; import { RequestError } from './common'; export const usePolling = ( request: () => Promise, interval: number, loadingHandler: (inc: number) => void, refresh: boolean ): RequestError | undefined => { const poll = useRef(undefined); const [error, setError] = useState(); useEffect(() => { let cancelled = false; const fetchData = async () => { loadingHandler(1); let result = false; try { result = await request(); setError(undefined); } catch (e) { setError(e); } finally { if (result) { if (!cancelled) { poll.current = window.setTimeout(() => fetchData(), interval); } } else { stopPolling(); } loadingHandler(-1); } }; fetchData(); return () => { cancelled = true; stopPolling(); }; }, [request, interval, refresh, loadingHandler]); const stopPolling = () => { if (poll.current) { clearTimeout(poll.current); poll.current = undefined; } }; return error; }; ================================================ FILE: console2/src/api/user/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ConcordId, fetchJson, queryParams } from '../common'; export enum UserType { LDAP = 'LDAP', LOCAL = 'LOCAL' } export interface UserEntry { id: ConcordId; name: string; domain?: string; type: UserType; displayName?: string; email?: string; } export interface PaginatedUserEntries { items: UserEntry[]; next: boolean; } export const get = async (id: ConcordId): Promise => fetchJson(`/api/v2/user/${id}`); export const list = async ( offset: number, limit: number, filter?: string ): Promise => { const offsetParam = offset > 0 && limit > 0 ? offset * limit : offset; const limitParam = limit > 0 ? limit + 1 : limit; const data: UserEntry[] = await fetchJson( `/api/v2/user?${queryParams({ offset: offsetParam, limit: limitParam, filter })}` ); const hasMoreElements: boolean = !!limit && data.length > limit; if (limit > 0 && hasMoreElements) { data.pop(); } return { items: data, next: hasMoreElements }; }; ================================================ FILE: console2/src/components/atoms/ClassIcon.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; export const ClassIcon: React.SFC<{ classes: string; style?: object }> = ({ classes, style }) => ( ); export default ClassIcon; ================================================ FILE: console2/src/components/atoms/ColumnSort/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { Button } from "semantic-ui-react"; import * as React from "react"; interface Props { ascSort: () => void; descSort: () => void; } export const ColumnSort: React.SFC = ({ ascSort, descSort }) => ( ); } } ================================================ FILE: console2/src/components/atoms/TableSearchFilter/styles.css ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ .tableSearchFilter { margin: 0 !important; background: none !important; padding: 0 !important; } ================================================ FILE: console2/src/components/atoms/Truncate.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; interface Props { text: string; allowedCharCount?: number; startSubstringCount?: number; endSubstringCount?: number; } // TODO: Handle non existant values // * Returns a smaller string than what was originally supplied export const Truncate: React.SFC = ({ text, allowedCharCount = 15, startSubstringCount = 6, endSubstringCount = 6 }) => { const separator: string = '...'; const start = text.substring(0, startSubstringCount); const end = text.substring(text.length - endSubstringCount, text.length); if (text.length > allowedCharCount) { return {`${start + separator + end}`}; } else { return {text}; } }; export default Truncate; ================================================ FILE: console2/src/components/atoms/index.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ export { default as FormikCheckbox } from './FormikCheckbox'; export { default as FormikDropdown } from './FormikDropdown'; export { default as FormikFileInput } from './FormikFileInput'; export { default as FormikInput } from './FormikInput'; export { default as RefreshButton } from './RefreshButton'; export { default as ReactJson } from './ReactJson'; export { default as Truncate } from './Truncate'; export { default as TableSearchFilter } from './TableSearchFilter'; ================================================ FILE: console2/src/components/molecules/BreadcrumbSegment/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Breadcrumb, Segment } from 'semantic-ui-react'; import './styles.css'; class BreadcrumbSegment extends React.PureComponent { render() { return ( {this.props.children} ); } } export default BreadcrumbSegment; ================================================ FILE: console2/src/components/molecules/BreadcrumbSegment/styles.css ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ .breadcrumbSegment { padding-bottom: 0 !important; padding-left: 0 !important; } ================================================ FILE: console2/src/components/molecules/BulkProcessActionDropdown/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Dropdown, Icon } from 'semantic-ui-react'; import { ConcordId } from '../../../api/common'; import { BulkCancelProcessPopup } from '../../organisms'; interface ExternalProps { data: ConcordId[]; refresh: () => void; } class BulkProcessActionDropdown extends React.PureComponent { render() { const { data, refresh } = this.props; return ( ( Cancel )} /> ); } } export default BulkProcessActionDropdown; ================================================ FILE: console2/src/components/molecules/ButtonWithConfirmation/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Button, Confirm, ButtonProps } from 'semantic-ui-react'; interface State { showConfirm: boolean; } interface Props extends ButtonProps { renderOverride?: React.ReactNode; confirmationHeader: string; confirmationContent: string; onConfirm: () => void; } class ButtonWithConfirmation extends React.Component { constructor(props: Props) { super(props); this.state = { showConfirm: false }; this.handleShowConfirm = this.handleShowConfirm.bind(this); this.handleCancel = this.handleCancel.bind(this); this.handleConfirm = this.handleConfirm.bind(this); } handleShowConfirm(ev: React.SyntheticEvent<{}>) { ev.preventDefault(); ev.stopPropagation(); this.setState({ showConfirm: true }); } handleCancel(ev: React.SyntheticEvent<{}>) { ev.stopPropagation(); this.setState({ showConfirm: false }); } handleConfirm(ev: React.SyntheticEvent<{}>) { ev.stopPropagation(); this.setState({ showConfirm: false }); this.props.onConfirm(); } render() { const { confirmationHeader, confirmationContent, onConfirm, renderOverride, ...rest } = this.props; return ( <> {renderOverride ? (
this.handleShowConfirm(ev)}>{renderOverride}
) : ( {isOpen && (
{lowRange !== undefined && lowRange !== 0 && ( <> ...showing only the last few lines. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} Full log {' '} )} {data.map((value, index) => (
                            ))}
                        
)}
); }; interface StatusIconProps { status?: SegmentStatus; processStatus?: ProcessStatus; loading?: boolean; warnings?: number; errors?: number; } const StatusIcon = ({ status, processStatus, warnings = 0, errors = 0 }: StatusIconProps) => { if (!status) { return ( ); } let color: SemanticCOLORS = 'green'; let icon: SemanticICONS = 'circle'; let spinning = false; if (status === SegmentStatus.RUNNING && isFinal(processStatus)) { color = 'yellow'; icon = 'question circle'; } else if (status === SegmentStatus.RUNNING) { color = 'teal'; icon = 'spinner'; spinning = true; } else if (status === SegmentStatus.SUSPENDED) { color = 'blue'; icon = 'hourglass half'; } else if (status === SegmentStatus.FAILED) { color = 'red'; icon = 'close'; } else if (warnings > 0) { color = 'orange'; icon = 'exclamation circle'; } else if (errors > 0) { color = 'red'; icon = 'exclamation circle'; } return ; }; export default LogSegment; ================================================ FILE: console2/src/components/molecules/LogSegment/styles.css ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ .LogSegment { color: rgb(22, 22, 22); } .LogSegment .Segment { text-align: left; background-color: white; position: sticky; top: 57px; } .LogSegment .Segment:focus { background: white none; } .LogSegment .Segment:hover { background-color: rgb(230, 230, 230); border-radius: 3px; } .LogSegment .Loader { position: absolute; top: 0; left: 0; width: 0; height: 100%; background: #2a2929; animation: progress-active 3s ease infinite; animation-delay: 100ms; } .LogSegment .Segment .Caption { padding-left: 10px; font-family: monospace; font-weight: normal; color: rgba(43, 43, 43, 0.9); } .LogSegment .Segment .State { margin: 0 !important; padding-right: 20px; } .LogSegment .Segment .Status { margin: 0 !important; } .LogSegment .Segment .EmptyStatus { margin: 0; width: 1.18em; display: inline-block; } .LogSegment .Segment .AdditionalAction { margin-top: 0 !important; float: right; padding-right: 10px; } .LogSegment .Segment .AdditionalAction.Last { padding-right: 0; color: #4183c4; } .LogSegment .Segment .AdditionalAction.Anchor { padding-right: 0; padding-left: 10px; color: #4183c4; } .LogSegment .Segment .AdditionalAction .on { color: #4183c4; } .LogSegment .Segment .AdditionalAction .off { color: #767676; } .LogSegment .Segment .AdditionalAction .off:hover { color: #000000; } .LogSegment .Segment .AdditionalAction i:hover{ color: rgb(43, 43, 43) !important; } .LogSegment .ContentContainer { padding: 0 0 0 40px; } .LogSegment .InnerContentContainer { overflow: auto; min-height: 12px; } .LogSegment .Loading { margin: 0; padding: 10px 0 10px 10px; color: rgb(241, 241, 241); background: rgb(43, 43, 43); } .LogSegment .Content { color: rgb(43, 43, 43); font-family: monospace; line-height: 18px; background: rgb(255, 255, 255); margin: 0; display: inline-block; min-width: 100%; } .LogSegment .Content pre { margin: 0; white-space: pre-wrap; } .LogSegment .Content a { color: #00B5F0; text-decoration: underline; } .LogSegment .Counter { font-weight: normal; color: #888888; margin-left: 10px; } .LogSegment .RunningFor { font-weight: normal; color: #888888; margin-left: 10px; } ================================================ FILE: console2/src/components/molecules/MainToolbar/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Icon, Menu, MenuItem, Sticky } from 'semantic-ui-react'; import { memo, useCallback, useState } from 'react'; import './styles.css'; interface ExternalProps { stickyRef: any; loading?: boolean; refresh?: () => void; breadcrumbs: React.ReactNode; } const MainToolbar = memo((props: ExternalProps) => { const { stickyRef, loading, refresh, breadcrumbs } = props; const [isFixed, setFixed] = useState(false); const onStick = useCallback(() => { setFixed(false); }, []); const onUnstick = useCallback(() => { setFixed(true); }, []); return ( {loading !== undefined && refresh !== undefined && ( )} {breadcrumbs} ); }); export default MainToolbar; ================================================ FILE: console2/src/components/molecules/MainToolbar/styles.css ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ .mainToolbar { padding-top: 1rem !important; padding-bottom: 1rem !important; } .mainToolbar.unfixed { background: white !important; border: 0 !important; border-bottom: 1px solid rgba(34,36,38,.15) !important; box-shadow: none !important; } .mainToolbar .item { padding: 0 !important; } .mainToolbar .button { font-size: .92857143rem !important; padding: .58928571em 1.125em .58928571em !important; } ================================================ FILE: console2/src/components/molecules/NewAPITokenForm/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { InjectedFormikProps, withFormik } from 'formik'; import * as React from 'react'; import { Button, Form } from 'semantic-ui-react'; import { ConcordKey } from '../../../api/common'; import { NewTokenEntry } from '../../../api/profile/api_token'; import { isApiTokenExists } from '../../../api/service/console'; import { notEmpty } from '../../../utils'; import { apiTokenAlreadyExistsError, secret as validation } from '../../../validation'; import { FormikInput } from '../../atoms'; interface FormValues { name: ConcordKey; } export type NewAPITokenFormValues = FormValues; interface Props { initial: FormValues; onSubmit: (values: NewTokenEntry) => void; submitting: boolean; } class NewAPITokenForm extends React.Component> { render() { const { submitting, handleSubmit, errors, dirty } = this.props; const hasErrors = notEmpty(errors); return (
); } } const validator = async (values: FormValues) => { let e; e = validation.name(values.name); if (e) { return Promise.resolve({ name: e }); } const exists = await isApiTokenExists(values.name); if (exists) { return Promise.resolve({ name: apiTokenAlreadyExistsError(values.name) }); } return {}; }; export default withFormik({ handleSubmit: (values, bag) => { bag.props.onSubmit(values); }, mapPropsToValues: (props) => props.initial, validate: validator, })(NewAPITokenForm); ================================================ FILE: console2/src/components/molecules/NewProjectForm/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { InjectedFormikProps, withFormik } from 'formik'; import * as React from 'react'; import { Divider, Form } from 'semantic-ui-react'; import { ConcordKey } from '../../../api/common'; import { ProjectVisibility } from '../../../api/org/project'; import { isProjectExists } from '../../../api/service/console'; import { notEmpty } from '../../../utils'; import { project as validation, projectAlreadyExistsError } from '../../../validation'; import { FormikDropdown, FormikInput } from '../../atoms'; interface FormValues { name: string; visibility: ProjectVisibility; description?: string; } export type NewProjectFormValues = FormValues; interface Props { orgName: ConcordKey; initial: FormValues; submitting: boolean; onSubmit: (values: FormValues) => void; } const visibilityOptions = [ { text: 'Public', value: ProjectVisibility.PUBLIC, description: 'Any user can start a process using a public project.', icon: 'unlock' }, { text: 'Private', value: ProjectVisibility.PRIVATE, description: "Private projects can be used only by their organization's teams.", icon: 'lock' } ]; class NewProjectForm extends React.PureComponent> { render() { const { handleSubmit, submitting } = this.props; const hasErrors = notEmpty(this.props.errors); return (
Create ); } } const validator = async (values: FormValues, props: Props) => { let e; e = validation.name(values.name); if (e) { return Promise.resolve({ name: e }); } const exists = await isProjectExists(props.orgName, values.name); if (exists) { return Promise.resolve({ name: projectAlreadyExistsError(values.name) }); } e = validation.description(values.description); if (e) { return Promise.resolve({ description: e }); } return {}; }; export default withFormik({ handleSubmit: (values, bag) => { bag.props.onSubmit(values); }, mapPropsToValues: (props) => props.initial, validate: validator })(NewProjectForm); ================================================ FILE: console2/src/components/molecules/NewSecretForm/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { InjectedFormikProps, withFormik } from 'formik'; import * as React from 'react'; import { Button, Divider, Form, Segment } from 'semantic-ui-react'; import { ConcordKey } from '../../../api/common'; import { ProjectEntry } from '../../../api/org/project'; import { NewSecretEntry, SecretStoreType, SecretTypeExt, SecretVisibility } from '../../../api/org/secret'; import { isSecretExists } from '../../../api/service/console'; import { notEmpty } from '../../../utils'; import { secret as validation, secretAlreadyExistsError } from '../../../validation'; import { FormikDropdown, FormikFileInput, FormikInput } from '../../atoms'; import { ProjectSearchFormField, SecretStoreDropdown } from '../../organisms'; enum StorePasswordType { DONT_USE, SPECIFY, GENERATE } interface FormValues { name: string; visibility: SecretVisibility; type: SecretTypeExt; publicFile?: File; privateFile?: File; username?: string; password?: string; valueString?: string; valueFile?: File; storePasswordType?: StorePasswordType; storePassword?: string; storeType?: SecretStoreType; projects?: ProjectEntry[]; } export type NewSecretFormValues = FormValues; interface Props { orgName: ConcordKey; initial: FormValues; onSubmit: (values: NewSecretEntry) => void; submitting: boolean; } const visibilityOptions = [ { text: 'Public', value: SecretVisibility.PUBLIC, description: 'Public secrets can be used by any user.', icon: 'unlock' }, { text: 'Private', value: SecretVisibility.PRIVATE, description: 'Private secrets can be used only by the specified teams.', icon: 'lock' } ]; const typeOptions = [ { text: 'Generate a new key pair', value: SecretTypeExt.NEW_KEY_PAIR }, { text: 'Existing key pair', value: SecretTypeExt.EXISTING_KEY_PAIR }, { text: 'Username/password', value: SecretTypeExt.USERNAME_PASSWORD }, { text: 'Single value (string)', value: SecretTypeExt.VALUE_STRING }, { text: 'Single value (file)', value: SecretTypeExt.VALUE_FILE } ]; const pwdTypeOptions = [ { text: "N/A (encrypt using the server's key)", value: StorePasswordType.DONT_USE }, { text: 'Specify a password', value: StorePasswordType.SPECIFY }, { text: 'Generate a new password', value: StorePasswordType.GENERATE } ]; class NewSecretForm extends React.Component> { render() { const { submitting, handleSubmit, errors, dirty, values, orgName } = this.props; const hasErrors = notEmpty(errors); return (
{values.type === SecretTypeExt.EXISTING_KEY_PAIR && ( )} {values.type === SecretTypeExt.USERNAME_PASSWORD && ( )} {values.type === SecretTypeExt.VALUE_STRING && ( )} {values.type === SecretTypeExt.VALUE_FILE && ( )} {values.storePasswordType === StorePasswordType.SPECIFY && ( )}

Project-scoped secrets can only be used in the processes of specified projects. They cannot be reused for multiple projects.

Secrets not linked to any project can be used anywhere. Standard permission checks are applied in both cases.

); } } const validator = async (values: FormValues, props: Props): Promise<{}> => { let e; e = validation.name(values.name); if (e) { return Promise.resolve({ name: e }); } const exists = await isSecretExists(props.orgName, values.name); if (exists) { return Promise.resolve({ name: secretAlreadyExistsError(values.name) }); } switch (values.type) { case SecretTypeExt.EXISTING_KEY_PAIR: { e = validation.publicFile(values.publicFile); if (e) { return Promise.resolve({ publicFile: e }); } e = validation.privateFile(values.privateFile); if (e) { return Promise.resolve({ privateFile: e }); } break; } case SecretTypeExt.USERNAME_PASSWORD: { e = validation.username(values.username); if (e) { return Promise.resolve({ username: e }); } e = validation.password(values.password); if (e) { return Promise.resolve({ password: e }); } break; } case SecretTypeExt.VALUE_STRING: { e = validation.valueString(values.valueString); if (e) { return Promise.resolve({ valueString: e }); } break; } case SecretTypeExt.VALUE_FILE: { e = validation.valueFile(values.valueFile); if (e) { return Promise.resolve({ valueFile: e }); } break; } default: break; } if (values.storePasswordType === StorePasswordType.SPECIFY) { e = validation.storePassword(values.storePassword); if (e) { return Promise.resolve({ storePassword: e }); } } return {}; }; export default withFormik({ handleSubmit: (values, bag) => { const entry: NewSecretEntry = values; switch (values.storePasswordType) { case StorePasswordType.DONT_USE: { entry.generatePassword = false; entry.storePassword = undefined; break; } case StorePasswordType.SPECIFY: { entry.generatePassword = false; break; } case StorePasswordType.GENERATE: { entry.generatePassword = true; entry.storePassword = undefined; break; } default: { break; } } bag.props.onSubmit(entry); }, mapPropsToValues: (props) => ({ storePasswordType: StorePasswordType.DONT_USE, ...props.initial }), validate: validator })(NewSecretForm); ================================================ FILE: console2/src/components/molecules/NewStorageForm/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { useEffect } from 'react'; import { Dropdown, Form, Input, Label } from 'semantic-ui-react'; import { useForm, type ValidateResult } from 'react-hook-form'; import { ConcordKey } from '../../../api/common'; import { StorageVisibility } from '../../../api/org/jsonstore'; import { storage as validation, jsonStoreAlreadyExistsError } from '../../../validation'; import { isStorageExists } from '../../../api/service/console'; interface FormValues { name: string; visibility: StorageVisibility; } export type NewStorageFormValues = FormValues; export interface Props { orgName: ConcordKey; initial: FormValues; submitting: boolean; onSubmit: (values: FormValues) => void; } const visibilityOptions = [ { text: 'Public', value: StorageVisibility.PUBLIC, icon: 'unlock' }, { text: 'Private', value: StorageVisibility.PRIVATE, icon: 'lock' } ]; const NewStoreForm = ({ orgName, onSubmit, submitting, initial }: Props) => { const { register, handleSubmit, formState: { errors }, setValue } = useForm({ defaultValues: initial }); useEffect(() => { register('name', { required: true, validate: (data) => validateName(orgName, data) }); register('visibility', { required: true }); }, [orgName, register]); return (
onSubmit(data))} loading={submitting}> setValue('name', event.target.value)} error={!!errors.name} /> {errors.name && ( )} setValue('visibility', data.value as StorageVisibility) } options={visibilityOptions} selection={true} defaultValue={StorageVisibility.PRIVATE} error={!!errors.visibility} /> {errors.visibility && ( )} Create
); }; const validateName = async (orgName: ConcordKey, name: string): Promise => { const invalidName = validation.name(name); if (invalidName) { return Promise.resolve(invalidName); } const exists = await isStorageExists(orgName, name); if (exists) { return Promise.resolve(jsonStoreAlreadyExistsError(name)); } return Promise.resolve(undefined); }; export default NewStoreForm; ================================================ FILE: console2/src/components/molecules/NewTeamForm/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { InjectedFormikProps, withFormik } from 'formik'; import * as React from 'react'; import { Button, Divider, Form } from 'semantic-ui-react'; import { ConcordKey } from '../../../api/common'; import { NewTeamEntry } from '../../../api/org/team'; import { isTeamExists } from '../../../api/service/console'; import { notEmpty } from '../../../utils'; import { team as validation, teamAlreadyExistsError } from '../../../validation'; import { FormikInput } from '../../atoms'; interface Props { orgName: ConcordKey; onSubmit: (values: NewTeamEntry) => void; submitting: boolean; } class NewTeamForm extends React.Component> { render() { const { submitting, handleSubmit, errors, dirty } = this.props; const hasErrors = notEmpty(errors); return (
); } } const validator = async (values: NewTeamEntry, { orgName }: Props): Promise<{}> => { let e; e = validation.name(values.name); if (e) { return Promise.resolve({ name: e }); } const exists = await isTeamExists(orgName, values.name); if (exists) { return Promise.resolve({ name: teamAlreadyExistsError(values.name) }); } e = validation.description(values.description); if (e) { return Promise.resolve({ description: e }); } return {}; }; export default withFormik({ handleSubmit: (values, bag) => { bag.props.onSubmit(values); }, validate: validator })(NewTeamForm); ================================================ FILE: console2/src/components/molecules/PaginationToolBar/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Button, Dropdown } from 'semantic-ui-react'; // TODO make customizable const defaultDropDownValues = [50, 100, 500]; interface Props { limit?: number; handleLimitChange?: (limit: any) => void; handleNext: () => void; handlePrev: () => void; handleFirst: () => void; disablePrevious?: boolean; disableNext?: boolean; disableFirst?: boolean; dropDownValues?: number[]; // Numbers maxValue?: number; // Maximum Items that could render disabled?: boolean; } class PaginationToolBar extends React.PureComponent { render() { const { limit, handleLimitChange, dropDownValues = defaultDropDownValues, maxValue, disabled } = this.props; return ( <> {handleLimitChange !== undefined && ( ({ text: value, value, disabled: maxValue ? value >= maxValue : false }))} value={limit || dropDownValues[0]} selection={true} basic={true} fluid={false} onChange={(v, data) => handleLimitChange(data.value)} disabled={disabled} /> )} )} ); } render() { const { instanceId, data } = this.props; const { expandedItems } = this.state; return ( <> {this.createLogToolbarActions()}
{ this.scrollAnchorRef = scroll; }} />
{ window.scrollTo({ top: 0 }); this.setState({ scrollAnchorRef: false }); }}>
); } } export default ProcessLogViewer; ================================================ FILE: console2/src/components/molecules/ProcessLogViewer/styles.css ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ .logEntry { font-family: monospace; white-space: pre-wrap; margin: 0; } .scrollToTopButton { position: fixed; bottom: 20px; right: 20px; cursor: pointer; opacity: 1; color: #2185D0; } .clickableTagHeader:hover { color: gray; cursor: zoom-in; } .logTagDetails { margin-bottom: 20px; } ================================================ FILE: console2/src/components/molecules/ProcessStatusIcon/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Icon, Popup, SemanticICONS, SemanticCOLORS } from 'semantic-ui-react'; import { ProcessEntry, ProcessStatus } from '../../../api/process'; import { formatDistanceToNow, isAfter, parseISO as parseDate } from 'date-fns'; enum AdditionalProcessStatus { /** * ENQUEUED + (startAt is not null) */ SCHEDULED = 'SCHEDULED' } export const statusToIcon: { [status: string]: { name: SemanticICONS; color?: SemanticCOLORS; loading?: boolean }; } = { NEW: { name: 'inbox', color: 'grey' }, PREPARING: { name: 'info', color: 'blue' }, ENQUEUED: { name: 'block layout', color: 'grey' }, WAITING: { name: 'block layout', color: 'grey' }, SCHEDULED: { name: 'hourglass start', color: 'grey' }, RESUMING: { name: 'circle notched', color: 'grey', loading: true }, SUSPENDED: { name: 'wait', color: 'blue' }, STARTING: { name: 'circle notched', color: 'grey', loading: true }, RUNNING: { name: 'circle notched', color: 'blue', loading: true }, FINISHED: { name: 'check', color: 'green' }, FAILED: { name: 'remove', color: 'red' }, CANCELLED: { name: 'remove', color: 'grey' }, TIMED_OUT: { name: 'wait', color: 'red' } }; type Status = ProcessStatus | AdditionalProcessStatus; interface ProcessStatusIconProps { process: ProcessEntry; } const getStatus = (process: ProcessEntry): Status => { if (process.status === ProcessStatus.ENQUEUED && process.startAt !== undefined) { return AdditionalProcessStatus.SCHEDULED; } return process.status; }; const getLabel = (process: ProcessEntry): string => { if (process.startAt && process.status === ProcessStatus.ENQUEUED) { const startAt = parseDate(process.startAt); if (isAfter(startAt, Date.now())) { return 'starts in ' + formatDistanceToNow(startAt); } } return process.status; }; export default ({ process }: ProcessStatusIconProps) => { const status = getStatus(process); let i = statusToIcon[status]; if (!i) { i = { name: 'question' }; } return ( } content={getLabel(process)} inverted={true} position="top center" /> ); }; ================================================ FILE: console2/src/components/molecules/ProcessStatusTable/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import {Link} from 'react-router'; import {Grid, Label, Popup, Table} from 'semantic-ui-react'; import {getStatusSemanticColor, ProcessEntry, ProcessKind, ProcessStatus} from '../../../api/process'; import {formatDuration} from '../../../utils'; import {GitHubLink, LocalTimestamp, ProcessLastErrorModal} from '../../molecules'; import {TriggeredByPopup} from '../../organisms'; interface Props { process?: ProcessEntry; } const kindToDescription = (k: ProcessKind): string => { switch (k) { case ProcessKind.DEFAULT: return 'Default'; case ProcessKind.FAILURE_HANDLER: return 'onFailure handler'; case ProcessKind.CANCEL_HANDLER: return 'onCancel handler'; case ProcessKind.TIMEOUT_HANDLER: return 'onTimeout handler'; default: return 'Unknown'; } }; class ProcessStatusTable extends React.PureComponent { static renderCommitId(process?: ProcessEntry) { if (!process || !process.commitId || !process.repoUrl) { return ' - '; } return ( ); } static renderProcessKind(process?: ProcessEntry) { if (!process) { return '-'; } return ( <> {kindToDescription(process.kind)} {process.kind === ProcessKind.FAILURE_HANDLER && process.status !== ProcessStatus.FAILED && ( )} ); } static renderTags(process?: ProcessEntry) { if (!process) { return '-'; } const tags = process.tags; if (!tags || tags.length === 0) { return ' - '; } const items = tags.map((t) => {t}); const result = []; for (let i = 0; i < items.length; i++) { result.push(items[i]); if (i + 1 !== items.length) { result.push(', '); } } return result; } static renderTriggeredBy(process?: ProcessEntry) { if (!process) { return ' - '; } return ; } static renderParentInstanceId(process?: ProcessEntry) { if (!process || !process.parentInstanceId) { return '-'; } const parentId = process.parentInstanceId; return {parentId}; } static renderInitiator(process?: ProcessEntry) { if (!process) { return '-'; } return process.initiator; } static renderCreatedAt(process?: ProcessEntry) { if (!process) { return '-'; } return ; } static renderStartAt(process?: ProcessEntry) { if (!process || !process.startAt) { return '-'; } return ; } static renderLastUpdatedAt(process?: ProcessEntry) { if (!process) { return '-'; } return ; } static renderTimeout(process?: ProcessEntry) { if (!process || (!process.timeout && !process.suspendTimeout)) { return '-'; } return ( <> {process.timeout && ( )} {process.suspendTimeout && ( )} ); } static renderProject(process?: ProcessEntry) { if (!process || !process.projectName) { return '-'; } return ( {process.projectName} ); } static renderRepo(process?: ProcessEntry) { if (!process || !process.repoName) { return '-'; } return ( {process.repoName} ); } static renderRepoUrl(process?: ProcessEntry) { if (!process || !process.repoUrl) { return '-'; } return ; } static renderRepoPath(process?: ProcessEntry) { if (!process || !process.commitId || !process.repoUrl) { return '-'; } return ( ); } render() { const { process } = this.props; return ( Parent ID {ProcessStatusTable.renderParentInstanceId(process)} Initiator {ProcessStatusTable.renderInitiator(process)} Type {ProcessStatusTable.renderProcessKind(process)} Created At {ProcessStatusTable.renderCreatedAt(process)} Start At {ProcessStatusTable.renderStartAt(process)} Last Update {ProcessStatusTable.renderLastUpdatedAt(process)} Timeout {ProcessStatusTable.renderTimeout(process)}
Project {ProcessStatusTable.renderProject(process)} Concord Repository {ProcessStatusTable.renderRepo(process)} Repository URL {ProcessStatusTable.renderRepoUrl(process)} Repository Path {ProcessStatusTable.renderRepoPath(process)} Commit ID {ProcessStatusTable.renderCommitId(process)} Process Tags {ProcessStatusTable.renderTags(process)} Triggered By {ProcessStatusTable.renderTriggeredBy(process)}
); } } export default ProcessStatusTable; ================================================ FILE: console2/src/components/molecules/ProcessToolbar/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Menu } from 'semantic-ui-react'; import './styles.css'; interface ExternalProps { children: React.ReactNode; } const ProcessToolbar = ({ children }: ExternalProps) => { return ( {children} ); }; export default ProcessToolbar; ================================================ FILE: console2/src/components/molecules/ProcessToolbar/styles.css ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ .processToolbar{ min-height: 0 !important; } .processToolbar .item{ padding-top: 0 !important; padding-bottom: 0 !important; } ================================================ FILE: console2/src/components/molecules/ProcessWaitList/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { ProcessLockCondition, ProcessSleepCondition, ProcessWaitCondition, WaitCondition, WaitType } from '../../../api/process'; import { Accordion, Icon, Table } from 'semantic-ui-react'; import { ConcordId } from '../../../api/common'; import { Link } from 'react-router'; import { LocalTimestamp } from '../index'; interface ExternalProps { data?: WaitCondition[]; } const ProcessWaitList = ({ data }: ExternalProps) => { return ( {renderTableHeader()}{renderElements(data)}
); }; const renderTableHeader = () => { return ( Condition Dependencies ); }; const renderElements = (data?: WaitCondition[]) => { if (!data) { return (   ); } if (data.length === 0) { return ( No data available ); } return data.map((p, index) => renderTableRow(index, p)); }; const renderProcessLink = (id: ConcordId) => { return (

{id}

); }; const renderCondition = (condition: WaitCondition) => { const type = condition.type; const reason = condition.reason; switch (type) { case WaitType.NONE: { return ( <> No wait conditions ); } case WaitType.PROCESS_COMPLETION: { return ( <> Waiting for the process to complete {reason && ` (${reason})`} ); } case WaitType.PROCESS_LOCK: { const lockPayload = condition as ProcessLockCondition; return ( <> Waiting for the lock ({lockPayload.name}) ); } case WaitType.PROCESS_SLEEP: { const sleepPayload = condition as ProcessSleepCondition; return ( <> Waiting until ({}) ); } default: return type; } }; const renderProcessWaitDetails = (condition: ProcessWaitCondition) => { if (condition.processes === undefined || condition.processes.length === 0) { return <>; } else if (condition.processes.length === 1) { return renderProcessLink(condition.processes[0]); } const panels = [ { key: 'k1', title: { content: ( {condition.processes[0]} ), style: { padding: 0 } }, content: [condition.processes.slice(1).map((id) => renderProcessLink(id))] } ]; return ; }; const renderProcessLockDetails = (payload: ProcessLockCondition) => { return renderProcessLink(payload.instanceId); }; const renderDependencies = (condition: WaitCondition) => { const type = condition.type; switch (type) { case WaitType.PROCESS_COMPLETION: { return renderProcessWaitDetails(condition as ProcessWaitCondition); } case WaitType.PROCESS_LOCK: { return renderProcessLockDetails(condition as ProcessLockCondition); } default: return ''; } }; const renderTableRow = (idx: number, row: WaitCondition) => { return ( {renderCondition(row)} {renderDependencies(row)} ); }; export default ProcessWaitList; ================================================ FILE: console2/src/components/molecules/ProjectConfiguration/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import React, { useRef, useState } from 'react'; import { Button } from 'semantic-ui-react'; import _ from 'lodash'; import './styles.css'; import LoadingEditor from '../LoadingEditor'; interface Props { config?: Object; submitting: boolean; submit: (config: Object) => void; } const ProjectConfiguration: React.FunctionComponent = ({ config, submitting, submit }) => { const [isEditorReady, setIsEditorReady] = useState(false); const [jsonError, setJsonError] = useState(''); const valueGetter = useRef(); const handleEditorDidMount = (_valueGetter: any) => { setIsEditorReady(true); valueGetter.current = _valueGetter; }; const handleSubmit = () => { if (valueGetter && valueGetter.current) { try { // @ts-ignore: Cannot invoke an object which is possibly 'undefined'. const jsonObj = JSON.parse(valueGetter.current()); setJsonError(''); if (!_.isEqual(jsonObj, config)) { submit(jsonObj); } else { setJsonError('No changes detected'); } } catch (error) { setJsonError(error.message); } } }; const loading = submitting || !isEditorReady; return (
); }; export default ProjectConfiguration; ================================================ FILE: console2/src/components/molecules/ProjectConfiguration/styles.css ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ .projectEditorContainer { display: flex; height: 75vh; min-height: 500px; } .projectEditorContainer .loader { display: flex; position: relative; text-align: initial; width: 100%; height: 100%; } ================================================ FILE: console2/src/components/molecules/ProjectRenameForm/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { InjectedFormikProps, withFormik } from 'formik'; import * as React from 'react'; import { Confirm, Form } from 'semantic-ui-react'; import { ConcordKey } from '../../../api/common'; import { isProjectExists } from '../../../api/service/console'; import { notEmpty } from '../../../utils'; import { project as validation, projectAlreadyExistsError } from '../../../validation'; import { FormikInput } from '../../atoms'; interface State { showConfirm: boolean; } interface FormValues { name: ConcordKey; } interface Props { orgName: ConcordKey; initial: FormValues; submitting: boolean; onSubmit: (values: FormValues) => void; } class ProjectRenameForm extends React.Component, State> { constructor(props: InjectedFormikProps) { super(props); this.state = { showConfirm: false }; } handleShowConfirm(ev: React.SyntheticEvent<{}>) { ev.preventDefault(); this.setState({ showConfirm: true }); } handleCancel() { this.setState({ showConfirm: false }); } handleConfirm() { this.props.submitForm(); } render() { const { dirty, handleSubmit, submitting } = this.props; const hasErrors = notEmpty(this.props.errors); return (
this.handleShowConfirm(ev)} /> this.handleConfirm()} onCancel={() => this.handleCancel()} /> ); } } const validator = async (values: FormValues, props: Props) => { let e; e = validation.name(values.name); if (e) { return Promise.resolve({ name: e }); } if (values.name !== props.initial.name) { const exists = await isProjectExists(props.orgName, values.name); if (exists) { return Promise.resolve({ name: projectAlreadyExistsError(values.name) }); } } return {}; }; export default withFormik({ handleSubmit: (values, bag) => { bag.props.onSubmit(values); }, mapPropsToValues: (props) => props.initial, validate: validator })(ProjectRenameForm); ================================================ FILE: console2/src/components/molecules/RepositoryForm/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2023 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { Field, getIn, InjectedFormikProps, withFormik } from 'formik'; import * as React from 'react'; import { Button, Divider, Form, Label, Popup, Segment } from 'semantic-ui-react'; import { ConcordId, ConcordKey } from '../../../api/common'; import { isRepositoryExists } from '../../../api/service/console'; import { notEmpty } from '../../../utils'; import { repository as validation, repositoryAlreadyExistsError } from '../../../validation'; import { FormikCheckbox, FormikDropdown, FormikInput } from '../../atoms'; import { SecretSearch } from '../../organisms'; import { FieldProps } from 'formik/dist/Field'; export enum RepositorySourceType { BRANCH_OR_TAG = 'branchOrTag', COMMIT_ID = 'commitId' } interface FormValues { id?: ConcordId; name: string; url: string; sourceType: RepositorySourceType; branch?: string; commitId?: string; path?: string; withSecret?: boolean; secretId?: string; secretName?: string; enabled: boolean; triggersEnabled: boolean; } export type RepositoryFormValues = FormValues; interface Props { orgName: ConcordKey; projectName: ConcordKey; initial: FormValues; submitting: boolean; editMode?: boolean; onSubmit: (values: FormValues, setSubmitting: (isSubmitting: boolean) => void) => void; testRepository: (values: FormValues) => Promise; } interface State { testRunning: boolean; testSuccess: boolean; testError?: string; testWarning?: string; } const sourceOptions = [ { text: 'Branch/tag/version', value: RepositorySourceType.BRANCH_OR_TAG }, { text: 'Commit ID', value: RepositorySourceType.COMMIT_ID } ]; const sanitize = (data: FormValues): FormValues => { const v = { ...data }; if (v.path === '') { v.path = undefined; } if (v.branch === '') { v.branch = undefined; } if (v.commitId === '') { v.commitId = undefined; } if (v.withSecret === false) { v.secretId = undefined; } return v; }; class RepositoryForm extends React.Component, State> { constructor(props: InjectedFormikProps) { super(props); this.state = { testRunning: false, testSuccess: false }; } handleTestConnection() { const { values, testRepository } = this.props; this.setState({ testRunning: true, testSuccess: false, testError: '', testWarning: '' }); testRepository(sanitize(values)) .then(() => { this.setState({ testSuccess: true, testRunning: false }); }) .catch((e) => { this.setState({ testSuccess: false, testRunning: false, testError: e.details && e.level !== 'WARN' ? e.details : e.message, testWarning: e.level === 'WARN' ? e.details : '' }); }); } render() { const { orgName, handleSubmit, values, errors, dirty, editMode = false, isValid } = this.props; const hasErrors = notEmpty(errors); const testConnectionDisabled = dirty && (!isValid || hasErrors); return ( <>
}> Personal repositories require additional authentication - a SSH key, a username/password pair or an OAuth (personal) token {values.withSecret && ( <> {({ field, form }: FieldProps) => { const fieldName = 'secretId'; const touched = getIn(form.touched, fieldName); const error = getIn(form.errors, fieldName); const invalid = !!(touched && error); return ( { form.setFieldTouched(fieldName, true); form.setFieldValue(fieldName, value?.id); }} onSelect={(value) => { form.setFieldValue(fieldName, value.id); }} /> {invalid && error && ( )} ); }} )} {values.sourceType === RepositorySourceType.BRANCH_OR_TAG && ( )} {values.sourceType === RepositorySourceType.COMMIT_ID && ( )} }> (Optional) Path in the repository that will be used as the root directory. { ev.preventDefault(); this.handleTestConnection(); }}> Test connection } open={ (!!this.state.testError || !!this.state.testWarning) && !this.props.isSubmitting } wide={true}> {!!this.state.testWarning && (

Warning: {this.state.testWarning}

)} {!!this.state.testError && (

Error: {this.state.testError}

)}
); } } const validator = async (values: FormValues, props: Props) => { let e; e = validation.name(values.name); if (e) { return Promise.resolve({ name: e }); } if (values.name !== props.initial.name) { const exists = await isRepositoryExists(props.orgName, props.projectName, values.name); if (exists) { return Promise.resolve({ name: repositoryAlreadyExistsError(values.name) }); } } e = validation.url(values.url); if (e) { return Promise.resolve({ url: e }); } switch (values.sourceType) { case RepositorySourceType.BRANCH_OR_TAG: e = validation.branch(values.branch); if (e) { return Promise.resolve({ branch: e }); } break; case RepositorySourceType.COMMIT_ID: e = validation.commitId(values.commitId); if (e) { return Promise.resolve({ commitId: e }); } break; default: throw new Error(`Unknown repository source type: ${values.sourceType}`); } e = validation.path(values.path); if (e) { return Promise.resolve({ path: e }); } if (!values.withSecret) { if (!values.url.startsWith('https://') && !values.url.startsWith('mvn://')) { return Promise.resolve({ url: "Invalid repository URL: must begin with 'https://' or 'mvn://'. SSH repository URLs require additional credentials to be specified." }); } } else { e = validation.secretId(values.secretId); if (e) { return Promise.resolve({ secretId: e }); } } return {}; }; export default withFormik({ handleSubmit: (values, bag) => { bag.props.onSubmit(sanitize(values), bag.setSubmitting); }, mapPropsToValues: (props) => ({ ...props.initial, withSecret: !!props.initial.secretId }), validate: validator, enableReinitialize: true })(RepositoryForm); ================================================ FILE: console2/src/components/molecules/RepositoryList/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Link } from 'react-router'; import { Icon, Table, Popup } from 'semantic-ui-react'; import { ConcordKey } from '../../../api/common'; import { RepositoryEntry, TriggerEntry } from '../../../api/org/project/repository'; import { GitHubLink } from '../../molecules'; import { RepositoryActionDropdown } from '../../organisms'; import { gitUrlParse } from "../GitHubLink"; interface ExternalProps { orgName: ConcordKey; projectName: ConcordKey; data?: RepositoryEntry[]; triggerMap : {[id: string] : TriggerEntry[]} loading: boolean; refresh: () => void; } const RepositoryList = ({ orgName, projectName, data, loading, refresh, triggerMap }: ExternalProps) => { return (
Name Repository URL Branch/Commit ID/Version Path Secret Execute {!loading && data?.length === 0 && ( No repositories found )} {data?.map((r) => renderTableRow(orgName, projectName, r, triggerMap[r.id], refresh))}
); }; const renderRepoPath = (r: RepositoryEntry) => { const urlLink = gitUrlParse(r.url); if (!urlLink) { return r.path; } if (r.commitId) { return ( ); } return ; }; const renderRepoCommitIdOrBranch = (r: RepositoryEntry) => { const urlLink = gitUrlParse(r.url); if (!urlLink) { return r.branch; } if (r.commitId) { return ; } return ; }; const renderTableRow = ( orgName: ConcordKey, projectName: ConcordKey, row: RepositoryEntry, triggerData: TriggerEntry[], refresh: () => void ) => { return ( }> {row.disabled ? 'Disabled' : 'Enabled'} {row.name} {renderRepoCommitIdOrBranch(row)} {renderRepoPath(row)} {row.secretName} ); }; export default RepositoryList; ================================================ FILE: console2/src/components/molecules/RequestErrorMessage/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Link } from 'react-router'; import { Message } from 'semantic-ui-react'; import { RequestError } from '../../../api/common'; interface Props { error: RequestError; } export default class extends React.PureComponent { render() { const { error } = this.props; if (!error) { return

No error

; } const details = error.details && error.details.length > 0 ? error.details : undefined; return ( {error.message && {error.message}} {details && details.split('\n').map((item, i) =>

{item}

)} {error.instanceId && (

Open the process log

)}
); } } ================================================ FILE: console2/src/components/molecules/SingleOperationPopup/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Button, Header, Modal, Icon, SemanticICONS, SemanticCOLORS } from 'semantic-ui-react'; import { RequestError, RequestErrorData } from '../../../api/common'; import { RequestErrorMessage } from '../../molecules'; interface State { open: boolean; } export type OnClickFn = () => void; interface Props { trigger: (onClick: OnClickFn) => React.ReactNode; title: string; introMsg: React.ReactNode; icon?: SemanticICONS; iconColor?: SemanticCOLORS; customStyle?: object; // CSS Object customNo?: string; customYes?: string; running: boolean; runningMsg?: React.ReactNode; success: boolean; successMsg?: React.ReactNode; error?: RequestError; errorRenderer?: (error: RequestErrorData) => React.ReactNode; reset?: () => void; onConfirm: () => void; onDone?: () => void; onOpen?: () => void; onDoneElements?: () => React.ReactNode; disableYes?: boolean; } class SingleOperationPopup extends React.Component { constructor(props: Props) { super(props); this.state = { open: false }; this.handleOpen = this.handleOpen.bind(this); this.handleClose = this.handleClose.bind(this); this.handleConfirm = this.handleConfirm.bind(this); this.renderContent = this.renderContent.bind(this); this.renderActions = this.renderActions.bind(this); this.stopPropagation = this.stopPropagation.bind(this); } handleOpen(event?: React.SyntheticEvent) { this.stopPropagation(event); this.props.reset && this.props.reset(); this.setState({ open: true }); } handleClose() { this.setState({ open: false }); } handleConfirm() { this.props.onConfirm(); } renderContent() { const { success, successMsg, error, errorRenderer, running, runningMsg, introMsg } = this.props; if (success) { return successMsg ? successMsg :

The operation was completed successfully.

; } if (error) { if (errorRenderer) { return errorRenderer(error); } return ; } if (running) { return runningMsg ? runningMsg :

Processing the request...

; } return introMsg; } renderActions() { const { success, error, running, onDone, onDoneElements, customNo, customYes, disableYes = false } = this.props; if (success) { return ( <> {onDoneElements && onDoneElements()} ); } if (error) { return ( <> ); } return ( <> ); } stopPropagation(event?: React.SyntheticEvent) { if (event) { event.stopPropagation(); } } render() { const { onOpen, trigger, title, icon, iconColor, customStyle = {} } = this.props; return ( this.handleOpen())}> {/* TODO: Header padding CSS */}
{title}
{this.renderContent()} {this.renderActions()}
); } } export default SingleOperationPopup; ================================================ FILE: console2/src/components/molecules/TeamAccessDropdown/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Dropdown, DropdownItemProps, DropdownProps } from 'semantic-ui-react'; import { ResourceAccessLevel } from '../../../api/org'; interface Props extends DropdownProps { onRoleChange: (value: ResourceAccessLevel) => void; } const options: DropdownItemProps[] = [ { text: 'Writer', value: ResourceAccessLevel.WRITER }, { text: 'Reader', value: ResourceAccessLevel.READER }, { text: 'Owner', value: ResourceAccessLevel.OWNER } ]; class TeamAccessDropdown extends React.PureComponent { render() { const { onRoleChange, ...rest } = this.props; return ( onRoleChange(ResourceAccessLevel[data.value as string])} {...rest} /> ); } } export default TeamAccessDropdown; ================================================ FILE: console2/src/components/molecules/TeamAccessList/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Button, Container, Menu, Form, Table } from 'semantic-ui-react'; import { ResourceAccessLevel, ResourceAccessEntry } from '../../../api/org'; import { TeamAccessDropdown } from '../../molecules'; import { FindTeamDropdown } from '../../organisms'; import { ConcordKey } from '../../../api/common'; import { TeamEntry } from '../../../api/org/team'; interface Entry extends ResourceAccessEntry { added: boolean; deleted: boolean; } interface State { data: Entry[]; dirty: boolean; editMode: boolean; } interface Props { data: ResourceAccessEntry[]; submitting: boolean; orgName: ConcordKey; submit: (entries: ResourceAccessEntry[]) => void; } const toState = (data: ResourceAccessEntry[]): Entry[] => data.map((e) => ({ ...e, added: false, deleted: false })); class TeamAccessList extends React.Component { constructor(props: Props) { super(props); this.state = { data: toState(props.data), dirty: false, editMode: false }; } handleEditMode() { this.setState({ editMode: true }); } handleCancelEdit() { this.setState({ data: toState(this.props.data), dirty: false, editMode: false }); } handleRoleChange(idx: number, level: ResourceAccessLevel) { const { data } = this.state; data[idx].level = level; this.setState({ data, dirty: true }); } handleDelete(idx: number) { const { data } = this.state; data[idx].deleted = !data[idx].deleted; this.setState({ data, dirty: true }); } handleAddTeam(u: TeamEntry) { if (!u.name) { return; } const { data } = this.state; const e = { teamId: u.id, teamName: u.name, level: ResourceAccessLevel.READER, added: true, deleted: false }; this.setState({ data: [e, ...data], dirty: true }); } handleSave(ev: React.SyntheticEvent<{}>) { ev.preventDefault(); this.setState({ editMode: false }); const { data } = this.state; const { submit } = this.props; submit(data.filter((e) => !e.deleted)); } componentDidUpdate(prevProps: Props) { if (prevProps !== this.props) { this.setState({ data: toState(this.props.data) }); } } render() { const { data, editMode, dirty } = this.state; const { submitting, orgName } = this.props; return ( <> {editMode && (
this.handleAddTeam(u)} orgName={orgName} name="teams" data-testid="team-access-add-dropdown" />
)} {editMode && ( <>
{data.length === 0 &&

No access rules defined.

} {data.length > 0 && ( Team Name Access Level {editMode && } {data.map((e, idx) => ( {e.teamName} {editMode ? ( this.handleRoleChange(idx, value) } data-testid={`team-access-dropdown-${e.teamName}`} /> ) : ( e.level )} {editMode && (
)} ); } } export default TeamAccessList; ================================================ FILE: console2/src/components/molecules/TeamRoleDropdown/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Dropdown, DropdownItemProps, DropdownProps } from 'semantic-ui-react'; import { TeamRole } from '../../../api/org/team'; interface Props extends DropdownProps { onRoleChange: (value: TeamRole) => void; } const options: DropdownItemProps[] = [ { text: 'Member', value: TeamRole.MEMBER }, { text: 'Maintainer', value: TeamRole.MAINTAINER }, { text: 'Owner', value: TeamRole.OWNER } ]; class TeamRoleDropdown extends React.PureComponent { render() { const { onRoleChange, ...rest } = this.props; return ( onRoleChange(TeamRole[data.value as string])} {...rest} /> ); } } export default TeamRoleDropdown; ================================================ FILE: console2/src/components/molecules/WithCopyToClipboard/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import copyToClipboard from 'copy-to-clipboard'; import * as React from 'react'; import { Icon } from 'semantic-ui-react'; import './styles.css'; export interface Props { children: React.ReactNode; value: string; } export default ({ children, value }: Props) => { return ( <> {children}   (copyToClipboard as any)(value)} /> ); }; ================================================ FILE: console2/src/components/molecules/WithCopyToClipboard/styles.css ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ .copyToClipboardButton:hover { color: gray; cursor: pointer; } ================================================ FILE: console2/src/components/molecules/ansible/AnsibleHostList/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Dropdown, DropdownItemProps, Grid, Input, Modal, Table } from 'semantic-ui-react'; import { AnsibleHost, AnsibleStatus, SearchFilter, SortField, SortOrder } from '../../../../api/process/ansible'; import { HumanizedDuration, PaginationToolBar } from '../../../molecules'; import { ConcordId } from '../../../../api/common'; import { AnsibleTaskListActivity } from '../../../organisms'; import ColumnSort from "../../../atoms/ColumnSort"; interface State { hostFilter?: string; prevHostFilter?: string; hostGroupFilter?: string; hostStatusFilter?: AnsibleStatus; hostSortFieldFilter?: SortField; hostSortByFilter?: SortOrder; } interface Props { instanceId: ConcordId; playbookId?: ConcordId; hosts?: AnsibleHost[]; hostGroups: string[]; showStatusFilter?: boolean; next?: number; prev?: number; refresh: (filter: SearchFilter) => void; } const hostStatusesOptions = [ { text: AnsibleStatus.RUNNING, value: AnsibleStatus.RUNNING }, { text: AnsibleStatus.CHANGED, value: AnsibleStatus.CHANGED }, { text: AnsibleStatus.FAILED, value: AnsibleStatus.FAILED }, { text: AnsibleStatus.OK, value: AnsibleStatus.OK }, { text: AnsibleStatus.SKIPPED, value: AnsibleStatus.SKIPPED }, { text: AnsibleStatus.UNREACHABLE, value: AnsibleStatus.UNREACHABLE } ]; const makeHostGroupOptions = (data: string[]): DropdownItemProps[] => { if (!data) { return []; } return data.map((value) => ({ value, text: value })); }; class AnsibleHostList extends React.Component { static renderHostItem( instanceId: ConcordId, host: string, hostGroup: string, hostStatus: AnsibleStatus, duration: number, idx: number, playbookId?: ConcordId ) { return ( {host} {hostGroup} {hostStatus} }> ); } static renderHosts(instanceId: ConcordId, playbookId?: ConcordId, hosts?: AnsibleHost[]) { if (!hosts) { return ( - ); } if (hosts.length === 0) { return ( No data available ); } return hosts.map((host, idx) => AnsibleHostList.renderHostItem( instanceId, host.host, host.hostGroup, host.status, host.duration, idx, playbookId ) ); } constructor(props: Props) { super(props); this.state = {}; this.handleNext = this.handleNext.bind(this); this.handlePrev = this.handlePrev.bind(this); this.handleFirst = this.handleFirst.bind(this); this.handleHostOnBlur = this.handleHostOnBlur.bind(this); this.handleHostChange = this.handleHostChange.bind(this); this.handleHostGroupChange = this.handleHostGroupChange.bind(this); this.handleHostStatusChange = this.handleHostStatusChange.bind(this); this.handleOrderByChange = this.handleOrderByChange.bind(this); } handleNext() { this.handleNavigation(this.props.next); } handlePrev() { this.handleNavigation(this.props.prev); } handleFirst() { this.handleNavigation(0); } handleNavigation(offset?: number) { const { hostFilter, hostGroupFilter, hostStatusFilter, hostSortFieldFilter, hostSortByFilter } = this.state; const { refresh } = this.props; refresh({ offset, host: hostFilter, hostGroup: hostGroupFilter, status: hostStatusFilter, sortField: hostSortFieldFilter, sortBy: hostSortByFilter }); } handleHostOnBlur() { const { hostFilter, prevHostFilter, hostGroupFilter, hostStatusFilter, hostSortFieldFilter, hostSortByFilter } = this.state; if (hostFilter !== prevHostFilter) { this.setState({ prevHostFilter: hostFilter }); this.props.refresh({ host: hostFilter, hostGroup: hostGroupFilter, status: hostStatusFilter, sortField: hostSortFieldFilter, sortBy: hostSortByFilter }); } } handleHostChange(s?: string) { const { hostFilter } = this.state; const host = s && s.length > 0 ? s : undefined; if (hostFilter !== host) { this.setState({ hostFilter: host }); } } handleHostGroupChange(s?: string) { const { hostFilter, hostGroupFilter, hostStatusFilter, hostSortFieldFilter, hostSortByFilter } = this.state; const hostGroup = s && s.length > 0 ? s : undefined; if (hostGroupFilter !== hostGroup) { this.setState({ hostGroupFilter: hostGroup }); this.props.refresh({ host: hostFilter, hostGroup, status: hostStatusFilter, sortField: hostSortFieldFilter, sortBy: hostSortByFilter }); } } handleHostStatusChange(s?: AnsibleStatus) { const { hostFilter, hostGroupFilter, hostStatusFilter, hostSortFieldFilter, hostSortByFilter } = this.state; const status = s && s.length > 0 ? s : undefined; if (status !== hostStatusFilter) { this.setState({ hostStatusFilter: status }); this.props.refresh({ host: hostFilter, hostGroup: hostGroupFilter, status, sortField: hostSortFieldFilter, sortBy: hostSortByFilter }); } } handleOrderByChange(sf: SortField, sb: SortOrder) { const { hostFilter, hostGroupFilter, hostStatusFilter, hostSortFieldFilter, hostSortByFilter } = this.state; const sortField = sf && sf.length > 0 ? sf : undefined; const sortBy = sb && sb.length > 0 ? sb : undefined; if (hostSortFieldFilter !== sortField || hostSortByFilter !== sortBy) { this.setState({ hostSortFieldFilter: sortField }); this.setState({hostSortByFilter: sortBy}); this.props.refresh({ host: hostFilter, hostGroup: hostGroupFilter, status: hostStatusFilter, sortField, sortBy }); } } render() { const { instanceId, playbookId, hosts, hostGroups, prev, next, showStatusFilter } = this.props; return ( <> this.handleHostChange(data.value)} /> this.handleHostGroupChange(data.value as string) } /> {showStatusFilter && ( this.handleHostStatusChange(data.value as AnsibleStatus) } /> )}
Host
this.handleOrderByChange(SortField.HOST, SortOrder.ASC)} descSort={() => this.handleOrderByChange(SortField.HOST, SortOrder.DESC)} />
Host Group
this.handleOrderByChange(SortField.HOST_GROUP, SortOrder.ASC)} descSort={() => this.handleOrderByChange(SortField.HOST_GROUP, SortOrder.DESC)} />
Host Status
this.handleOrderByChange(SortField.STATUS, SortOrder.ASC)} descSort={() => this.handleOrderByChange(SortField.STATUS, SortOrder.DESC)} />
Duration
this.handleOrderByChange(SortField.DURATION, SortOrder.ASC)} descSort={() => this.handleOrderByChange(SortField.DURATION, SortOrder.DESC)} />
{AnsibleHostList.renderHosts(instanceId, playbookId, hosts)}
); } } export default AnsibleHostList; ================================================ FILE: console2/src/components/molecules/ansible/AnsibleTaskList/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { Header, Table } from 'semantic-ui-react'; import { AnsibleEvent, AnsibleStatus } from '../../../../api/process/ansible'; import { ProcessEventEntry } from '../../../../api/process/event'; import { ReactJson } from '../../../atoms'; import { HumanizedDuration, LocalTimestamp } from '../../../molecules'; interface Props { title?: string; showHosts?: boolean; hideStatus?: boolean; hidePlaybook?: boolean; tasks?: Array>; } class AnsibleTaskList extends React.Component { renderTaskList = () => { const { tasks, showHosts, hideStatus, hidePlaybook } = this.props; return ( {showHosts && Host} Ansible Task Action {!hideStatus && ( Status )} Event Time Duration Results {!hidePlaybook && ( Playbook )} {tasks && tasks.map((value, index) => { const { status, ignore_errors } = value.data; const error = status === AnsibleStatus.FAILED && !ignore_errors; const positive = status === AnsibleStatus.OK; const warning = value.data.status === AnsibleStatus.UNREACHABLE; const statusString = status + (ignore_errors ? ' (errors ignored)' : ''); return ( {showHosts && ( {value.data.host} )} {value.data.task} {value.data.action ? value.data.action : '-'} {!hideStatus && {statusString}} {!hidePlaybook && ( {value.data.playbook} )} ); })} {!tasks && ( - )} {tasks && tasks.length === 0 && ( No data available )}
); }; render() { const { title } = this.props; if (!title) { return this.renderTaskList(); } return ( <> {/*TODO: replace this table in table with two line header */}
{title}
{this.renderTaskList()}
); } } export default AnsibleTaskList; ================================================ FILE: console2/src/components/molecules/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ export { default as AnsibleHostList } from './ansible/AnsibleHostList'; export { default as AnsibleTaskList } from './ansible/AnsibleTaskList'; export { default as BreadcrumbSegment } from './BreadcrumbSegment'; export { default as BulkProcessActionDropdown } from './BulkProcessActionDropdown'; export { default as ButtonWithConfirmation } from './ButtonWithConfirmation'; export { default as CreateNewEntityButton } from './CreateNewEntityButton'; export { default as DropdownWithAddition } from './DropdownWithAddition'; export { default as EditProjectForm } from './EditProjectForm'; export { default as EntityOwnerChangeForm } from './EntityOwnerChangeForm'; export { default as EntityOwnerPopup } from './EntityOwnerPopup'; export { default as EntityRenameForm } from './EntityRenameForm'; export { default as FormWizardAction } from './FormWizardAction'; export { default as GitHubLink } from './GitHubLink'; export { default as GlobalNavMenu } from './GlobalNavMenu'; export { default as Highlighter } from './Highlighter'; export { default as HumanizedDuration } from './HumanizedDuration'; export { default as LocalTimestamp } from './LocalTimestamp'; export { default as LogSegment } from './LogSegment'; export { default as MainToolbar } from './MainToolbar'; export { default as NewAPITokenForm } from './NewAPITokenForm'; export { default as NewProjectForm } from './NewProjectForm'; export { default as NewSecretForm } from './NewSecretForm'; export { default as NewStorageForm } from './NewStorageForm'; export { default as NewTeamForm } from './NewTeamForm'; export { default as PaginationToolBar } from './PaginationToolBar'; export { default as ProcessActionList } from './ProcessActionList'; export { default as ProcessAttachmentsList } from './ProcessAttachmentsList'; export { default as ProcessElementList } from './ProcessElementList'; export { default as ProcessForm } from './ProcessForm'; export { default as ProcessHistoryList } from './ProcessHistoryList'; export { default as ProcessLastErrorModal } from './ProcessLastErrorModal'; export { default as ProcessList } from './ProcessList'; export { default as ProcessLogContainer } from './ProcessLogContainer'; export { default as ProcessLogViewer } from './ProcessLogViewer'; export { default as ProcessStatusIcon } from './ProcessStatusIcon'; export { default as ProcessStatusTable } from './ProcessStatusTable'; export { default as ProcessToolbar } from './ProcessToolbar'; export { default as ProcessWaitList } from './ProcessWaitList'; export { default as RepositoryForm } from './RepositoryForm'; export { default as RepositoryList } from './RepositoryList'; export { default as RequestErrorMessage } from './RequestErrorMessage'; export { default as SingleOperationPopup } from './SingleOperationPopup'; export { default as TeamAccessDropdown } from './TeamAccessDropdown'; export { default as TeamAccessList } from './TeamAccessList'; export { default as TeamRoleDropdown } from './TeamRoleDropdown'; export { default as WithCopyToClipboard } from './WithCopyToClipboard'; // https://github.com/facebook/create-react-app/issues/6054 export * from './EditProjectForm'; export * from './GlobalNavMenu'; export * from './NewAPITokenForm'; export * from './NewProjectForm'; export * from './NewSecretForm'; export * from './RepositoryForm'; ================================================ FILE: console2/src/components/organisms/APITokenDeleteActivity/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { useState } from 'react'; import { Button } from 'semantic-ui-react'; import {ConcordId, ConcordKey, GenericOperationResult, RequestError} from '../../../api/common'; import { RequestErrorMessage, SingleOperationPopup } from '../../molecules'; import { deleteToken as apiDelete } from '../../../api/profile/api_token'; interface Props { id: ConcordId; name: ConcordKey; onDone: () => void; } export default ({ id, name, onDone }: Props) => { const [running, setRunning] = useState(false); const [error, setError] = useState(); const [response, setResponse] = useState(); const postData = async () => { try { setError(undefined); setRunning(true); setResponse(await apiDelete(id)); } catch (e) { setError(e); } finally { setRunning(false); } }; return ( <> {error && } (
} on={['click', 'hover']} closeOnTriggerClick={true} hoverable={true} hideOnScroll={true} position="top center" flowing={true} style={{ maxWidth: '300px' }} content={ <>
{checkpoint.startTime.toDateString()}
{formatDate(checkpoint.startTime, 'HH:mm:ss')}

{error && } } /> ); }; export default CheckpointPopup; ================================================ FILE: console2/src/components/organisms/CheckpointView/Container/__mocks__/checkpointUtils.mocks.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { CheckpointRestoreHistoryEntry, ProcessCheckpointEntry, ProcessHistoryEntry, ProcessStatus } from '../../../../../api/process'; import { ColumnDefinition, RenderType } from '../../../../../api/org'; export const validProcessCheckpoints: ProcessCheckpointEntry[] = [ { id: '1', name: 'checkpoint 1', createdAt: '2019-02-18T17:23:32.520Z' }, { id: '2', name: 'checkpoint 2', createdAt: '2019-02-18T17:23:32.790Z' }, { id: '3', name: 'checkpoint 3', createdAt: '2019-02-18T17:23:32.950Z' } ]; export const validProcessHistory: CheckpointRestoreHistoryEntry[] = [ { changeDate: '2019-02-18T17:23:29.678Z', id: 1, processStatus: ProcessStatus.PREPARING, checkpointId: 'e90e2280-33a1-11e9-855e-fa163e7ef419' }, { id: 2, changeDate: '2019-02-18T17:23:30.426Z', checkpointId: 'e90e2280-33a1-11e9-855e-fa163e7ef419', processStatus: ProcessStatus.ENQUEUED }, { id: 2, changeDate: '2019-02-18T17:23:30.907Z', checkpointId: 'e9590f7a-33a1-11e9-aa54-fa163e7ef419', processStatus: ProcessStatus.STARTING }, { id: 3, changeDate: '2019-02-18T17:23:31.878Z', checkpointId: 'e9ec1a36-33a1-11e9-bbef-fa163e7ef419', processStatus: ProcessStatus.RUNNING }, { id: 4, changeDate: '2019-02-18T17:23:33.445Z', checkpointId: 'eadac2da-33a1-11e9-bbef-fa163e7ef419', processStatus: ProcessStatus.FINISHED } ]; export const emptyProcessHistory: CheckpointRestoreHistoryEntry[] = []; export const emptyProcessCheckpoints: ProcessCheckpointEntry[] = []; export const hasMetaColumnDefinition: ColumnDefinition = { source: 'meta.repoMetadata', caption: 'Target Repo', searchType: 'substring', searchValueType: 'string' }; export const missingMetaColumnDefinition: ColumnDefinition = { render: RenderType.TIMESTAMP, source: 'lastUpdatedAt', caption: 'Updated' }; ================================================ FILE: console2/src/components/organisms/CheckpointView/Container/__tests__/__snapshots__/checkpointUtils.test.ts.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`generateCheckpointGroups handles empty checkpoints 1`] = ` Array [ Object { "checkpoints": Array [], "end": 2019-02-18T17:23:33.445Z, "name": "#1", "start": 2019-02-18T17:23:29.678Z, }, ] `; exports[`generateCheckpointGroups handles empty history 1`] = `Array []`; exports[`generateCheckpointGroups handles valid data 1`] = ` Array [ Object { "checkpoints": Array [ Object { "createdAt": "2019-02-18T17:23:32.520Z", "endTime": 2019-02-18T17:23:32.790Z, "id": "1", "name": "checkpoint 1", "startTime": 2019-02-18T17:23:32.520Z, "status": "FINISHED", }, Object { "createdAt": "2019-02-18T17:23:32.790Z", "endTime": 2019-02-18T17:23:32.950Z, "id": "2", "name": "checkpoint 2", "startTime": 2019-02-18T17:23:32.790Z, "status": "FINISHED", }, Object { "createdAt": "2019-02-18T17:23:32.950Z", "endTime": 2019-02-18T17:23:33.445Z, "id": "3", "name": "checkpoint 3", "startTime": 2019-02-18T17:23:32.950Z, "status": "FINISHED", }, ], "end": 2019-02-18T17:23:33.445Z, "name": "#1", "start": 2019-02-18T17:23:29.678Z, }, ] `; ================================================ FILE: console2/src/components/organisms/CheckpointView/Container/__tests__/checkpointUtils.test.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { generateCheckpointGroups } from '../checkpointUtils'; import { emptyProcessCheckpoints, emptyProcessHistory, validProcessCheckpoints, validProcessHistory } from '../__mocks__/checkpointUtils.mocks'; import { ProcessStatus } from '../../../../../api/process'; test('generateCheckpointGroups handles valid data', () => { const result = generateCheckpointGroups( ProcessStatus.FINISHED, validProcessCheckpoints, validProcessHistory ); expect(result).toMatchSnapshot(); }); test('generateCheckpointGroups handles no data', () => { const result = generateCheckpointGroups( ProcessStatus.FINISHED, emptyProcessCheckpoints, emptyProcessHistory ); expect(result).toEqual([]); }); test('generateCheckpointGroups handles empty checkpoints', () => { const result = generateCheckpointGroups( ProcessStatus.FINISHED, emptyProcessCheckpoints, validProcessHistory ); expect(result).toMatchSnapshot(); }); test('generateCheckpointGroups handles empty history', () => { const result = generateCheckpointGroups( ProcessStatus.FINISHED, validProcessCheckpoints, emptyProcessHistory ); expect(result).toMatchSnapshot(); }); ================================================ FILE: console2/src/components/organisms/CheckpointView/Container/__tests__/useQueryParams.test.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import React from 'react'; const dummyComponent = () => {}; test.skip('Test the thing ', () => {}); ================================================ FILE: console2/src/components/organisms/CheckpointView/Container/checkpointUtils.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { isAfter, isBefore, isEqual, parseISO as parseDate } from 'date-fns'; import { CheckpointRestoreHistoryEntry, ProcessCheckpointEntry, ProcessStatus } from '../../../../api/process'; import { comparators } from '../../../../utils'; import { CheckpointGroup, CustomCheckpoint } from '../shared/types'; /** * Generate CustomCheckpoint array between time * * @param start Start Date Object * @param end End Date Object * @param checkpoints Concord Process Checkpoint array * @param status Process Status type for this run * */ export const geCheckpointsBetweenTime = ( checkpoints: ProcessCheckpointEntry[], status: ProcessStatus, start: Date, end?: Date ): CustomCheckpoint[] => { // * Custom checkpoints extend checkpoints and add start/end times and status const resultCheckpoints: CustomCheckpoint[] = []; checkpoints // * Filter for checkpoints between a start and end date .filter((checkpoint) => { const checkTime = parseDate(checkpoint.createdAt); return ( (isEqual(checkTime, start) || isAfter(checkTime, start)) && (end === undefined || isBefore(checkTime, end)) ); }) .forEach((checkpoint, index, array) => { if (index !== array.length - 1) { resultCheckpoints.push({ ...checkpoint, status: ProcessStatus.FINISHED, startTime: parseDate(checkpoint.createdAt) }); } else { resultCheckpoints.push({ ...checkpoint, status, startTime: parseDate(checkpoint.createdAt) }); } }); return resultCheckpoints; }; /** * Generates a custom object array of type CheckpointGroup * Correlates checkpoint data with history data to generate said object. * * @param processStatus status of the process * @param checkpoints Original Process Checkpoint Array * @param checkpointRestoreHistory */ export const generateCheckpointGroups = ( processStatus: ProcessStatus, checkpoints: ProcessCheckpointEntry[], checkpointRestoreHistory?: CheckpointRestoreHistoryEntry[] ): CheckpointGroup[] => { const points = checkpoints.sort(comparators.byProperty((i) => parseDate(i.createdAt))); if (points.length === 0) { return []; } const history = (checkpointRestoreHistory || []).sort( comparators.byProperty((i) => parseDate(i.changeDate)) ); const groups: CheckpointGroup[] = []; let currentGroup: CheckpointGroup = { name: `#1`, status: ProcessStatus.FINISHED, checkpoints: [] }; let currentGroupStart = parseDate(points[0].createdAt); history.forEach((h) => { let currentGroupEnd = parseDate(h.changeDate); currentGroup.status = h.processStatus; currentGroup.checkpoints = geCheckpointsBetweenTime( points, h.processStatus, currentGroupStart, currentGroupEnd ); groups.push(currentGroup); currentGroup = { name: `#${groups.length + 1}`, status: processStatus, checkpoints: [] }; currentGroupStart = currentGroupEnd; }); currentGroup.checkpoints = geCheckpointsBetweenTime( checkpoints, processStatus, currentGroupStart, undefined ); groups.push(currentGroup); return groups; }; ================================================ FILE: console2/src/components/organisms/CheckpointView/Container/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { useEffect, useState } from 'react'; import constate from "constate"; import { ProcessEntry, PaginatedProcessEntries, list as apiList, ProcessListQuery } from '../../../../api/process'; import { CheckpointGroup } from '../shared/types'; import { generateCheckpointGroups } from './checkpointUtils'; import { ProjectEntry } from '../../../../api/org/project'; import useQueryParams from './useQueryParams'; import { ColumnDefinition } from '../../../../api/org'; /** * Interface for the inital props passed to this container. */ export interface InitialProps { project: ProjectEntry; refreshInterval?: number; } /** * Custom React hook to isolate state and features specific to the Checkpoint page. * We are exporting this as a [constate](https://github.com/diegohaz/constate) container * Use with the useContext react hook * * TODO: This hook is probably too big at this point, think about refactoring into more composable hooks * TODO: Add Error handling and error states * * @param initial: InitialProps values you can pass in via a Context or Provider */ export const useCheckpoint = (initial: InitialProps) => { // currentPage for pagination const [currentPage, setPage] = useState(1); // limitPerPage is the limit of items we will show on a page at one time const [limitPerPage, setLimitPerPage] = useState(10); // loadingData boolean to tell us if data is loading or not const [loadingData, setLoadingData] = useState(false); // orgId comes from initial, represents a concord org Id const [orgId] = useState(initial.project!.orgId); // projectId comes from iniital, represents a concord project Id const [projectId] = useState(initial.project!.id); // project comes from iniital, represents a concord project const [project] = useState(initial.project!); // processes an array of ProcessEntries which we can map over to render components const [processes, setProcesses] = useState([]); // checkpointGroups are a custom object correlating a processes checkpoints to timestamps const [checkpointGroups, setCheckpointGroups] = useState<{ [key: string]: CheckpointGroup[]; }>({}); // the current query parameters existing const { queryParams, replaceQueryParams, getCurrentParams } = useQueryParams(); /** * an active filter is represented by the source name of a ui config object * @see getConfigBySourceName to pull specific filter data * */ const [activeFilters, setActiveFilters] = useState<{ [source: string]: string }>({ ...queryParams }); /** * Selector for the project meta configs * @return An array of configs or return empty object if no data is found */ const getProjectUIConfigs = (): ColumnDefinition[] => { const project = initial.project; if (project.meta && project.meta.ui && project.meta.ui.processList) { return project.meta.ui.processList; } // No data to return return []; }; /** * Selector for metadata configs * @return An array of metadata configs or return empty object if no data is found */ const getMetaDataConfigs = () => { // Filter for meta properties and return the results return getProjectUIConfigs().filter((value) => { if (value.source && value.source.includes('meta')) { // Found some meta return true; } // This object is not meta return false; }); }; /** * Select a specific UI config for details by it's source name * @return a single metadata config */ const getConfigBySourceName = (sourceName: string) => { return getProjectUIConfigs().find((value) => { if (value.source === sourceName) { // Found an exact match return true; } // Nothing found return false; }); }; /** * Add Active filter * @param sourceName is the unique key these configs are known by * @param value the filter input for the source field */ const addActiveFilter = (sourceName: string, value: string) => { // Try to get the new filter const newFilter = getConfigBySourceName(sourceName); // Did we find the config item? if (newFilter) { // Add the source name to the active filters setActiveFilters({ ...activeFilters, [newFilter.source]: value }); } }; /** * Remove a specific filter from the list * Updates the url query params to new values * @param sourceName the unique key of the meta filter to remove */ const removeFilter = (sourceName: string) => { // find out if the property exist on the object const exists = Object.keys(activeFilters).includes(sourceName); if (exists === false) { return; // does not exist, do_nothing() } // It exists, so lets delete it let newFilters = activeFilters; delete newFilters[sourceName]; setActiveFilters(newFilters); replaceQueryParams(newFilters); }; /** * Remove all active filters * This just wipes the array clean */ const removeAllFilters = () => { setActiveFilters({}); replaceQueryParams({}); }; /** * Set current page to the provided page * @param newPage to be set */ const setCurrentPage = (newPage: number) => setPage(newPage); /** * Set page limit to the new limit * @param newLimit to be set */ const setPageLimit = (newLimit: number) => setLimitPerPage(newLimit); /** * Refresh the process data and generate checkpoint data * Function is async for the nice await api. * This is not exposed through the container api and should be called internally. * * @param args Arguments for the fetch @see ProcessListQuery type for args */ const refreshProcessData = async (args: ProcessListQuery) => { if (loadingData === true) { return; } setLoadingData(true); const { items: processes }: PaginatedProcessEntries = await apiList({ ...args, include: ['checkpoints', 'checkpointsHistory'] }); const checkpointGroups = {}; processes.forEach((p) => { if (p.checkpoints) { checkpointGroups[p.instanceId] = generateCheckpointGroups( p.status, p.checkpoints, p.checkpointRestoreHistory ); } }); setCheckpointGroups(checkpointGroups); setProcesses(processes); setLoadingData(false); }; /** * Get the total processes length of a process * * @return number of process length */ const getProcessCount = (): number => (processes ? processes.length : 0); /** * Get a humanized display of what page they are on. * * @return humanized string if the data exists otherwise return an empty string */ const getPaginationAsString = (): string => { if (processes) { const upperLimit = currentPage * limitPerPage; const lowerLimit = (currentPage - 1) * limitPerPage + 1; return `Showing ${lowerLimit} - ${upperLimit}`; // e.g. "Showing 1 - 10" } else { return ''; } }; /** * Reload function calls refreshProcessData function with parameters to refresh * the current page. * * @param filters - additional filter values, currently used to grab initial query params on page load * * @returns nothing */ const reloadData = (filters?: { [source: string]: string }): void => { refreshProcessData({ orgId, projectId, limit: limitPerPage, offset: (currentPage - 1) * limitPerPage, ...activeFilters, ...filters }); }; /** * Similar to reload data, calls refreshProcessData, but allows you to customize * the args passed to the reload function. * * TODO This function has the potential to throw warning if unmounted whilst fetch is in progress * ! Warning: Can't perform a React state update on an unmounted component. * * @param args Arguments for the fetch @see ProcessListQuery type for args */ const loadData = (args: ProcessListQuery): void => { refreshProcessData({ ...args, ...activeFilters }); }; // TODO react-hooks/exhaustive-deps warning useEffect(() => { if (initial.refreshInterval !== undefined) { // Load initial dataset reloadData(getCurrentParams()); // Continue to request data updates on const onPollInterval = setInterval(() => { reloadData(); }, initial.refreshInterval); return () => { clearInterval(onPollInterval); }; } }, [activeFilters, currentPage]); // eslint-disable-line react-hooks/exhaustive-deps // If activefilters change useEffect(() => { // Reset to page 1 setPage(1); }, [activeFilters]); /** * Update active filter when query params change */ useEffect(() => { setActiveFilters({ ...queryParams }); }, [queryParams]); /** * Selector to see if we are currently on the first page. * * @return true if first page, otherwise false */ const isFirstPage = (): boolean => currentPage === 1; /** * Selector to determine if filtering is possible based on current state * @return true if filtering is allowed, false if it is not */ const canFilter = (): boolean => { if (getMetaDataConfigs().length > 0) { return true; } // Nothing true, so false return false; }; return { checkpointGroups, currentPage, limitPerPage, setCurrentPage, getProcessCount, getPaginationAsString, isFirstPage, loadData, reloadData, loadingData, orgId, projectId, project, setPageLimit, processes, queryParams, replaceQueryParams, getProjectUIConfigs, getMetaDataConfigs, getConfigBySourceName, addActiveFilter, removeFilter, removeAllFilters, canFilter, activeFilters, setActiveFilters }; }; export const [CheckpointProvider, useCheckpointContext] = constate(useCheckpoint); ================================================ FILE: console2/src/components/organisms/CheckpointView/Container/useForm.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { useState } from 'react'; export interface FormValues { // name of field and it's value [name: string]: string; } /** * React hook to handle general form values * @param initialFormValues * * @return {FormValues} form - The current form values * @return setField - Function to set a new form value * @return clear - Function to clear all form values * @return reset - Function to reset values to the initial form values */ export const useForm = (initialFormValues: FormValues) => { const [initialForm] = useState(initialFormValues); const [form, setForm] = useState(initialForm); /** * Deletes a field from form state * * @param name - name of the field to delete */ const deleteField = (name: string) => { const newForm = form; delete newForm[name]; setForm(newForm); }; /** * Sets a form field value in state * * @param name - name of the field to modify in state * @param value - value to set on the field property */ const setField = (name: string, value: string) => { // If name given but value is empty, delete the field if (name && value === '') { deleteField(name); } setForm({ ...form, [name]: value }); }; /** * Form will be empty */ const clear = () => { setForm({}); }; /** * Form will be set to it's initial values */ const reset = () => { setForm(initialForm); }; return { form, setField, clear, reset }; }; export default useForm; ================================================ FILE: console2/src/components/organisms/CheckpointView/Container/usePopup.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { useState } from 'react'; /** * Custom React hook to manage state of a popup externally */ export const usePopup = () => { const [visible, setVisible] = useState(false); const close = () => { setVisible(false); }; const open = () => { setVisible(true); }; return { visible, setVisible, open, close }; }; export default usePopup; ================================================ FILE: console2/src/components/organisms/CheckpointView/Container/useQueryParams.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { useState, useEffect, useLayoutEffect } from 'react'; import { parseQueryParams, QueryParams } from '../../../../api/common'; import 'url-search-params-polyfill'; /** * Custom React Hook to provide the current query parameters * When this hook is used it creates a HachChangeEvent listener * * When the event fires query params in state are updated. */ export function useQueryParams() { const [queryParams, setQueryParams] = useState(); const [currentUrl, setCurrentUrl] = useState(''); const [oldUrl, setOldUrl] = useState(''); /** * Get url parameters directly * Useful if you need to load parameters on initial render * * @returns QueryParameterObject */ const getCurrentParams = (): QueryParams => { // Parse, decode, return return decodeAllUriValues(parseQueryParams(window.location.href)); }; /** * Remove empty values from the queryParamObject * @param params a query object to check */ const removeEmptyValues = (params: QueryParams): QueryParams => { const newValues = Object.entries(params).reduce((previous, current) => { // is the value empty? if (current[1] === '') { // value is empty don't add to the result return { ...previous }; } else { // there is a value so add it to the result return { ...previous, [current[0]]: current[1] }; } }, {}); return newValues; }; /** * decode all uri parameters * @param params key/value pairs to iterate through */ const decodeAllUriValues = (params: QueryParams): QueryParams => { let dec = decodeURIComponent; let decodedResult = {}; Object.keys(params).forEach((key) => { decodedResult[key] = dec(params[key]); }); return decodedResult; }; /** * Replaces all query params in the url with the object provided * Store that value in queryParams state * * @param params a QueryParams e.g. { key: value, ... } */ const replaceQueryParams = (params: QueryParams = {}) => { // Construct URLSearchParams instance let UrlParams = new URLSearchParams(); Object.keys(removeEmptyValues(params)).forEach((i) => UrlParams.append(i, params[i])); // The full url that shows in the browser currently let baseUrl = window.location.href; // Store oldUrl in state setOldUrl(baseUrl); // Edgecase if the url happens to have a ? in it if (baseUrl.includes('?')) { baseUrl = baseUrl.split('?')[0]; } // Generate the new complete href let newUrl = baseUrl; // Only add query params if there are query params if (UrlParams.toString().length > 0) newUrl += `?${UrlParams.toString()}`; // Set the query params in the URL window.location.assign(newUrl); // Save new url to current setCurrentUrl(newUrl); // Store params in state setQueryParams(parseQueryParams(UrlParams.toString())); }; /** * Sets the queryParams state values if the URL hash changes * Saves the old and new url to state * @param event Window event object containing new and old url addresses */ const onHashChange = (event: HashChangeEvent) => { if (event.newURL === event.oldURL) { // No change to URL. do_nothing(); return; } // Store so we can expose these setOldUrl(event.oldURL); setCurrentUrl(event.newURL); // Parse, decode, set setQueryParams(decodeAllUriValues(parseQueryParams(event.newURL))); }; /** * Sets up a hashchange event listener for any changes to the url * This Effect only runs on initial mount * * On unmount the hashchange event listener is removed via a useEffect * cleanup function */ // TODO react-hooks/exhaustive-deps warning useEffect(() => { const eventName = 'hashchange'; window.addEventListener(eventName, onHashChange, false); return () => { window.removeEventListener(eventName, onHashChange, false); }; }, []); // eslint-disable-line react-hooks/exhaustive-deps /** * Set queryParams on first render */ // TODO react-hooks/exhaustive-deps warning useLayoutEffect(() => { setQueryParams(getCurrentParams()); }, []); // eslint-disable-line react-hooks/exhaustive-deps return { queryParams, replaceQueryParams, decodeAllUriValues, getCurrentParams, currentUrl, oldUrl }; } export default useQueryParams; ================================================ FILE: console2/src/components/organisms/CheckpointView/MetaFilterForm/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import React, { FunctionComponent } from 'react'; import { Grid, Header, Button, Divider, Popup } from 'semantic-ui-react'; import { useCheckpointContext } from '../Container'; import useQueryParams from '../Container/useQueryParams'; import useForm from '../Container/useForm'; import { usePopup } from '../Container/usePopup'; /** * This form uses the metadata config data to allow for filtering of processes * * On submit, the URL parameters will be updated with the results * On load it should pull from state to populate it's data */ export const MetaFilterForm: FunctionComponent<{ onClear: () => void }> = ({ onClear }) => { const { getMetaDataConfigs, activeFilters, removeAllFilters } = useCheckpointContext(); const { replaceQueryParams } = useQueryParams(); // Loop over all possible meta configs and create an object with active filters set const initialFormState = getMetaDataConfigs().reduce((previous, current) => { // Is the current config currently active? if (Object.keys(activeFilters).includes(current.source)) { // Found an active filter for a config, set value to the active filter value return { ...previous, [current.source]: activeFilters[current.source] }; } else { // No active filter found, set value to empty string return { ...previous, [current.source]: '' }; } }, {}); const { form, setField, clear } = useForm(initialFormState); return (
Meta Filters Set filters based on your project metadata
{ e.preventDefault(); replaceQueryParams(form); }}> {getMetaDataConfigs().map(({ source, caption }) => { return (
{/* Display caption if it exists, otherwise display the source-name } */} {caption ? caption : source}
setField(source, e.target.value)} />
); })}
); }; export const MetaFilterPopup: FunctionComponent = () => { const { canFilter } = useCheckpointContext(); const { visible, open, close } = usePopup(); return ( } content={} open={visible} onClose={close} onOpen={open} position="bottom right" flowing wide closeOnEscape keepInViewPort openOnTriggerClick /> ); }; ================================================ FILE: console2/src/components/organisms/CheckpointView/NoCheckpointsMessage/NoCheckpointsMessge.test.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import React from 'react'; import { render } from '@testing-library/react'; import Message from './'; test('Renders a message describing there are no checkpoints', () => { const { container } = render(); expect(container.innerHTML).toContain('No checkpoints'); }); ================================================ FILE: console2/src/components/organisms/CheckpointView/NoCheckpointsMessage/__tests__/NoCheckpointsMessge.test.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import React from 'react'; import { render } from '@testing-library/react'; import Message from '../'; test('Renders a message describing there are no checkpoints', () => { const { container } = render(); expect(container.innerHTML).toContain('No checkpoints'); }); ================================================ FILE: console2/src/components/organisms/CheckpointView/NoCheckpointsMessage/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { LoadError } from '../shared/Labels'; export default () => No checkpoints have been created for this process.; ================================================ FILE: console2/src/components/organisms/CheckpointView/ProcessCheckpoint/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { ContentBlock } from '../ProcessList/styles'; import CheckpointGroup from '../CheckpointGroup'; import { generateCheckpointGroups } from '../Container/checkpointUtils'; import { ProcessEntry } from '../../../../api/process'; import NoCheckpointsMessage from '../NoCheckpointsMessage'; import CheckpointErrorBoundary from '../CheckpointErrorBoundry'; interface Props { process: ProcessEntry; } export const ProcessCheckpoint: React.SFC = ({ process }) => { if (process.checkpoints) { return ( ); } else { return ( ); } }; export default ProcessCheckpoint; ================================================ FILE: console2/src/components/organisms/CheckpointView/ProcessCheckpointView.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2019 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { ProcessEntry } from '../../../api/process'; interface ExternalProps { process: ProcessEntry; } const ProcessCheckpointView = (props: ExternalProps) => {}; export default ProcessCheckpointView; ================================================ FILE: console2/src/components/organisms/CheckpointView/ProcessList/LeftContent.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { formatDistanceToNow, parseISO as parseDate } from 'date-fns'; import React from 'react'; import { Link } from 'react-router'; import { Divider, Icon } from 'semantic-ui-react'; import { ProjectEntry, ProjectEntryMeta } from '../../../../api/org/project'; import { ProcessEntry } from '../../../../api/process'; import { Truncate } from '../../../atoms'; import { Label, Status } from '../shared/Labels'; import { LeftWrap, ListItem } from './styles'; interface Props { project: ProjectEntry; process: ProcessEntry; } const renderMeta = (projectMeta?: ProjectEntryMeta, processMeta?: {}) => { if (!projectMeta || !projectMeta.ui || !projectMeta.ui.processList) { return; } // here we're going to render only `meta.` variables return projectMeta.ui.processList .filter((i) => i.source && i.source.startsWith('meta.')) .map((i, idx) => { const k = i.source.substr(5); // cut off the `meta.` prefix const v = processMeta ? (processMeta[k] ? processMeta[k] : 'n/a') : 'n/a'; return (
{v}
); }); }; export default ({ project, process }: Props) => { return (
{/* Show repository name if it exists */} {process.repoName && (
{process.repoName}
)} {renderMeta(project.meta, process.meta)}
{process.status}
{/* Last update time, tooltip on mouse hover */}
{formatDistanceToNow(parseDate(process.lastUpdatedAt))}
); }; ================================================ FILE: console2/src/components/organisms/CheckpointView/ProcessList/RightContent.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ // @ts-nocheck import React from 'react'; import { RightWrap } from './styles'; import CheckpointGroup from '../CheckpointGroup'; import NoCheckpointsMessage from '../NoCheckpointsMessage'; import { ProcessEntry } from '../../../../api/process'; import {useCheckpointContext} from "../Container"; interface Props { process: ProcessEntry; } export default ({ process }: Props) => { const { checkpointGroups } = useCheckpointContext(); return ( {process.checkpoints && ( <> {process.checkpoints.length && ( )} )} {!process.checkpoints && } ); }; ================================================ FILE: console2/src/components/organisms/CheckpointView/ProcessList/styles.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import styled from 'styled-components'; import { Column } from '../shared/Layout'; export const LeftWrap = styled(Column)` border: 1px solid #dedfde; border-top-left-radius: 5px; border-bottom-left-radius: 5px; float: left; `; export const ContentBlock = styled(Column)` border: 1px solid #dedfde; border-radius: 5px; overflow-y: hidden; overflow-x: auto; --shadow-height: 100%; --shadow-color: rgba(0, 0, 0, 0.1); --shadow-weight: 9px; /* Left start and right start 'inside' container colors (they overlap the shadows) */ background: linear-gradient(90deg, white 0%, rgba(255, 255, 255, 0)), linear-gradient(-90deg, white 0%, rgba(255, 255, 255, 0)) 100% 0, /* Left and right scroll shadows */ linear-gradient(90deg, var(--shadow-color), rgba(0, 0, 0, 0)), linear-gradient(-90deg, var(--shadow-color), rgba(0, 0, 0, 0)) 100% 0; background-repeat: no-repeat; background-color: #fff; background-size: 100px 100%, 100px 100%, var(--shadow-weight) var(--shadow-height), var(--shadow-weight) var(--shadow-height); background-attachment: local, local, scroll, scroll; /* Scrollbar has a bit of a custom look */ &::-webkit-scrollbar { height: 6px; } &::-webkit-scrollbar-track { box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.3); border-radius: 10px; } &::-webkit-scrollbar-thumb { border-radius: 10px; box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.5); } `; export const RightWrap = styled(ContentBlock)` border-top-left-radius: 0px; border-bottom-left-radius: 0px; `; export const ListItem = styled('li')` font-family: lato; text-align: left; list-style-type: none; color: #706f70; padding: 16px; i { padding: 0px 8px; display: inline; position: relative; bottom: 2px; } `; ================================================ FILE: console2/src/components/organisms/CheckpointView/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import React, { FunctionComponent } from 'react'; import CheckpointErrorBoundary from './CheckpointErrorBoundry'; import ActionBar from './ActionBar'; import LeftContent from './ProcessList/LeftContent'; import { Row } from './shared/Layout'; import RightContent from './ProcessList/RightContent'; import { ProjectEntry } from '../../../api/org/project'; import {useCheckpointContext, CheckpointProvider} from "./Container"; /** * This View renders the two bigger components that make up the this checkpoint view * * @Component ActionBar contains refresh, filter, and pagination elements * @Component Map over process details to create list of process items and details */ export const View = () => { const { project, processes } = useCheckpointContext(); return ( {processes && processes.map((process) => { return ( ); })} ); }; /** * Renders Context Providers for the Checkpoint View to consume * @param project the Concord Project */ export const CheckpointView: FunctionComponent<{ project: ProjectEntry }> = ({ project }) => ( ); export default CheckpointView; ================================================ FILE: console2/src/components/organisms/CheckpointView/shared/Labels.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { ClassIcon } from '../../../atoms/ClassIcon'; import styled from 'styled-components'; const TextBase = styled.span` font-family: lato; color: #706f70; `; export const Label = styled(TextBase)` font-weight: bold; font-size: 1rem; `; export const StatusText = styled(TextBase)` font-size: 1rem; display: inline; `; export const CheckpointName = styled(TextBase)` font-size: 1rem; font-weight: bold; margin-bottom: 4px; `; export const CheckpointGroupName = styled(CheckpointName)` font-size: 1.2rem; font-weight: bold; `; export const LoadError = styled.div` color: grey; font-weight: bold; font-size: 1.2rem; margin: auto auto; padding: 1em; `; export const Status: React.SFC<{ as?: 'span' | 'div' | 'td' }> = ({ as = 'span', children }) => { switch (children) { case 'FAILED': return React.createElement( as, { style: { color: '#DB2928' } }, <> {children} ); case 'FINISHED': return React.createElement( as, { style: { color: 'green' } }, <> {children} ); default: return React.createElement(as, null, children); } }; ================================================ FILE: console2/src/components/organisms/CheckpointView/shared/Layout.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import styled from 'styled-components'; export const Row = styled.div` /* display: grid; grid-template-columns: fit-content(300px) 1fr; */ /* // TODO: Determine a better way to handle long repoMetadata */ display: flex; flex-direction: row; flex-wrap: nowrap; width: 100%; margin: 24px 0px; border-radius: 5px; box-shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15); `; interface ColumnProps { flex?: number; background?: string; maxWidth?: number; } export const Column = styled('div')` display: flex; flex-direction: column; flex-basis: 100%; flex: ${(props: ColumnProps) => (props.flex ? props.flex : 1)}; background-color: ${(props: ColumnProps) => props.background}; ${(props: ColumnProps) => (props.maxWidth ? `max-width: ${props.maxWidth}px` : null)}; `; ================================================ FILE: console2/src/components/organisms/CheckpointView/shared/types.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import { ProcessCheckpointEntry, ProcessStatus } from '../../../../api/process'; export interface CheckpointGroup { name: string; SubText?: string; checkpoints: CustomCheckpoint[]; status?: ProcessStatus; } export interface CustomCheckpoint extends ProcessCheckpointEntry { startTime: Date; status: ProcessStatus; } ================================================ FILE: console2/src/components/organisms/CheckpointView/shared/utils.ts ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ export { getStatusSemanticColor } from '../../../../api/process'; ================================================ FILE: console2/src/components/organisms/DeleteRepositoryPopup/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { useCallback, useState } from 'react'; import { Input } from 'semantic-ui-react'; import { ConcordKey, GenericOperationResult } from '../../../api/common'; import { SingleOperationPopup } from '../../molecules'; import { deleteRepository as apiRepoDelete } from '../../../api/org/project/repository'; import { useApi } from '../../../hooks/useApi'; interface ExternalProps { orgName: ConcordKey; projectName: ConcordKey; repoName: ConcordKey; trigger: (onClick: () => void) => React.ReactNode; onDone: () => void; } const DeleteRepositoryPopup = (props: ExternalProps) => { const { orgName, projectName, repoName, trigger, onDone } = props; const [confirmation, setConfirmation] = useState(''); const deleteDataRequest = useCallback(() => { return apiRepoDelete(orgName, projectName, repoName); }, [orgName, projectName, repoName]); const { data, isLoading, error, clearState, fetch } = useApi( deleteDataRequest, { fetchOnMount: false, requestByFetch: true } ); const resetHandler = useCallback(() => { clearState(); }, [clearState]); return (

Are you sure you want to delete the repository? Any process or repository that uses this repository may stop working correctly.

Please type {repoName} to confirm.

setConfirmation(data.value)} />
} running={isLoading} runningMsg={

Removing the repository...

} success={data !== undefined} successMsg={

The repository was removed successfully.

} error={error} reset={resetHandler} onConfirm={fetch} onDone={onDone} disableYes={confirmation !== repoName} /> ); }; export default DeleteRepositoryPopup; ================================================ FILE: console2/src/components/organisms/DisableProcessPopup/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { ConcordId, RequestError } from '../../../api/common'; import { SingleOperationPopup } from '../../molecules'; import { memo } from 'react'; import { useState } from 'react'; import { useCallback } from 'react'; import { disable as apiDisable } from '../../../api/process'; interface ExternalProps { instanceId: ConcordId; disabled: boolean; refresh: () => void; trigger: (onClick: () => void) => React.ReactNode; } const DisableProcessPopup = memo((props: ExternalProps) => { const [disabling, setDisabling] = useState(false); const [error, setError] = useState(); const [success, setSuccess] = useState(false); const { instanceId, disabled } = props; const disableProcess = useCallback(async () => { setDisabling(true); try { await apiDisable(instanceId, disabled); setSuccess(true); } catch (e) { setError(e); } finally { setDisabling(false); } }, [instanceId, disabled]); const reset = useCallback(() => { setDisabling(false); setSuccess(false); setError(undefined); }, []); const { trigger, refresh } = props; const operation = disabled ? 'Disable' : 'Enable'; return ( Are you sure you want to {operation.toLowerCase()} the selected process?

} running={disabling} runningMsg={disabled ? 'Disabling...' : 'Enabling...'} success={success} error={error} reset={reset} onDone={refresh} onConfirm={disableProcess} /> ); }); export default DisableProcessPopup; ================================================ FILE: console2/src/components/organisms/EditProjectActivity/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import {ConcordKey, GenericOperationResult} from '../../../api/common'; import {EditProjectForm, FormValues} from '../../molecules'; import { UpdateProjectEntry, ProjectEntry } from '../../../api/org/project'; import { RequestErrorActivity } from '../index'; import {useCallback, useState} from "react"; import {createOrUpdate as apiUpdate} from "../../../api/org/project"; import {useApi} from "../../../hooks/useApi"; import {LoadingDispatch} from "../../../App"; interface ExternalProps { orgName: ConcordKey; projectName: ConcordKey; initial?: ProjectEntry; } const toUpdateProjectEntry = (p?: ProjectEntry): UpdateProjectEntry => { return { id: p?.id, name: p?.name, visibility: p?.visibility, description: p?.description }; }; const EditProjectActivity = (props: ExternalProps) => { const {orgName, initial} = props; const dispatch = React.useContext(LoadingDispatch); const [updateEntry, setUpdateEntry] = useState(toUpdateProjectEntry(initial)); const postData = useCallback(() => { return apiUpdate(orgName, updateEntry); }, [orgName, updateEntry]); const { error, isLoading, fetch } = useApi(postData, { fetchOnMount: false, requestByFetch: true, dispatch }); const handleSubmit = useCallback( (values: FormValues) => { setUpdateEntry(values.data); fetch(); }, [fetch] ); if (!initial) { return <>; } return ( <> {error && } ); }; export default EditProjectActivity; ================================================ FILE: console2/src/components/organisms/EditRepositoryActivity/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { testRepository } from '../../../api/service/console'; import { RepositoryForm, RepositoryFormValues, RepositorySourceType } from '../../molecules'; import { RequestErrorActivity } from '../index'; import { createOrUpdate as apiCreateOrUpdate, EditRepositoryEntry, get as apiGetRepo, RepositoryEntry, } from '../../../api/org/project/repository'; import { useApi } from '../../../hooks/useApi'; import { ConcordKey, RequestError } from '../../../api/common'; import { Navigate } from 'react-router'; interface ExternalProps { orgName: ConcordKey; projectName: ConcordKey; /** defined for edit, undefined for new repos */ repoName?: ConcordKey; forceRefresh: any; } const INITIAL_VALUES: RepositoryFormValues = { name: '', url: '', enabled: true, sourceType: RepositorySourceType.BRANCH_OR_TAG, triggersEnabled: true, }; const EditRepositoryActivity = (props: ExternalProps) => { const { orgName, projectName, repoName, forceRefresh } = props; const [success, setSuccess] = useState(false); const [error, setError] = useState(); const [isLoading, setLoading] = useState(false); const loadRepo = useCallback(() => { return apiGetRepo(orgName, projectName, repoName!); }, [orgName, projectName, repoName]); const { fetch: loadRepoFetch, clearState: loadRepoClearState, data: loadRepoData, error: loadError, } = useApi(loadRepo, { fetchOnMount: false }); useEffect(() => { if (repoName === undefined) { return; } loadRepoClearState(); loadRepoFetch(); }, [loadRepoFetch, loadRepoClearState, forceRefresh, repoName]); const handleSubmit = useCallback( async (values: RepositoryFormValues, setSubmitting: (isSubmitting: boolean) => void) => { setLoading(true); try { const result = await apiCreateOrUpdate( orgName, projectName, toEditRepositoryEntry(values) ); setSuccess(result.ok); } catch (e) { setError(e); } finally { setLoading(false); setSubmitting(false); } }, [orgName, projectName] ); if (success) { return ; } if (loadError) { return ; } return ( <> {error && } testRepository({ orgName, projectName, ...rest }) } /> ); }; const toFormValues = (r?: RepositoryEntry): RepositoryFormValues | undefined => { if (!r) { return; } const sourceType = r.commitId ? RepositorySourceType.COMMIT_ID : RepositorySourceType.BRANCH_OR_TAG; return { id: r.id, name: r.name, url: r.url, sourceType, branch: r.branch, commitId: r.commitId, path: r.path, secretId: r.secretId, secretName: r.secretName, enabled: !r.disabled, triggersEnabled: !r.triggersDisabled, }; }; const notEmpty = (s: string | undefined): string | undefined => { if (!s) { return; } if (s === '') { return; } return s; }; const toEditRepositoryEntry = (repo: RepositoryFormValues): EditRepositoryEntry => { let branch = notEmpty(repo.branch); if (repo.sourceType !== RepositorySourceType.BRANCH_OR_TAG) { branch = undefined; } let commitId = notEmpty(repo.commitId); if (repo.sourceType !== RepositorySourceType.COMMIT_ID) { commitId = undefined; } return { id: repo.id, name: repo.name, url: repo.url, branch, commitId, path: repo.path, secretId: repo.secretId!, disabled: !repo.enabled, triggersDisabled: !repo.triggersEnabled, }; }; export default EditRepositoryActivity; ================================================ FILE: console2/src/components/organisms/EncryptValueActivity/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import copyToClipboard from 'copy-to-clipboard'; import * as React from 'react'; import { Form, Icon, Input, Message } from 'semantic-ui-react'; import { ConcordKey } from '../../../api/common'; import { encrypt } from '../../../api/org/project'; import './styles.css'; interface ExternalProps { orgName: ConcordKey; projectName: ConcordKey; } interface State { encrypting: boolean; result: any; success: boolean | undefined; data: string; dirty: boolean; } class EncryptValueActivity extends React.PureComponent { constructor(props: ExternalProps) { super(props); this.state = { encrypting: false, result: undefined, success: undefined, data: '', dirty: false }; } handleEncryptValue(value: string) { this.setState({ data: value }); if (value !== '') { this.setState({ dirty: true }); } else { this.setState({ dirty: false }); } } reset() { this.setState({ data: '', result: undefined, success: undefined }); } encryptValue(data: string) { this.setState({ encrypting: true, result: false }); encrypt(this.props.orgName, this.props.projectName, data) .then((responseData) => { this.setState({ encrypting: false, success: true, result: responseData }); }) .catch((error) => { this.setState({ encrypting: false, success: false, result: error.details }); }); } render() { const { result, success, encrypting, data, dirty } = this.state; return ( <>
this.handleEncryptValue(value)} /> { ev.preventDefault(); this.encryptValue(data); }} />

The encrypted value can be later decrypted in flows using{' '} {`\${crypto.decryptString("value")}`}{' '} expression.

The value is valid for the current project only.

{result && ( this.reset()}> {success ? 'Success' : 'Failure'}
{success ? (
(copyToClipboard as any)(result.data) } /> } fluid={true} value={result.data} className="encryptedValue" />
) : (
{result}
)}
)} ); } } export default EncryptValueActivity; ================================================ FILE: console2/src/components/organisms/EncryptValueActivity/styles.css ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ .codeSnippet { color: #B03060; background-color: #F0F0F0; font-family: monospace; } .encryptedValue input { font-family: monospace !important; } ================================================ FILE: console2/src/components/organisms/FindLdapGroupField/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { Search, SearchResultData, SearchResultProps } from 'semantic-ui-react'; import { RequestError } from '../../../api/common'; import { findLdapGroups as apiFindLdapGroups, LdapGroupSearchResult } from '../../../api/service/console'; import { useThrottle } from '../../../hooks/useThrottle'; interface Props { onSelect: (value: LdapGroupSearchResult) => void; onChange?: (value?: string) => void; placeholder?: string; } // TODO remove when the Search component will support custom result types const toResults = (items: LdapGroupSearchResult[]) => items.map((i) => ({ title: i.displayName, description: i.groupName })); // TODO remove when the Search component will support custom result types const resultToItem = (result: SearchResultProps, items: LdapGroupSearchResult[]) => items.find((i) => i.groupName === result.description)!; const FindLdapGroupField = ({ onSelect, onChange, placeholder }: Props) => { const [filter, setFilter] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(); const [items, setItems] = useState([]); const performSearch = useCallback(async (searchFilter: string) => { if (searchFilter.length < 5) { setItems([]); return; } try { setLoading(true); setError(undefined); const result = await apiFindLdapGroups(searchFilter); setItems(result || []); } catch (e) { setError(e); setItems([]); } finally { setLoading(false); } }, []); const throttledSearch = useThrottle(performSearch, 2000); useEffect(() => { throttledSearch(filter); }, [filter, throttledSearch]); const handleSelect = useCallback( ({ result }: SearchResultData) => { if (!result) { return; } const item = resultToItem(result, items); if (!item) { return; } onSelect(item); }, [items, onSelect] ); return ( { const newFilter = data.value || ''; setFilter(newFilter); if (onChange) { onChange(newFilter); } }} onResultSelect={(ev, data) => handleSelect(data)} results={toResults(items)} /> ); }; export default FindLdapGroupField; ================================================ FILE: console2/src/components/organisms/FindOrganizationsField/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { Search } from 'semantic-ui-react'; import { list as apiFindOrganizations, get as apiGet, OrganizationEntry } from '../../../api/org'; import { SearchProps } from 'semantic-ui-react/dist/commonjs/modules/Search/Search'; interface Props { defaultOrgName?: string; placeholder?: string; required?: boolean; onReset?: (value?: OrganizationEntry) => void; onClear?: () => void; onSelect?: (value: OrganizationEntry) => void; } interface Result { title: string; description: string; } const renderTitle = (e: OrganizationEntry) => `${e.name}`; const renderDescription = (e: OrganizationEntry): string => ''; export default ({ defaultOrgName, placeholder, required, onClear, onReset, onSelect }: Props) => { const [defaultItem, setDefaultItem] = useState(); const [value, setValue] = useState(); const [loading, setLoading] = useState(false); const [error, setError] = useState(); const [items, setItems] = useState([]); const [results, setResults] = useState([]); // perform search whenever the filter changes useEffect(() => { if (!value || value.trim().length < 3) { setResults([]); return; } const fetchData = async () => { setLoading(true); try { const result = await apiFindOrganizations(true, 0, 10, value); setItems(result.items); } catch (e) { setError(e); } finally { setLoading(false); } }; fetchData(); }, [value]); // convert OrganizationEntries into whatever accepts useEffect(() => { const r = items.map((i) => ({ key: i.id, title: renderTitle(i), description: renderDescription(i) })); setResults(r); }, [items]); // load the default organization's data useEffect(() => { if (!defaultOrgName) { setDefaultItem(undefined); return; } const fetchData = async () => { setLoading(true); try { const result = await apiGet(defaultOrgName); setValue(result ? renderTitle(result) : ''); setDefaultItem(result); } catch (e) { setError(e); } finally { setLoading(false); } }; fetchData(); }, [defaultOrgName]); const onChangeCallBack = useCallback( (event: React.MouseEvent, data: SearchProps) => { setValue(data.value); }, [] ); const handleItemSelected = useCallback( (item?: OrganizationEntry) => { setValue(item ? renderTitle(item) : ''); const isDefault = item?.id === defaultItem?.id; if (isDefault) { onReset?.(item); } else if (item) { onSelect?.(item); } else { if (required) { setValue(defaultItem ? renderTitle(defaultItem) : ''); onReset?.(defaultItem); } else { onClear?.(); } } }, [required, onReset, onSelect, onClear, defaultItem] ); return ( { if (data.value !== '') { const item = items.find((i) => i.name === data.value); handleItemSelected(item || defaultItem); } else { handleItemSelected(undefined); } }} showNoResults={!loading} onSearchChange={onChangeCallBack} onResultSelect={(ev, data) => { const item = items.find((i) => i.id === data.result.key); handleItemSelected(item || defaultItem); }} /> ); }; ================================================ FILE: console2/src/components/organisms/FindTeamDropdown/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { Dropdown, DropdownItemProps } from 'semantic-ui-react'; import { ConcordKey, RequestError } from '../../../api/common'; import { comparators } from '../../../utils'; import { list as apiList, TeamEntry } from '../../../api/org/team'; interface Props { onSelect: (item: TeamEntry) => void; orgName: ConcordKey; name: string; } const makeOptions = (data: TeamEntry[]): DropdownItemProps[] => { if (!data || data.length === 0) { return []; } return data.sort(comparators.byName).map(({ name, id }) => ({ value: id, text: name })); }; export default ({ orgName, name, onSelect, ...rest }: Props) => { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(); const handleChange = useCallback( (id: ConcordKey) => { const item = items.find((i) => i.id === id); if (!item) { return; } onSelect(item); }, [items, onSelect] ); useEffect(() => { const load = async () => { setLoading(true); setError(undefined); try { setItems(await apiList(orgName)); } catch (e) { setError(e); } finally { setLoading(false); } }; load(); }, [orgName]); return ( handleChange(value as ConcordKey)} /> ); }; ================================================ FILE: console2/src/components/organisms/FindUserField2/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2020 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { useEffect, useState } from 'react'; import { Search } from 'semantic-ui-react'; import { get as apiGet, list as apiList, UserEntry } from '../../../api/user'; import { ConcordId, RequestError } from '../../../api/common'; interface Props { defaultUserId?: ConcordId; placeholder?: string; onSelect?: (value: UserEntry) => void; } interface Result { title: string; description: string; } const renderDescription = (e: UserEntry): string => (e.email ? `${e.name} - ${e.email}` : e.name); const renderTitle = (e: UserEntry) => (e.displayName ? e.displayName : e.name); export default ({ defaultUserId, placeholder, onSelect }: Props) => { const [value, setValue] = useState(); const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [results, setResults] = useState([]); const [error, setError] = useState(); // perform search whenever the filter changes useEffect(() => { if (!value || value.trim().length < 3) { setResults([]); return; } const fetchData = async () => { setLoading(true); try { const result = await apiList(0, 10, value); setItems(result.items); } catch (e) { setError(e); } finally { setLoading(false); } }; fetchData(); }, [value]); // convert UserEntries into whatever accepts useEffect(() => { const r = items.map((i) => ({ key: i.id, title: renderTitle(i), description: renderDescription(i) })); setResults(r); }, [items]); // load the default user's data useEffect(() => { if (!defaultUserId) { return; } const fetchData = async () => { setLoading(true); try { const result = await apiGet(defaultUserId); setValue(result ? renderTitle(result) : undefined); } catch (e) { setError(e); } finally { setLoading(false); } }; fetchData(); }, [defaultUserId]); return ( setValue(data.value)} onResultSelect={(ev, data) => { setValue(data.result.title); if (onSelect) { const item = items.find((i) => i.id === data.result.key); if (item) { onSelect(item); } } }} /> ); }; ================================================ FILE: console2/src/components/organisms/Login2/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { RouteComponentProps, withRouter } from '@/router'; import { useCallback, useContext, useState } from 'react'; import { Card, CardContent, Dimmer, Divider, Form, Image, Loader, Message, } from 'semantic-ui-react'; import { whoami as apiWhoami } from '../../../api/service/console'; import { UserSessionContext } from '../../../session'; import './styles.css'; import { Link } from 'react-router'; import { parse as parseQueryString } from 'query-string'; const nonEmpty = (s?: string) => { if (!s) { return; } const v = s.trim(); if (v.length === 0) { return; } return v; }; const getLastLoginType = (): string | null => { return localStorage.getItem('lastLoginType'); }; const saveLastLoginType = (type: string) => { localStorage.setItem('lastLoginType', type); }; const clearLastLoginType = () => { localStorage.removeItem('lastLoginType'); }; const DEFAULT_FROM_VALUE = '/'; const getFrom = (props: RouteComponentProps<{}>): string => { const location = props.location as any; if (location && location.state && location.state.from && location.state.from.pathname) { return location.state.from.pathname; } const fromUrl = parseQueryString(props.location.search); if (fromUrl && typeof fromUrl.from === 'string') { return fromUrl.from; } return DEFAULT_FROM_VALUE; }; const getRedirectTo = (props: RouteComponentProps<{}>): string | undefined => { const qs = parseQueryString(props.location.search); const redirectTo = qs ? qs.redirectTo : undefined; if (typeof redirectTo === 'string') { return redirectTo; } }; const Login = (props: RouteComponentProps<{}>) => { const [apiError, setApiError] = useState(); const [validationError] = useState(); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [apiKey, setApiKey] = useState(''); const [rememberMe, setRememberMe] = useState(); const { loggingIn, setLoggingIn, setUserInfo } = useContext(UserSessionContext); const handleSubmit = useCallback(async () => { setLoggingIn(true); try { const response = await apiWhoami( nonEmpty(username), nonEmpty(password), rememberMe, nonEmpty(apiKey) ); setUserInfo({ ...response }); saveLastLoginType(nonEmpty(apiKey) ? 'apiKey' : 'username'); // with 'redirectTo' the user will be redirected to the specified href // the values can be arbitrary endpoints const redirectTo = getRedirectTo(props); // the 'from' query parameter value will be pushed to history // the values must be valid routes // e.g. from=/org will be turned into http.../#/org const from = getFrom(props); if (redirectTo) { setTimeout(() => { window.location.href = redirectTo; }, 100); } else { props.history.push(from); } } catch (e) { let msg = e.message || 'Log in error'; if (e.status === 401) { msg = 'Invalid username and/or password'; } setApiError(msg); } finally { setLoggingIn(false); } }, [username, password, rememberMe, apiKey, setLoggingIn, setUserInfo, props]); const onChangeLoginType = useCallback(() => { clearLastLoginType(); setApiKey(''); setUsername(''); setPassword(''); setRememberMe(false); }, []); const lastLoginType = getLastLoginType(); const useApiKey = props.location.search.search('useApiKey=true') >= 0 || lastLoginType === 'apiKey'; const usernameHint = (window.concord?.login || {}).usernameHint || 'Username'; return (
handleSubmit()} > {!useApiKey && ( <> setUsername(value)} /> setPassword(value)} /> )} {useApiKey && ( setApiKey(value)} /> )} setRememberMe(checked)} /> Login
{useApiKey && ( Login with Username and Password )} {!useApiKey && ( Login with API Key )}
); }; export default withRouter(Login); ================================================ FILE: console2/src/components/organisms/Login2/styles.css ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ #useApiKeyLink { font-size: smaller; text-align: right; width: 100%; } ================================================ FILE: console2/src/components/organisms/NewAPITokenActivity/index.tsx ================================================ /*- * ***** * Concord * ----- * Copyright (C) 2017 - 2018 Walmart Inc. * ----- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ===== */ import * as React from 'react'; import { useState } from 'react'; import { useHistory } from '@/router'; import { Button, Icon, Message } from 'semantic-ui-react'; import { RequestError } from '../../../api/common'; import { NewAPITokenForm, RequestErrorMessage, WithCopyToClipboard } from '../../molecules'; import { create as apiCreate, CreateApiKeyResult, NewTokenEntry, } from '../../../api/profile/api_token'; const renderResponse = (response: CreateApiKeyResult, done: () => void, error?: RequestError) => { if (error) { return ; } const { key } = response; return ( <> API Token created
Token: {key}

Store this token for future use.