Compare commits

...

10 Commits

61 changed files with 1227 additions and 723 deletions

4
Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM amazoncorretto:17.0.6-al2023
EXPOSE 8081
ADD ./build/libs/*.jar device-deployer.jar
ENTRYPOINT ["java", "-jar", "/device-deployer.jar"]

97
README.md Normal file
View File

@ -0,0 +1,97 @@
# [BlokWorks] Device Deployer Service #
사용자에게 Command 를 입력받아 MQTT 를 발행하고 배포된 앱의 정보를 저장하고, 조회하는 Spring boot 기반 Java 애플리케이션입니다.
## 기반 기술 ##
---
* Java 17
* Spring Boot 3.1.3
* Gradle 8.2.1
## 주요 의존성 ##
---
* MQTT
* PostgreSQL
## 빌드 ##
---
### 프로젝트 패키징 ###
Gradle을 사용하여 패키징합니다.
```shell
$ ./gradlew clean build -x test
```
### 도커 빌드 ###
```dockerfile
FROM amazoncorretto:17.0.6-al2023
EXPOSE 8081
ADD ./build/libs/*.jar device-deployer.jar
ENTRYPOINT ["java", "-jar", "/device-deployer.jar"]
```
Image Registry로 사용할 AWS ECR에 로그인을 합니다.
```shell
$ aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 003960268191.dkr.ecr.ap-northeast-2.amazonaws.com
```
패키징된 jar 파일을 Docker image로 빌드합니다.
```shell
$ docker build --platform linux/amd64 -t device-deployer:0.0.7-SNAPSHOT .
$ docker tag sdt-cloud/device-deployer:0.0.7-SNAPSHOT 003960268191.dkr.ecr.ap-northeast-2.amazonaws.com/sdt-cloud/device-deployer:{{version}}
```
### 이미지 업로드 ###
빌드 및 태깅된 Docker image를 Image Registry에 업로드 합니다.
```shell
$ docker push 003960268191.dkr.ecr.ap-northeast-2.amazonaws.com/sdt-cloud/device-deployer:{{version}}
```
## 구동 ##
---
### 사전 구동 애플리케이션 및 모듈 ###
1. MQTT 컨테이너 모듈
- 사용자의 Command 를 Edge(Device)에게 전달하기 위한 Message Queue 입니다.
2. PostgreSQL 컨테이너 모듈
- 실행된 Command 의 정보를 저장하고 조회할 수 있는 RDB 입니다.
### 설정 정보 ###
설정 정보 등록은 설정 관리 애플리케이션의 POST /configuration API 를 이용합니다.
| 옵션 | 설명 | 필수 여부 | 기본값 |
|:--------------------------:|:-----------------------------------:|:-----:|:---:|
| server.port | 서버 포트 | O | - |
| inbound.mqtt.url | MQTT 모듈의 url | O | - |
| inbound.mqtt.username | MQTT 모듈의 username | O | - |
| inbound.mqtt.password | MQTT 모듈의 password | O | - |
| inbound.mqtt.topics | MQTT 모듈이 구독할 Inference 결과 데이터 topic | O | - |
| spring.datasource.uri | PostgreSQL 모듈의 연결을 위한 URI | O | - |
| spring.datasource.username | PostgreSQL 모듈의 username | O | - |
| spring.datasource.pssword | PostgreSQL 모듈의 password | O | - |
### 배포 ###
배포는 k8s manifest로 작성한 deploy.yaml을 이용합니다.
Deployment 오브젝트에 반드시 명시해야 하는 설정 정보(`env`)는 아래와 같습니다.
| 옵션 | 설명 | 필수 여부 | 예시 |
|:-----------------------------:|:-------------------------------------------------:|:-----:|:-----------------------------------------------------------------------------------------------------------------------------------------:|
| SPRING_PROFILES_ACTIVE | 구동 profile | O | k8s |
| SERVER_PORT | 애플리케이션이 등록될 포트 | O | 8085 |
| POSTGRES_CREDENTIALS_URL | PostgreSQL의 URL | O | jdbc:postgresql://localhost:5432/blokworks |
| POSTGRES_CREDENTIALS_USERNAME | PostgreSQL의 계정 | O | sdt |
| POSTGRES_CREDENTIALS_PASSWORD | PostgreSQL의 비밀번호 | O | 2xxxxx |
| IAM_AMQP_HOST | RabbitMQ의 URL | O | rabbitmq.sdt-cloud.svc.cluster.local |
| IAM_AMQP_PORT | RabbitMQ의 포트 | O | 5672 |
| IAM_AMQP_USERNAME | RabbitMQ에 접속할 계정 | O | guest |
| IAM_AMQP_PASSWORD | RabbitMQ에 접속할 비밀번호 | O | guest |

View File

@ -15,6 +15,10 @@ jar {
enabled = false enabled = false
} }
ext {
jjwtVersion = "0.11.5"
}
repositories { repositories {
mavenCentral() mavenCentral()
} }
@ -26,6 +30,9 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-amqp") implementation("org.springframework.boot:spring-boot-starter-amqp")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.integration:spring-integration-mqtt") implementation("org.springframework.integration:spring-integration-mqtt")
implementation("io.jsonwebtoken:jjwt-api:${jjwtVersion}")
runtimeOnly("io.jsonwebtoken:jjwt-impl:${jjwtVersion}")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:${jjwtVersion}")
runtimeOnly("org.postgresql:postgresql") runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"

View File

@ -1,7 +1,11 @@
package inc.sdt.blokworks.devicedeployer; package inc.sdt.blokworks.devicedeployer;
import inc.sdt.blokworks.devicedeployer.presentation.rest.RestTemplateResponseErrorHandler;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication @SpringBootApplication
public class DeviceDeployerApplication { public class DeviceDeployerApplication {
@ -10,4 +14,11 @@ public class DeviceDeployerApplication {
SpringApplication.run(DeviceDeployerApplication.class, args); SpringApplication.run(DeviceDeployerApplication.class, args);
} }
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.errorHandler(new RestTemplateResponseErrorHandler())
.build();
}
} }

View File

@ -0,0 +1,16 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
@Component
public class BashCommand implements CommandInfo{
@Override
public LinkedHashMap<String, Object> put(OutboundMessage message) {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("cmd", message.getCommand());
return map;
}
}

View File

@ -0,0 +1,9 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import java.util.LinkedHashMap;
public interface CommandInfo {
LinkedHashMap<String, Object> put(OutboundMessage message);
}

View File

@ -0,0 +1,50 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
@Component
public class CommandInvoker {
private final BashCommand bashCommand;
private final SystemdCommand systemdCommand;
private final DockerCommand dockerCommand;
private final JsonCommand jsonCommand;
private final DeployCommandInvoker deployCommandInvoker;
public CommandInvoker(BashCommand bashCommand,
SystemdCommand systemdCommand,
DockerCommand dockerCommand,
JsonCommand jsonCommand,
DeployCommandInvoker deployCommandInvoker) {
this.bashCommand = bashCommand;
this.systemdCommand = systemdCommand;
this.dockerCommand = dockerCommand;
this.jsonCommand = jsonCommand;
this.deployCommandInvoker = deployCommandInvoker;
}
public LinkedHashMap<String, Object> invoke(OutboundMessage message) {
if(message.getCommandType() == null) {
throw new IllegalArgumentException();
}
switch (message.getCommandType()) {
case bash -> {
return bashCommand.put(message);
}
case systemd -> {
return systemdCommand.put(message);
}
case docker -> {
return dockerCommand.put(message);
}
case deploy -> {
return deployCommandInvoker.invoke(message);
}
default -> {
return jsonCommand.put(message);
}
}
}
}

View File

@ -2,13 +2,9 @@ package inc.sdt.blokworks.devicedeployer.application;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import inc.sdt.blokworks.devicedeployer.domain.AssetApp; import inc.sdt.blokworks.devicedeployer.domain.*;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import inc.sdt.blokworks.devicedeployer.domain.OperationType;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundDeployMessagePayload; import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundDeployMessagePayload;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.OutboundMessagePayload; import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.OutboundMessagePayload;
import inc.sdt.blokworks.devicedeployer.presentation.exception.ConflictException;
import jakarta.servlet.http.HttpSession;
import org.eclipse.paho.client.mqttv3.IMqttClient; import org.eclipse.paho.client.mqttv3.IMqttClient;
import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage; import org.eclipse.paho.client.mqttv3.MqttMessage;
@ -18,59 +14,67 @@ import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.util.Optional;
@Service @Service
public class DefaultDeployerService implements DeployerService{ public class DefaultDeployerService implements DeployerService{
private final Logger log; private final Logger log;
private final IMqttClient mqttClient; private final IMqttClient mqttClient;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final DeployerRepositoryDelegate deployerRepositoryDelegate; private final DeployerRepositoryDelegate deployerRepositoryDelegate;
private String requestId; private final DeployRequestRepositoryDelegate requestRepositoryDelegate;
public DefaultDeployerService(IMqttClient mqttClient, public DefaultDeployerService(IMqttClient mqttClient,
ObjectMapper objectMapper, ObjectMapper objectMapper,
DeployerRepositoryDelegate deployerRepositoryDelegate) { DeployerRepositoryDelegate deployerRepositoryDelegate,
DeployRequestRepositoryDelegate requestRepositoryDelegate) {
this.log = LoggerFactory.getLogger(this.getClass()); this.log = LoggerFactory.getLogger(this.getClass());
this.mqttClient = mqttClient; this.mqttClient = mqttClient;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.deployerRepositoryDelegate = deployerRepositoryDelegate; this.deployerRepositoryDelegate = deployerRepositoryDelegate;
this.requestRepositoryDelegate = requestRepositoryDelegate;
} }
@Override @Override
public void publish(OutboundMessage deployMessage, String assetCode) { public void publish(OutboundMessage outboundMessage) {
log.info("[publish]"); log.info("[publish] outboundMessage = {}", outboundMessage);
try { try {
OutboundMessagePayload payload = new OutboundMessagePayload( OutboundMessagePayload payload = new OutboundMessagePayload(
deployMessage.getUrl(), outboundMessage.getCommandInfo(),
deployMessage.getName(), outboundMessage.getCommandType(),
deployMessage.getCommand(), outboundMessage.getSubCommandType(),
deployMessage.getEnv(), outboundMessage.getAssetCode(),
OperationType.DEPLOY, outboundMessage.getRequestId()
deployMessage.getRequestId()
); );
byte[] bytes = objectMapper.writeValueAsBytes(payload); byte[] bytes = objectMapper.writeValueAsBytes(payload);
MqttMessage message = new MqttMessage(); MqttMessage message = new MqttMessage();
message.setPayload(bytes); message.setPayload(bytes);
mqttClient.publish("/assets/"+assetCode+"/apps/deploy", message); mqttClient.publish("/device-control/"+outboundMessage.getAssetCode(), message);
requestId = deployMessage.getRequestId(); log.info("[publish] payload = {}", payload);
log.info("[publish] message = {}", message);
}catch (JsonProcessingException | MqttException e) { }catch (JsonProcessingException | MqttException e) {
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
} }
@Override
public DeployRequest save(DeployRequest deployRequest) {
log.info("[save] deployRequest = {}", deployRequest);
requestRepositoryDelegate.save(deployRequest);
return deployRequest;
}
@Override @Override
public Mono<Void> apply(InboundDeployMessagePayload payload) { public Mono<Void> apply(InboundDeployMessagePayload payload) {
log.info("[apply] inboundDeployMessagePayload = {}", payload); log.info("[apply] payload = {}", payload);
// 배포된 앱 정보 저장 Optional<DeployRequest> deployRequest = requestRepositoryDelegate.findByRequestId(payload.requestId());
// request Id 판별 if(deployRequest.isPresent()) {
if(requestId.equals(payload.requestId())) {
return Mono.just(payload) return Mono.just(payload)
.doOnNext(deployerRepositoryDelegate) .doOnNext(deployerRepositoryDelegate)
.then(); .then();
}else { }else {
throw new ConflictException("This process is already exists."); throw new IllegalArgumentException();
} }
} }

View File

@ -1,37 +0,0 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundProcessMessagePayload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
public class DefaultProcessService implements ProcessService{
private final Logger log;
private final ProcessRepositoryDelegate processRepositoryDelegate;
public DefaultProcessService(ProcessRepositoryDelegate processRepositoryDelegate) {
this.log = LoggerFactory.getLogger(this.getClass());
this.processRepositoryDelegate = processRepositoryDelegate;
}
@Override
public Mono<Void> apply(InboundProcessMessagePayload inboundProcessMessagePayload) {
log.info("[apply] inboundProcessMessagePayload = {}", inboundProcessMessagePayload);
// 프로세스 저장
return Mono.just(inboundProcessMessagePayload)
.doOnNext(processRepositoryDelegate)
.then();
}
@Override
public Page<Process> getAll(String assetCode, int page, int size) {
log.info("[getAll] assetCode = {}, page = {}, size = {}", assetCode, page, size);
return processRepositoryDelegate.findAllByAssetCode(assetCode, page, size);
}
}

View File

@ -0,0 +1,21 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
@Component
public class DeployCommand implements CommandInfo {
@Override
public LinkedHashMap<String, Object> put(OutboundMessage message) {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("cmd", message.getCommand());
map.put("appName", message.getAppName());
map.put("name", message.getName());
map.put("fileUrl", message.getUrl());
map.put("fileType", message.getFileType());
return map;
}
}

View File

@ -0,0 +1,37 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
@Component
public class DeployCommandInvoker {
private final DockerCommand dockerCommand;
private final DeployCommand deployCommand;
private final JsonCommand jsonCommand;
public DeployCommandInvoker(DockerCommand dockerCommand, DeployCommand deployCommand, JsonCommand jsonCommand) {
this.dockerCommand = dockerCommand;
this.deployCommand = deployCommand;
this.jsonCommand = jsonCommand;
}
public LinkedHashMap<String, Object> invoke(OutboundMessage message) {
if(message.getSubCommandType() == null) {
throw new IllegalArgumentException();
}
switch (message.getSubCommandType()) {
case systemd -> {
return deployCommand.put(message);
}
case docker -> {
return dockerCommand.put(message);
}
default -> {
return jsonCommand.put(message);
}
}
}
}

View File

@ -0,0 +1,10 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.DeployRequest;
import java.util.Optional;
public interface DeployRequestRepositoryDelegate {
DeployRequest save(DeployRequest deployRequest);
Optional<DeployRequest> findByRequestId(String requestId);
}

View File

@ -1,6 +1,7 @@
package inc.sdt.blokworks.devicedeployer.application; package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.AssetApp; import inc.sdt.blokworks.devicedeployer.domain.AssetApp;
import inc.sdt.blokworks.devicedeployer.domain.DeployRequest;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage; import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundDeployMessagePayload; import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundDeployMessagePayload;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@ -9,6 +10,7 @@ import reactor.core.publisher.Mono;
import java.util.function.Function; import java.util.function.Function;
public interface DeployerService extends Function<InboundDeployMessagePayload, Mono<Void>> { public interface DeployerService extends Function<InboundDeployMessagePayload, Mono<Void>> {
void publish(OutboundMessage assetApp, String assetCode); void publish(OutboundMessage message);
DeployRequest save(DeployRequest deployRequest);
Page<AssetApp> getAll(String assetCode, int page, int size); Page<AssetApp> getAll(String assetCode, int page, int size);
} }

View File

@ -0,0 +1,21 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
@Component
public class DockerCommand implements CommandInfo {
@Override
public LinkedHashMap<String, Object> put(OutboundMessage message) {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("cmd", message.getCommand());
map.put("appName", message.getAppName());
map.put("name", message.getName()); // container 이름
map.put("image", message.getImage());
map.put("options", message.getOptions());
return map;
}
}

View File

@ -0,0 +1,19 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
@Component
public class JsonCommand implements CommandInfo {
@Override
public LinkedHashMap<String, Object> put(OutboundMessage message) {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("cmd", String.valueOf(message.getCommandType()));
map.put("appName", message.getAppName());
map.put("filename", message.getName());
map.put("parameter", message.getParameters());
return map;
}
}

View File

@ -1,12 +0,0 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundProcessMessagePayload;
import org.springframework.data.domain.Page;
import java.util.function.Consumer;
public interface ProcessRepositoryDelegate extends Consumer<InboundProcessMessagePayload> {
Process save(Process process);
Page<Process> findAllByAssetCode(String assetCode, int page, int size);
}

View File

@ -1,12 +0,0 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundProcessMessagePayload;
import org.springframework.data.domain.Page;
import reactor.core.publisher.Mono;
import java.util.function.Function;
public interface ProcessService extends Function<InboundProcessMessagePayload, Mono<Void>> {
Page<Process> getAll(String assetCode, int page, int size);
}

View File

@ -0,0 +1,17 @@
package inc.sdt.blokworks.devicedeployer.application;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
@Component
public class SystemdCommand implements CommandInfo {
@Override
public LinkedHashMap<String, Object> put(OutboundMessage message) {
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("cmd", message.getCommand());
map.put("service", message.getAppName());
return map;
}
}

View File

@ -3,18 +3,24 @@ package inc.sdt.blokworks.devicedeployer.domain;
public class AssetApp { public class AssetApp {
private String assetCode; private String assetCode;
private String name; private String name;
private Long size; private int size;
private int pid;
private String message;
private Long releaseAt; private Long releaseAt;
private Long updatedAt; private Long updatedAt;
private Status status;
protected AssetApp() {} protected AssetApp() {}
public AssetApp(String assetCode, String name, Long size, Long releaseAt, Long updatedAt) { public AssetApp(String assetCode, String name, int size, int pid, String message, Long releaseAt, Long updatedAt, Status status) {
this.assetCode = assetCode; this.assetCode = assetCode;
this.name = name; this.name = name;
this.size = size; this.size = size;
this.pid = pid;
this.message = message;
this.releaseAt = releaseAt; this.releaseAt = releaseAt;
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
this.status = status;
} }
public String getAssetCode() { public String getAssetCode() {
@ -25,10 +31,18 @@ public class AssetApp {
return name; return name;
} }
public Long getSize() { public int getSize() {
return size; return size;
} }
public int getPid() {
return pid;
}
public String getMessage() {
return message;
}
public Long getReleaseAt() { public Long getReleaseAt() {
return releaseAt; return releaseAt;
} }
@ -37,14 +51,21 @@ public class AssetApp {
return updatedAt; return updatedAt;
} }
public Status getStatus() {
return status;
}
@Override @Override
public String toString() { public String toString() {
return "AssetApp{" + return "AssetApp{" +
"assetCode='" + assetCode + '\'' + "assetCode='" + assetCode + '\'' +
", name='" + name + '\'' + ", name='" + name + '\'' +
", size=" + size + ", size=" + size +
", pid=" + pid +
", message='" + message + '\'' +
", releaseAt=" + releaseAt + ", releaseAt=" + releaseAt +
", updatedAt=" + updatedAt + ", updatedAt=" + updatedAt +
", status=" + status +
'}'; '}';
} }
@ -55,9 +76,12 @@ public class AssetApp {
public static final class Builder { public static final class Builder {
private String assetCode; private String assetCode;
private String name; private String name;
private Long size; private int size;
private int pid;
private String message;
private Long releasedAt; private Long releasedAt;
private Long updatedAt; private Long updatedAt;
private Status status;
public Builder assetCode(String assetCode) { public Builder assetCode(String assetCode) {
this.assetCode = assetCode; this.assetCode = assetCode;
@ -69,11 +93,21 @@ public class AssetApp {
return this; return this;
} }
public Builder size(Long size) { public Builder size(int size) {
this.size = size; this.size = size;
return this; return this;
} }
public Builder pid(int pid) {
this.pid = pid;
return this;
}
public Builder message(String message) {
this.message = message;
return this;
}
public Builder releasedAt(Long releasedAt) { public Builder releasedAt(Long releasedAt) {
this.releasedAt = releasedAt; this.releasedAt = releasedAt;
return this; return this;
@ -84,13 +118,21 @@ public class AssetApp {
return this; return this;
} }
public Builder status(Status status) {
this.status = status;
return this;
}
public AssetApp build() { public AssetApp build() {
AssetApp assetApp = new AssetApp(); AssetApp assetApp = new AssetApp();
assetApp.assetCode = this.assetCode; assetApp.assetCode = this.assetCode;
assetApp.name = this.name; assetApp.name = this.name;
assetApp.size = this.size; assetApp.size = this.size;
assetApp.pid = this.pid;
assetApp.message = this.message;
assetApp.releaseAt = this.releasedAt; assetApp.releaseAt = this.releasedAt;
assetApp.updatedAt = this.updatedAt; assetApp.updatedAt = this.updatedAt;
assetApp.status = this.status;
return assetApp; return assetApp;
} }
} }

View File

@ -0,0 +1,9 @@
package inc.sdt.blokworks.devicedeployer.domain;
public enum CommandType {
bash,
systemd,
docker,
deploy,
json
}

View File

@ -0,0 +1,111 @@
package inc.sdt.blokworks.devicedeployer.domain;
public class DeployRequest {
private String requestId;
private String assetCode;
private String appName;
private OperationType operationType;
private CommandType commandType;
private SubCommandType subCommandType;
protected DeployRequest() {}
public DeployRequest(String requestId, String assetCode, String appName, OperationType operationType, CommandType commandType, SubCommandType subCommandType) {
this.requestId = requestId;
this.assetCode = assetCode;
this.appName = appName;
this.operationType = operationType;
this.commandType = commandType;
this.subCommandType = subCommandType;
}
public String getRequestId() {
return requestId;
}
public String getAssetCode() {
return assetCode;
}
public String getAppName() {
return appName;
}
public OperationType getOperationType() {
return operationType;
}
public CommandType getCommandType() {
return commandType;
}
public SubCommandType getSubCommandType() {
return subCommandType;
}
@Override
public String toString() {
return "DeployRequest{" +
"requestId='" + requestId + '\'' +
", assetCode='" + assetCode + '\'' +
", appName='" + appName + '\'' +
", operationType=" + operationType +
", commandType=" + commandType +
", subCommandType=" + subCommandType +
'}';
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private String requestId;
private String assetCode;
private String appName;
private OperationType operationType;
private CommandType commandType;
private SubCommandType subCommandType;
public Builder requestId(String requestId) {
this.requestId = requestId;
return this;
}
public Builder assetCode(String assetCode) {
this.assetCode = assetCode;
return this;
}
public Builder appName(String appName) {
this.appName = appName;
return this;
}
public Builder operationType(OperationType operationType) {
this.operationType = operationType;
return this;
}
public Builder commandType(CommandType commandType) {
this.commandType = commandType;
return this;
}
public Builder subCommandType(SubCommandType subCommandType) {
this.subCommandType = subCommandType;
return this;
}
public DeployRequest build() {
DeployRequest deployRequest = new DeployRequest();
deployRequest.requestId = this.requestId;
deployRequest.assetCode = this.assetCode;
deployRequest.appName = this.appName;
deployRequest.operationType = this.operationType;
deployRequest.commandType = this.commandType;
deployRequest.subCommandType = this.subCommandType;
return deployRequest;
}
}
}

View File

@ -0,0 +1,6 @@
package inc.sdt.blokworks.devicedeployer.domain;
public enum DeviceType {
ecn,
nodeq
}

View File

@ -1,15 +1,21 @@
package inc.sdt.blokworks.devicedeployer.domain; package inc.sdt.blokworks.devicedeployer.domain;
import java.util.HashMap; import java.util.LinkedHashMap;
import java.util.Set;
public class OutboundMessage { public class OutboundMessage {
private String url; private String url;
private String name; private String fileType;
private HashMap<String, String> env; private String assetCode;
private String appName; // 사용자가 정한 파일 이름
private String name; // stackbase 에 저장된 파일 이름
private String image;
private LinkedHashMap<String, Object> options;
private String command; private String command;
private OperationType operationType;
private String requestId; private String requestId;
private CommandType commandType;
private SubCommandType subCommandType;
private LinkedHashMap<String, String> parameters;
private LinkedHashMap<String, Object> commandInfo;
protected OutboundMessage() {} protected OutboundMessage() {}
@ -17,22 +23,38 @@ public class OutboundMessage {
return url; return url;
} }
public String getFileType() {
return fileType;
}
public String getAssetCode() {
return assetCode;
}
public void setAssetCode(String assetCode) {
this.assetCode = assetCode;
}
public String getAppName() {
return appName;
}
public String getName() { public String getName() {
return name; return name;
} }
public HashMap<String, String> getEnv() { public String getImage() {
return env; return image;
}
public LinkedHashMap<String, Object> getOptions() {
return options;
} }
public String getCommand() { public String getCommand() {
return command; return command;
} }
public OperationType getOperationType() {
return operationType;
}
public void setRequestId(String requestId) { public void setRequestId(String requestId) {
this.requestId = requestId; this.requestId = requestId;
} }
@ -41,15 +63,42 @@ public class OutboundMessage {
return requestId; return requestId;
} }
public CommandType getCommandType() {
return commandType;
}
public SubCommandType getSubCommandType() {
return subCommandType;
}
public LinkedHashMap<String, String> getParameters() {
return parameters;
}
public LinkedHashMap<String, Object> getCommandInfo() {
return commandInfo;
}
public void setCommandInfo(LinkedHashMap<String, Object> commandInfo) {
this.commandInfo = commandInfo;
}
@Override @Override
public String toString() { public String toString() {
return "AssetApp{" + return "OutboundMessage{" +
"url='" + url + '\'' + "url='" + url + '\'' +
", fileType='" + fileType + '\'' +
", assetCode='" + assetCode + '\'' +
", appName='" + appName + '\'' +
", name='" + name + '\'' + ", name='" + name + '\'' +
", env=" + env + ", image='" + image + '\'' +
", options=" + options +
", command='" + command + '\'' + ", command='" + command + '\'' +
", operationType=" + operationType +
", requestId='" + requestId + '\'' + ", requestId='" + requestId + '\'' +
", commandType=" + commandType +
", subCommandType=" + subCommandType +
", parameters=" + parameters +
", commandInfo=" + commandInfo +
'}'; '}';
} }
@ -59,24 +108,51 @@ public class OutboundMessage {
public static final class Builder { public static final class Builder {
private String url; private String url;
private String fileType;
private String assetCode;
private String appName;
private String name; private String name;
private HashMap<String, String> env; private String image;
private LinkedHashMap<String, Object> options;
private String command; private String command;
private OperationType operationType;
private String requestId; private String requestId;
private CommandType commandType;
private SubCommandType subCommandType;
private LinkedHashMap<String, String> parameters;
private LinkedHashMap<String, Object> commandInfo;
public Builder url(String url) { public Builder url(String url) {
this.url = url; this.url = url;
return this; return this;
} }
public Builder fileType(String fileType) {
this.fileType = fileType;
return this;
}
public Builder assetCode(String assetCode) {
this.assetCode = assetCode;
return this;
}
public Builder appName(String appName) {
this.appName = appName;
return this;
}
public Builder name(String name) { public Builder name(String name) {
this.name = name; this.name = name;
return this; return this;
} }
public Builder env(HashMap<String, String> env) { public Builder image(String image) {
this.env = env; this.image = image;
return this;
}
public Builder options(LinkedHashMap<String, Object> options) {
this.options = options;
return this; return this;
} }
@ -85,24 +161,46 @@ public class OutboundMessage {
return this; return this;
} }
public Builder operationType(OperationType operationType) { public Builder requestId(String requestId) {
this.operationType = operationType; this.requestId = requestId;
return this; return this;
} }
public Builder requestId(String requestId) { public Builder commandType(CommandType commandType) {
this.requestId = requestId; this.commandType = commandType;
return this;
}
public Builder subCommandType(SubCommandType subCommandType) {
this.subCommandType = subCommandType;
return this;
}
public Builder parameters(LinkedHashMap<String, String> parameters) {
this.parameters = parameters;
return this;
}
public Builder commandInfo(LinkedHashMap<String, Object> commandInfo) {
this.commandInfo = commandInfo;
return this; return this;
} }
public OutboundMessage build() { public OutboundMessage build() {
OutboundMessage deployMessage = new OutboundMessage(); OutboundMessage deployMessage = new OutboundMessage();
deployMessage.url = this.url; deployMessage.url = this.url;
deployMessage.fileType = this.fileType;
deployMessage.assetCode = this.assetCode;
deployMessage.appName = this.appName;
deployMessage.name = this.name; deployMessage.name = this.name;
deployMessage.env = this.env; deployMessage.image = this.image;
deployMessage.options = this.options;
deployMessage.command = this.command; deployMessage.command = this.command;
deployMessage.operationType = this.operationType;
deployMessage.requestId = this.requestId; deployMessage.requestId = this.requestId;
deployMessage.commandType = this.commandType;
deployMessage.subCommandType = this.subCommandType;
deployMessage.parameters = this.parameters;
deployMessage.commandInfo = this.commandInfo;
return deployMessage; return deployMessage;
} }
} }

View File

@ -1,63 +0,0 @@
package inc.sdt.blokworks.devicedeployer.domain;
public class Port {
private String protocol;
private Integer hostPort;
private Integer containerPort;
protected Port() {}
public String getProtocol() {
return protocol;
}
public Integer getHostPort() {
return hostPort;
}
public Integer getContainerPort() {
return containerPort;
}
@Override
public String toString() {
return "Port{" +
"protocol='" + protocol + '\'' +
", hostPort=" + hostPort +
", containerPort=" + containerPort +
'}';
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private String protocol;
private Integer hostPort;
private Integer containerPort;
public Builder protocol(String protocol) {
this.protocol = protocol;
return this;
}
public Builder hostPort(Integer hostPort) {
this.hostPort = hostPort;
return this;
}
public Builder containerPort(Integer containerPort) {
this.containerPort = containerPort;
return this;
}
public Port build() {
Port port = new Port();
port.protocol = this.protocol;
port.hostPort = this.hostPort;
port.containerPort = this.containerPort;
return port;
}
}
}

View File

@ -1,139 +0,0 @@
package inc.sdt.blokworks.devicedeployer.domain;
public class Process {
private String assetCode;
private Integer pid;
private String name;
private Integer cpu;
private Integer memory;
private Integer network;
private Long processedAt;
private OperationType operationType;
protected Process() {}
public Process(String assetCode, Integer pid, String name, Integer cpu, Integer memory, Integer network, Long processedAt, OperationType operationType) {
this.assetCode = assetCode;
this.pid = pid;
this.name = name;
this.cpu = cpu;
this.memory = memory;
this.network = network;
this.processedAt = processedAt;
this.operationType = operationType;
}
public String getAssetCode() {
return assetCode;
}
public Integer getPid() {
return pid;
}
public String getName() {
return name;
}
public Integer getCpu() {
return cpu;
}
public Integer getMemory() {
return memory;
}
public Integer getNetwork() {
return network;
}
public Long getProcessedAt() {
return processedAt;
}
public OperationType getOperationType() {
return operationType;
}
@Override
public String toString() {
return "Process{" +
"assetCode='" + assetCode + '\'' +
", pid=" + pid +
", name='" + name + '\'' +
", cpu=" + cpu +
", memory=" + memory +
", network=" + network +
", processedAt=" + processedAt +
", operationType=" + operationType +
'}';
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private String assetCode;
private Integer pid;
private String name;
private Integer cpu;
private Integer memory;
private Integer network;
private Long processedAt;
private OperationType operationType;
public Builder assetCode(String assetCode) {
this.assetCode = assetCode;
return this;
}
public Builder pid(Integer pid) {
this.pid = pid;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder cpu(Integer cpu) {
this.cpu = cpu;
return this;
}
public Builder memory(Integer memory) {
this.memory = memory;
return this;
}
public Builder network(Integer network) {
this.network = network;
return this;
}
public Builder processedAt(Long processedAt) {
this.processedAt = processedAt;
return this;
}
public Builder operationType(OperationType operationType) {
this.operationType = operationType;
return this;
}
public Process build() {
Process process = new Process();
process.assetCode = this.assetCode;
process.pid = this.pid;
process.name = this.name;
process.cpu = this.cpu;
process.memory = this.memory;
process.network = this.network;
process.processedAt = this.processedAt;
process.operationType = this.operationType;
return process;
}
}
}

View File

@ -0,0 +1,10 @@
package inc.sdt.blokworks.devicedeployer.domain;
public record Result(
String name,
int pid,
int size,
String message,
long releasedAt,
long updatedAt
){}

View File

@ -1,6 +1,7 @@
package inc.sdt.blokworks.devicedeployer.domain; package inc.sdt.blokworks.devicedeployer.domain;
public enum Status { public record Status(
success, int succeed,
fail int statusCode,
} String errMsg
){}

View File

@ -0,0 +1,7 @@
package inc.sdt.blokworks.devicedeployer.domain;
public enum SubCommandType {
systemd,
docker,
single
}

View File

@ -1,14 +1,13 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.mqtt; package inc.sdt.blokworks.devicedeployer.infrastructure.mqtt;
import inc.sdt.blokworks.devicedeployer.domain.Result;
import inc.sdt.blokworks.devicedeployer.domain.Status; import inc.sdt.blokworks.devicedeployer.domain.Status;
public record InboundDeployMessagePayload( public record InboundDeployMessagePayload(
Status status,
String assetCode, String assetCode,
String name, String deviceType,
Long size, Status status,
Long releasedAt, Result result,
Long updatedAt,
String requestId String requestId
) { ) {
} }

View File

@ -1,15 +0,0 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.mqtt;
import inc.sdt.blokworks.devicedeployer.domain.Status;
public record InboundProcessMessagePayload(
Status status,
String assetCode,
Integer pid,
String name,
Integer cpu,
Integer memory,
Integer network,
Long processedAt
) {
}

View File

@ -1,19 +1,15 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.mqtt; package inc.sdt.blokworks.devicedeployer.infrastructure.mqtt;
import inc.sdt.blokworks.devicedeployer.domain.OperationType; import inc.sdt.blokworks.devicedeployer.domain.CommandType;
import inc.sdt.blokworks.devicedeployer.domain.Port; import inc.sdt.blokworks.devicedeployer.domain.SubCommandType;
import inc.sdt.blokworks.devicedeployer.presentation.PortResource;
import inc.sdt.blokworks.devicedeployer.presentation.PortResourceConverter;
import java.util.HashMap; import java.util.HashMap;
import java.util.Set;
public record OutboundMessagePayload( public record OutboundMessagePayload(
String url, HashMap<String, Object> cmdInfo,
String name, CommandType cmdType,
String command, SubCommandType subCmdType,
HashMap<String, String> env, String assetCode,
OperationType operationType,
String requestId String requestId
) { ) {
} }

View File

@ -2,8 +2,6 @@ package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.sql.Timestamp;
@Entity(name = "asset_app") @Entity(name = "asset_app")
class AssetAppEntity { class AssetAppEntity {
@Id @Id
@ -15,20 +13,35 @@ class AssetAppEntity {
@Column(name = "app_name", length = 255) @Column(name = "app_name", length = 255)
private String name; private String name;
@Column(name = "size") @Column(name = "size")
private Long size; private int size;
@Column(name = "released_at") @Column(name = "released_at")
private Long releasedAt; private Long releasedAt;
@Column(name = "updated_at") @Column(name = "updated_at")
private Long updatedAt; private Long updatedAt;
@Column(name = "pid")
private int pid;
@Column(name = "succeed")
private int succeed;
@Column(name = "statusCode")
private int statusCode;
@Column(name = "message", columnDefinition = "TEXT")
private String message;
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
protected AssetAppEntity() {} protected AssetAppEntity() {}
public AssetAppEntity(String assetCode, String name, long size, Long releasedAt, Long updatedAt) { public AssetAppEntity(String assetCode, String name, int size, Long releasedAt, Long updatedAt, int pid, int succeed, int statusCode, String message, String errorMessage) {
this.assetCode = assetCode; this.assetCode = assetCode;
this.name = name; this.name = name;
this.size = size; this.size = size;
this.releasedAt = releasedAt; this.releasedAt = releasedAt;
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
this.pid = pid;
this.succeed = succeed;
this.statusCode = statusCode;
this.message = message;
this.errorMessage = errorMessage;
} }
public String getId() { public String getId() {
@ -43,7 +56,7 @@ class AssetAppEntity {
return name; return name;
} }
public long getSize() { public int getSize() {
return size; return size;
} }
@ -55,5 +68,23 @@ class AssetAppEntity {
return updatedAt; return updatedAt;
} }
public int getPid() {
return pid;
}
public int getSucceed() {
return succeed;
}
public int getStatusCode() {
return statusCode;
}
public String getMessage() {
return message;
}
public String getErrorMessage() {
return errorMessage;
}
} }

View File

@ -2,6 +2,7 @@ package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import inc.sdt.blokworks.devicedeployer.application.DeployerRepositoryDelegate; import inc.sdt.blokworks.devicedeployer.application.DeployerRepositoryDelegate;
import inc.sdt.blokworks.devicedeployer.domain.AssetApp; import inc.sdt.blokworks.devicedeployer.domain.AssetApp;
import inc.sdt.blokworks.devicedeployer.domain.Status;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundDeployMessagePayload; import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundDeployMessagePayload;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -23,6 +24,7 @@ public class AssetAppRelationalRepository implements DeployerRepositoryDelegate
@Override @Override
public void accept(InboundDeployMessagePayload inboundDeployMessagePayload) { public void accept(InboundDeployMessagePayload inboundDeployMessagePayload) {
log.info("[accept] payload = {}", inboundDeployMessagePayload);
this.save(this.fromMessage(inboundDeployMessagePayload)); this.save(this.fromMessage(inboundDeployMessagePayload));
} }
@ -49,7 +51,12 @@ public class AssetAppRelationalRepository implements DeployerRepositoryDelegate
assetApp.getName(), assetApp.getName(),
assetApp.getSize(), assetApp.getSize(),
assetApp.getReleaseAt(), assetApp.getReleaseAt(),
assetApp.getUpdatedAt() assetApp.getUpdatedAt(),
assetApp.getPid(),
assetApp.getStatus().succeed(),
assetApp.getStatus().statusCode(),
assetApp.getMessage(),
assetApp.getStatus().errMsg()
); );
} }
@ -58,18 +65,24 @@ public class AssetAppRelationalRepository implements DeployerRepositoryDelegate
.assetCode(entity.getAssetCode()) .assetCode(entity.getAssetCode())
.name(entity.getName()) .name(entity.getName())
.size(entity.getSize()) .size(entity.getSize())
.pid(entity.getPid())
.message(entity.getMessage())
.releasedAt(entity.getReleasedAt()) .releasedAt(entity.getReleasedAt())
.updatedAt(entity.getUpdatedAt()) .updatedAt(entity.getUpdatedAt())
.status(new Status(entity.getSucceed(), entity.getStatusCode(), entity.getErrorMessage()))
.build(); .build();
} }
private AssetApp fromMessage(InboundDeployMessagePayload payload) { private AssetApp fromMessage(InboundDeployMessagePayload payload) {
return AssetApp.builder() return AssetApp.builder()
.assetCode(payload.assetCode()) .assetCode(payload.assetCode())
.name(payload.name()) .name(payload.result().name())
.size(payload.size()) .size(payload.result().size())
.releasedAt(payload.releasedAt()) .pid(payload.result().pid())
.updatedAt(payload.updatedAt()) .message(payload.result().message())
.releasedAt(payload.result().releasedAt())
.updatedAt(payload.result().updatedAt())
.status(payload.status())
.build(); .build();
} }
} }

View File

@ -0,0 +1,68 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import inc.sdt.blokworks.devicedeployer.domain.CommandType;
import inc.sdt.blokworks.devicedeployer.domain.OperationType;
import inc.sdt.blokworks.devicedeployer.domain.SubCommandType;
import jakarta.persistence.*;
@Entity(name = "deploy_request")
public class DeployRequestEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "id")
private String id;
@Column(name = "request_id")
private String requestId;
@Column(name = "asset_code", length = 255)
private String assetCode;
@Column(name = "app_name", length = 255)
private String appName;
@Enumerated(EnumType.STRING)
@Column(name = "operation_type", length = 255)
private OperationType operationType;
@Enumerated(EnumType.STRING)
@Column(name = "command_type", length = 255)
private CommandType commandType;
@Enumerated(EnumType.STRING)
@Column(name = "sub_command_type", length = 255)
private SubCommandType subCommandType;
protected DeployRequestEntity() {}
public DeployRequestEntity(String requestId, String assetCode, String appName, OperationType operationType, CommandType commandType, SubCommandType subCommandType) {
this.requestId = requestId;
this.assetCode = assetCode;
this.appName = appName;
this.operationType = operationType;
this.commandType = commandType;
this.subCommandType = subCommandType;
}
public String getId() {
return id;
}
public String getRequestId() {
return requestId;
}
public String getAssetCode() {
return assetCode;
}
public String getAppName() {
return appName;
}
public OperationType getOperationType() {
return operationType;
}
public CommandType getCommandType() {
return commandType;
}
public SubCommandType getSubCommandType() {
return subCommandType;
}
}

View File

@ -0,0 +1,7 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import org.springframework.data.jpa.repository.JpaRepository;
public interface DeployRequestJpaRepository extends JpaRepository<DeployRequestEntity, String> {
DeployRequestEntity findByRequestId(String requestId);
}

View File

@ -0,0 +1,57 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import inc.sdt.blokworks.devicedeployer.application.DeployRequestRepositoryDelegate;
import inc.sdt.blokworks.devicedeployer.domain.DeployRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class DeployRequestRelationalRepository implements DeployRequestRepositoryDelegate {
private final Logger log;
private final DeployRequestJpaRepository deployRequestJpaRepository;
public DeployRequestRelationalRepository(DeployRequestJpaRepository deployRequestJpaRepository) {
this.log = LoggerFactory.getLogger(this.getClass());
this.deployRequestJpaRepository = deployRequestJpaRepository;
}
@Override
public DeployRequest save(DeployRequest deployRequest) {
log.info("[save] deployRequest = {}", deployRequest);
DeployRequestEntity entity = this.toEntity(deployRequest);
deployRequestJpaRepository.save(entity);
return deployRequest;
}
@Override
public Optional<DeployRequest> findByRequestId(String requestId) {
log.info("[findByRequestId] requestId = {}", requestId);
DeployRequestEntity entity = deployRequestJpaRepository.findByRequestId(requestId);
return Optional.of(this.fromEntity(entity));
}
private DeployRequestEntity toEntity(DeployRequest deployRequest) {
return new DeployRequestEntity(
deployRequest.getRequestId(),
deployRequest.getAssetCode(),
deployRequest.getAppName(),
deployRequest.getOperationType(),
deployRequest.getCommandType(),
deployRequest.getSubCommandType()
);
}
private DeployRequest fromEntity(DeployRequestEntity entity) {
return DeployRequest.builder()
.requestId(entity.getRequestId())
.assetCode(entity.getAssetCode())
.appName(entity.getAppName())
.operationType(entity.getOperationType())
.commandType(entity.getCommandType())
.subCommandType(entity.getSubCommandType())
.build();
}
}

View File

@ -1,69 +0,0 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import jakarta.persistence.*;
@Entity(name = "app_process")
class ProcessEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "process_id", length = 36)
private String id;
@Column(name = "asset_code", length = 255)
private String assetCode;
@Column(name = "pid")
private Integer pid;
@Column(name = "app_name", length = 255)
private String appName;
@Column(name = "cpu")
private Integer cpu;
@Column(name = "memory")
private Integer memory;
@Column(name = "network")
private Integer network;
@Column(name = "processed_at")
private Long processedAt;
protected ProcessEntity() {}
public ProcessEntity(String assetCode, Integer pid, String appName, Integer cpu, Integer memory, Integer network, Long processedAt) {
this.assetCode = assetCode;
this.pid = pid;
this.appName = appName;
this.cpu = cpu;
this.memory = memory;
this.network = network;
this.processedAt = processedAt;
}
public String getId() {
return id;
}
public String getAssetCode() {
return assetCode;
}
public Integer getPid() {
return pid;
}
public String getAppName() {
return appName;
}
public Integer getCpu() {
return cpu;
}
public Integer getMemory() {
return memory;
}
public Integer getNetwork() {
return network;
}
public Long getProcessedAt() {
return processedAt;
}
}

View File

@ -1,9 +0,0 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProcessJpaRepository extends JpaRepository<ProcessEntity, String> {
Page<ProcessEntity> findAllByAssetCode(String assetCode, Pageable pageable);
}

View File

@ -1,80 +0,0 @@
package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import inc.sdt.blokworks.devicedeployer.application.ProcessRepositoryDelegate;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundProcessMessagePayload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
@Component
public class ProcessRelationalRepository implements ProcessRepositoryDelegate {
private final Logger log;
private final ProcessJpaRepository processJpaRepository;
public ProcessRelationalRepository(ProcessJpaRepository processJpaRepository) {
this.log = LoggerFactory.getLogger(this.getClass());
this.processJpaRepository = processJpaRepository;
}
@Override
public void accept(InboundProcessMessagePayload payload) {
this.save(this.fromMessage(payload));
}
@Override
public Process save(Process process) {
log.info("[save] process = {}", process);
ProcessEntity entity = this.toEntity(process);
processJpaRepository.save(entity);
return process;
}
@Override
public Page<Process> findAllByAssetCode(String assetCode, int page, int size) {
log.info("[findAllByAssetCode] assetCode = {}", assetCode);
Pageable pageable = page < 0 ? Pageable.unpaged() : PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "processedAt"));
return processJpaRepository.findAllByAssetCode(assetCode, pageable)
.map(this::fromEntity);
}
private ProcessEntity toEntity(Process process) {
return new ProcessEntity(
process.getAssetCode(),
process.getPid(),
process.getName(),
process.getCpu(),
process.getMemory(),
process.getNetwork(),
process.getProcessedAt()
);
}
private Process fromEntity(ProcessEntity entity) {
return Process.builder()
.assetCode(entity.getAssetCode())
.pid(entity.getPid())
.name(entity.getAppName())
.cpu(entity.getCpu())
.memory(entity.getMemory())
.network(entity.getNetwork())
.processedAt(entity.getProcessedAt())
.build();
}
private Process fromMessage(InboundProcessMessagePayload payload) {
return Process.builder()
.assetCode(payload.assetCode())
.pid(payload.pid())
.name(payload.name())
.cpu(payload.cpu())
.memory(payload.memory())
.network(payload.network())
.processedAt(payload.processedAt())
.build();
}
}

View File

@ -1,10 +1,15 @@
package inc.sdt.blokworks.devicedeployer.presentation; package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.Status;
record AssetAppResource( record AssetAppResource(
String assetCode, String assetCode,
String name, String name,
Long size, int size,
int pid,
String message,
Long releaseAt, Long releaseAt,
Long updatedAt Long updatedAt,
Status status
) { ) {
} }

View File

@ -6,23 +6,16 @@ import org.springframework.stereotype.Component;
@Component @Component
public class AssetAppResourceConverter { public class AssetAppResourceConverter {
public AssetApp fromResource(AssetAppResource resource) {
return AssetApp.builder()
.assetCode(resource.assetCode())
.name(resource.name())
.size(resource.size())
.releasedAt(resource.releaseAt())
.updatedAt(resource.updatedAt())
.build();
}
public AssetAppResource toResource(AssetApp assetApp) { public AssetAppResource toResource(AssetApp assetApp) {
return new AssetAppResource( return new AssetAppResource(
assetApp.getAssetCode(), assetApp.getAssetCode(),
assetApp.getName(), assetApp.getName(),
assetApp.getSize(), assetApp.getSize(),
assetApp.getPid(),
assetApp.getMessage(),
assetApp.getReleaseAt(), assetApp.getReleaseAt(),
assetApp.getUpdatedAt() assetApp.getUpdatedAt(),
assetApp.getStatus()
); );
} }
} }

View File

@ -1,13 +1,10 @@
package inc.sdt.blokworks.devicedeployer.presentation; package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.application.CommandInvoker;
import inc.sdt.blokworks.devicedeployer.application.DeployerService; import inc.sdt.blokworks.devicedeployer.application.DeployerService;
import inc.sdt.blokworks.devicedeployer.application.ProcessService; import inc.sdt.blokworks.devicedeployer.domain.*;
import inc.sdt.blokworks.devicedeployer.domain.AssetApp; import inc.sdt.blokworks.devicedeployer.infrastructure.amqp.ResourceMapping;
import inc.sdt.blokworks.devicedeployer.domain.OperationType; import jakarta.servlet.http.HttpServletRequest;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@ -22,36 +19,68 @@ public class DeployerController {
private final DeployerService deployerService; private final DeployerService deployerService;
private final OutboundMessageResourceConverter outboundMessageResourceConverter; private final OutboundMessageResourceConverter outboundMessageResourceConverter;
private final AssetAppResourceConverter assetAppResourceConverter; private final AssetAppResourceConverter assetAppResourceConverter;
private final CommandInvoker commandInvoker;
private final GiteaApiRequestHandler giteaApiRequestHandler;
public DeployerController(DeployerService deployerService, public DeployerController(DeployerService deployerService,
OutboundMessageResourceConverter outboundMessageResourceConverter, OutboundMessageResourceConverter outboundMessageResourceConverter,
AssetAppResourceConverter assetAppResourceConverter) { AssetAppResourceConverter assetAppResourceConverter,
CommandInvoker commandInvoker,
GiteaApiRequestHandler giteaApiRequestHandler) {
this.log = LoggerFactory.getLogger(this.getClass()); this.log = LoggerFactory.getLogger(this.getClass());
this.deployerService = deployerService; this.deployerService = deployerService;
this.outboundMessageResourceConverter = outboundMessageResourceConverter; this.outboundMessageResourceConverter = outboundMessageResourceConverter;
this.assetAppResourceConverter = assetAppResourceConverter; this.assetAppResourceConverter = assetAppResourceConverter;
this.commandInvoker = commandInvoker;
this.giteaApiRequestHandler = giteaApiRequestHandler;
} }
/** /**
* *
* @param assetCode * @param assetCode
* @param assetAppResource * @param resource
*/ */
@ResponseStatus(HttpStatus.CREATED) @ResourceMapping(name = "Deploy_App", method = "POST", uri = "/assets/{code}/apps", description = "앱 배포 명령")
@ResponseStatus(HttpStatus.OK)
@PostMapping("/assets/{assetCode}/apps") @PostMapping("/assets/{assetCode}/apps")
public void deploy(@PathVariable String assetCode, public void deploy(@PathVariable String assetCode,
@Valid @RequestBody OutboundMessageResource assetAppResource) { @RequestBody OutboundMessageResource resource,
log.info("[deploy] assetCode = {}, assetAppResource = {}", assetCode, assetAppResource); HttpServletRequest httpServletRequest) {
log.info("[deploy] assetCode = {}, resource = {}", assetCode, resource);
String authorization = httpServletRequest.getHeader("Authorization");
String requestId = UUID.randomUUID().toString(); String requestId = UUID.randomUUID().toString();
OutboundMessage outboundMessage = outboundMessageResourceConverter.fromResource(assetAppResource);
OutboundMessage outboundMessage = outboundMessageResourceConverter.fromResource(resource);
outboundMessage.setRequestId(requestId); outboundMessage.setRequestId(requestId);
deployerService.publish(outboundMessage, assetCode); outboundMessage.setAssetCode(assetCode);
outboundMessage.setCommandInfo(commandInvoker.invoke(outboundMessage));
if(resource.commandType() == null) {
throw new IllegalArgumentException();
}
if(resource.commandType() == CommandType.deploy) {
giteaApiRequestHandler.get(authorization, outboundMessage);
}
DeployRequest deployRequest = DeployRequest.builder()
.requestId(requestId)
.assetCode(assetCode)
.appName(outboundMessage.getName())
.operationType(OperationType.DEPLOY)
.commandType(resource.commandType())
.subCommandType(resource.subCommandType())
.build();
deployerService.save(deployRequest);
deployerService.publish(outboundMessage);
} }
/** /**
* ( ) * ( )
* @param assetCode * @param assetCode
*/ */
@ResourceMapping(name = "Get_Asset_App", method = "GET", uri = "/assets/{code}/apps", description = "배포된 앱 정보 조회")
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
@GetMapping("/assets/{assetCode}/apps") @GetMapping("/assets/{assetCode}/apps")
public PageableResponse<AssetAppResource> get(@PathVariable String assetCode, public PageableResponse<AssetAppResource> get(@PathVariable String assetCode,

View File

@ -0,0 +1,85 @@
package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import inc.sdt.blokworks.devicedeployer.domain.SubCommandType;
import inc.sdt.blokworks.devicedeployer.presentation.exception.NotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@Component
public class GiteaApiRequestHandler {
private final Logger log;
private final RestTemplate restTemplate;
private final String url;
private final List<String> extensions;
public GiteaApiRequestHandler(RestTemplate restTemplate,
@Value("${stackbase.api.host}") String url) {
this.log = LoggerFactory.getLogger(this.getClass());
this.restTemplate = restTemplate;
this.url = url;
this.extensions = Arrays.asList(".py", ".jar", ".sh", ".service");
}
public void get(String authorization, OutboundMessage message) {
log.info("[get] message = {}", message);
if(message.getSubCommandType() == null) {
throw new IllegalArgumentException();
}
if(message.getSubCommandType() == SubCommandType.systemd) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", authorization);
try {
download(message.getAssetCode(), message.getUrl());
}catch (IOException e) {
throw new RuntimeException();
}
}
}
private void download(String assetCode, String url) throws IOException {
log.info("[download] assetCode = {}, url = {}", assetCode, url);
byte[] bytes = restTemplate.getForObject(url, byte[].class);
File tempFile = File.createTempFile(assetCode+"_"+LocalDateTime.now(), ".zip");
try(FileOutputStream fos = new FileOutputStream(tempFile)) {
assert bytes != null;
fos.write(bytes);
}
Set<String> entries = new HashSet<>();
try(ZipInputStream zis = new ZipInputStream(Files.newInputStream(Path.of(tempFile.getAbsolutePath())))) {
ZipEntry zipEntry;
while((zipEntry = zis.getNextEntry()) != null) {
if(!zipEntry.isDirectory()) {
for(String ext : extensions) {
if(zipEntry.getName().contains(ext)) {
entries.add(ext);
}
}
}
}
if(entries.size() < 3) {
throw new NotFoundException("Executable file not found");
}
}
}
}

View File

@ -1,62 +1,40 @@
package inc.sdt.blokworks.devicedeployer.presentation; package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.application.DeployerService; import inc.sdt.blokworks.devicedeployer.application.DeployerService;
import inc.sdt.blokworks.devicedeployer.application.ProcessService;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundDeployMessagePayload; import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundDeployMessagePayload;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundProcessMessagePayload;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.integration.annotation.MessageEndpoint; import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
@MessageEndpoint @MessageEndpoint
public class MqttMessageHandler { public class MqttMessageHandler {
private final Logger log; private final Logger log;
private final DeployerService deployerService; private final DeployerService deployerService;
private final ProcessService processService;
private final MqttMessageConverter<InboundDeployMessagePayload> deployMessagePayloadConverter; private final MqttMessageConverter<InboundDeployMessagePayload> deployMessagePayloadConverter;
private final MqttMessageConverter<InboundProcessMessagePayload> processMessagePayloadConverter;
public MqttMessageHandler(DeployerService deployerService, public MqttMessageHandler(DeployerService deployerService,
ProcessService processService, MqttMessageConverter<InboundDeployMessagePayload> deployMessagePayloadConverter) {
MqttMessageConverter<InboundDeployMessagePayload> deployMessagePayloadConverter,
MqttMessageConverter<InboundProcessMessagePayload> processMessagePayloadConverter) {
this.log = LoggerFactory.getLogger(this.getClass()); this.log = LoggerFactory.getLogger(this.getClass());
this.deployerService = deployerService; this.deployerService = deployerService;
this.processService = processService;
this.deployMessagePayloadConverter = deployMessagePayloadConverter; this.deployMessagePayloadConverter = deployMessagePayloadConverter;
this.processMessagePayloadConverter = processMessagePayloadConverter;
} }
@ServiceActivator(inputChannel = "mqttInboundChannel") @ServiceActivator(inputChannel = "mqttInboundChannel")
void handleMessage(Message<String> message) { void handleMessage(Message<String> message) {
log.info("[handleMessage] message={}", message); log.info("[handleMessage] message={}", message);
if(!message.getPayload().contains("pid")) {
String topic = String.valueOf(message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC));
String id = topic.split("/")[4];
log.info("[handleMessage] topic = {}, id = {}", topic, id);
deployMessagePayloadConverter.convertFromByte(message.getPayload(), InboundDeployMessagePayload.class) deployMessagePayloadConverter.convertFromByte(message.getPayload(), InboundDeployMessagePayload.class)
.map(p -> new InboundDeployMessagePayload( .map(p -> new InboundDeployMessagePayload(
p.status(),
p.assetCode(), p.assetCode(),
p.name(), p.deviceType(),
p.size(), p.status(),
p.releasedAt(), p.result(),
p.updatedAt(), p.requestId()
id)) ))
.flatMap(deployerService) .flatMap(deployerService)
.subscribe(); .subscribe();
}else {
processMessagePayloadConverter.convertFromByte(message.getPayload(), InboundProcessMessagePayload.class)
.flatMap(processService)
.subscribe();
}
} }
} }

View File

@ -1,16 +1,21 @@
package inc.sdt.blokworks.devicedeployer.presentation; package inc.sdt.blokworks.devicedeployer.presentation;
import org.wildfly.common.annotation.NotNull; import inc.sdt.blokworks.devicedeployer.domain.CommandType;
import inc.sdt.blokworks.devicedeployer.domain.SubCommandType;
import java.util.HashMap; import java.util.LinkedHashMap;
record OutboundMessageResource( record OutboundMessageResource(
@NotNull
String url, String url,
@NotNull String fileType,
String name, String appName, // 사용자가 정한 파일 이름
String name, // stackbase 에 저장된 파일 이름
String image,
String command, String command,
HashMap<String, String> env CommandType commandType,
SubCommandType subCommandType,
LinkedHashMap<String, Object> options,
LinkedHashMap<String, String> parameters
) { ) {
} }

View File

@ -1,27 +1,27 @@
package inc.sdt.blokworks.devicedeployer.presentation; package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.CommandType;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage; import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.*;
@Component @Component
public class OutboundMessageResourceConverter { public class OutboundMessageResourceConverter {
public OutboundMessageResource toResource(OutboundMessage outboundMessage) {
return new OutboundMessageResource(
outboundMessage.getUrl(),
outboundMessage.getName(),
outboundMessage.getCommand(),
outboundMessage.getEnv()
);
}
public OutboundMessage fromResource(OutboundMessageResource resource) { public OutboundMessage fromResource(OutboundMessageResource resource) {
return OutboundMessage.builder() return OutboundMessage.builder()
.url(resource.url()) .url(resource.url())
.fileType(resource.fileType())
.appName(resource.appName())
.name(resource.name()) .name(resource.name())
.image(resource.image())
.command(resource.command()) .command(resource.command())
.env(resource.env()) .options(resource.options() == null ? new LinkedHashMap<>() : resource.options())
.commandType(resource.commandType() == null ? CommandType.deploy : resource.commandType())
.subCommandType(resource.subCommandType())
.parameters(resource.parameters() == null ? new LinkedHashMap<>() : resource.parameters())
.build(); .build();
} }
} }

