Skip to content

Commit

Permalink
test CD Workflow 추가
Browse files Browse the repository at this point in the history
임시 배포 설정 및 Test (#13)

* Merge pull request #8 from gamemuncheol/feat/7

봉투 패턴에 맞춰서 공통 Response Pattern 개발

* Swagger, Spring Security Dependency 추가, Security Config 등록 (#10)

* config Swagger Dependency 업데이트, SwaggerConfig 추가

* config Spring Security Config 설정 및 Dependency 추가

fix Workflow Folder 이름 변경

fix Username, 에러나던 script 변경

Update cd-wordflow.yml

feat Dockerfile 추가

Update Dockerfile

fix Dockerfile Java Jdk Version 수정

fix Dockerfile Error

ERROR: failed to solve: build: pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
Error: Process completed with exit code 1.
  • Loading branch information
rookedsysc committed Jan 28, 2024
1 parent a51b05b commit dc27f33
Show file tree
Hide file tree
Showing 20 changed files with 428 additions and 1 deletion.
Binary file added .DS_Store
Binary file not shown.
66 changes: 66 additions & 0 deletions .github/workflows/cd-wordflow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 자바 프로젝트를 위한 Gradle을 사용한 지속적 배포(CD) 워크플로우 설정
name: CD with Gradle

# main 브랜치로 push되거나 pull request가 발생했을 때 워크플로우를 실행
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

# 워크플로우에서는 저장소 내용을 읽을 수 있는 권한만 설정
permissions:
contents: read

# 워크플로우에서 실행될 작업들을 정의
jobs:
build:
runs-on: ubuntu-latest # 작업을 실행할 환경으로 ubuntu 최신 버전을 사용

steps:
# 실행주체: GitHub Actions > GitHub Infra
# 첫 번째 단계: 워크플로우를 트리거한 커밋에서 저장소 코드를 체크아웃
- uses: actions/checkout@v3

# 두 번째 단계: Java Development Kit(JDK) 버전 17을 설정
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: "adopt"

# 세 번째 단계: GitHub secrets에서 데이터를 가져와 application.properties 파일 생성
# - name: Make application.properties
# run: |
# cd ./src/main/resources
# touch ./application.properties
# echo "${{ secrets.PROPERTIES }}" > ./application.properties
# shell: bash

# 네 번째 단계: Gradle을 사용해 프로젝트 빌드, 테스트는 제외
- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew clean build -x test
# 다섯 번째 단계: Docker 이미지를 빌드하고 Docker 저장소에 푸시
- name: Docker build & push to docker repo
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile -t ${{ secrets.DOCKER_REPO }}/directors-dev .
docker push ${{ secrets.DOCKER_REPO }}/directors-dev
# 실행주체: GitHub Actions > GitHub Infra // End

# 여섯 번째 단계: SSH를 사용하여 서버에 배포
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ubuntu
key: ${{ secrets.KEY }}
envs: GITHUB_SHA
script: |
sudo docker rm -f $(docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_REPO }}/directors-dev
docker-compose up -d
docker image prune -f
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 사용할 Java JDK 버전을 선택
FROM openjdk:17-oracle

COPY ./build/libs/*-SNAPSHOT.jar app.jar

ARG ENVIRONMENT

ENV SPRIING_PROFIlES_ACTIVE=${ENVIRONMENT}

ENV TZ Asia/Seoul

# 컨테이너가 시작될 때 실행할 명령어
ENTRYPOINT ["java","-jar","/app.jar"]
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ repositories {
}

dependencies {
// Swagger start
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
implementation 'javax.xml.bind:jaxb-api:2.3.1' // xml 에러 방지
// Swagger end

// spring security start
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
// spring security end

implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/com/gamemoonchul/common/ObjectMapperConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.gamemoonchul.common;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ObjectMapperConfig {
// Spring이 실행될 때 Object Mapper를 불러오는데 설정된 Object Mapper가 없으면
// Spring이 임의적으로 Object Mapper를 생성하고 설정된 Object Mapper가 있다면 설정된 Mapper를 불러오게 된다.
@Bean
public ObjectMapper objectMapper() {
var objectMapper = new ObjectMapper();
// JDK 버전 이후 클래스 사용 가능
objectMapper.registerModule(new Jdk8Module());

objectMapper.registerModule(new JavaTimeModule());

// 모르는 Json Value에 대해서는 무시하고 나머지 값만 파싱한다.
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// 비어있는 Bean을 만들 때
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 날짜 관련 직렬화 설정 해제
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

// 스네이크 케이스
// objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategies.SnakeCaseStrategy());

return objectMapper;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.gamemoonchul.common.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

import java.util.List;

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
private List<String> SWAGGER = List.of("/swagger-ui.html", "/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs", "/v3/api-docs/**");
private List<String> EXCEPTION = List.of("/test/**");

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(it -> {
// static Resource에 대해서는 모두 허가함
it.requestMatchers(
PathRequest.toStaticResources()
.atCommonLocations()
)
.permitAll()
.requestMatchers(SWAGGER.toArray(new String[0]))
.permitAll()
.requestMatchers(EXCEPTION.toArray(new String[0]))
.permitAll()
.anyRequest()
.authenticated();
});
return httpSecurity.build();
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/gamemoonchul/common/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.gamemoonchul.common.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("Gamemuncheol Project API")
.description("게임문철 프로젝트의 API 문서입니다.")
.version("1.0.0"));
}
}
27 changes: 27 additions & 0 deletions src/main/java/com/gamemoonchul/common/exception/ApiException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.gamemoonchul.common.exception;

import com.gamemoonchul.common.status.ApiStatusIfs;
import lombok.Getter;

@Getter
public class ApiException extends RuntimeException implements ApiExceptionIfs {
/***
* Custom하게 생성된 ApiStatus
* {
* "status" : {
* "statusCode" : 400,
* "message" : "Bad Request"
* },
* "detail" : "파라미터의 인자값이 잘못되었습니다."
* }
*/
private final ApiStatusIfs status;
// 좀 더 세부적인 Error Detail이 필요할 경우
private final String detail;

public ApiException(ApiStatusIfs status) {
super(status.getMessage());
this.status = status;
this.detail = status.getMessage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gamemoonchul.common.exception;

import com.gamemoonchul.common.status.ApiStatusIfs;

public interface ApiExceptionIfs {
ApiStatusIfs getStatus();
String getDetail();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.gamemoonchul.common.exceptionHandler;

import com.gamemoonchul.common.exception.ApiException;
import com.gamemoonchul.infrastructure.web.common.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(value = ApiException.class)
public ResponseEntity<ApiResponse> apiException(
ApiException apiException
) {
log.error("", apiException);

int customErrorCode = apiException.getStatus().getStatusCode();
HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
if((customErrorCode % 1000) / 100 == 4) {
httpStatus = HttpStatus.BAD_REQUEST;
}

return ResponseEntity.status(httpStatus)
.body(ApiResponse.ERROR(apiException.getStatus()));
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/gamemoonchul/common/status/ApiStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.gamemoonchul.common.status;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ApiStatus implements ApiStatusIfs{
OK(200, "OK"),
BAD_REQUEST(400, "Bad Request"),
SERVER_ERROR(500, "Server Error"),
NULL_POINT(512, "Null Point Error"),
;

private final Integer statusCode;
private final String message;
}
13 changes: 13 additions & 0 deletions src/main/java/com/gamemoonchul/common/status/ApiStatusIfs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.gamemoonchul.common.status;

/***
* Custom하게 정의되서 사용할 ApiStatus의 Interface
* 예) UserApiStatus의 경우 2000번대로 시작하게 된다면
* statusCode : 2404
* message : "User Not Found"
* 이런식으로 된다.
*/
public interface ApiStatusIfs {
public Integer getStatusCode();
public String getMessage();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.gamemoonchul.infrastructure.web;

import com.gamemoonchul.infrastructure.web.common.RestControllerWithEnvelopPattern;
import com.gamemoonchul.infrastructure.web.common.dto.ApiTest;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@AllArgsConstructor
@RestControllerWithEnvelopPattern
@RequestMapping("/test")
public class ApiTestController {

private final ApiTestService apiTestService;

@GetMapping("/{bool}")
public ApiTest hello(
@PathVariable boolean bool
) {
return apiTestService.hello(bool);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.gamemoonchul.infrastructure.web;

import com.gamemoonchul.common.exception.ApiException;
import com.gamemoonchul.common.status.ApiStatus;
import com.gamemoonchul.infrastructure.web.common.dto.ApiTest;
import org.springframework.stereotype.Service;

@Service
public class ApiTestService {
public ApiTest hello(boolean bool) {
if (bool) {
return ApiTest.mock();
}
throw new ApiException(ApiStatus.BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.gamemoonchul.infrastructure.web.common;

import com.gamemoonchul.common.status.ApiStatus;
import com.gamemoonchul.common.status.ApiStatusIfs;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
public class ApiResponse<T> {
private boolean success;
private ApiStatusInfo status;
private T data;

// Builder
public ApiResponse(boolean success, T data, ApiStatusIfs status) {
this.success = success;
this.data = data;
this.status = new ApiStatusInfo(status);
}

@Getter
@AllArgsConstructor
public static class ApiStatusInfo {
private final Integer statusCode;
private final String message;

public ApiStatusInfo(ApiStatusIfs status) {
this.statusCode = status.getStatusCode();
this.message = status.getMessage();
}
}

public static ApiResponse OK() {
return new ApiResponse(
true,
null,
ApiStatus.OK
);
}

public static <T> ApiResponse<T> OK(T data) {
return new ApiResponse<>(true, data, ApiStatus.OK);
}

public static <T> ApiResponse<T> ERROR(ApiStatusIfs status) {
return new ApiResponse<>(false, null, status);
}
}
Loading

0 comments on commit dc27f33

Please sign in to comment.