package org.zalando.stups.fullstop.jobs.ami;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.DescribeImagesRequest;
import com.amazonaws.services.ec2.model.DescribeImagesResult;
import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.Filter;
import com.amazonaws.services.ec2.model.Image;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.Reservation;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.zalando.stups.fullstop.aws.ClientProvider;
import org.zalando.stups.fullstop.jobs.FullstopJob;
import org.zalando.stups.fullstop.jobs.common.AccountIdSupplier;
import org.zalando.stups.fullstop.jobs.common.FetchTaupageYaml;
import org.zalando.stups.fullstop.jobs.config.JobsProperties;
import org.zalando.stups.fullstop.taupage.TaupageYaml;
import org.zalando.stups.fullstop.violation.ViolationBuilder;
import org.zalando.stups.fullstop.violation.ViolationSink;
import org.zalando.stups.fullstop.violation.service.ViolationService;
import javax.annotation.PostConstruct;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static com.amazonaws.regions.Region.getRegion;
import static com.amazonaws.regions.Regions.fromName;
import static java.time.LocalDate.now;
import static java.time.format.DateTimeFormatter.ofPattern;
import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.trimToNull;
import static org.zalando.stups.fullstop.violation.ViolationType.OUTDATED_TAUPAGE;
@Component
public class FetchAmiJob implements FullstopJob {
static final String EVENT_ID = "checkAmiJob";
private final String taupageNamePrefix;
private final List<String> taupageOwners;
private final Logger log = LoggerFactory.getLogger(FetchAmiJob.class);
private final ViolationSink violationSink;
private final ClientProvider clientProvider;
private final AccountIdSupplier allAccountIds;
private final JobsProperties jobsProperties;
private final FetchTaupageYaml fetchTaupageYaml;
private final ViolationService violationService;
private static final Splitter TAUPAGE_NAME_SPLITTER = Splitter.on('-');
@Autowired
public FetchAmiJob(final ViolationSink violationSink,
final ClientProvider clientProvider,
final AccountIdSupplier allAccountIds,
final JobsProperties jobsProperties,
final ViolationService violationService,
final FetchTaupageYaml fetchTaupageYaml,
@Value("${FULLSTOP_TAUPAGE_NAME_PREFIX}") final String taupageNamePrefix,
@Value("${FULLSTOP_TAUPAGE_OWNERS}") final String taupageOwners) {
this.violationSink = violationSink;
this.clientProvider = clientProvider;
this.allAccountIds = allAccountIds;
this.jobsProperties = jobsProperties;
this.violationService = violationService;
this.taupageNamePrefix = taupageNamePrefix;
this.fetchTaupageYaml = fetchTaupageYaml;
this.taupageOwners = Stream.of(taupageOwners.split(",")).filter(s -> !s.isEmpty()).collect(toList());
}
@PostConstruct
public void init() {
log.info("{} initalized", getClass().getSimpleName());
}
@Scheduled(fixedRate = 60_000 * 60 * 4, initialDelay = -1) // ((1 min * 60) * 4) = 4 hours rate, 0 min delay
public void run() {
log.info("Running job {}", getClass().getSimpleName());
final List<String> regions = jobsProperties.getWhitelistedRegions();
for (final String account : allAccountIds.get()) {
for (final String region : regions) {
runOn(account, region);
}
}
}
private void runOn(final String account, final String region) {
try {
log.info("Scanning EC2 instances to fetch AMIs {}/{}", account, region);
final AmazonEC2Client ec2Client = clientProvider.getClient(
AmazonEC2Client.class,
account,
getRegion(fromName(region)));
Optional<String> nextToken = empty();
do {
final DescribeInstancesRequest request = new DescribeInstancesRequest();
if (nextToken.isPresent()) {
request.withNextToken(nextToken.get());
} else {
request.withFilters(new Filter("instance-state-name").withValues("running"));
}
final DescribeInstancesResult result = ec2Client.describeInstances(request);
nextToken = Optional.ofNullable(trimToNull(result.getNextToken()));
for (final Reservation reservation : result.getReservations()) {
for (final Instance instance : reservation.getInstances()) {
processInstance(ec2Client, account, region, instance);
}
}
} while (nextToken.isPresent());
} catch (final AmazonServiceException a) {
log.error(a.getMessage(), a);
}
}
private void processInstance(final AmazonEC2Client ec2Client, final String account, final String region, final Instance instance) {
if (violationService.violationExists(account, region, EVENT_ID, instance.getInstanceId(), OUTDATED_TAUPAGE)) {
return;
}
final Optional<Image> optionalImage = getAmiFromEC2Api(ec2Client, instance.getImageId());
final Optional<Boolean> isTaupageAmi = optionalImage
.filter(img -> img.getName().startsWith(taupageNamePrefix))
.map(Image::getOwnerId)
.map(taupageOwners::contains);
// will not check for all non taupage ami
// or images with taupage as name but created from another owner
if (!isTaupageAmi.orElse(false)) {
return;
}
final Image image = optionalImage.get();
final Optional<LocalDate> optionalExpirationDate = getExpirationDate(image);
if (optionalExpirationDate.isPresent()) {
final LocalDate expirationDate = optionalExpirationDate.get();
if (now().isAfter(expirationDate)) {
final Optional<TaupageYaml> taupageYaml = fetchTaupageYaml.getTaupageYaml(instance.getInstanceId(), account, region);
violationSink.put(new ViolationBuilder()
.withAccountId(account)
.withRegion(region)
.withPluginFullyQualifiedClassName(FetchAmiJob.class)
.withEventId(EVENT_ID)
.withType(OUTDATED_TAUPAGE)
.withInstanceId(instance.getInstanceId())
.withApplicationId(taupageYaml.map(TaupageYaml::getApplicationId).map(StringUtils::trimToNull).orElse(null))
.withApplicationVersion(taupageYaml.map(TaupageYaml::getApplicationVersion).map(StringUtils::trimToNull).orElse(null))
.withMetaInfo(ImmutableMap.of(
"ami_owner_id", image.getOwnerId(),
"ami_id", image.getImageId(),
"ami_name", image.getName(),
"expiration_date", expirationDate.toString()))
.build());
}
} else {
log.warn("Could not expiration date of taupage AMI {}", image);
}
}
private Optional<LocalDate> getExpirationDate(final Image image) {
// current implementation parse creation date from name + add 60 days support period
return Optional.ofNullable(image.getName())
.filter(name -> !name.isEmpty())
.map(TAUPAGE_NAME_SPLITTER::splitToList)
.filter(list -> list.size() == 4) // "Taupage-AMI-20160201-123456"
.map(parts -> parts.get(2))
.map(timestamp -> LocalDate.parse(timestamp, ofPattern("yyyyMMdd")))
.map(creationDate -> creationDate.plusDays(60));
}
private Optional<Image> getAmiFromEC2Api(final AmazonEC2Client ec2Client, final String imageId) {
try {
final DescribeImagesResult response = ec2Client.describeImages(new DescribeImagesRequest().withImageIds(imageId));
return ofNullable(response)
.map(DescribeImagesResult::getImages)
.map(List::stream)
.flatMap(Stream::findFirst);
} catch (final AmazonClientException e) {
log.warn("Could not describe image " + imageId, e);
return empty();
}
}
}