View File

@ -25,21 +25,21 @@ class PageableResourceImpl implements PageableResource {
@Override @Override
public long getTotalElements() { public long getTotalElements() {
return 0; return totalElements;
} }
@Override @Override
public int getTotalPages() { public int getTotalPages() {
return 0; return totalPages;
} }
@Override @Override
public int getSize() { public int getSize() {
return 0; return size;
} }
@Override @Override
public int getPage() { public int getPage() {
return 0; return page;
} }
} }

View File

@ -1,8 +0,0 @@
package inc.sdt.blokworks.devicedeployer.presentation;
public record PortResource(
String protocol,
Integer hostPort,
Integer containerPort
) {
}

View File

@ -1,23 +0,0 @@
package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.Port;
import org.springframework.stereotype.Component;
@Component
public class PortResourceConverter {
public PortResource toResource(Port port) {
return new PortResource(
port.getProtocol(),
port.getHostPort(),
port.getContainerPort()
);
}
public Port fromResource(PortResource resource) {
return Port.builder()
.protocol(resource.protocol())
.hostPort(resource.hostPort())
.containerPort(resource.containerPort())
.build();
}
}

View File

@ -1,39 +0,0 @@
package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.application.ProcessService;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@RestController
public class ProcessController {
private final Logger log;
private final ProcessService processService;
private final ProcessResourceConverter processResourceConverter;
public ProcessController(ProcessService processService,
ProcessResourceConverter processResourceConverter) {
this.log = LoggerFactory.getLogger(this.getClass());
this.processService = processService;
this.processResourceConverter = processResourceConverter;
}
/**
* ( )
* @param assetCode
*/
@ResponseStatus(HttpStatus.OK)
@GetMapping("/assets/{assetCode}/apps/process")
public PageableResponse<ProcessResource> get(@PathVariable String assetCode,
@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "20") int size) {
log.info("[get] assetCode = {}, page = {}, size = {}", assetCode, page, size);
Page<Process> process = processService.getAll(assetCode, page, size);
return PageableResponse.from(process, processResourceConverter::toResource);
}
}

