All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.spinnaker.echo.jira.JiraNotificationService Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2018 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.netflix.spinnaker.echo.jira;

import static net.logstash.logback.argument.StructuredArguments.kv;

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.netflix.spinnaker.echo.api.Notification;
import com.netflix.spinnaker.echo.controller.EchoResponse;
import com.netflix.spinnaker.echo.jira.JiraService.CommentIssueRequest;
import com.netflix.spinnaker.echo.jira.JiraService.CreateIssueRequest;
import com.netflix.spinnaker.echo.jira.JiraService.CreateIssueResponse;
import com.netflix.spinnaker.echo.jira.JiraService.IssueTransitions;
import com.netflix.spinnaker.echo.jira.JiraService.TransitionIssueRequest;
import com.netflix.spinnaker.echo.notification.NotificationService;
import com.netflix.spinnaker.kork.core.RetrySupport;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseStatus;
import retrofit.RetrofitError;
import retrofit.client.Response;

@Component
@ConditionalOnProperty("jira.enabled")
public class JiraNotificationService implements NotificationService {
  private static final Logger LOGGER = LoggerFactory.getLogger(JiraNotificationService.class);
  private static final int MAX_RETRY = 3;
  private static final long RETRY_BACKOFF = 100;

  private final JiraService jiraService;
  private final RetrySupport retrySupport;
  private final ObjectMapper mapper;

  @Autowired
  public JiraNotificationService(
      JiraService jiraService, RetrySupport retrySupport, ObjectMapper objectMapper) {
    this.jiraService = jiraService;
    this.retrySupport = retrySupport;
    this.mapper = objectMapper;
  }

  @Override
  public boolean supportsType(String type) {
    return "JIRA".equalsIgnoreCase(type);
  }

  @Override
  public EchoResponse handle(Notification notification) {
    return isTransition(notification) ? transitionIssue(notification) : create(notification);
  }

  private boolean isTransition(Notification notification) {
    return notification.getAdditionalContext().get("transitionContext") != null;
  }

  private EchoResponse.Void transitionIssue(Notification notification) {
    TransitionJiraNotification transitionNotification =
        mapper.convertValue(notification.getAdditionalContext(), TransitionJiraNotification.class);
    String jiraIssue = transitionNotification.getJiraIssue();

    try {
      // transitionContext is the full Jira transition API payload (which is stored in
      // transitionDetails) - except the transition ID is probably unknown.  So, we get the
      // transition ID from the transition name.
      Map transition =
          transitionNotification.getTransitionContext().getTransition();
      Map transitionDetails =
          transitionNotification.getTransitionContext().getTransitionDetails();
      String transitionName = transition.get("name");

      IssueTransitions issueTransitions =
          retrySupport.retry(getIssueTransitions(jiraIssue), MAX_RETRY, RETRY_BACKOFF, false);

      issueTransitions.getTransitions().stream()
          .filter(it -> it.getName().equals(transitionName))
          .findFirst()
          .ifPresentOrElse(
              t -> {
                transition.put("id", t.getId());
                transitionDetails.put("transition", transition);
              },
              () -> {
                throw new IllegalArgumentException(
                    ImmutableMap.of(
                            "issue", jiraIssue,
                            "transitionName", transitionName,
                            "validTransitionNames",
                                issueTransitions.getTransitions().stream()
                                    .map(IssueTransitions.Transition::getName)
                                    .collect(Collectors.toList()))
                        .toString());
              });

      retrySupport.retry(
          transitionIssue(jiraIssue, transitionDetails), MAX_RETRY, RETRY_BACKOFF, false);

      if (transitionNotification.getComment() != null) {
        retrySupport.retry(
            addComment(jiraIssue, transitionNotification.getComment()),
            MAX_RETRY,
            RETRY_BACKOFF,
            false);
      }

      return new EchoResponse.Void();
    } catch (Exception e) {
      throw new TransitionJiraIssueException(
          String.format("Failed to transition Jira issue %s: %s", jiraIssue, errors(e)), e);
    }
  }

  private EchoResponse create(Notification notification) {
    Map issueRequestBody = issueRequestBody(notification);
    try {
      CreateIssueResponse response =
          retrySupport.retry(createIssue(issueRequestBody), MAX_RETRY, RETRY_BACKOFF, false);

      return new EchoResponse<>(response);
    } catch (Exception e) {
      throw new CreateJiraIssueException(
          String.format(
              "Failed to create Jira Issue %s: %s",
              kv("issueRequestBody", issueRequestBody), errors(e)),
          e);
    }
  }

  private Supplier getIssueTransitions(String issueIdOrKey) {
    return () -> jiraService.getIssueTransitions(issueIdOrKey);
  }

  private Supplier transitionIssue(
      String issueIdOrKey, Map transitionDetails) {
    return () ->
        jiraService.transitionIssue(issueIdOrKey, new TransitionIssueRequest(transitionDetails));
  }

  private Supplier addComment(String issueIdOrKey, String comment) {
    return () -> jiraService.addComment(issueIdOrKey, new CommentIssueRequest(comment));
  }

  private Supplier createIssue(Map issueRequestBody) {
    return () -> jiraService.createIssue(new CreateIssueRequest(issueRequestBody));
  }

  private Map issueRequestBody(Notification notification) {
    Map issue =
        (Map) notification.getAdditionalContext().get("issueContext");
    // Move up additional details to main level
    // details contains undefined fields in orca strongly typed request
    // it allows the flexibility of arbitrary fields in the request
    Optional.ofNullable((Map) issue.get("details"))
        .ifPresent(i -> i.forEach(issue::put));
    issue.remove("details");
    return issue;
  }

  private Map errors(Exception exception) {
    if (exception instanceof RetrofitError) {
      try {
        return mapper.readValue(
            ((RetrofitError) exception).getResponse().getBody().in(), Map.class);
      } catch (Exception e) {
        LOGGER.warn("failed retrieving error messages {}", e.getMessage());
      }
    }

    return ImmutableMap.of("errors", exception.getMessage());
  }

  @ResponseStatus(value = HttpStatus.BAD_REQUEST)
  static class CreateJiraIssueException extends RuntimeException {
    public CreateJiraIssueException(String message, Throwable cause) {
      super(message, cause);
    }
  }

  @ResponseStatus(value = HttpStatus.BAD_REQUEST)
  static class TransitionJiraIssueException extends RuntimeException {
    public TransitionJiraIssueException(String message, Throwable cause) {
      super(message, cause);
    }
  }

  static class TransitionJiraNotification {
    private String jiraIssue;
    private String comment;
    private TransitionContext transitionContext;

    public String getJiraIssue() {
      return jiraIssue;
    }

    public void setJiraIssue(String jiraIssue) {
      this.jiraIssue = jiraIssue;
    }

    public String getComment() {
      return comment;
    }

    public void setComment(String comment) {
      this.comment = comment;
    }

    public TransitionContext getTransitionContext() {
      return transitionContext;
    }

    public void setTransitionContext(TransitionContext transitionContext) {
      this.transitionContext = transitionContext;
    }

    static class TransitionContext {
      private Map transition;

      // placeholder for all the other remaining transition context payload
      private Map transitionDetails = new HashMap<>();

      public Map getTransition() {
        return transition;
      }

      public void setTransition(Map transition) {
        this.transition = transition;
      }

      public Map getTransitionDetails() {
        return transitionDetails;
      }

      @JsonAnySetter
      public void setTransitionDetails(String name, Object value) {
        this.transitionDetails.put(name, value);
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy