728x90
반응형
사용자 요청에 따라 웹 서버(nginx) 컨테이너를 실행, 삭제, 조회하는 웹 애플리케이션을 작성해 보겠습니다.
실습은 https://myanjini.tistory.com/entry/board-%EC%86%8C%EC%8A%A4%EC%BD%94%EB%93%9C 에서 가져온 코드를 이용해서 진행하겠습니다.
컨트롤러 추가
RestDockerApiController.java 파일을 추가하고, 컨테이너 목록을 조회하는 메서드, 컨테이너를 실행하는 메서드, 컨테이너를 삭제하는 메서드를 구현합니다.
package board.controller;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.UUID;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import board.dto.BoardDto;
@RestController
@CrossOrigin(origins = "*", allowedHeaders = "*")
public class RestDockerApiController {
// 컨테이너 별로 사용할 디렉터리를 생성할 디렉터리
private final String CONST_AWS_DIR = "c:\\aws\\";
// 모든 상태의 컨테이너를 조회
@GetMapping("/api/docker")
public List<String> dockerList() throws Exception {
final String command = "docker container ls -a --format=\"{{json .}}\" ";
List<String> result = null;
Process process = null;
try {
process = Runtime.getRuntime().exec(command);
result = new BufferedReader(new InputStreamReader(process.getInputStream()))
.lines()
.toList();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
// 컨테이너 생성
// 생성 시 임의의 이름과 포트를 사용하도록 하고, 이름에 해당하는 폴더를 생성해 컨테이너와 공유
@PostMapping("/api/docker/write")
public List<String> dockerRun(@RequestBody BoardDto boardDto) throws Exception {
List<String> result = null;
final String name = UUID.randomUUID().toString();
final String path = CONST_AWS_DIR + name + "\\";
final String file = path + boardDto.getTitle();
final String commands =
"cmd.exe /c "
+ String.format("mkdir %s", path)
+ String.format("&& docker container run -d -v %s:/usr/share/nginx/html/ --name %s -p 80 nginx", path, name)
+ String.format("&& docker inspect --format=\"{{(index (index .NetworkSettings.Ports \\\"80/tcp\\\") 0).HostPort}}\" %s", name)
;
try {
Process process = Runtime.getRuntime().exec(commands);
result = new BufferedReader(new InputStreamReader(process.getInputStream()))
.lines()
.toList();
Path filePath = Paths.get(file);
Files.createFile(filePath);
Files.write(filePath, boardDto.getContents().getBytes(), StandardOpenOption.CREATE);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
// 파라미터로 전달한 값과 컨테이너 이름이 일치하는 컨테이너를 삭제 후 결과를 반환
@DeleteMapping("/api/docker/{name}")
public List<String> dockerRemove(@PathVariable("name") String name) throws Exception {
final String commands =
"cmd.exe /c "
+ String.format("rmdir /S /Q %s%s ", CONST_AWS_DIR, name)
+ String.format("&& docker container rm -f %s", name)
;
List<String> result = null;
Process process = null;
try {
process = Runtime.getRuntime().exec(commands);
result = new BufferedReader(new InputStreamReader(process.getInputStream()))
.lines()
.toList();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
// 파라미터로 전달한 값과 컨테이너 ID가 일치하는 컨테이너의 상세 정보를 조회해서 반환
@GetMapping("/api/docker/{id}")
public List<String> dockerInspect(@PathVariable("id") String id) throws Exception {
final String command = String.format("docker container inspect %s", id);
List<String> result = null;
Process process = null;
try {
process = Runtime.getRuntime().exec(command);
result = new BufferedReader(new InputStreamReader(process.getInputStream()))
.lines()
.toList();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
}
컨테이너 조회
BoardList.js 파일을 앞에서 작성한 컨트롤러 메서드를 호출하도록 수정합니다. 웹 페이지 보기 아이콘을 클릭하면 새 창(탭)에서 컨테이너에 바인딩된 호스트 포트로 요청을 전달하고, 웹 서버 삭제 아이콘을 클릭하면 삭제할 컨테이너의 이름을 삭제 페이지로 전달하고, 목록의 컨테이너 ID를 클릭하면 상세 페이지로 이동합니다.
import axios from 'axios';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { IconName, MdDeleteOutline, MdOutlineArticle, MdOutlinePlayCircle, MdOutlineStopCircle } from "react-icons/md";
const BoardList = ({ history }) => {
const [datas, setDatas] = useState([]);
const styles = {width: 24, height: 24, cursor: 'pointer'};
// 최초 마운트 시 컨테이너 목록 조회 결과를 상태 변수의 값으로 설정
useEffect(() => {
axios.get('http://localhost:8080/api/docker')
.then(response => {
console.log(response);
setDatas(response.data);
})
.catch(error => {
console.log(error);
});
}, []);
// 새 창(탭)에서 컨테이너에 바인딩된 호스트 포트로 요청
const handlerViewPage = (port) => {
window.open(`http://localhost:${port}`)
};
// 삭제할 컨테이너의 이름을 파라미터로 전달
const handlerDelete = (name) => {
axios.delete(`http://localhost:8080/api/docker/${name}`)
.then(response => {
if (response.data.length == 0)
return;
alert(`${response.data} 이름의 웹 서버를 정상적으로 삭제했습니다.`);
const newDatas = datas.filter(data => {
data = JSON.parse(data);
return data.Names != response.data;
});
setDatas(newDatas);
})
.catch(error => {
console.log(error);
});
};
return (
<>
<div className="container">
<h2>웹 서버 목록</h2>
<table className="board_list">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">IMAGE</th>
<th scope="col">COMMAND</th>
<th scope="col">CREATED</th>
<th scope="col">STATUS</th>
<th scope="col">PORTS</th>
<th scope="col">NAMES</th>
<th scope="col"></th>
{/* 상세 내용 (필요 시 사용)
<th scope="col">Labels</th>
<th scope="col">LocalVolumes</th>
<th scope="col">Mounts</th>
<th scope="col">Networks</th>
<th scope="col">RunningFor</th>
<th scope="col">Size</th>
<th scope="col">State</th> */}
</tr>
</thead>
<tbody>
{
datas.length === 0 && (
<tr>
<td colSpan="14">웹 서버가 존재하지 않습니다.</td>
</tr>
)
}
{
datas && datas.map(data => {
const c = JSON.parse(data);
const port = c.Ports.split('-')[0].split(':')[1];
const id = c.ID;
return (
<tr key={c.ID}>
<td>
<Link to={`/board/detail/${c.ID}`}>{c.ID}</Link>
</td>
<td>{c.Image}</td>
<td>{c.Command}</td>
<td>{c.CreatedAt}</td>
<td>{c.Status}</td>
<td>{c.Ports}</td>
<td>{c.Names}</td>
<td>
{/* 새창에서 웹 페이지 보기, 웹 서버 삭제 버튼 */}
<MdOutlineArticle style={styles} onClick={() => handlerViewPage(port)} title="웹 페이지 보기" />
<MdDeleteOutline style={styles} onClick={() => handlerDelete(c.Names)} title="웹 서버 삭제" />
</td>
{/* 상세 내용 (필요 시 사용)
<td>{c.Labels}</td>
<td>{c.LocalVolumes}</td>
<td>{c.Mounts}</td>
<td>{c.Networks}</td>
<td>{c.RunningFor}</td>
<td>{c.Size}</td>
<td>{c.State}</td>*/}
</tr>
)
})
}
</tbody>
</table>
<Link to="/board/write" className="btn">웹 서버 생성</Link>
</div>
</>
);
};
export default BoardList;
컨테이너 생성
BoardWrite.js 파일을 앞에서 작성한 컨트롤러 메서드를 호출하도록 수정합니다. 입력한 파일명과 파일의 내용을 서버로 전달하고, 실행한 컨테이너의 ID를 반환받습니다.
import { useState } from "react";
import axios from 'axios';
const BoardWrite = ({ location, history }) => {
const [title, setTitle] = useState('index.html');
const [contents, setContents] = useState('');
const handlerChangeTitle = e => setTitle(e.target.value);
const handlerChangeContents = e => setContents(e.target.value);
console.log(window.location);
const handlerSubmit = e => {
e.preventDefault();
axios.post(`http://localhost:8080/api/docker/write`, { title, contents })
.then(response => {
const { protocol, hostname } = window.location;
if (response.data.length == 2) {
alert(`웹 서버가 생성되었습니다.\n${protocol}://${hostname}:${response.data[1]}로 접속해 확인할 수 있습니다.`);
history.push('/board');
} else {
alert('웹 서버 생성에 실패했습니다.');
return;
}
})
.catch(error => {
alert(`웹 서버 생성에 실패했습니다. (${error.message})`);
return;
});
};
return (
<>
<div className="container">
<h2>웹 서버 생성</h2>
<form id="frm" name="frm" onSubmit={handlerSubmit}>
<table className="board_detail">
<tbody>
<tr>
<td>파일명</td>
<td><input type="text" id="title" name="title" value={title} onChange={handlerChangeTitle} /></td>
</tr>
<tr>
<td colSpan="2"><textarea id="contents" name="contents" value={contents} onChange={handlerChangeContents}></textarea></td>
</tr>
</tbody>
</table>
<input type="submit" id="submit" value="생성" className="btn" />
</form>
</div>
</>
);
};
export default BoardWrite;
컨테이너 상세
BoardWrite.js 파일을 앞에서 작성한 컨트롤러 메서드를 호출하도록 수정합니다. 컨테이너 ID에 해당하는 컨테이너의 상세 정보를 요청하고, 해당 내용을 화면에 출력합니다.
import axios from 'axios';
import { useEffect, useState } from 'react';
function BoardDetail({ match, history }) {
const {boardIdx} = match.params;
const [datas, setDatas] = useState([]);
useEffect(() => {
axios.get(`http://localhost:8080/api/docker/${boardIdx}`)
.then(response => {
console.log(response);
setDatas(response.data);
})
.catch(error => console.log(error));
}, []);
const handlerClickList = () => {
history.push('/board');
};
return (
<>
<div className="container">
<h2>웹 서버 상세</h2>
<form action="" method="POST" id="frm" name="frm">
<table className="board_detail">
<tbody>
<tr>
<td>
<pre>
{
datas.map(data => {
console.log(data);
return data + "\n";
})
}
</pre>
</td>
</tr>
</tbody>
</table>
</form>
<input type="button" id="list" className="btn" value="목록으로" onClick={handlerClickList} />
</div>
</>
);
}
export default BoardDetail;
실행결과
728x90
반응형
'개발' 카테고리의 다른 글
웹 소켓을 이용한 채팅 (React + SpringBoot) (0) | 2023.04.19 |
---|---|
폼 데이터와 함께 파일 업로드 (다중 파일 선택창 & 다중 파일 선택) (0) | 2023.04.12 |
카카오 로그인 (0) | 2023.04.06 |
네이버 로그인 (0) | 2023.04.06 |
EC2 인스턴스에 React + SpringBoot + MySQL 연동 (0) | 2023.03.29 |
댓글