View File

@ -1,15 +0,0 @@
package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.OperationType;
record ProcessResource(
String assetCode,
Integer pid,
String name,
Integer cpu,
Integer memory,
Integer network,
Long processedAt,
OperationType operationType
) {
}

View File

@ -1,33 +0,0 @@
package inc.sdt.blokworks.devicedeployer.presentation;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import org.springframework.stereotype.Component;
@Component
public class ProcessResourceConverter {
public Process fromResource(ProcessResource resource) {
return Process.builder()
.assetCode(resource.assetCode())
.name(resource.name())
.pid(resource.pid())
.cpu(resource.cpu())
.memory(resource.memory())
.network(resource.network())
.processedAt(resource.processedAt())
.operationType(resource.operationType())
.build();
}
public ProcessResource toResource(Process process) {
return new ProcessResource(
process.getAssetCode(),
process.getPid(),
process.getName(),
process.getCpu(),
process.getMemory(),
process.getNetwork(),
process.getProcessedAt(),
process.getOperationType()
);
}
}

View File

@ -0,0 +1,31 @@
package inc.sdt.blokworks.devicedeployer.presentation.configuration;
import inc.sdt.blokworks.devicedeployer.presentation.filter.AuthorizationFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Component
class WebConfiguration implements WebMvcConfigurer {
private final AuthorizationFilter authorizationFilter;
public WebConfiguration(AuthorizationFilter authorizationFilter) {
this.authorizationFilter = authorizationFilter;
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationFilter).addPathPatterns(
"/assets/**"
);
}
}

