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
}
ext {
jjwtVersion = "0.11.5"
}
repositories {
mavenCentral()
}
@ -26,6 +30,9 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-amqp")
implementation("org.springframework.boot:spring-boot-starter-web")
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")
testImplementation("org.springframework.boot:spring-boot-starter-test")
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"

View File

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

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 {
private String assetCode;
private String name;
private Long size;
private int size;
private int pid;
private String message;
private Long releaseAt;
private Long updatedAt;
private Status status;
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.name = name;
this.size = size;
this.pid = pid;
this.message = message;
this.releaseAt = releaseAt;
this.updatedAt = updatedAt;
this.status = status;
}
public String getAssetCode() {
@ -25,10 +31,18 @@ public class AssetApp {
return name;
}
public Long getSize() {
public int getSize() {
return size;
}
public int getPid() {
return pid;
}
public String getMessage() {
return message;
}
public Long getReleaseAt() {
return releaseAt;
}
@ -37,14 +51,21 @@ public class AssetApp {
return updatedAt;
}
public Status getStatus() {
return status;
}
@Override
public String toString() {
return "AssetApp{" +
"assetCode='" + assetCode + '\'' +
", name='" + name + '\'' +
", size=" + size +
", pid=" + pid +
", message='" + message + '\'' +
", releaseAt=" + releaseAt +
", updatedAt=" + updatedAt +
", status=" + status +
'}';
}
@ -55,9 +76,12 @@ public class AssetApp {
public static final class Builder {
private String assetCode;
private String name;
private Long size;
private int size;
private int pid;
private String message;
private Long releasedAt;
private Long updatedAt;
private Status status;
public Builder assetCode(String assetCode) {
this.assetCode = assetCode;
@ -69,11 +93,21 @@ public class AssetApp {
return this;
}
public Builder size(Long size) {
public Builder size(int size) {
this.size = size;
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) {
this.releasedAt = releasedAt;
return this;
@ -84,13 +118,21 @@ public class AssetApp {
return this;
}
public Builder status(Status status) {
this.status = status;
return this;
}
public AssetApp build() {
AssetApp assetApp = new AssetApp();
assetApp.assetCode = this.assetCode;
assetApp.name = this.name;
assetApp.size = this.size;
assetApp.pid = this.pid;
assetApp.message = this.message;
assetApp.releaseAt = this.releasedAt;
assetApp.updatedAt = this.updatedAt;
assetApp.status = this.status;
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;
import java.util.HashMap;
import java.util.Set;
import java.util.LinkedHashMap;
public class OutboundMessage {
private String url;
private String name;
private HashMap<String, String> env;
private String fileType;
private String assetCode;
private String appName; // 사용자가 정한 파일 이름
private String name; // stackbase 에 저장된 파일 이름
private String image;
private LinkedHashMap<String, Object> options;
private String command;
private OperationType operationType;
private String requestId;
private CommandType commandType;
private SubCommandType subCommandType;
private LinkedHashMap<String, String> parameters;
private LinkedHashMap<String, Object> commandInfo;
protected OutboundMessage() {}
@ -17,22 +23,38 @@ public class OutboundMessage {
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() {
return name;
}
public HashMap<String, String> getEnv() {
return env;
public String getImage() {
return image;
}
public LinkedHashMap<String, Object> getOptions() {
return options;
}
public String getCommand() {
return command;
}
public OperationType getOperationType() {
return operationType;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
@ -41,15 +63,42 @@ public class OutboundMessage {
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
public String toString() {
return "AssetApp{" +
return "OutboundMessage{" +
"url='" + url + '\'' +
", fileType='" + fileType + '\'' +
", assetCode='" + assetCode + '\'' +
", appName='" + appName + '\'' +
", name='" + name + '\'' +
", env=" + env +
", image='" + image + '\'' +
", options=" + options +
", command='" + command + '\'' +
", operationType=" + operationType +
", requestId='" + requestId + '\'' +
", commandType=" + commandType +
", subCommandType=" + subCommandType +
", parameters=" + parameters +
", commandInfo=" + commandInfo +
'}';
}
@ -59,24 +108,51 @@ public class OutboundMessage {
public static final class Builder {
private String url;
private String fileType;
private String assetCode;
private String appName;
private String name;
private HashMap<String, String> env;
private String image;
private LinkedHashMap<String, Object> options;
private String command;
private OperationType operationType;
private String requestId;
private CommandType commandType;
private SubCommandType subCommandType;
private LinkedHashMap<String, String> parameters;
private LinkedHashMap<String, Object> commandInfo;
public Builder url(String url) {
this.url = url;
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) {
this.name = name;
return this;
}
public Builder env(HashMap<String, String> env) {
this.env = env;
public Builder image(String image) {
this.image = image;
return this;
}
public Builder options(LinkedHashMap<String, Object> options) {
this.options = options;
return this;
}
@ -85,24 +161,46 @@ public class OutboundMessage {
return this;
}
public Builder operationType(OperationType operationType) {
this.operationType = operationType;
public Builder requestId(String requestId) {
this.requestId = requestId;
return this;
}
public Builder requestId(String requestId) {
this.requestId = requestId;
public Builder commandType(CommandType commandType) {
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;
}
public OutboundMessage build() {
OutboundMessage deployMessage = new OutboundMessage();
deployMessage.url = this.url;
deployMessage.fileType = this.fileType;
deployMessage.assetCode = this.assetCode;
deployMessage.appName = this.appName;
deployMessage.name = this.name;
deployMessage.env = this.env;
deployMessage.image = this.image;
deployMessage.options = this.options;
deployMessage.command = this.command;
deployMessage.operationType = this.operationType;
deployMessage.requestId = this.requestId;
deployMessage.commandType = this.commandType;
deployMessage.subCommandType = this.subCommandType;
deployMessage.parameters = this.parameters;
deployMessage.commandInfo = this.commandInfo;
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;
public enum Status {
success,
fail
}
public record Status(
int succeed,
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;
import inc.sdt.blokworks.devicedeployer.domain.Result;
import inc.sdt.blokworks.devicedeployer.domain.Status;
public record InboundDeployMessagePayload(
Status status,
String assetCode,
String name,
Long size,
Long releasedAt,
Long updatedAt,
String deviceType,
Status status,
Result result,
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;
import inc.sdt.blokworks.devicedeployer.domain.OperationType;
import inc.sdt.blokworks.devicedeployer.domain.Port;
import inc.sdt.blokworks.devicedeployer.presentation.PortResource;
import inc.sdt.blokworks.devicedeployer.presentation.PortResourceConverter;
import inc.sdt.blokworks.devicedeployer.domain.CommandType;
import inc.sdt.blokworks.devicedeployer.domain.SubCommandType;
import java.util.HashMap;
import java.util.Set;
public record OutboundMessagePayload(
String url,
String name,
String command,
HashMap<String, String> env,
OperationType operationType,
HashMap<String, Object> cmdInfo,
CommandType cmdType,
SubCommandType subCmdType,
String assetCode,
String requestId
) {
}

View File

@ -2,8 +2,6 @@ package inc.sdt.blokworks.devicedeployer.infrastructure.relational;
import jakarta.persistence.*;
import java.sql.Timestamp;
@Entity(name = "asset_app")
class AssetAppEntity {
@Id
@ -15,20 +13,35 @@ class AssetAppEntity {
@Column(name = "app_name", length = 255)
private String name;
@Column(name = "size")
private Long size;
private int size;
@Column(name = "released_at")
private Long releasedAt;
@Column(name = "updated_at")
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() {}
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.name = name;
this.size = size;
this.releasedAt = releasedAt;
this.updatedAt = updatedAt;
this.pid = pid;
this.succeed = succeed;
this.statusCode = statusCode;
this.message = message;
this.errorMessage = errorMessage;
}
public String getId() {
@ -43,7 +56,7 @@ class AssetAppEntity {
return name;
}
public long getSize() {
public int getSize() {
return size;
}
@ -55,5 +68,23 @@ class AssetAppEntity {
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.domain.AssetApp;
import inc.sdt.blokworks.devicedeployer.domain.Status;
import inc.sdt.blokworks.devicedeployer.infrastructure.mqtt.InboundDeployMessagePayload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -23,6 +24,7 @@ public class AssetAppRelationalRepository implements DeployerRepositoryDelegate
@Override
public void accept(InboundDeployMessagePayload inboundDeployMessagePayload) {
log.info("[accept] payload = {}", inboundDeployMessagePayload);
this.save(this.fromMessage(inboundDeployMessagePayload));
}
@ -49,7 +51,12 @@ public class AssetAppRelationalRepository implements DeployerRepositoryDelegate
assetApp.getName(),
assetApp.getSize(),
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())
.name(entity.getName())
.size(entity.getSize())
.pid(entity.getPid())
.message(entity.getMessage())
.releasedAt(entity.getReleasedAt())
.updatedAt(entity.getUpdatedAt())
.status(new Status(entity.getSucceed(), entity.getStatusCode(), entity.getErrorMessage()))
.build();
}
private AssetApp fromMessage(InboundDeployMessagePayload payload) {
return AssetApp.builder()
.assetCode(payload.assetCode())
.name(payload.name())
.size(payload.size())
.releasedAt(payload.releasedAt())
.updatedAt(payload.updatedAt())
.name(payload.result().name())
.size(payload.result().size())
.pid(payload.result().pid())
.message(payload.result().message())
.releasedAt(payload.result().releasedAt())
.updatedAt(payload.result().updatedAt())
.status(payload.status())
.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;
import inc.sdt.blokworks.devicedeployer.domain.Status;
record AssetAppResource(
String assetCode,
String name,
Long size,
int size,
int pid,
String message,
Long releaseAt,
Long updatedAt
Long updatedAt,
Status status
) {
}

View File

@ -6,23 +6,16 @@ import org.springframework.stereotype.Component;
@Component
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) {
return new AssetAppResource(
assetApp.getAssetCode(),
assetApp.getName(),
assetApp.getSize(),
assetApp.getPid(),
assetApp.getMessage(),
assetApp.getReleaseAt(),
assetApp.getUpdatedAt()
assetApp.getUpdatedAt(),
assetApp.getStatus()
);
}
}

View File

@ -1,13 +1,10 @@
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.ProcessService;
import inc.sdt.blokworks.devicedeployer.domain.AssetApp;
import inc.sdt.blokworks.devicedeployer.domain.OperationType;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import inc.sdt.blokworks.devicedeployer.domain.Process;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import inc.sdt.blokworks.devicedeployer.domain.*;
import inc.sdt.blokworks.devicedeployer.infrastructure.amqp.ResourceMapping;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
@ -22,36 +19,68 @@ public class DeployerController {
private final DeployerService deployerService;
private final OutboundMessageResourceConverter outboundMessageResourceConverter;
private final AssetAppResourceConverter assetAppResourceConverter;
private final CommandInvoker commandInvoker;
private final GiteaApiRequestHandler giteaApiRequestHandler;
public DeployerController(DeployerService deployerService,
OutboundMessageResourceConverter outboundMessageResourceConverter,
AssetAppResourceConverter assetAppResourceConverter) {
AssetAppResourceConverter assetAppResourceConverter,
CommandInvoker commandInvoker,
GiteaApiRequestHandler giteaApiRequestHandler) {
this.log = LoggerFactory.getLogger(this.getClass());
this.deployerService = deployerService;
this.outboundMessageResourceConverter = outboundMessageResourceConverter;
this.assetAppResourceConverter = assetAppResourceConverter;
this.commandInvoker = commandInvoker;
this.giteaApiRequestHandler = giteaApiRequestHandler;
}
/**
*
* @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")
public void deploy(@PathVariable String assetCode,
@Valid @RequestBody OutboundMessageResource assetAppResource) {
log.info("[deploy] assetCode = {}, assetAppResource = {}", assetCode, assetAppResource);
@RequestBody OutboundMessageResource resource,
HttpServletRequest httpServletRequest) {
log.info("[deploy] assetCode = {}, resource = {}", assetCode, resource);
String authorization = httpServletRequest.getHeader("Authorization");
String requestId = UUID.randomUUID().toString();
OutboundMessage outboundMessage = outboundMessageResourceConverter.fromResource(assetAppResource);
OutboundMessage outboundMessage = outboundMessageResourceConverter.fromResource(resource);
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
*/
@ResourceMapping(name = "Get_Asset_App", method = "GET", uri = "/assets/{code}/apps", description = "배포된 앱 정보 조회")
@ResponseStatus(HttpStatus.OK)
@GetMapping("/assets/{assetCode}/apps")
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;
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.InboundProcessMessagePayload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.integration.annotation.MessageEndpoint;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.Message;
@MessageEndpoint
public class MqttMessageHandler {
private final Logger log;
private final DeployerService deployerService;
private final ProcessService processService;
private final MqttMessageConverter<InboundDeployMessagePayload> deployMessagePayloadConverter;
private final MqttMessageConverter<InboundProcessMessagePayload> processMessagePayloadConverter;
public MqttMessageHandler(DeployerService deployerService,
ProcessService processService,
MqttMessageConverter<InboundDeployMessagePayload> deployMessagePayloadConverter,
MqttMessageConverter<InboundProcessMessagePayload> processMessagePayloadConverter) {
MqttMessageConverter<InboundDeployMessagePayload> deployMessagePayloadConverter) {
this.log = LoggerFactory.getLogger(this.getClass());
this.deployerService = deployerService;
this.processService = processService;
this.deployMessagePayloadConverter = deployMessagePayloadConverter;
this.processMessagePayloadConverter = processMessagePayloadConverter;
}
@ServiceActivator(inputChannel = "mqttInboundChannel")
void handleMessage(Message<String> 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)
.map(p -> new InboundDeployMessagePayload(
p.status(),
p.assetCode(),
p.name(),
p.size(),
p.releasedAt(),
p.updatedAt(),
id))
.flatMap(deployerService)
.subscribe();
}else {
processMessagePayloadConverter.convertFromByte(message.getPayload(), InboundProcessMessagePayload.class)
.flatMap(processService)
.subscribe();
}
deployMessagePayloadConverter.convertFromByte(message.getPayload(), InboundDeployMessagePayload.class)
.map(p -> new InboundDeployMessagePayload(
p.assetCode(),
p.deviceType(),
p.status(),
p.result(),
p.requestId()
))
.flatMap(deployerService)
.subscribe();
}
}

View File

@ -1,16 +1,21 @@
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(
@NotNull
String url,
@NotNull
String name,
String fileType,
String appName, // 사용자가 정한 파일 이름
String name, // stackbase 에 저장된 파일 이름
String image,
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;
import inc.sdt.blokworks.devicedeployer.domain.CommandType;
import inc.sdt.blokworks.devicedeployer.domain.OutboundMessage;
import org.springframework.stereotype.Component;
import java.util.*;
@Component
public class OutboundMessageResourceConverter {
public OutboundMessageResource toResource(OutboundMessage outboundMessage) {
return new OutboundMessageResource(
outboundMessage.getUrl(),
outboundMessage.getName(),
outboundMessage.getCommand(),
outboundMessage.getEnv()
);
}
public OutboundMessage fromResource(OutboundMessageResource resource) {
return OutboundMessage.builder()
.url(resource.url())
.fileType(resource.fileType())
.appName(resource.appName())
.name(resource.name())
.image(resource.image())
.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();
}
}

View File

@ -25,21 +25,21 @@ class PageableResourceImpl implements PageableResource {
@Override
public long getTotalElements() {
return 0;
return totalElements;
}
@Override
public int getTotalPages() {
return 0;
return totalPages;
}
@Override
public int getSize() {
return 0;
return size;
}
@Override
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) {
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:
enabled: ${IAM_REGISTER_ENABLED}
amqp:
@ -6,4 +26,8 @@ iam:
username: ${IAM_AMQP_CREDENTIALS_USERNAME}
password: ${IAM_AMQP_CREDENTIALS_PASSWORD}
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:
mqtt:
url: tcp://localhost:1883
#url: tcp://13.209.39.139:32259
username: sdt
password: 251327
topics:
- /assets/+/command-req/+
- /assets/+/apps/process
- /device-control/+/result
stackbase:
api:
host: https://sdt-site-bucket.s3.ap-northeast-2.amazonaws.com/app/

View File

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