package com.atlassian.jira.cloud.jenkins.deploymentinfo.service; import com.atlassian.jira.cloud.jenkins.auth.AccessTokenRetriever; import com.atlassian.jira.cloud.jenkins.common.client.JiraApi; import com.atlassian.jira.cloud.jenkins.common.client.PostUpdateResult; import com.atlassian.jira.cloud.jenkins.common.config.JiraSiteConfigRetriever; import com.atlassian.jira.cloud.jenkins.common.model.AppCredential; import com.atlassian.jira.cloud.jenkins.common.response.JiraCommonResponse; import com.atlassian.jira.cloud.jenkins.common.response.JiraSendInfoResponse; import com.atlassian.jira.cloud.jenkins.common.service.IssueKeyExtractor; import com.atlassian.jira.cloud.jenkins.config.JiraCloudSiteConfig; import com.atlassian.jira.cloud.jenkins.deploymentinfo.client.DeploymentPayloadBuilder; import com.atlassian.jira.cloud.jenkins.deploymentinfo.client.model.Association; import com.atlassian.jira.cloud.jenkins.deploymentinfo.client.model.AssociationType; import com.atlassian.jira.cloud.jenkins.deploymentinfo.client.model.DeploymentApiResponse; import com.atlassian.jira.cloud.jenkins.deploymentinfo.client.model.Deployments; import com.atlassian.jira.cloud.jenkins.deploymentinfo.client.model.Environment; import com.atlassian.jira.cloud.jenkins.tenantinfo.CloudIdResolver; import com.atlassian.jira.cloud.jenkins.util.JenkinsToJiraStatus; import com.atlassian.jira.cloud.jenkins.util.RunWrapperProvider; import com.atlassian.jira.cloud.jenkins.util.SecretRetriever; import com.atlassian.jira.cloud.jenkins.util.StateValidator; import hudson.model.Result; import hudson.model.Run; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper; import javax.annotation.Nullable; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.Function; import static java.util.Objects.requireNonNull; /** * Implementation of JiraDeploymentInfoSender to send build updates to Jira by building the payload, * generating the access token, sending the request and parsing the response. */ public class JiraDeploymentInfoSenderImpl implements JiraDeploymentInfoSender { private static final String HTTPS_PROTOCOL = "https://"; // this code do the same thing as RunWrapper#getCurrentResult() private static final Function<WorkflowRun, String> getJenkinsBuildStatus = run -> Optional.ofNullable(run.getResult()) .map(Result::toString) .orElseGet(Result.SUCCESS::toString); private final JiraSiteConfigRetriever siteConfigRetriever; private final SecretRetriever secretRetriever; private final CloudIdResolver cloudIdResolver; private final AccessTokenRetriever accessTokenRetriever; private final JiraApi deploymentsApi; private final RunWrapperProvider runWrapperProvider; private final IssueKeyExtractor issueKeyExtractor; public JiraDeploymentInfoSenderImpl( final JiraSiteConfigRetriever siteConfigRetriever, final SecretRetriever secretRetriever, final CloudIdResolver cloudIdResolver, final AccessTokenRetriever accessTokenRetriever, final JiraApi jiraApi, final IssueKeyExtractor issueKeyExtractor, final RunWrapperProvider runWrapperProvider) { this.siteConfigRetriever = requireNonNull(siteConfigRetriever); this.secretRetriever = requireNonNull(secretRetriever); this.cloudIdResolver = requireNonNull(cloudIdResolver); this.accessTokenRetriever = requireNonNull(accessTokenRetriever); this.deploymentsApi = requireNonNull(jiraApi); this.runWrapperProvider = requireNonNull(runWrapperProvider); this.issueKeyExtractor = requireNonNull(issueKeyExtractor); } @Override public JiraSendInfoResponse sendDeploymentInfo(final JiraDeploymentInfoRequest request) { final String jiraSite = request.getSite(); final WorkflowRun deployment = request.getDeployment(); final Set<String> serviceIds = request.getServiceIds(); final Optional<JiraCloudSiteConfig> maybeSiteConfig = getSiteConfigFor(jiraSite); if (!maybeSiteConfig.isPresent()) { return JiraCommonResponse.failureSiteConfigNotFound(jiraSite); } final String resolvedSiteConfig = maybeSiteConfig.get().getSite(); final JiraCloudSiteConfig siteConfig = maybeSiteConfig.get(); final Optional<String> maybeSecret = getSecretFor(siteConfig.getCredentialsId()); if (!maybeSecret.isPresent()) { return JiraCommonResponse.failureSecretNotFound(resolvedSiteConfig); } final Environment environment = buildEnvironment(request); List<String> errorMessages = EnvironmentValidator.validate(environment); if (!errorMessages.isEmpty()) { return JiraDeploymentInfoResponse.failureEnvironmentInvalid(jiraSite, errorMessages); } final String deploymentState = getDeploymentState(deployment, request.getState()); errorMessages = StateValidator.validate(deploymentState); if (!errorMessages.isEmpty()) { return JiraDeploymentInfoResponse.failureStateInvalid(errorMessages); } final Set<String> issueKeys = issueKeyExtractor.extractIssueKeys(deployment); if (issueKeys.isEmpty() && serviceIds.isEmpty()) { return JiraDeploymentInfoResponse.skippedIssueKeysNotFoundAndServiceIdsAreEmpty(resolvedSiteConfig); } final Set<Association> associations = buildAssociations(issueKeys, serviceIds); final Optional<String> maybeCloudId = getCloudIdFor(resolvedSiteConfig); if (!maybeCloudId.isPresent()) { return JiraCommonResponse.failureSiteNotFound(resolvedSiteConfig); } final Optional<String> maybeAccessToken = getAccessTokenFor(siteConfig, maybeSecret.get()); if (!maybeAccessToken.isPresent()) { return JiraCommonResponse.failureAccessToken(resolvedSiteConfig); } final Deployments deploymentInfo = createJiraDeploymentInfo(deployment, environment, associations, deploymentState); final PostUpdateResult<DeploymentApiResponse> postUpdateResult = sendDeploymentInfo( maybeCloudId.get(), maybeAccessToken.get(), resolvedSiteConfig, deploymentInfo); if (postUpdateResult.getResponseEntity().isPresent()) { return handleDeploymentApiResponse( resolvedSiteConfig, postUpdateResult.getResponseEntity().get()); } else { final String errorMessage = postUpdateResult.getErrorMessage().orElse(""); return handleDeploymentApiError(resolvedSiteConfig, errorMessage); } } private Optional<JiraCloudSiteConfig> getSiteConfigFor(@Nullable final String jiraSite) { return siteConfigRetriever.getJiraSiteConfig(jiraSite); } private Optional<String> getCloudIdFor(final String jiraSite) { final String jiraSiteUrl = HTTPS_PROTOCOL + jiraSite; return cloudIdResolver.getCloudId(jiraSiteUrl); } private Optional<String> getAccessTokenFor( final JiraCloudSiteConfig siteConfig, final String secret) { final AppCredential appCredential = new AppCredential(siteConfig.getClientId(), secret); return accessTokenRetriever.getAccessToken(appCredential); } private Optional<String> getSecretFor(final String credentialsId) { return secretRetriever.getSecretFor(credentialsId); } private Deployments createJiraDeploymentInfo( final Run build, final Environment environment, final Set<Association> associations, final String state) { final RunWrapper buildWrapper = runWrapperProvider.getWrapper(build); return DeploymentPayloadBuilder.getDeploymentInfo(buildWrapper, environment, associations, state); } private PostUpdateResult<DeploymentApiResponse> sendDeploymentInfo( final String cloudId, final String accessToken, final String jiraSite, final Deployments deploymentInfo) { return deploymentsApi.postUpdate( cloudId, accessToken, jiraSite, deploymentInfo, DeploymentApiResponse.class); } private JiraSendInfoResponse handleDeploymentApiResponse( final String jiraSite, final DeploymentApiResponse response) { if (!response.getAcceptedDeployments().isEmpty()) { return JiraDeploymentInfoResponse.successDeploymentAccepted(jiraSite, response); } if (!response.getUnknownAssociations().isEmpty()) { return JiraDeploymentInfoResponse.failureUnknownAssociations(jiraSite, response); } if (!response.getRejectedDeployments().isEmpty()) { return JiraDeploymentInfoResponse.failureDeploymentRejected(jiraSite, response); } return JiraDeploymentInfoResponse.failureUnexpectedResponse(); } private JiraDeploymentInfoResponse handleDeploymentApiError( final String jiraSite, final String errorMessage) { return JiraDeploymentInfoResponse.failureDeploymentsApiResponse(jiraSite, errorMessage); } private Environment buildEnvironment(final JiraDeploymentInfoRequest request) { // JENKINS-59862: if environmentType parameter was not provided, we should fallback to // "unmapped" final String environmentType = StringUtils.isNotBlank(request.getEnvironmentType()) ? request.getEnvironmentType() : "unmapped"; return Environment.builder() .withId(request.getEnvironmentId()) .withDisplayName(request.getEnvironmentName()) .withType(environmentType) .build(); } /** * Gets deployment state, in case if user didn't pass state explicitly it will be extracted from * Jenkins build */ private String getDeploymentState(final WorkflowRun build, @Nullable final String state) { return Optional.ofNullable(state) .orElseGet(() -> JenkinsToJiraStatus.getStatus(getJenkinsBuildStatus.apply(build))); } private Set<Association> buildAssociations(final Set<String> issueKeys, final Set<String> serviceIds) { final HashSet<Association> associations = new HashSet<>(); if (!issueKeys.isEmpty()) { associations.add( Association.builder() .withAssociationType(AssociationType.ISSUE_KEYS) .withValues(issueKeys) .build()); } if (!serviceIds.isEmpty()) { associations.add( Association.builder() .withAssociationType(AssociationType.SERVICE_ID_OR_KEYS) .withValues(serviceIds) .build()); } return associations; } }