View File

@ -32,4 +32,16 @@ class ControllerAdvice {
public ErrorResponse handleConflictException(Exception exception) { public ErrorResponse handleConflictException(Exception exception) {
return new ErrorResponse(HttpStatus.CONFLICT, exception.getMessage()); return new ErrorResponse(HttpStatus.CONFLICT, exception.getMessage());
} }
@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFoundException(Exception exception) {
return new ErrorResponse(HttpStatus.NOT_FOUND, exception.getMessage());
}
@ExceptionHandler(UnauthorizedException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ErrorResponse handleUnauthorizedException(Exception exception) {
return new ErrorResponse(HttpStatus.UNAUTHORIZED, exception.getMessage());
}
} }

View File

@ -0,0 +1,12 @@
package inc.sdt.blokworks.devicedeployer.presentation.exception;
public class NotFoundException extends RuntimeException{
private static final String DEFAULT_MESSAGE = "File not found";
public NotFoundException() {
super(DEFAULT_MESSAGE);
}
public NotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,14 @@
package inc.sdt.blokworks.devicedeployer.presentation.exception;
public class UnauthorizedException extends RuntimeException {
private static final String DEFAULT_MESSAGE = "Unauthorized";
public UnauthorizedException() {
super(DEFAULT_MESSAGE);
}
public UnauthorizedException(String message) {
super(message);
}
}

View File

@ -0,0 +1,50 @@
package inc.sdt.blokworks.devicedeployer.presentation.filter;
import inc.sdt.blokworks.devicedeployer.presentation.exception.UnauthorizedException;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.NoSuchElementException;
@Component
public class AuthorizationFilter implements HandlerInterceptor {
private final Logger log;
private final String secretKey;
public AuthorizationFilter(@Value("${application.security.jwt.secret-key}") String secretKey) {
this.secretKey = secretKey;
this.log = LoggerFactory.getLogger(this.getClass());
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.info("[preHandle]");
if(request.getHeader("Authorization") == null) {
throw new NoSuchElementException("token does not exist.");
}
String token = request.getHeader("Authorization").substring("Bearer ".length());
byte[] keyBytes = io.jsonwebtoken.io.Decoders.BASE64.decode(secretKey);
javax.crypto.SecretKey secretKey = io.jsonwebtoken.security.Keys.hmacShaKeyFor(keyBytes);
try {
io.jsonwebtoken.Claims claims = io.jsonwebtoken.Jwts
.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
request.setAttribute("userId", claims.get("id").toString());
return true;
}catch (ExpiredJwtException e) {
throw new UnauthorizedException("Token has expired");
}
}
}

View File

@ -0,0 +1,28 @@
package inc.sdt.blokworks.devicedeployer.presentation.rest;
import inc.sdt.blokworks.devicedeployer.presentation.exception.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.DefaultResponseErrorHandler;
import java.io.IOException;
public class RestTemplateResponseErrorHandler extends DefaultResponseErrorHandler {
private final Logger log;
public RestTemplateResponseErrorHandler() {
this.log = LoggerFactory.getLogger(this.getClass());
}
@Override
protected void handleError(ClientHttpResponse response, HttpStatusCode statusCode) throws IOException {
log.error("[handleError] statusCode = {}, response = {}", statusCode, response);
if(response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
throw new UnauthorizedException();
}
}
}

View File

@ -1,3 +1,23 @@
spring:
datasource:
url: ${POSTGRES_URL}
username: ${POSTGRES_CREDENTIALS_USERNAME}
password: ${POSTGRES_CREDENTIALS_PASSWORD}
hikari:
maximum-pool-size: 3
jpa:
hibernate:
ddl-auto: update
show-sql: false
inbound:
mqtt:
url: ${INBOUND_MQTT_URL}
username: ${INBOUND_MQTT_CREDENTIALS_USERNAME}
password: ${INBOUND_MQTT_CREDENTIALS_PASSWORD}
topics:
- /device-control/+/result
iam: iam:
enabled: ${IAM_REGISTER_ENABLED} enabled: ${IAM_REGISTER_ENABLED}
amqp: amqp:
@ -7,3 +27,7 @@ iam:
password: ${IAM_AMQP_CREDENTIALS_PASSWORD} password: ${IAM_AMQP_CREDENTIALS_PASSWORD}
exchange: ${IAM_AMQP_EXCHANGE} exchange: ${IAM_AMQP_EXCHANGE}
routing-key: ${IAM_AMQP_ROUTING_KEY} routing-key: ${IAM_AMQP_ROUTING_KEY}
stackbase:
api:
host: https://sdt-site-bucket.s3.ap-northeast-2.amazonaws.com/app/

View File

@ -13,8 +13,12 @@ spring:
inbound: inbound:
mqtt: mqtt:
url: tcp://localhost:1883 url: tcp://localhost:1883
#url: tcp://13.209.39.139:32259
username: sdt username: sdt
password: 251327 password: 251327
topics: topics:
- /assets/+/command-req/+ - /device-control/+/result
- /assets/+/apps/process
stackbase:
api:
host: https://sdt-site-bucket.s3.ap-northeast-2.amazonaws.com/app/

View File

@ -5,12 +5,20 @@ spring:
name: device-deployer name: device-deployer
datasource: datasource:
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
application:
security:
jwt:
secret-key: D3KJ3G92IWW3W3QWZLU416IN4T9AFYEWM84P5HKHRFEA8C4I1HWALFXGP2HYD87Q
inbound: inbound:
mqtt: mqtt:
url: tcp://192.168.1.162:32102 url: tcp://13.209.39.139:32259
username: sdt username: sdt
password: 251327 password: 251327
topics: topics:
- /assets/+/command-req/+ - /assets/+/command-req/+
- /assets/+/apps/process - /assets/+/apps/process
stackbase:
api:
host: https://sdt-site-bucket.s3.ap-northeast-2.amazonaws.com/app/