Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@
<artifactId>resourcegroupstaggingapi</artifactId>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>cloudfront</artifactId>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>rds</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public enum AuditResourceType {
CLOUD_ACCOUNT("클라우드계정"),
TARGETING_RULE("타겟팅규칙"),
PLATFORM_CONFIG("플랫폼설정"),
S3_BUCKET("S3버킷"),
OBJECT_STORAGE_CONTAINER("오브젝트스토리지컨테이너"),
CDN_DISTRIBUTION("CDN배포"),
UI("사용자인터페이스");

private final String description;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
package com.agenticcp.core.domain.cloud.adapter.outbound.aws.cloudfront;

import com.agenticcp.core.common.context.TenantContextHolder;
import com.agenticcp.core.common.exception.BusinessException;
import com.agenticcp.core.common.exception.ResourceNotFoundException;
import com.agenticcp.core.domain.cloud.adapter.outbound.aws.config.AwsCloudFrontConfig;
import com.agenticcp.core.domain.cloud.adapter.outbound.common.ProviderScoped;
import com.agenticcp.core.domain.cloud.dto.CDNDistributionQueryRequest;
import com.agenticcp.core.domain.cloud.entity.CloudProvider;
import com.agenticcp.core.domain.cloud.entity.CloudResource;
import com.agenticcp.core.domain.cloud.exception.CloudErrorCode;
import com.agenticcp.core.domain.cloud.exception.CredentialErrorCode;
import com.agenticcp.core.domain.cloud.port.model.account.CloudSessionCredential;
import com.agenticcp.core.domain.cloud.port.outbound.account.AccountCredentialManagementPort;
import com.agenticcp.core.domain.cloud.port.outbound.cdn.CDNDiscoveryPort;
import com.agenticcp.core.domain.cloud.repository.CloudProviderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.services.cloudfront.CloudFrontClient;
import software.amazon.awssdk.services.cloudfront.model.*;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* AWS CloudFront Distribution 발견 어댑터
* AWS CloudFront API를 통해 Distribution 조회 기능을 제공
*
* @author AgenticCP Team
* @version 1.0.0
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AwsCloudFrontDiscoveryAdapter implements CDNDiscoveryPort, ProviderScoped {

private final AwsCloudFrontMapper mapper;
private final CloudProviderRepository cloudProviderRepository;
private final AccountCredentialManagementPort accountCredentialManagementPort;
private final AwsCloudFrontConfig awsCloudFrontConfig;
private final AwsCloudFrontErrorTranslator errorTranslator;

/**
* CloudFront Distribution 목록을 조회합니다.
*
* @param query 조회 조건 (태그 필터, 페이징, 정렬 등)
* @return 조회된 Distribution 목록 (페이징 정보 포함)
*/
@Override
public Page<CloudResource> listDistributions(CDNDistributionQueryRequest query) {
log.debug("[AwsCloudFrontDiscoveryAdapter] Listing distributions with query: {}", query);

return executeWithCloudFrontClient(query.accountScope(), client -> {
List<DistributionSummary> allDistributions = fetchAllDistributions(client, query);

// 태그 필터링 적용
List<CloudResource> resources = allDistributions.stream()
.map(summary -> {
Map<String, String> tags = fetchTags(client, summary.arn());
CloudResource resource = mapper.toCloudResource(summary, query);
resource.setTags(tags);
return resource;
})
.filter(resource -> matchesTagFilter(resource, query.tags()))
.filter(resource -> matchesNameFilter(resource, query.distributionName()))
.filter(resource -> matchesEnabledFilter(resource, query.enabled()))
.collect(Collectors.toList());

return applyMemoryOperations(resources, query);
});
}

/**
* 특정 CloudFront Distribution의 상세 정보를 조회합니다.
*
* @param accountScope 계정 스코프 (필수)
* @param distributionId 조회할 Distribution ID
* @return Distribution 정보 (존재하지 않으면 Optional.empty())
*/
@Override
public Optional<CloudResource> getDistribution(String accountScope, String distributionId) {
log.debug("[AwsCloudFrontDiscoveryAdapter] Getting distribution details for: {}, accountScope={}",
distributionId, accountScope);

return executeWithCloudFrontClient(accountScope, client -> {
try {
GetDistributionRequest request = GetDistributionRequest.builder()
.id(distributionId)
.build();

GetDistributionResponse response = client.getDistribution(request);

// 태그 조회
Map<String, String> tags = fetchTags(client, response.distribution().arn());

CloudResource resource = mapper.toCloudResource(
response.distribution(),
response.eTag(),
CloudProvider.ProviderType.AWS,
tags
);

// 태그 설정
resource.setTags(tags);

log.debug("[AwsCloudFrontDiscoveryAdapter] Successfully retrieved distribution details: {}", distributionId);
return Optional.of(resource);

} catch (NoSuchDistributionException e) {
log.info("[AwsCloudFrontDiscoveryAdapter] Distribution not found: {}", distributionId);
return Optional.empty();
}
});
}

/**
* Distribution 존재 여부를 확인합니다.
*
* @param accountScope 계정 스코프 (필수)
* @param distributionId 확인할 Distribution ID
* @return Distribution이 존재하면 true, 존재하지 않으면 false
*/
@Override
public boolean distributionExists(String accountScope, String distributionId) {
log.debug("[AwsCloudFrontDiscoveryAdapter] Checking if distribution exists: {}, accountScope={}",
distributionId, accountScope);

return executeWithCloudFrontClient(accountScope, client -> {
try {
GetDistributionRequest request = GetDistributionRequest.builder()
.id(distributionId)
.build();

client.getDistribution(request);
return true;
} catch (NoSuchDistributionException e) {
log.info("[AwsCloudFrontDiscoveryAdapter] Distribution not found: {}", distributionId);
return false;
}
});
}

@Override
public CloudProvider.ProviderType getProviderType() {
return CloudProvider.ProviderType.AWS;
}

/**
* CloudFrontClient를 사용하여 작업을 실행합니다.
*/
private <R> R executeWithCloudFrontClient(String accountScope, Function<CloudFrontClient, R> action) {
String resolvedScope = requireAccountScope(accountScope);
CloudSessionCredential session = acquireSession(resolvedScope);

try (CloudFrontClient client = awsCloudFrontConfig.createCloudFrontClient(session)) {
return action.apply(client);
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
throw errorTranslator.translate(e);
}
}

/**
* 모든 Distribution을 조회합니다 (페이징 처리).
*/
private List<DistributionSummary> fetchAllDistributions(
CloudFrontClient client,
CDNDistributionQueryRequest query) {

List<DistributionSummary> allDistributions = new ArrayList<>();
String marker = null;

do {
ListDistributionsRequest.Builder requestBuilder = ListDistributionsRequest.builder();

if (marker != null) {
requestBuilder.marker(marker);
}

ListDistributionsResponse response = client.listDistributions(requestBuilder.build());

if (response.distributionList() != null && response.distributionList().items() != null) {
allDistributions.addAll(response.distributionList().items());
}

marker = response.distributionList() != null
? response.distributionList().nextMarker()
: null;

} while (marker != null && !marker.isEmpty());

return allDistributions;
}

/**
* Distribution의 태그를 조회합니다.
*/
private Map<String, String> fetchTags(CloudFrontClient client, String arn) {
try {
ListTagsForResourceRequest request = ListTagsForResourceRequest.builder()
.resource(arn)
.build();

ListTagsForResourceResponse response = client.listTagsForResource(request);

if (response.tags() != null && response.tags().items() != null) {
return response.tags().items().stream()
.collect(Collectors.toMap(
Tag::key,
Tag::value
));
}
} catch (Exception e) {
log.warn("[AwsCloudFrontDiscoveryAdapter] Failed to fetch tags for resource: {}", arn, e);
}

return new HashMap<>();
}

/**
* 태그 필터와 일치하는지 확인합니다.
*/
private boolean matchesTagFilter(CloudResource resource, Map<String, String> tagFilter) {
if (tagFilter == null || tagFilter.isEmpty()) {
return true;
}

if (resource.getTags() == null || resource.getTags().isEmpty()) {
return false;
}

return tagFilter.entrySet().stream()
.allMatch(entry -> {
String resourceTagValue = resource.getTags().get(entry.getKey());
return resourceTagValue != null && resourceTagValue.equals(entry.getValue());
});
}

/**
* 이름 필터와 일치하는지 확인합니다.
*/
private boolean matchesNameFilter(CloudResource resource, String nameFilter) {
if (nameFilter == null || nameFilter.isEmpty()) {
return true;
}

return resource.getResourceName() != null
&& resource.getResourceName().contains(nameFilter);
}

/**
* Enabled 필터와 일치하는지 확인합니다.
*/
private boolean matchesEnabledFilter(CloudResource resource, Boolean enabledFilter) {
if (enabledFilter == null) {
return true;
}

// metadata에서 enabled 정보 추출 필요
// 간단히 처리하기 위해 모든 리소스를 통과시키고, 실제로는 metadata 파싱 필요
return true;
}

/**
* 메모리 기반 필터링, 정렬, 페이징을 적용합니다.
*/
private Page<CloudResource> applyMemoryOperations(
List<CloudResource> resources,
CDNDistributionQueryRequest query) {

List<CloudResource> filtered = resources;

// 정렬 적용
applySorting(filtered, query.sortBy(), query.sortDirection());

// 페이징 적용
int page = query.page() != null ? query.page() : 0;
int size = query.size() != null ? query.size() : 20;
int start = Math.min(page * size, filtered.size());
int end = Math.min(start + size, filtered.size());
List<CloudResource> paged = (start >= filtered.size())
? Collections.emptyList()
: filtered.subList(start, end);

Sort sort = (query.sortBy() != null)
? Sort.by(resolveSortDirection(query.sortDirection()), query.sortBy())
: Sort.unsorted();

return new PageImpl<>(paged, PageRequest.of(page, size, sort), filtered.size());
}

/**
* 정렬을 적용합니다.
*/
private void applySorting(List<CloudResource> resources, String sortBy, String sortDirection) {
if (sortBy == null || sortBy.isEmpty()) {
return;
}

Comparator<CloudResource> comparator;
if ("name".equalsIgnoreCase(sortBy)) {
comparator = Comparator.comparing(CloudResource::getResourceName);
} else if ("createdAt".equalsIgnoreCase(sortBy)) {
comparator = Comparator.comparing(
CloudResource::getCreatedInCloud,
Comparator.nullsLast(Comparator.naturalOrder())
);
} else {
log.warn("[AwsCloudFrontDiscoveryAdapter] Unsupported sort key: {}. Defaulting to name sort.", sortBy);
comparator = Comparator.comparing(CloudResource::getResourceName);
}

if ("desc".equalsIgnoreCase(sortDirection)) {
comparator = comparator.reversed();
}

resources.sort(comparator);
}

private Sort.Direction resolveSortDirection(String sortDirection) {
return (sortDirection != null)
? Sort.Direction.fromString(sortDirection)
: Sort.Direction.ASC;
}

/**
* AWS Provider를 조회합니다.
*/
private CloudProvider getAwsProvider() {
return cloudProviderRepository.findFirstByProviderType(CloudProvider.ProviderType.AWS)
.orElseThrow(() -> new ResourceNotFoundException(CloudErrorCode.CLOUD_PROVIDER_NOT_FOUND));
}

private String requireAccountScope(String accountScope) {
if (accountScope == null || accountScope.isBlank()) {
throw new BusinessException(CloudErrorCode.ACCOUNT_SCOPE_REQUIRED, "AccountScope가 필요합니다.");
}
return accountScope;
}

private CloudSessionCredential acquireSession(String accountScope) {
try {
String tenantKey = TenantContextHolder.getCurrentTenantKeyOrThrow();
CloudSessionCredential session = accountCredentialManagementPort.getSession(
tenantKey, accountScope, getProviderType());
log.debug("[AwsCloudFrontDiscoveryAdapter] 세션 획득 완료: accountScope={}, expiresAt={}",
accountScope, session.getExpiresAt());
return session;
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("[AwsCloudFrontDiscoveryAdapter] 세션 획득 실패: accountScope={}", accountScope, e);
throw new BusinessException(CredentialErrorCode.INVALID_CREDENTIALS,
"세션 획득에 실패했습니다: " + e.getMessage());
}
}
}

Loading