├── src ├── main │ ├── resources │ │ ├── messages │ │ │ └── messages.properties │ │ ├── master-service-ui.yaml │ │ ├── master-service.yaml │ │ ├── worker-deployment.yaml │ │ ├── master-deployment.yaml │ │ └── application.yml │ └── java │ │ └── com │ │ └── trendyol │ │ └── kubernetesoperatorapi │ │ ├── adapter │ │ ├── kubernetes │ │ │ ├── strategy │ │ │ │ ├── KubernetesClientStrategy.java │ │ │ │ ├── DataCenterSettings.java │ │ │ │ ├── KubernetesClient.java │ │ │ │ └── datacenter │ │ │ │ │ ├── AwsDcStrategy.java │ │ │ │ │ ├── DummyDcStrategy.java │ │ │ │ │ └── GoogleDcStrategy.java │ │ │ ├── KubernetesApiMockAdapter.java │ │ │ ├── component │ │ │ │ ├── KubernetesYamlConverterComponent.java │ │ │ │ └── KubernetesDeploymentComponent.java │ │ │ └── KubernetesApiAdapter.java │ │ └── rest │ │ │ ├── index │ │ │ └── IndexController.java │ │ │ └── operator │ │ │ ├── scale │ │ │ ├── ScaleOperatorController.java │ │ │ └── request │ │ │ │ └── ScaleDeploymentRequest.java │ │ │ ├── terminate │ │ │ └── TerminateOperatorController.java │ │ │ └── create │ │ │ ├── request │ │ │ └── CreateDeploymentRequest.java │ │ │ └── CreateOperatorController.java │ │ ├── infra │ │ ├── config │ │ │ ├── swagger │ │ │ │ ├── License.java │ │ │ │ ├── Host.java │ │ │ │ ├── SwaggerProperties.java │ │ │ │ └── SwaggerConfig.java │ │ │ ├── message │ │ │ │ └── MessageSourceConfiguration.java │ │ │ └── web │ │ │ │ └── WebConfiguration.java │ │ ├── common │ │ │ ├── ErrorDetailDTO.java │ │ │ ├── ErrorDTO.java │ │ │ ├── BaseResponse.java │ │ │ └── GlobalControllerExceptionHandler.java │ │ ├── annotation │ │ │ ├── RetryOperation.java │ │ │ └── aspect │ │ │ │ └── RetryOperationAspect.java │ │ ├── constant │ │ │ ├── AuditorConstants.java │ │ │ └── Constant.java │ │ └── interceptor │ │ │ ├── ExecutorUserInterceptor.java │ │ │ ├── AgentNameInterceptor.java │ │ │ ├── CorrelationIdInterceptor.java │ │ │ └── InterceptorConfiguration.java │ │ ├── domain │ │ ├── exception │ │ │ ├── BusinessException.java │ │ │ ├── BaseRuntimeException.java │ │ │ ├── ClientApiBusinessException.java │ │ │ ├── ClientApiReadTimeoutException.java │ │ │ ├── ClientApiInternalServerException.java │ │ │ ├── ClientApiBaseException.java │ │ │ ├── RollBackBusinessException.java │ │ │ └── BaseTrendyolException.java │ │ ├── enumtype │ │ │ ├── DataCenter.java │ │ │ └── RollBackLevel.java │ │ └── command │ │ │ ├── ScaleDeploymentCommand.java │ │ │ ├── CreateDeploymentCommand.java │ │ │ └── TerminateDeploymentCommand.java │ │ ├── application │ │ ├── port │ │ │ └── OperatorApiPort.java │ │ ├── ScaleOperatorFacade.java │ │ ├── TerminateOperatorFacade.java │ │ └── CreateOperatorFacade.java │ │ └── KubernetesOperatorApiApplication.java └── test │ ├── java │ └── com │ │ └── trendyol │ │ └── kubernetesoperatorapi │ │ ├── base │ │ ├── AbstractMvc.java │ │ ├── BaseTest.java │ │ └── OrderTest.java │ │ ├── domain │ │ └── command │ │ │ └── TerminateDeploymentCommandTest.java │ │ ├── adapter │ │ └── rest │ │ │ └── operator │ │ │ ├── scale │ │ │ ├── request │ │ │ │ └── ScaleDeploymentRequestTest.java │ │ │ └── ScaleOperatorControllerMockMvcTest.java │ │ │ ├── create │ │ │ ├── request │ │ │ │ └── CreateDeploymentRequestTest.java │ │ │ └── CreateOperatorControllerMockMvcTest.java │ │ │ └── terminate │ │ │ └── TerminateOperatorControllerMockMvcTest.java │ │ └── application │ │ ├── TerminateOperatorFacadeTest.java │ │ ├── ScaleOperatorFacadeTest.java │ │ └── CreateOperatorFacadeTest.java │ └── resources │ └── application.yml ├── LICENSE ├── .gitignore ├── README.md └── pom.xml /src/main/resources/messages/messages.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/kubernetes/strategy/KubernetesClientStrategy.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy; 2 | 3 | public interface KubernetesClientStrategy { 4 | 5 | String getDataCenter(); 6 | 7 | DataCenterSettings getClient(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/config/swagger/License.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.config.swagger; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | class License { 9 | 10 | private String name; 11 | private String url; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/config/swagger/Host.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.config.swagger; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | class Host { 9 | 10 | private String url; 11 | private String protocol = "http"; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/exception/BusinessException.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.exception; 2 | 3 | public class BusinessException extends BaseTrendyolException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | public BusinessException(String exception) { 8 | super(exception); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/enumtype/DataCenter.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.enumtype; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | public enum DataCenter { 9 | 10 | GOOGLE("GOOGLE"), 11 | AWS("AWS"), 12 | DUMMY("DUMMY"); 13 | 14 | private final String value; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/master-service-ui.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: master-service-ui-__RUN_ID__ 5 | labels: 6 | app: master-service 7 | id: __RUN_ID__ 8 | namespace: service 9 | spec: 10 | ports: 11 | - port: 1234 12 | targetPort: master-web 13 | protocol: TCP 14 | name: master-web 15 | selector: 16 | app: master-service 17 | id: __RUN_ID__ 18 | type: NodePort 19 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/exception/BaseRuntimeException.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.exception; 2 | 3 | public abstract class BaseRuntimeException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | protected BaseRuntimeException(String message) { 8 | super(message); 9 | } 10 | 11 | public abstract Integer getCode(); 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/base/AbstractMvc.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.base; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.test.web.servlet.MockMvc; 6 | 7 | public abstract class AbstractMvc { 8 | 9 | @Autowired 10 | protected MockMvc mockMvc; 11 | 12 | @Autowired 13 | protected ObjectMapper objectMapper; 14 | } -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/base/BaseTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.base; 2 | 3 | import org.junit.jupiter.api.DisplayNameGeneration; 4 | import org.junit.jupiter.api.DisplayNameGenerator; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.junit.jupiter.MockitoExtension; 7 | 8 | @ExtendWith(MockitoExtension.class) 9 | @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 10 | public class BaseTest { 11 | 12 | } -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/common/ErrorDetailDTO.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.common; 2 | 3 | import lombok.*; 4 | 5 | import java.io.Serializable; 6 | 7 | @Setter 8 | @Getter 9 | @Builder 10 | @ToString 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class ErrorDetailDTO implements Serializable { 14 | 15 | private String key; 16 | private String message; 17 | private String errorCode; 18 | private String[] args; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/annotation/RetryOperation.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.METHOD) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface RetryOperation { 11 | 12 | int retryCount(); 13 | 14 | int waitSeconds(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/master-service.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: master-service-__RUN_ID__ 5 | labels: 6 | app: master-service 7 | id: __RUN_ID__ 8 | namespace: service 9 | spec: 10 | ports: 11 | - port: 1235 12 | targetPort: master-p1 13 | protocol: TCP 14 | name: master-p1 15 | - port: 1236 16 | targetPort: master-p2 17 | protocol: TCP 18 | name: master-p2 19 | selector: 20 | app: master-service 21 | id: __RUN_ID__ -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/common/ErrorDTO.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.common; 2 | 3 | import lombok.*; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | @Setter 9 | @Getter 10 | @Builder 11 | @ToString 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class ErrorDTO { 15 | 16 | private String exception; 17 | private long timestamp = System.currentTimeMillis(); 18 | private List errorDetail = new ArrayList<>(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/enumtype/RollBackLevel.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.enumtype; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | public enum RollBackLevel { 9 | 10 | //Queue is important ! 11 | 12 | ALL_FLOW(5), 13 | BEFORE_SERVICE_UI(4), 14 | BEFORE_SERVICE(3), 15 | BEFORE_WORKER_DEPLOYMENT(2), 16 | BEFORE_MASTER_DEPLOYMENT(1); 17 | 18 | private final int value; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/exception/ClientApiBusinessException.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.exception; 2 | 3 | import com.trendyol.kubernetesoperatorapi.infra.common.BaseResponse; 4 | 5 | public class ClientApiBusinessException extends ClientApiBaseException { 6 | 7 | private static final long serialVersionUID = 918472304740612225L; 8 | 9 | public ClientApiBusinessException(BaseResponse baseResponse, String exception) { 10 | super(baseResponse, exception); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: 'Kubernetes Operator Api' 4 | description: 'Kubernetes Operator Documentation' 5 | main: 6 | allow-bean-definition-overriding: true 7 | 8 | mvc: 9 | pathmatch: 10 | matching-strategy: ant_path_matcher 11 | 12 | profiles: 13 | active: test 14 | 15 | logging: 16 | level: 17 | root: WARN 18 | 19 | cors: 20 | webInternalIp: http://localhost:3000 21 | webExternalIp: https://www.trendyol.com 22 | 23 | mock: 24 | kubernetes.api.enabled: true -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/kubernetes/strategy/DataCenterSettings.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy; 2 | 3 | import io.kubernetes.client.openapi.ApiClient; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class DataCenterSettings { 14 | 15 | private ApiClient apiClient; 16 | private String imageNamePrefix; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/exception/ClientApiReadTimeoutException.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.exception; 2 | 3 | import com.trendyol.kubernetesoperatorapi.infra.common.BaseResponse; 4 | 5 | public class ClientApiReadTimeoutException extends ClientApiBaseException { 6 | 7 | private static final long serialVersionUID = 3235684905554169944L; 8 | 9 | public ClientApiReadTimeoutException(BaseResponse baseResponse, String exception) { 10 | super(baseResponse, exception); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/exception/ClientApiInternalServerException.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.exception; 2 | 3 | import com.trendyol.kubernetesoperatorapi.infra.common.BaseResponse; 4 | 5 | public class ClientApiInternalServerException extends ClientApiBaseException { 6 | 7 | private static final long serialVersionUID = 8873864100950989673L; 8 | 9 | public ClientApiInternalServerException(BaseResponse baseResponse, String exception) { 10 | super(baseResponse, exception); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/command/ScaleDeploymentCommand.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.command; 2 | 3 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class ScaleDeploymentCommand { 14 | 15 | private String runId; 16 | private DataCenter dataCenter; 17 | private int workerCount; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/command/CreateDeploymentCommand.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.command; 2 | 3 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class CreateDeploymentCommand { 14 | 15 | private String runId; 16 | private DataCenter dataCenter; 17 | private int workerCount; 18 | private String imageName = "imageName"; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/application/port/OperatorApiPort.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.application.port; 2 | 3 | import com.trendyol.kubernetesoperatorapi.domain.command.CreateDeploymentCommand; 4 | import com.trendyol.kubernetesoperatorapi.domain.command.ScaleDeploymentCommand; 5 | 6 | public interface OperatorApiPort { 7 | 8 | void createDeployment(CreateDeploymentCommand command); 9 | 10 | void scaleDeployment(ScaleDeploymentCommand command); 11 | 12 | void deleteDeployment(String dataCenterName, String deploymentName); 13 | 14 | void deleteService(String dataCenterName, String serviceName); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/KubernetesOperatorApiApplication.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.hystrix.EnableHystrix; 6 | import org.springframework.retry.annotation.EnableRetry; 7 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 8 | 9 | @EnableRetry 10 | @EnableHystrix 11 | @EnableSwagger2 12 | @SpringBootApplication 13 | public class KubernetesOperatorApiApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(KubernetesOperatorApiApplication.class, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/exception/ClientApiBaseException.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.exception; 2 | 3 | import com.trendyol.kubernetesoperatorapi.infra.common.BaseResponse; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | public class ClientApiBaseException extends RuntimeException { 8 | 9 | private static final long serialVersionUID = 918472304740612225L; 10 | 11 | private final BaseResponse baseResponse; 12 | private final String exception; 13 | 14 | public ClientApiBaseException(BaseResponse baseResponse, String exception) { 15 | super(baseResponse.toString()); 16 | this.baseResponse = baseResponse; 17 | this.exception = exception; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/constant/AuditorConstants.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.constant; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | public final class AuditorConstants { 8 | 9 | public static final String X_CORRELATION_ID = "x-correlationid"; 10 | public static final String X_CORRELATION_ID_KEBAB_CASE = "x-correlation-id"; 11 | public static final String X_CORRELATION_ID_CAMEL_CASE = "x-correlationId"; 12 | public static final String X_AGENTNAME = "x-agentname"; 13 | public static final String X_AGENTNAME_KEBAB_CASE = "x-agentname"; 14 | public static final String X_EXECUTOR_USER = "x-executor-user"; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/config/message/MessageSourceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.config.message; 2 | 3 | import org.springframework.context.MessageSource; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.support.ResourceBundleMessageSource; 7 | 8 | @Configuration 9 | public class MessageSourceConfiguration { 10 | 11 | @Bean 12 | public MessageSource messageSource() { 13 | var messageSource = new ResourceBundleMessageSource(); 14 | messageSource.setBasenames("messages/messages"); 15 | messageSource.setDefaultEncoding("UTF-8"); 16 | return messageSource; 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/exception/RollBackBusinessException.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.exception; 2 | 3 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.RollBackLevel; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | public class RollBackBusinessException extends BaseTrendyolException { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | private int level; 14 | 15 | public RollBackBusinessException(String exception) { 16 | super(exception); 17 | } 18 | 19 | public RollBackBusinessException(String exception, RollBackLevel rollBackLevel) { 20 | super(exception); 21 | this.level = rollBackLevel.getValue(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/command/TerminateDeploymentCommand.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.command; 2 | 3 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @Builder 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class TerminateDeploymentCommand { 14 | 15 | private String runId; 16 | private DataCenter dataCenter; 17 | 18 | public static TerminateDeploymentCommand of(String runId, DataCenter dataCenter) { 19 | return TerminateDeploymentCommand.builder() 20 | .runId(runId) 21 | .dataCenter(dataCenter) 22 | .build(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/config/swagger/SwaggerProperties.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.config.swagger; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import springfox.documentation.service.Contact; 8 | 9 | @Getter 10 | @Setter 11 | @ConfigurationProperties(prefix = "swagger") 12 | class SwaggerProperties { 13 | 14 | @Value("${spring.application.name}") 15 | private String appName; 16 | 17 | @Value("${spring.application.description}") 18 | private String description; 19 | 20 | private String version; 21 | 22 | private Host host; 23 | private Contact contact; 24 | private License license; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/domain/exception/BaseTrendyolException.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.exception; 2 | 3 | import lombok.Getter; 4 | import org.apache.commons.lang3.ArrayUtils; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | @Getter 8 | public abstract class BaseTrendyolException extends RuntimeException { 9 | 10 | private final String key; 11 | private final String[] args; 12 | 13 | protected BaseTrendyolException(String key) { 14 | this.key = key; 15 | this.args = ArrayUtils.EMPTY_STRING_ARRAY; 16 | } 17 | 18 | protected BaseTrendyolException(String key, String... args) { 19 | this.key = key; 20 | this.args = args; 21 | } 22 | 23 | @Override 24 | public String getMessage() { 25 | return "Business exception occurred " + key + " " + StringUtils.join(args); 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/domain/command/TerminateDeploymentCommandTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.domain.command; 2 | 3 | import com.trendyol.kubernetesoperatorapi.base.BaseTest; 4 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 8 | 9 | class TerminateDeploymentCommandTest extends BaseTest { 10 | 11 | @Test 12 | void should_convert() { 13 | //given 14 | String runId = "runid"; 15 | DataCenter dataCenter = DataCenter.AWS; 16 | 17 | //when 18 | TerminateDeploymentCommand command = TerminateDeploymentCommand.of(runId, dataCenter); 19 | 20 | //then 21 | assertThat(command.getRunId()).isEqualTo("runid"); 22 | assertThat(command.getDataCenter()).isEqualTo(DataCenter.AWS); 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/base/OrderTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.base; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.mockito.InOrder; 5 | import org.mockito.Mock; 6 | import org.mockito.Spy; 7 | 8 | import java.util.Arrays; 9 | 10 | import static org.mockito.Mockito.inOrder; 11 | 12 | public class OrderTest extends BaseTest { 13 | 14 | protected InOrder inOrder; 15 | 16 | @BeforeEach 17 | void setUpInOrder() { 18 | inOrder = inOrder(Arrays.stream(this.getClass().getDeclaredFields()) 19 | .filter(field -> (field.isAnnotationPresent(Mock.class) || field.isAnnotationPresent(Spy.class))) 20 | .map(field -> { 21 | try { 22 | field.setAccessible(true); 23 | return field.get(this); 24 | } catch (IllegalAccessException e) { 25 | throw new RuntimeException(e); 26 | } 27 | }).toArray()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/common/BaseResponse.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.common; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | import org.apache.commons.lang3.StringUtils; 8 | 9 | import java.io.Serializable; 10 | import java.util.List; 11 | import java.util.stream.Stream; 12 | 13 | @Getter 14 | @Setter 15 | @ToString 16 | @NoArgsConstructor 17 | public class BaseResponse implements Serializable { 18 | 19 | private String exception; 20 | private List errorDetailModel; 21 | private long timeStamp; 22 | 23 | public BaseResponse(String exception, String... exceptionMessage) { 24 | if (StringUtils.isNotBlank(exception)) { 25 | this.exception = exception; 26 | } 27 | 28 | Stream.of(exceptionMessage).forEach(msg -> { 29 | var errorDetail = new ErrorDetailDTO(); 30 | errorDetail.setMessage(msg); 31 | this.errorDetailModel.add(errorDetail); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/application/ScaleOperatorFacade.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.application; 2 | 3 | import com.trendyol.kubernetesoperatorapi.application.port.OperatorApiPort; 4 | import com.trendyol.kubernetesoperatorapi.domain.command.ScaleDeploymentCommand; 5 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Slf4j 11 | @Service 12 | @RequiredArgsConstructor 13 | public class ScaleOperatorFacade { 14 | 15 | private final OperatorApiPort operatorApiPort; 16 | 17 | public void scaleDeployment(ScaleDeploymentCommand command) { 18 | try { 19 | operatorApiPort.scaleDeployment(command); 20 | } catch (Exception e) { 21 | log.error("Encountered an exception while scaling deployment for runId: {}", command.getRunId()); 22 | throw new BusinessException("Encountered an exception while scaling deployment"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Trendyol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/rest/index/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.index; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | import org.springframework.web.servlet.view.RedirectView; 9 | import springfox.documentation.annotations.ApiIgnore; 10 | 11 | import javax.servlet.http.HttpServletResponse; 12 | 13 | @ApiIgnore 14 | @RestController 15 | @RequestMapping("/") 16 | public class IndexController { 17 | 18 | @GetMapping 19 | public RedirectView redirectToSwaggerUi(HttpServletResponse response) { 20 | response.setHeader("Cache-Control", "no-cache"); 21 | return new RedirectView("/swagger-ui/index.html"); 22 | } 23 | 24 | @GetMapping("_monitoring/health") 25 | public ResponseEntity health() { 26 | return new ResponseEntity<>(HttpStatus.OK); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/rest/operator/scale/ScaleOperatorController.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.operator.scale; 2 | 3 | import com.trendyol.kubernetesoperatorapi.adapter.rest.operator.scale.request.ScaleDeploymentRequest; 4 | import com.trendyol.kubernetesoperatorapi.application.ScaleOperatorFacade; 5 | import com.trendyol.kubernetesoperatorapi.domain.command.ScaleDeploymentCommand; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.web.bind.annotation.PutMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import javax.validation.Valid; 12 | 13 | @RestController 14 | @RequiredArgsConstructor 15 | public class ScaleOperatorController { 16 | 17 | private final ScaleOperatorFacade scaleOperatorFacade; 18 | 19 | @PutMapping("/v1/deployments") 20 | public void scaleDeployment(@RequestBody @Valid ScaleDeploymentRequest request) { 21 | ScaleDeploymentCommand command = request.toModel(); 22 | (new Thread(() -> scaleOperatorFacade.scaleDeployment(command))).start(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/adapter/rest/operator/scale/request/ScaleDeploymentRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.operator.scale.request; 2 | 3 | import com.trendyol.kubernetesoperatorapi.base.BaseTest; 4 | import com.trendyol.kubernetesoperatorapi.domain.command.ScaleDeploymentCommand; 5 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 9 | 10 | class ScaleDeploymentRequestTest extends BaseTest { 11 | 12 | @Test 13 | void should_convert() { 14 | //given 15 | ScaleDeploymentRequest request = ScaleDeploymentRequest.builder() 16 | .runId("runid") 17 | .dataCenter(DataCenter.AWS) 18 | .workerCount(3) 19 | .build(); 20 | 21 | //when 22 | ScaleDeploymentCommand command = request.toModel(); 23 | 24 | //then 25 | assertThat(command.getRunId()).isEqualTo("runid"); 26 | assertThat(command.getDataCenter()).isEqualTo(DataCenter.AWS); 27 | assertThat(command.getWorkerCount()).isEqualByComparingTo(3); 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/kubernetes/strategy/KubernetesClient.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy; 2 | 3 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 4 | import org.springframework.beans.factory.ListableBeanFactory; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | 11 | @Service 12 | public class KubernetesClient { 13 | 14 | private final Map kubernetesClientStrategyHashMap = new HashMap<>(); 15 | 16 | public KubernetesClient(ListableBeanFactory beanFactory) { 17 | beanFactory.getBeansOfType(KubernetesClientStrategy.class) 18 | .values() 19 | .forEach(service -> kubernetesClientStrategyHashMap.put(service.getDataCenter(), service)); 20 | } 21 | 22 | public DataCenterSettings getClient(String dataCenter) { 23 | return Optional.ofNullable(kubernetesClientStrategyHashMap.get(dataCenter)) 24 | .orElseThrow(() -> new BusinessException("Kubernetes client strategy is not found !")) 25 | .getClient(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/rest/operator/terminate/TerminateOperatorController.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.operator.terminate; 2 | 3 | import com.trendyol.kubernetesoperatorapi.application.TerminateOperatorFacade; 4 | import com.trendyol.kubernetesoperatorapi.domain.command.TerminateDeploymentCommand; 5 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.web.bind.annotation.DeleteMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @RequiredArgsConstructor 13 | public class TerminateOperatorController { 14 | 15 | private final TerminateOperatorFacade terminateOperatorFacade; 16 | 17 | @DeleteMapping("/v1/deployments/{runId}/data-center/{dataCenter}") 18 | public void terminateDeployment(@PathVariable String runId, @PathVariable DataCenter dataCenter) { 19 | TerminateDeploymentCommand command = TerminateDeploymentCommand.of(runId, dataCenter); 20 | (new Thread(() -> terminateOperatorFacade.terminateDeployment(command))).start(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/worker-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: worker-deployment-__RUN_ID__ 5 | labels: 6 | name: worker-deployment 7 | id: __RUN_ID__ 8 | namespace: deployment 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: worker-deployment 14 | template: 15 | labels: 16 | app: worker-deployment 17 | id: __RUN_ID__ 18 | spec: 19 | containers: 20 | - name: worker-deployment 21 | image: __IMAGE__ 22 | imagePullPolicy: Always 23 | resources: 24 | requests: 25 | memory: 500Mi 26 | cpu: "200m" 27 | limits: 28 | memory: 3Gi 29 | cpu: "1" 30 | lifecycle: 31 | preStop: 32 | exec: 33 | command: 34 | - /bin/sh 35 | - -c 36 | - pkill -f deployment 37 | env: 38 | - name: MODE 39 | value: worker 40 | - name: MASTER_URL 41 | value: worker-deployment-__RUN_ID__ 42 | - name: DATA_CENTER 43 | value: __DATA_CENTER__ 44 | - name: RUN_ID 45 | value: __RUN_ID__ -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/adapter/rest/operator/create/request/CreateDeploymentRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.operator.create.request; 2 | 3 | import com.trendyol.kubernetesoperatorapi.base.BaseTest; 4 | import com.trendyol.kubernetesoperatorapi.domain.command.CreateDeploymentCommand; 5 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 9 | 10 | class CreateDeploymentRequestTest extends BaseTest { 11 | 12 | @Test 13 | void should_convert() { 14 | //given 15 | CreateDeploymentRequest request = CreateDeploymentRequest.builder() 16 | .runId("runid") 17 | .dataCenter(DataCenter.AWS) 18 | .workerCount(3) 19 | .build(); 20 | 21 | //when 22 | CreateDeploymentCommand command = request.toModel(); 23 | 24 | //then 25 | assertThat(command.getRunId()).isEqualTo("runid"); 26 | assertThat(command.getDataCenter()).isEqualTo(DataCenter.AWS); 27 | assertThat(command.getWorkerCount()).isEqualByComparingTo(3); 28 | assertThat(command.getImageName()).isNull(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/rest/operator/create/request/CreateDeploymentRequest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.operator.create.request; 2 | 3 | import com.trendyol.kubernetesoperatorapi.domain.command.CreateDeploymentCommand; 4 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.NotNull; 12 | import javax.validation.constraints.Pattern; 13 | import javax.validation.constraints.Positive; 14 | 15 | @Data 16 | @Builder 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class CreateDeploymentRequest { 20 | 21 | @NotBlank 22 | @Pattern(regexp = "^[a-z0-9]*$") 23 | private String runId; 24 | 25 | @NotNull 26 | private DataCenter dataCenter; 27 | 28 | @NotNull 29 | @Positive 30 | private int workerCount; 31 | 32 | public CreateDeploymentCommand toModel() { 33 | return CreateDeploymentCommand.builder() 34 | .runId(runId) 35 | .dataCenter(dataCenter) 36 | .workerCount(workerCount) 37 | .build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/rest/operator/scale/request/ScaleDeploymentRequest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.operator.scale.request; 2 | 3 | import com.trendyol.kubernetesoperatorapi.domain.command.ScaleDeploymentCommand; 4 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.NotNull; 12 | import javax.validation.constraints.Pattern; 13 | import javax.validation.constraints.PositiveOrZero; 14 | 15 | @Data 16 | @Builder 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class ScaleDeploymentRequest { 20 | 21 | @NotBlank 22 | @Pattern(regexp = "^[a-z0-9]*$") 23 | private String runId; 24 | 25 | @NotNull 26 | private DataCenter dataCenter; 27 | 28 | @NotNull 29 | @PositiveOrZero 30 | private int workerCount; 31 | 32 | public ScaleDeploymentCommand toModel() { 33 | return ScaleDeploymentCommand.builder() 34 | .runId(runId) 35 | .dataCenter(dataCenter) 36 | .workerCount(workerCount) 37 | .build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/rest/operator/create/CreateOperatorController.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.operator.create; 2 | 3 | import com.trendyol.kubernetesoperatorapi.adapter.rest.operator.create.request.CreateDeploymentRequest; 4 | import com.trendyol.kubernetesoperatorapi.application.CreateOperatorFacade; 5 | import com.trendyol.kubernetesoperatorapi.domain.command.CreateDeploymentCommand; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.ResponseStatus; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import javax.validation.Valid; 14 | 15 | @RestController 16 | @RequiredArgsConstructor 17 | public class CreateOperatorController { 18 | 19 | private final CreateOperatorFacade createOperatorFacade; 20 | 21 | @PostMapping("/v1/deployments") 22 | @ResponseStatus(HttpStatus.CREATED) 23 | public void createDeployment(@RequestBody @Valid CreateDeploymentRequest request) { 24 | CreateDeploymentCommand command = request.toModel(); 25 | (new Thread(() -> createOperatorFacade.createDeployment(command))).start(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/master-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: master-deployment-__RUN_ID__ 5 | labels: 6 | name: master-deployment 7 | id: __RUN_ID__ 8 | namespace: deployment 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: master-deployment 14 | id: __RUN_ID__ 15 | template: 16 | metadata: 17 | labels: 18 | app: master-deployment 19 | id: __RUN_ID__ 20 | spec: 21 | containers: 22 | - name: master-deployment 23 | image: __IMAGE__ 24 | imagePullPolicy: Always 25 | resources: 26 | requests: 27 | memory: "256Mi" 28 | cpu: "200m" 29 | limits: 30 | memory: "1Gi" 31 | cpu: "500m" 32 | env: 33 | - name: MODE 34 | value: master 35 | - name: RUN_ID 36 | value: __RUN_ID__ 37 | - name: DATA_CENTER 38 | value: __DATA_CENTER__ 39 | ports: 40 | - name: master-web 41 | containerPort: 1234 42 | protocol: TCP 43 | - name: master-p1 44 | containerPort: 1235 45 | protocol: TCP 46 | - name: master-p2 47 | containerPort: 1236 48 | protocol: TCP 49 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/interceptor/ExecutorUserInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.interceptor; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.slf4j.MDC; 5 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.util.Optional; 10 | 11 | import static com.trendyol.kubernetesoperatorapi.infra.constant.AuditorConstants.X_EXECUTOR_USER; 12 | 13 | public class ExecutorUserInterceptor extends HandlerInterceptorAdapter { 14 | 15 | @Override 16 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 17 | String executorUser = Optional.ofNullable(request.getHeader(X_EXECUTOR_USER)) 18 | .map(Object::toString) 19 | .orElse(Optional 20 | .ofNullable(request.getAttribute(X_EXECUTOR_USER)) 21 | .map(Object::toString).orElse(StringUtils.EMPTY)); 22 | MDC.put(X_EXECUTOR_USER, executorUser); 23 | return true; 24 | } 25 | 26 | @Override 27 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { 28 | MDC.remove(X_EXECUTOR_USER); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/interceptor/AgentNameInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.interceptor; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.slf4j.MDC; 6 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 7 | 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | 11 | import static com.trendyol.kubernetesoperatorapi.infra.constant.AuditorConstants.X_AGENTNAME; 12 | import static com.trendyol.kubernetesoperatorapi.infra.constant.AuditorConstants.X_AGENTNAME_KEBAB_CASE; 13 | 14 | @RequiredArgsConstructor 15 | public class AgentNameInterceptor extends HandlerInterceptorAdapter { 16 | 17 | private final String applicationName; 18 | 19 | @Override 20 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 21 | String agentName = request.getHeader(X_AGENTNAME); 22 | 23 | if (StringUtils.isBlank(agentName)) { 24 | agentName = request.getHeader(X_AGENTNAME_KEBAB_CASE); 25 | } 26 | 27 | if (StringUtils.isBlank(agentName)) { 28 | agentName = applicationName; 29 | } 30 | 31 | MDC.put(X_AGENTNAME, agentName); 32 | return true; 33 | } 34 | 35 | @Override 36 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { 37 | MDC.remove(X_AGENTNAME); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/interceptor/CorrelationIdInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.interceptor; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.slf4j.MDC; 5 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.util.UUID; 10 | 11 | import static com.trendyol.kubernetesoperatorapi.infra.constant.AuditorConstants.*; 12 | 13 | public class CorrelationIdInterceptor extends HandlerInterceptorAdapter { 14 | 15 | @Override 16 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { 17 | String correlationId = request.getHeader(X_CORRELATION_ID); 18 | 19 | if (StringUtils.isBlank(correlationId)) { 20 | correlationId = request.getHeader(X_CORRELATION_ID_CAMEL_CASE); 21 | } 22 | 23 | if (StringUtils.isBlank(correlationId)) { 24 | correlationId = request.getHeader(X_CORRELATION_ID_KEBAB_CASE); 25 | } 26 | 27 | if (StringUtils.isBlank(correlationId)) { 28 | correlationId = UUID.randomUUID().toString(); 29 | } 30 | 31 | MDC.put(X_CORRELATION_ID, correlationId); 32 | return true; 33 | } 34 | 35 | @Override 36 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { 37 | MDC.remove(X_CORRELATION_ID); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/constant/Constant.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.constant; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.NoArgsConstructor; 5 | 6 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 7 | public final class Constant { 8 | 9 | public static final String MASTER_SERVICE_UI_NAME_PREFIX = "master-service-ui-"; 10 | public static final String MASTER_SERVICE_NAME_PREFIX = "master-service-"; 11 | 12 | public static final String MASTER_DEPLOYMENT_NAME_PREFIX = "master-deployment-"; 13 | public static final String WORKER_DEPLOYMENT_NAME_PREFIX = "worker-deployment-"; 14 | 15 | public static final String DEFAULT_IMAGE_NAME_PREFIX = "default-image-prefix"; 16 | 17 | public static final String APP_NAME_MASTER = "app-name-master"; 18 | public static final String APP_NAME_WORKER = "app-name-worker"; 19 | 20 | public static final String NAMESPACE = "namespace"; 21 | 22 | public static final String MASTER_DEPLOYMENT_YAML_PATH = "/master-deployment.yaml"; 23 | public static final String WORKER_DEPLOYMENT_YAML_PATH = "/worker-deployment.yaml"; 24 | public static final String MASTER_SERVICE_YAML_PATH = "/master-service.yaml"; 25 | public static final String MASTER_SERVICE_UI_YAML_PATH = "/master-service-ui.yaml"; 26 | 27 | public static final String MASTER_SERVICE = "/master-service"; 28 | public static final String MASTER_SERVICE_UI = "/master-service-ui"; 29 | 30 | public static final String SUCCESS = "Success"; 31 | 32 | public static final int INTERVAL_VALUE_FOR_ROLLOUT = 1000; 33 | public static final int TIMEOUT_VALUE_FOR_ROLLOUT = 60000 * 3; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/interceptor/InterceptorConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.interceptor; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 6 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | import static org.springframework.web.bind.annotation.RequestMethod.*; 10 | 11 | @Configuration 12 | public class InterceptorConfiguration implements WebMvcConfigurer { 13 | 14 | private static final int MAX_AGE = 3600; 15 | 16 | @Value("${spring.application.name}") 17 | private String applicationName; 18 | 19 | @Value("${cors.webInternalIp}") 20 | private String webInternalIp; 21 | 22 | @Value("${cors.webExternalIp}") 23 | private String webExternalIp; 24 | 25 | @Override 26 | public void addInterceptors(InterceptorRegistry registry) { 27 | registry.addInterceptor(new CorrelationIdInterceptor()); 28 | registry.addInterceptor(new ExecutorUserInterceptor()); 29 | registry.addInterceptor(new AgentNameInterceptor(applicationName)); 30 | } 31 | 32 | @Override 33 | public void addCorsMappings(CorsRegistry registry) { 34 | registry.addMapping("/**") 35 | .allowedOrigins(webInternalIp, webExternalIp) 36 | .allowedHeaders("*") 37 | .allowCredentials(true) 38 | .allowedMethods(GET.name(), POST.name(), DELETE.name(), PUT.name(), OPTIONS.name(), PATCH.name()) 39 | .maxAge(MAX_AGE); 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/kubernetes/KubernetesApiMockAdapter.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.kubernetes; 2 | 3 | import com.trendyol.kubernetesoperatorapi.application.port.OperatorApiPort; 4 | import com.trendyol.kubernetesoperatorapi.domain.command.CreateDeploymentCommand; 5 | import com.trendyol.kubernetesoperatorapi.domain.command.ScaleDeploymentCommand; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Slf4j 11 | @Service 12 | @ConditionalOnProperty(value = "mock.kubernetes.api.enabled", havingValue = "true") 13 | public class KubernetesApiMockAdapter implements OperatorApiPort { 14 | 15 | @Override 16 | public void createDeployment(CreateDeploymentCommand command) { 17 | log.info("Started mock create deployment by runId: {}", command.getRunId()); 18 | log.info("Finished mock create deployment by runId: {}", command.getRunId()); 19 | } 20 | 21 | @Override 22 | public void scaleDeployment(ScaleDeploymentCommand command) { 23 | log.info("Started mock scale deployment by runId: {}", command.getRunId()); 24 | log.info("Finished mock scale deployment by runId: {}", command.getRunId()); 25 | } 26 | 27 | @Override 28 | public void deleteDeployment(String dataCenterName, String deploymentName) { 29 | log.info("Started mock delete deployment"); 30 | log.info("Finished mock delete deployment"); 31 | } 32 | 33 | @Override 34 | public void deleteService(String dataCenterName, String serviceName) { 35 | log.info("Started mock delete service"); 36 | log.info("Finished mock delete service"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/kubernetes/component/KubernetesYamlConverterComponent.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.kubernetes.component; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 5 | import com.google.gson.Gson; 6 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.InputStreamReader; 12 | import java.util.Map; 13 | import java.util.stream.Collectors; 14 | 15 | @Slf4j 16 | @Component 17 | public class KubernetesYamlConverterComponent { 18 | 19 | public Object convertKubernetesYml(Map variables, String path, Class clazz) { 20 | var mapper = new ObjectMapper(new YAMLFactory()); 21 | try { 22 | var inputStream = getClass().getResourceAsStream(path); 23 | var reader = new BufferedReader(new InputStreamReader(inputStream)); 24 | var contents = reader.lines().collect(Collectors.joining(System.lineSeparator())); 25 | var deployment = mapper.readValue(contents, clazz); 26 | var json = new Gson().toJson(deployment); 27 | for (var entry : variables.entrySet()) { 28 | String variable = "__" + entry.getKey() + "__"; 29 | json = json.replaceAll(variable, entry.getValue()); 30 | } 31 | return new Gson().fromJson(json, clazz); 32 | } catch (Exception e) { 33 | log.error("Cannot convert kubernetes yml exceptionMessage: {}", e.getMessage()); 34 | throw new BusinessException("Cannot convert kubernetes yml."); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | spring: 3 | application: 4 | name: 'Kubernetes Operator Api' 5 | description: 'Kubernetes Operator Api Documentation' 6 | main: 7 | allow-bean-definition-overriding: true 8 | mvc: 9 | pathmatch: 10 | matching-strategy: ant_path_matcher 11 | swagger: 12 | host: 13 | url: localhost:1234 14 | protocol: http 15 | contact: 16 | name: Trendyol 17 | url: https://www.trendyol.com 18 | license: 19 | name: Apache 2.0 20 | url: http://www.apache.org/licenses/LICENSE-2.0.html 21 | version: '@version@' 22 | 23 | server: 24 | servlet: 25 | context-path: / 26 | port: 1234 27 | error: 28 | whitelabel: 29 | enabled: false 30 | compression: 31 | enabled: true 32 | mime-types: application/json,application/xml,text/html,text/xml,text/plain 33 | 34 | hystrix: 35 | command: 36 | default: 37 | execution: 38 | timeout: 39 | enabled: false 40 | isolation: 41 | thread: 42 | timeoutInMilliseconds:5000 43 | 44 | logging: 45 | pattern: 46 | console: "{\"time\": \"%d{yyyy-MM-dd'T'HH:mm:ss}Z\", \"level\": \"%p\", \"agent-name\": \"%X{x-agentname}\", \"correlation-id\": \"%X{x-correlationid}\", \"executor-user\": \"%X{x-executor-user}\", \"remote-host\": \"%X{x-remote-host}\", \"request-path\":\"%X{Request-Path}\", \"user-agent\":\"%X{User-Agent}\", \"source\":\"%logger{63}:%L\", \"message\": \"%replace(%m%wEx{10}){'[\r\n]+', '\n'}%nopex\"}%n" 47 | level: 48 | com.trendyol: INFO 49 | 50 | --- 51 | spring: 52 | profiles: stage 53 | 54 | cors: 55 | webInternalIp: http://localhost:3000 56 | webExternalIp: https://www.trendyol.com 57 | 58 | --- 59 | spring: 60 | profiles: prod 61 | 62 | cors: 63 | webInternalIp: http://localhost:3000 64 | webExternalIp: https://www.trendyol.com -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/annotation/aspect/RetryOperationAspect.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.annotation.aspect; 2 | 3 | import com.trendyol.kubernetesoperatorapi.infra.annotation.RetryOperation; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.aspectj.lang.ProceedingJoinPoint; 6 | import org.aspectj.lang.annotation.Around; 7 | import org.aspectj.lang.annotation.Aspect; 8 | import org.aspectj.lang.reflect.MethodSignature; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.lang.reflect.Method; 12 | 13 | @Slf4j 14 | @Aspect 15 | @Component 16 | public class RetryOperationAspect { 17 | 18 | @Around(value = "@annotation(com.trendyol.kubernetesoperatorapi.infra.annotation.RetryOperation)") 19 | public Object retryOperation(ProceedingJoinPoint joinPoint) throws Throwable { 20 | Object response = null; 21 | Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); 22 | RetryOperation annotation = method.getAnnotation(RetryOperation.class); 23 | int retryCount = annotation.retryCount(); 24 | int waitSeconds = annotation.waitSeconds(); 25 | boolean successful = false; 26 | 27 | do { 28 | try { 29 | response = joinPoint.proceed(); 30 | successful = true; 31 | } catch (Exception e) { 32 | log.error("Operation failed, retries remaining: {}", retryCount); 33 | retryCount--; 34 | if (retryCount <= 0) { 35 | throw e; 36 | } 37 | if (waitSeconds > 0) { 38 | log.warn("Waiting for {} second(s) before next retry", waitSeconds); 39 | Thread.sleep(waitSeconds * 1000L); 40 | } 41 | } 42 | } while (!successful); 43 | 44 | return response; 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/config/swagger/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.config.swagger; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import springfox.documentation.builders.RequestHandlerSelectors; 8 | import springfox.documentation.service.ApiInfo; 9 | import springfox.documentation.spi.DocumentationType; 10 | import springfox.documentation.spring.web.plugins.Docket; 11 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 12 | 13 | import java.util.Collections; 14 | import java.util.Optional; 15 | 16 | @Configuration 17 | @EnableSwagger2 18 | @RequiredArgsConstructor 19 | @EnableConfigurationProperties(SwaggerProperties.class) 20 | public class SwaggerConfig { 21 | 22 | private static final String EMPTY_VALUE = "-"; 23 | 24 | private final SwaggerProperties properties; 25 | 26 | @Bean 27 | public Docket api() { 28 | return new Docket(DocumentationType.SWAGGER_2) 29 | .select() 30 | .apis(RequestHandlerSelectors.any()) 31 | .build() 32 | .apiInfo(apiInfo()); 33 | } 34 | 35 | private ApiInfo apiInfo() { 36 | var license = properties.getLicense(); 37 | return new ApiInfo( 38 | properties.getAppName(), 39 | properties.getDescription(), 40 | properties.getVersion(), 41 | EMPTY_VALUE, 42 | properties.getContact(), 43 | Optional.ofNullable(license).map(License::getName).orElse(EMPTY_VALUE), 44 | Optional.ofNullable(license).map(License::getUrl).orElse(EMPTY_VALUE), 45 | Collections.emptyList()); 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/application/TerminateOperatorFacadeTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.application; 2 | 3 | import com.trendyol.kubernetesoperatorapi.application.port.OperatorApiPort; 4 | import com.trendyol.kubernetesoperatorapi.base.OrderTest; 5 | import com.trendyol.kubernetesoperatorapi.domain.command.TerminateDeploymentCommand; 6 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.mockito.Mock; 10 | 11 | import static com.trendyol.kubernetesoperatorapi.infra.constant.Constant.*; 12 | 13 | class TerminateOperatorFacadeTest extends OrderTest { 14 | 15 | private TerminateOperatorFacade terminateOperatorFacade; 16 | 17 | @Mock 18 | private OperatorApiPort operatorApiPort; 19 | 20 | @BeforeEach 21 | void setUp() { 22 | terminateOperatorFacade = new TerminateOperatorFacade(operatorApiPort); 23 | } 24 | 25 | @Test 26 | void should_terminate_deployment() { 27 | //given 28 | TerminateDeploymentCommand command = TerminateDeploymentCommand.builder() 29 | .runId("runid") 30 | .dataCenter(DataCenter.AWS) 31 | .build(); 32 | 33 | //when 34 | terminateOperatorFacade.terminateDeployment(command); 35 | 36 | //then 37 | inOrder.verify(operatorApiPort).deleteDeployment(command.getDataCenter().getValue(), MASTER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 38 | inOrder.verify(operatorApiPort).deleteDeployment(command.getDataCenter().getValue(), WORKER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 39 | inOrder.verify(operatorApiPort).deleteService(command.getDataCenter().getValue(), MASTER_SERVICE_NAME_PREFIX + command.getRunId()); 40 | inOrder.verify(operatorApiPort).deleteService(command.getDataCenter().getValue(), MASTER_SERVICE_UI_NAME_PREFIX + command.getRunId()); 41 | inOrder.verifyNoMoreInteractions(); 42 | } 43 | } -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/adapter/rest/operator/terminate/TerminateOperatorControllerMockMvcTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.operator.terminate; 2 | 3 | import com.trendyol.kubernetesoperatorapi.application.TerminateOperatorFacade; 4 | import com.trendyol.kubernetesoperatorapi.base.AbstractMvc; 5 | import com.trendyol.kubernetesoperatorapi.domain.command.TerminateDeploymentCommand; 6 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 9 | import org.springframework.boot.test.mock.mockito.MockBean; 10 | import org.springframework.http.MediaType; 11 | 12 | import static org.mockito.Mockito.verify; 13 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 16 | 17 | @WebMvcTest(TerminateOperatorController.class) 18 | class TerminateOperatorControllerMockMvcTest extends AbstractMvc { 19 | 20 | @MockBean 21 | private TerminateOperatorFacade terminateOperatorFacade; 22 | 23 | @Test 24 | void should_terminate_deployment() throws Exception { 25 | //given 26 | String runId = "runid"; 27 | DataCenter dataCenter = DataCenter.AWS; 28 | 29 | TerminateDeploymentCommand command = TerminateDeploymentCommand.of(runId, dataCenter); 30 | //when 31 | mockMvc.perform( 32 | delete("/v1/deployments/" + runId + "/data-center/" + dataCenter) 33 | .accept(MediaType.APPLICATION_JSON) 34 | .contentType(MediaType.APPLICATION_JSON)) 35 | .andDo(print()) 36 | .andExpect(status().isOk()); 37 | 38 | //then 39 | verify(terminateOperatorFacade).terminateDeployment(command); 40 | } 41 | } -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/adapter/rest/operator/scale/ScaleOperatorControllerMockMvcTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.operator.scale; 2 | 3 | import com.trendyol.kubernetesoperatorapi.adapter.rest.operator.scale.request.ScaleDeploymentRequest; 4 | import com.trendyol.kubernetesoperatorapi.application.ScaleOperatorFacade; 5 | import com.trendyol.kubernetesoperatorapi.base.AbstractMvc; 6 | import com.trendyol.kubernetesoperatorapi.domain.command.ScaleDeploymentCommand; 7 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.http.MediaType; 12 | 13 | import static org.mockito.Mockito.verify; 14 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 16 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 17 | 18 | @WebMvcTest(ScaleOperatorController.class) 19 | class ScaleOperatorControllerMockMvcTest extends AbstractMvc { 20 | 21 | @MockBean 22 | private ScaleOperatorFacade scaleOperatorFacade; 23 | 24 | @Test 25 | void should_scale_deployment() throws Exception { 26 | //given 27 | ScaleDeploymentRequest request = ScaleDeploymentRequest.builder() 28 | .runId("runid") 29 | .dataCenter(DataCenter.AWS) 30 | .workerCount(3) 31 | .build(); 32 | 33 | ScaleDeploymentCommand command = request.toModel(); 34 | 35 | //when 36 | mockMvc.perform( 37 | put("/v1/deployments") 38 | .accept(MediaType.APPLICATION_JSON) 39 | .contentType(MediaType.APPLICATION_JSON) 40 | .content(objectMapper.writeValueAsString(request))) 41 | .andDo(print()) 42 | .andExpect(status().isOk()); 43 | 44 | //then 45 | verify(scaleOperatorFacade).scaleDeployment(command); 46 | } 47 | } -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/adapter/rest/operator/create/CreateOperatorControllerMockMvcTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.rest.operator.create; 2 | 3 | import com.trendyol.kubernetesoperatorapi.adapter.rest.operator.create.request.CreateDeploymentRequest; 4 | import com.trendyol.kubernetesoperatorapi.application.CreateOperatorFacade; 5 | import com.trendyol.kubernetesoperatorapi.base.AbstractMvc; 6 | import com.trendyol.kubernetesoperatorapi.domain.command.CreateDeploymentCommand; 7 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.http.MediaType; 12 | 13 | import static org.mockito.Mockito.verify; 14 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 16 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 17 | 18 | @WebMvcTest(CreateOperatorController.class) 19 | class CreateOperatorControllerMockMvcTest extends AbstractMvc { 20 | 21 | @MockBean 22 | private CreateOperatorFacade createOperatorFacade; 23 | 24 | @Test 25 | void should_create_deployment() throws Exception { 26 | //given 27 | CreateDeploymentRequest request = CreateDeploymentRequest.builder() 28 | .runId("runid") 29 | .dataCenter(DataCenter.AWS) 30 | .workerCount(3) 31 | .build(); 32 | 33 | CreateDeploymentCommand command = request.toModel(); 34 | 35 | //when 36 | mockMvc.perform( 37 | post("/v1/deployments") 38 | .accept(MediaType.APPLICATION_JSON) 39 | .contentType(MediaType.APPLICATION_JSON) 40 | .content(objectMapper.writeValueAsString(request))) 41 | .andDo(print()) 42 | .andExpect(status().isCreated()); 43 | 44 | //then 45 | verify(createOperatorFacade).createDeployment(command); 46 | } 47 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Intellij ### 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 4 | 5 | # User-specific stuff - Generated files - Sensitive or high-churn files - Gradle 6 | .idea/ 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # File-based project format 12 | *.iws 13 | 14 | # mpeltonen/sbt-idea plugin 15 | .idea_modules/ 16 | 17 | # JIRA plugin 18 | atlassian-ide-plugin.xml 19 | 20 | ### Intellij Patch ### 21 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 22 | 23 | *.iml 24 | modules.xml 25 | *.ipr 26 | 27 | ### Java ### 28 | # Compiled class file 29 | target/ 30 | test-output/ 31 | *.class 32 | .classpath 33 | .metadata 34 | 35 | # Log file 36 | logs/ 37 | *.log 38 | src/test/resources/external/*.txt 39 | 40 | # BlueJ files 41 | *.ctxt 42 | 43 | # Mobile Tools for Java (J2ME) 44 | .mtj.tmp/ 45 | 46 | # Package Files # 47 | *.jar 48 | *.war 49 | *.nar 50 | *.ear 51 | *.zip 52 | *.tar.gz 53 | *.rar 54 | 55 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 56 | hs_err_pid* 57 | 58 | ### macOS ### 59 | # General 60 | .DS_Store 61 | .AppleDouble 62 | .LSOverride 63 | 64 | # Icon must end with two \r 65 | Icon 66 | 67 | # Thumbnails 68 | ._* 69 | 70 | # Files that might appear in the root of a volume 71 | .DocumentRevisions-V100 72 | .fseventsd 73 | .Spotlight-V100 74 | .TemporaryItems 75 | .Trashes 76 | .VolumeIcon.icns 77 | .com.apple.timemachine.donotpresent 78 | 79 | # Directories potentially created on remote AFP share 80 | .AppleDB 81 | .AppleDesktop 82 | Network Trash Folder 83 | Temporary Items 84 | .apdisk 85 | 86 | ### Windows ### 87 | # Windows thumbnail cache files 88 | Thumbs.db 89 | ehthumbs.db 90 | ehthumbs_vista.db 91 | 92 | # Dump file 93 | *.stackdump 94 | 95 | # Folder common file 96 | [Dd]esktop.ini 97 | 98 | # Recycle Bin used on file shares 99 | $RECYCLE.BIN/ 100 | 101 | # Windows Installer files 102 | *.cab 103 | *.msi 104 | *.msix 105 | *.msm 106 | *.msp 107 | 108 | # Windows shortcuts 109 | *.lnk 110 | 111 | .allure 112 | .allure-results 113 | 114 | screenshots/ 115 | 116 | ajcore* 117 | *.csv -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/kubernetes/strategy/datacenter/AwsDcStrategy.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.datacenter; 2 | 3 | import com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.DataCenterSettings; 4 | import com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.KubernetesClientStrategy; 5 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 6 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 7 | import io.kubernetes.client.openapi.ApiClient; 8 | import io.kubernetes.client.util.ClientBuilder; 9 | import io.kubernetes.client.util.KubeConfig; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.io.FileReader; 14 | import java.io.IOException; 15 | 16 | import static com.trendyol.kubernetesoperatorapi.infra.constant.Constant.DEFAULT_IMAGE_NAME_PREFIX; 17 | 18 | @Slf4j 19 | @Service 20 | public class AwsDcStrategy implements KubernetesClientStrategy { 21 | 22 | private volatile DataCenterSettings dataCenterSettings = null; 23 | 24 | @Override 25 | public String getDataCenter() { 26 | return DataCenter.AWS.getValue(); 27 | } 28 | 29 | @Override 30 | public DataCenterSettings getClient() { 31 | String kubeConfigPath = "/etc/kube/aws/AWS"; 32 | 33 | if (dataCenterSettings == null) { 34 | synchronized (AwsDcStrategy.class) { 35 | if (dataCenterSettings == null) { 36 | try { 37 | ApiClient apiClient = ClientBuilder.kubeconfig(KubeConfig.loadKubeConfig(new FileReader(kubeConfigPath))).build(); 38 | dataCenterSettings = DataCenterSettings.builder() 39 | .apiClient(apiClient) 40 | .imageNamePrefix(DEFAULT_IMAGE_NAME_PREFIX) 41 | .build(); 42 | } catch (IOException e) { 43 | log.error("Encountered an exception while consuming get client: {} , exception message: {}", e.getMessage(), e); 44 | throw new BusinessException("Encountered an exception while consuming get client"); 45 | } 46 | } 47 | } 48 | } 49 | return dataCenterSettings; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/kubernetes/strategy/datacenter/DummyDcStrategy.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.datacenter; 2 | 3 | import com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.DataCenterSettings; 4 | import com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.KubernetesClientStrategy; 5 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 6 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 7 | import io.kubernetes.client.openapi.ApiClient; 8 | import io.kubernetes.client.util.ClientBuilder; 9 | import io.kubernetes.client.util.KubeConfig; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.io.FileReader; 14 | import java.io.IOException; 15 | 16 | import static com.trendyol.kubernetesoperatorapi.infra.constant.Constant.DEFAULT_IMAGE_NAME_PREFIX; 17 | 18 | @Slf4j 19 | @Service 20 | public class DummyDcStrategy implements KubernetesClientStrategy { 21 | 22 | private volatile DataCenterSettings dataCenterSettings = null; 23 | 24 | @Override 25 | public String getDataCenter() { 26 | return DataCenter.DUMMY.getValue(); 27 | } 28 | 29 | @Override 30 | public DataCenterSettings getClient() { 31 | String kubeConfigPath = "/etc/kube/dummy/DUMMY"; 32 | 33 | if (dataCenterSettings == null) { 34 | synchronized (DummyDcStrategy.class) { 35 | if (dataCenterSettings == null) { 36 | try { 37 | ApiClient apiClient = ClientBuilder.kubeconfig(KubeConfig.loadKubeConfig(new FileReader(kubeConfigPath))).build(); 38 | dataCenterSettings = DataCenterSettings.builder() 39 | .apiClient(apiClient) 40 | .imageNamePrefix(DEFAULT_IMAGE_NAME_PREFIX) 41 | .build(); 42 | } catch (IOException e) { 43 | log.error("Encountered an exception while consuming get client: {} , exception message: {}", e.getMessage(), e); 44 | throw new BusinessException("Encountered an exception while consuming get client"); 45 | } 46 | } 47 | } 48 | } 49 | return dataCenterSettings; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/kubernetes/strategy/datacenter/GoogleDcStrategy.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.datacenter; 2 | 3 | import com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.DataCenterSettings; 4 | import com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.KubernetesClientStrategy; 5 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 6 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 7 | import io.kubernetes.client.openapi.ApiClient; 8 | import io.kubernetes.client.util.ClientBuilder; 9 | import io.kubernetes.client.util.KubeConfig; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.io.FileReader; 14 | import java.io.IOException; 15 | 16 | import static com.trendyol.kubernetesoperatorapi.infra.constant.Constant.DEFAULT_IMAGE_NAME_PREFIX; 17 | 18 | @Slf4j 19 | @Service 20 | public class GoogleDcStrategy implements KubernetesClientStrategy { 21 | 22 | private volatile DataCenterSettings dataCenterSettings = null; 23 | 24 | @Override 25 | public String getDataCenter() { 26 | return DataCenter.GOOGLE.getValue(); 27 | } 28 | 29 | @Override 30 | public DataCenterSettings getClient() { 31 | String kubeConfigPath = "/etc/kube/google/GOOGLE"; 32 | 33 | if (dataCenterSettings == null) { 34 | synchronized (GoogleDcStrategy.class) { 35 | if (dataCenterSettings == null) { 36 | try { 37 | ApiClient apiClient = ClientBuilder.kubeconfig(KubeConfig.loadKubeConfig(new FileReader(kubeConfigPath))).build(); 38 | dataCenterSettings = DataCenterSettings.builder() 39 | .apiClient(apiClient) 40 | .imageNamePrefix(DEFAULT_IMAGE_NAME_PREFIX) 41 | .build(); 42 | } catch (IOException e) { 43 | log.error("Encountered an exception while consuming get client: {} , exception message: {}", e.getMessage(), e); 44 | throw new BusinessException("Encountered an exception while consuming get client"); 45 | } 46 | } 47 | } 48 | } 49 | return dataCenterSettings; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/application/TerminateOperatorFacade.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.application; 2 | 3 | import com.trendyol.kubernetesoperatorapi.application.port.OperatorApiPort; 4 | import com.trendyol.kubernetesoperatorapi.domain.command.TerminateDeploymentCommand; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | 9 | import static com.trendyol.kubernetesoperatorapi.infra.constant.Constant.*; 10 | 11 | @Slf4j 12 | @Service 13 | @RequiredArgsConstructor 14 | public class TerminateOperatorFacade { 15 | 16 | private final OperatorApiPort operatorApiPort; 17 | 18 | public void terminateDeployment(TerminateDeploymentCommand command) { 19 | deleteMasterDeployment(command); 20 | deleteWorkerDeployment(command); 21 | deleteMasterService(command); 22 | deleteMasterUiService(command); 23 | } 24 | 25 | private void deleteMasterDeployment(TerminateDeploymentCommand command) { 26 | try { 27 | operatorApiPort.deleteDeployment(command.getDataCenter().getValue(), MASTER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 28 | } catch (Exception e) { 29 | log.error("Encountered an exception while deleting master deployment for runId: {}", command.getRunId()); 30 | // ! Don't use throw 31 | } 32 | } 33 | 34 | private void deleteWorkerDeployment(TerminateDeploymentCommand command) { 35 | try { 36 | operatorApiPort.deleteDeployment(command.getDataCenter().getValue(), WORKER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 37 | } catch (Exception e) { 38 | log.error("Encountered an exception while deleting worker deployment for runId: {}", command.getRunId()); 39 | // ! Don't use throw 40 | } 41 | } 42 | 43 | private void deleteMasterService(TerminateDeploymentCommand command) { 44 | try { 45 | operatorApiPort.deleteService(command.getDataCenter().getValue(), MASTER_SERVICE_NAME_PREFIX + command.getRunId()); 46 | } catch (Exception e) { 47 | log.error("Encountered an exception while deleting master component for runId: {}", command.getRunId()); 48 | // ! Don't use throw 49 | } 50 | } 51 | 52 | private void deleteMasterUiService(TerminateDeploymentCommand command) { 53 | try { 54 | operatorApiPort.deleteService(command.getDataCenter().getValue(), MASTER_SERVICE_UI_NAME_PREFIX + command.getRunId()); 55 | } catch (Exception e) { 56 | log.error("Encountered an exception while deleting master ui component for runId: {}", command.getRunId()); 57 | // ! Don't use throw 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/application/CreateOperatorFacade.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.application; 2 | 3 | import com.trendyol.kubernetesoperatorapi.application.port.OperatorApiPort; 4 | import com.trendyol.kubernetesoperatorapi.domain.command.CreateDeploymentCommand; 5 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 6 | import com.trendyol.kubernetesoperatorapi.domain.exception.RollBackBusinessException; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.stereotype.Service; 10 | 11 | import static com.trendyol.kubernetesoperatorapi.infra.constant.Constant.*; 12 | 13 | @Slf4j 14 | @Service 15 | @RequiredArgsConstructor 16 | public class CreateOperatorFacade { 17 | 18 | private final OperatorApiPort operatorApiPort; 19 | 20 | public void createDeployment(CreateDeploymentCommand command) { 21 | try { 22 | operatorApiPort.createDeployment(command); 23 | } catch (RollBackBusinessException rollBackBusinessException) { 24 | rollBackDeployment(rollBackBusinessException, command); 25 | } 26 | } 27 | 28 | private void rollBackDeployment(RollBackBusinessException rollBackBusinessException, CreateDeploymentCommand command) { 29 | switch (rollBackBusinessException.getLevel()) { 30 | case 5: 31 | log.error("Rollback master ui service for runId: {}", command.getRunId()); 32 | operatorApiPort.deleteService(command.getDataCenter().name(), MASTER_SERVICE_UI_NAME_PREFIX + command.getRunId()); 33 | // ! Don't use break operator 34 | case 4: 35 | log.error("Rollback service for runId: {}", command.getRunId()); 36 | operatorApiPort.deleteService(command.getDataCenter().name(), MASTER_SERVICE_NAME_PREFIX + command.getRunId()); 37 | // ! Don't use break operator 38 | case 3: 39 | log.error("Rollback worker deployment for runId: {}", command.getRunId()); 40 | operatorApiPort.deleteDeployment(command.getDataCenter().name(), WORKER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 41 | // ! Don't use break operator 42 | case 2: 43 | log.error("Rollback master deployment for runId: {}", command.getRunId()); 44 | operatorApiPort.deleteDeployment(command.getDataCenter().name(), MASTER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 45 | // ! Don't use break operator 46 | case 1: 47 | log.error("Encountered an exception while creating deployment for runId: {}", command.getRunId()); 48 | throw new BusinessException("Encountered an exception while creating deployment"); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Operator Api 2 | 3 | The Kubernetes API is a sample Kubernetes operator API using the Java Kubernetes Client.
4 | The API is designed for master-worker deployment to satisfy the requirements. 5 | 6 | ## Overview 7 | 8 | * The CI/CD tool we used before this API started to not be enough.
9 | The reasons why it is not enough are: 10 | - Deployments take too long to create, scale, and terminate. 11 | - In synchronous requests, some of the scenarios' statuses were pending or failed. 12 | - The pending scenarios could not be retried. 13 | - Rollbacks could not be performed automatically. 14 | - Since there was a strict structure to the deployment process, we could not do the business development at any stage. 15 | 16 | By using this API, above problems have been solved. You can use this API for such requirements.
17 | 18 | * In the resources folder of our application we have:
19 | master-deployment.yaml, worker-deployment.yaml, master-service.yaml, master-service-ui.yaml
20 | You must customize these files to suit you. This pattern __ was used in file reading operations. Let's not overlook it.
21 | Note that you must use all four of these deployment yaml's.
22 | 23 | * The strategy pattern has been used so that the project can work with more than one data center.
24 | You must define the data center strategy you want to use. After that, it will be enough to send it in the data_center field.
25 | 26 | The following are the flows that we have implemented in this project. 27 | 28 | ### Create deployment flow 29 | - Deploy master 30 | - Roll out status 31 | - Deploy worker 32 | - Roll out status 33 | - Create service 34 | - Create service ui 35 | - Print pod status 36 | - Retrieve service list 37 | - Print service status 38 | 39 | ### Scale deployment flow 40 | - Scale deployment 41 | - Roll out status 42 | 43 | ### Terminate deployment flow 44 | - Delete master deployment 45 | - Delete worker deployment 46 | - Delete master service 47 | - Delete master ui service 48 | 49 | --- 50 | 51 | ##### Tech Stack 52 | - Java 11 53 | - Spring Boot 54 | - Kubernetes Client 55 | 56 | ##### Requirements 57 | 58 | For building and running the application, you need: 59 | - [JDK 11](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html) 60 | - [Maven](https://maven.apache.org) 61 | - [Lombok](https://projectlombok.org/) 62 | 63 | ##### Build & Run 64 | 65 | ``` 66 | mvn clean install 67 | mvn --projects kubernetes-operator-api spring-boot:run 68 | ``` 69 | 70 | ##### Port 71 | ``` 72 | http://localhost:1234 73 | ``` 74 | 75 | ##### License 76 | 77 | Distributed under the MIT License. See [LICENSE](LICENSE) for more information. 78 | -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/application/ScaleOperatorFacadeTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.application; 2 | 3 | import com.trendyol.kubernetesoperatorapi.application.port.OperatorApiPort; 4 | import com.trendyol.kubernetesoperatorapi.base.OrderTest; 5 | import com.trendyol.kubernetesoperatorapi.domain.command.ScaleDeploymentCommand; 6 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 7 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.mockito.ArgumentCaptor; 11 | import org.mockito.Captor; 12 | import org.mockito.Mock; 13 | 14 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 15 | import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable; 16 | import static org.mockito.Mockito.doThrow; 17 | 18 | class ScaleOperatorFacadeTest extends OrderTest { 19 | 20 | private ScaleOperatorFacade scaleOperatorFacade; 21 | 22 | @Mock 23 | private OperatorApiPort operatorApiPort; 24 | 25 | @Captor 26 | private ArgumentCaptor scaleDeploymentCommandArgumentCaptor; 27 | 28 | @BeforeEach 29 | void setUp() { 30 | scaleOperatorFacade = new ScaleOperatorFacade(operatorApiPort); 31 | } 32 | 33 | @Test 34 | void should_scale_deployment() { 35 | //given 36 | ScaleDeploymentCommand command = ScaleDeploymentCommand.builder() 37 | .runId("runid") 38 | .dataCenter(DataCenter.AWS) 39 | .workerCount(3) 40 | .build(); 41 | 42 | //when 43 | scaleOperatorFacade.scaleDeployment(command); 44 | 45 | //then 46 | inOrder.verify(operatorApiPort).scaleDeployment(scaleDeploymentCommandArgumentCaptor.capture()); 47 | inOrder.verifyNoMoreInteractions(); 48 | 49 | ScaleDeploymentCommand value = scaleDeploymentCommandArgumentCaptor.getValue(); 50 | assertThat(value.getRunId()).isEqualTo("runid"); 51 | assertThat(value.getDataCenter()).isEqualTo(DataCenter.AWS); 52 | assertThat(value.getWorkerCount()).isEqualByComparingTo(3); 53 | } 54 | 55 | @Test 56 | void throw_business_exception_for_scale_deployment() { 57 | //given 58 | ScaleDeploymentCommand command = ScaleDeploymentCommand.builder().runId("runid").workerCount(3).build(); 59 | 60 | doThrow(new BusinessException("Scale deployment exception")) 61 | .doNothing() 62 | .when(operatorApiPort).scaleDeployment(command); 63 | 64 | //when 65 | Throwable throwable = catchThrowable(() -> scaleOperatorFacade.scaleDeployment(command)); 66 | 67 | //then 68 | assertThat(throwable) 69 | .isInstanceOf(BusinessException.class) 70 | .hasMessageContaining("Encountered an exception while scaling deployment"); 71 | 72 | inOrder.verify(operatorApiPort).scaleDeployment(command); 73 | inOrder.verifyNoMoreInteractions(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/config/web/WebConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.config.web; 2 | 3 | import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; 4 | import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; 5 | import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; 6 | import org.springframework.boot.actuate.endpoint.ExposableEndpoint; 7 | import org.springframework.boot.actuate.endpoint.web.*; 8 | import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier; 9 | import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier; 10 | import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.core.env.Environment; 14 | import org.springframework.util.StringUtils; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.List; 19 | 20 | @Configuration 21 | public class WebConfiguration { 22 | 23 | @Bean 24 | public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier, 25 | ServletEndpointsSupplier servletEndpointsSupplier, 26 | ControllerEndpointsSupplier controllerEndpointsSupplier, 27 | EndpointMediaTypes endpointMediaTypes, 28 | CorsEndpointProperties corsProperties, 29 | WebEndpointProperties webEndpointProperties, 30 | Environment environment) { 31 | List> allEndpoints = new ArrayList<>(); 32 | Collection webEndpoints = webEndpointsSupplier.getEndpoints(); 33 | allEndpoints.addAll(webEndpoints); 34 | allEndpoints.addAll(servletEndpointsSupplier.getEndpoints()); 35 | allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints()); 36 | String basePath = webEndpointProperties.getBasePath(); 37 | EndpointMapping endpointMapping = new EndpointMapping(basePath); 38 | boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath); 39 | return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null); 40 | } 41 | 42 | private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) { 43 | return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/infra/common/GlobalControllerExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.infra.common; 2 | 3 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 4 | import com.trendyol.kubernetesoperatorapi.domain.exception.ClientApiBusinessException; 5 | import com.trendyol.kubernetesoperatorapi.domain.exception.ClientApiInternalServerException; 6 | import com.trendyol.kubernetesoperatorapi.domain.exception.ClientApiReadTimeoutException; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.springframework.context.MessageSource; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.validation.BindingResult; 14 | import org.springframework.web.bind.MethodArgumentNotValidException; 15 | import org.springframework.web.bind.MissingServletRequestParameterException; 16 | import org.springframework.web.bind.annotation.ControllerAdvice; 17 | import org.springframework.web.bind.annotation.ExceptionHandler; 18 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 19 | 20 | import java.util.List; 21 | import java.util.Locale; 22 | import java.util.Optional; 23 | import java.util.function.Supplier; 24 | 25 | import static org.springframework.http.HttpStatus.BAD_REQUEST; 26 | import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; 27 | 28 | @Slf4j 29 | @ControllerAdvice 30 | @RequiredArgsConstructor 31 | public class GlobalControllerExceptionHandler { 32 | 33 | private static final Locale TR = new Locale("tr"); 34 | private static final String UNEXPECTED_ERROR = "Beklenmeyen bir hata oluştu"; 35 | 36 | private final MessageSource messageSource; 37 | 38 | @ExceptionHandler(Exception.class) 39 | public ResponseEntity handleException(Exception ex) { 40 | var errorDetailDTO = getErrorDetailTO(INTERNAL_SERVER_ERROR, UNEXPECTED_ERROR); 41 | 42 | var errorDTO = ErrorDTO.builder() 43 | .exception(ex.getClass().getCanonicalName()) 44 | .errorDetail(List.of(errorDetailDTO)) 45 | .build(); 46 | log.error("Exception Caused By: {}", ex.getMessage()); 47 | return new ResponseEntity<>(errorDTO, HttpStatus.INTERNAL_SERVER_ERROR); 48 | } 49 | 50 | @ExceptionHandler(MethodArgumentTypeMismatchException.class) 51 | public ResponseEntity handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException matme) { 52 | var errorDetailDTO = getErrorDetailTO(BAD_REQUEST, matme.getMessage()); 53 | 54 | var errorDTO = ErrorDTO.builder() 55 | .exception(matme.getClass().getCanonicalName()) 56 | .errorDetail(List.of(errorDetailDTO)) 57 | .build(); 58 | 59 | log.error("Enum could not found. Caused By: {}", errorDTO); 60 | return new ResponseEntity<>(errorDTO, BAD_REQUEST); 61 | } 62 | 63 | @ExceptionHandler(MissingServletRequestParameterException.class) 64 | public ResponseEntity handleMissingServletRequestParameterException(MissingServletRequestParameterException msrpe) { 65 | var errorDetailDTO = getErrorDetailTO(BAD_REQUEST, msrpe.getMessage()); 66 | 67 | var errorDTO = ErrorDTO.builder() 68 | .exception(msrpe.getClass().getCanonicalName()) 69 | .errorDetail(List.of(errorDetailDTO)) 70 | .build(); 71 | 72 | log.error("Request parameter could not found. Caused By: {}", errorDTO); 73 | return new ResponseEntity<>(errorDTO, BAD_REQUEST); 74 | } 75 | 76 | @ExceptionHandler(MethodArgumentNotValidException.class) 77 | public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException manve) { 78 | var errorDTO = ErrorDTO.builder() 79 | .exception(manve.getClass().getCanonicalName()) 80 | .build(); 81 | 82 | prepareBindingResult(manve.getBindingResult(), errorDTO); 83 | log.error("Field validation failed. Caused By: {}", manve.getMessage()); 84 | return new ResponseEntity<>(errorDTO, BAD_REQUEST); 85 | } 86 | 87 | @ExceptionHandler(BusinessException.class) 88 | public ResponseEntity handleGeneralBusinessException(BusinessException be) { 89 | var errorDetailDTO = getErrorDetailTO(BAD_REQUEST, be.getMessage()); 90 | 91 | var errorDTO = ErrorDTO.builder() 92 | .exception(be.getClass().getCanonicalName()) 93 | .errorDetail(List.of(errorDetailDTO)) 94 | .build(); 95 | log.error("GeneralBusinessException Caused By: {}", be.getMessage()); 96 | return new ResponseEntity<>(errorDTO, BAD_REQUEST); 97 | } 98 | 99 | @ExceptionHandler(ClientApiBusinessException.class) 100 | public ResponseEntity handleClientApiBusinessException(ClientApiBusinessException cabe) { 101 | var errorDTO = new ErrorDTO(); 102 | errorDTO.setException(cabe.getException()); 103 | 104 | prepareHystrixErrorDetail(cabe.getBaseResponse(), errorDTO); 105 | log.error("Client Api Business Exception Caused By: {}", cabe.getException()); 106 | return new ResponseEntity<>(errorDTO, BAD_REQUEST); 107 | } 108 | 109 | @ExceptionHandler(ClientApiReadTimeoutException.class) 110 | public ResponseEntity handleClientReadTimeoutException(ClientApiReadTimeoutException carte) { 111 | var errorDTO = new ErrorDTO(); 112 | errorDTO.setException(carte.getException()); 113 | 114 | prepareHystrixErrorDetail(carte.getBaseResponse(), errorDTO); 115 | log.warn("Client Api Read Timeout Exception Caused By: {}", carte.getMessage()); 116 | return new ResponseEntity<>(errorDTO, BAD_REQUEST); 117 | } 118 | 119 | @ExceptionHandler(ClientApiInternalServerException.class) 120 | public ResponseEntity handleClientApiInternalServerException(ClientApiInternalServerException caise) { 121 | var errorDTO = new ErrorDTO(); 122 | errorDTO.setException(caise.getException()); 123 | 124 | prepareHystrixErrorDetail(caise.getBaseResponse(), errorDTO); 125 | log.warn("Client Api Internal Server Exception Caused By: {}", caise.getMessage()); 126 | return new ResponseEntity<>(errorDTO, HttpStatus.INTERNAL_SERVER_ERROR); 127 | } 128 | 129 | private void prepareHystrixErrorDetail(BaseResponse baseResponse, ErrorDTO errorDTO) { 130 | baseResponse.getErrorDetailModel().forEach(errorDetail -> { 131 | var fieldError = new ErrorDetailDTO(); 132 | fieldError.setMessage(getMessage(errorDetail.getKey(), errorDetail.getArgs(), errorDetail.getMessage())); 133 | fieldError.setKey(errorDetail.getKey()); 134 | fieldError.setArgs(errorDetail.getArgs()); 135 | errorDTO.setErrorDetail(List.of(fieldError)); 136 | }); 137 | } 138 | 139 | private String getMessage(String key, Object[] args, String defaultMessage) { 140 | return Optional.of(getMessage(() -> messageSource.getMessage(key, args, TR))) 141 | .filter(StringUtils::isNotBlank) 142 | .orElse(defaultMessage); 143 | } 144 | 145 | private String getMessage(Supplier supplier) { 146 | String message = StringUtils.EMPTY; 147 | try { 148 | message = supplier.get(); 149 | } catch (Exception exception) { 150 | log.warn("Exception occurred : ", exception); 151 | } 152 | return message; 153 | } 154 | 155 | private void prepareBindingResult(BindingResult bindingResult, ErrorDTO errorDTO) { 156 | bindingResult.getFieldErrors().forEach(i -> { 157 | var errorDetailDTO = new ErrorDetailDTO(); 158 | errorDetailDTO.setMessage(getMessage(i.getDefaultMessage(), i.getArguments(), StringUtils.EMPTY)); 159 | errorDetailDTO.setKey(i.getDefaultMessage()); 160 | errorDTO.setErrorDetail(List.of(errorDetailDTO)); 161 | }); 162 | } 163 | 164 | private ErrorDetailDTO getErrorDetailTO(HttpStatus httpStatus, String message) { 165 | return ErrorDetailDTO.builder() 166 | .message(message) 167 | .errorCode(String.valueOf(httpStatus.value())) 168 | .build(); 169 | } 170 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.7.4 10 | 11 | 12 | 13 | com.trendyol 14 | kubernetes-operator-api 15 | 0.0.1-SNAPSHOT 16 | kubernetes-operator-api 17 | Kubernetes Operator Api 18 | 19 | 20 | 11 21 | 2021.0.4 22 | 5.3.23 23 | 2.2.10.RELEASE 24 | 1.3.4 25 | 16.0.1 26 | 3.0.0 27 | 3.11 28 | 2.14.0-rc2 29 | 4.0.0-rc-2 30 | 1.33 31 | 4.4.15 32 | 4.5.13 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-web 46 | 47 | 48 | 49 | 50 | 51 | 52 | org.springframework 53 | spring-core 54 | ${spring.version} 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.springframework 62 | spring-beans 63 | ${spring.version} 64 | 65 | 66 | 67 | 68 | 69 | 70 | org.springframework.retry 71 | spring-retry 72 | ${spring-retry.version} 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.springframework.cloud 80 | spring-cloud-starter-netflix-hystrix 81 | ${hystrix.version} 82 | 83 | 84 | 85 | 86 | 87 | 88 | org.springframework.boot 89 | spring-boot-starter-actuator 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.springframework.boot 97 | spring-boot-starter-validation 98 | 99 | 100 | 101 | 102 | 103 | 104 | org.springframework.boot 105 | spring-boot-configuration-processor 106 | true 107 | 108 | 109 | 110 | 111 | 112 | 113 | org.springframework.boot 114 | spring-boot-devtools 115 | runtime 116 | true 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | io.kubernetes 128 | client-java 129 | ${k8-client-java.version} 130 | 131 | 132 | 133 | 134 | 135 | 136 | org.projectlombok 137 | lombok 138 | true 139 | 140 | 141 | 142 | 143 | 144 | 145 | io.springfox 146 | springfox-swagger-ui 147 | ${springfox.version} 148 | compile 149 | 150 | 151 | 152 | 153 | 154 | 155 | io.springfox 156 | springfox-boot-starter 157 | ${springfox.version} 158 | 159 | 160 | 161 | 162 | 163 | 164 | org.apache.commons 165 | commons-lang3 166 | 167 | 168 | 169 | 170 | 171 | 172 | com.fasterxml.jackson.dataformat 173 | jackson-dataformat-yaml 174 | ${jackson.version} 175 | 176 | 177 | 178 | 179 | 180 | 181 | com.google.protobuf 182 | protobuf-java 183 | ${protobuf-java.version} 184 | 185 | 186 | 187 | 188 | 189 | 190 | org.yaml 191 | snakeyaml 192 | ${snakeyaml.version} 193 | 194 | 195 | 196 | 197 | 198 | 199 | org.apache.httpcomponents 200 | httpcore 201 | ${httpcore.version} 202 | 203 | 204 | 205 | 206 | 207 | 208 | org.apache.httpcomponents 209 | httpclient 210 | ${httpclient.version} 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | org.springframework.boot 222 | spring-boot-starter-test 223 | test 224 | 225 | 226 | 227 | 228 | 229 | 230 | org.springframework.cloud 231 | spring-cloud-dependencies 232 | ${spring-cloud.version} 233 | pom 234 | import 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | org.springframework.boot 243 | spring-boot-maven-plugin 244 | 245 | 246 | 247 | org.projectlombok 248 | lombok 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/kubernetes/KubernetesApiAdapter.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.kubernetes; 2 | 3 | import com.google.gson.JsonSyntaxException; 4 | import com.trendyol.kubernetesoperatorapi.adapter.kubernetes.component.KubernetesDeploymentComponent; 5 | import com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.DataCenterSettings; 6 | import com.trendyol.kubernetesoperatorapi.adapter.kubernetes.strategy.KubernetesClient; 7 | import com.trendyol.kubernetesoperatorapi.application.port.OperatorApiPort; 8 | import com.trendyol.kubernetesoperatorapi.domain.command.CreateDeploymentCommand; 9 | import com.trendyol.kubernetesoperatorapi.domain.command.ScaleDeploymentCommand; 10 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.RollBackLevel; 11 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 12 | import com.trendyol.kubernetesoperatorapi.domain.exception.RollBackBusinessException; 13 | import com.trendyol.kubernetesoperatorapi.infra.annotation.RetryOperation; 14 | import io.kubernetes.client.custom.V1Patch; 15 | import io.kubernetes.client.openapi.ApiClient; 16 | import io.kubernetes.client.openapi.ApiException; 17 | import io.kubernetes.client.openapi.Configuration; 18 | import io.kubernetes.client.openapi.apis.AppsV1Api; 19 | import io.kubernetes.client.openapi.apis.CoreV1Api; 20 | import io.kubernetes.client.openapi.models.V1ServiceList; 21 | import io.kubernetes.client.openapi.models.V1Status; 22 | import lombok.RequiredArgsConstructor; 23 | import lombok.extern.slf4j.Slf4j; 24 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 25 | import org.springframework.stereotype.Service; 26 | 27 | import java.util.HashMap; 28 | import java.util.Map; 29 | 30 | import static com.trendyol.kubernetesoperatorapi.infra.constant.Constant.*; 31 | 32 | @Slf4j 33 | @Service 34 | @RequiredArgsConstructor 35 | @ConditionalOnProperty(value = "mock.kubernetes.api.enabled", havingValue = "false", matchIfMissing = true) 36 | public class KubernetesApiAdapter implements OperatorApiPort { 37 | 38 | private final KubernetesClient kubernetesClient; 39 | private final KubernetesDeploymentComponent kubernetesDeploymentComponent; 40 | 41 | @Override 42 | public void createDeployment(CreateDeploymentCommand command) { 43 | log.info("Started create deployment by runId: {}", command.getRunId()); 44 | 45 | DataCenterSettings dataCenterSettings = kubernetesClient.getClient(command.getDataCenter().name()); 46 | ApiClient apiClient = dataCenterSettings.getApiClient(); 47 | String imageNamePrefix = dataCenterSettings.getImageNamePrefix(); 48 | Map variables = prepareVariablesMap(command, imageNamePrefix); 49 | 50 | String deploymentNameForMaster = kubernetesDeploymentComponent.deployMaster(apiClient, variables); 51 | if (!kubernetesDeploymentComponent.rollOutStatus(command.getRunId(), apiClient, deploymentNameForMaster, APP_NAME_MASTER, 1)) { 52 | log.error("Encountered an exception while roll out created name spaced deployment for master"); 53 | throw new RollBackBusinessException("Encountered an exception while roll out created name spaced deployment for master", RollBackLevel.BEFORE_WORKER_DEPLOYMENT); 54 | } 55 | 56 | String deploymentNameForWorker = kubernetesDeploymentComponent.deployWorker(apiClient, variables, command.getWorkerCount()); 57 | if (!kubernetesDeploymentComponent.rollOutStatus(command.getRunId(), apiClient, deploymentNameForWorker, APP_NAME_WORKER, command.getWorkerCount())) { 58 | log.error("Encountered an exception while roll out created name spaced deployment for worker"); 59 | throw new RollBackBusinessException("Encountered an exception while roll out created name spaced deployment for worker", RollBackLevel.BEFORE_SERVICE); 60 | } 61 | 62 | kubernetesDeploymentComponent.createService(apiClient, command.getRunId()); 63 | kubernetesDeploymentComponent.createServiceUi(apiClient, command.getRunId()); 64 | kubernetesDeploymentComponent.printPodStatus(apiClient, command.getRunId()); 65 | V1ServiceList v1ServiceList = kubernetesDeploymentComponent.retrieveNamespacedServiceList(apiClient, command.getRunId()); 66 | kubernetesDeploymentComponent.printServiceStatus(v1ServiceList); 67 | 68 | log.info("Finished create deployment by runId: {}", command.getRunId()); 69 | } 70 | 71 | private Map prepareVariablesMap(CreateDeploymentCommand command, String imageNamePrefix) { 72 | Map variables = new HashMap<>(); 73 | variables.put("RUN_ID", command.getRunId()); 74 | variables.put("IMAGE", imageNamePrefix + command.getImageName()); 75 | variables.put("DATA_CENTER", command.getDataCenter().name()); 76 | return variables; 77 | } 78 | 79 | @Override 80 | @RetryOperation(retryCount = 5, waitSeconds = 1) 81 | public void scaleDeployment(ScaleDeploymentCommand command) { 82 | log.info("Started scale deployment by runId: {} , workerCount: {}", command.getRunId(), command.getWorkerCount()); 83 | DataCenterSettings settings = kubernetesClient.getClient(command.getDataCenter().name()); 84 | ApiClient apiClient = settings.getApiClient(); 85 | Configuration.setDefaultApiClient(apiClient); 86 | 87 | AppsV1Api appsV1Api = new AppsV1Api(apiClient); 88 | String deploymentName = WORKER_DEPLOYMENT_NAME_PREFIX + command.getRunId(); 89 | String jsonPatchStr = String.format("[{\"op\":\"replace\",\"path\":\"/spec/replicas\",\"value\":%d}]", command.getWorkerCount()); 90 | 91 | try { 92 | appsV1Api.patchNamespacedDeploymentScale(deploymentName, NAMESPACE, new V1Patch(jsonPatchStr), null, null, null, null, null); 93 | if (!kubernetesDeploymentComponent.rollOutStatus(command.getRunId(), apiClient, deploymentName, APP_NAME_WORKER, command.getWorkerCount())) { 94 | log.error("Encountered an exception while roll out scaled name spaced deployment for worker"); 95 | throw new RollBackBusinessException("Encountered an exception while roll out scaled name spaced deployment for worker", RollBackLevel.BEFORE_SERVICE); 96 | } 97 | } catch (ApiException e) { 98 | log.error("Encountered an exception while consuming scale deployment by deploymentName: {} , exceptionMessage: {}", deploymentName, e.getResponseBody()); 99 | throw new BusinessException("Encountered an exception while consuming scale deployment"); 100 | } 101 | 102 | log.info("Finished scale deployment by runId: {} , workerCount: {}", command.getRunId(), command.getWorkerCount()); 103 | } 104 | 105 | @Override 106 | @RetryOperation(retryCount = 5, waitSeconds = 1) 107 | public void deleteDeployment(String dataCenterName, String deploymentName) { 108 | log.info("Started delete deployment by deploymentName: {}", deploymentName); 109 | 110 | DataCenterSettings settings = kubernetesClient.getClient(dataCenterName); 111 | ApiClient apiClient = settings.getApiClient(); 112 | Configuration.setDefaultApiClient(apiClient); 113 | AppsV1Api appsV1Api = new AppsV1Api(apiClient); 114 | 115 | V1Status status; 116 | try { 117 | status = appsV1Api.deleteNamespacedDeployment(deploymentName, NAMESPACE, null, null, null, null, null, null); 118 | } catch (ApiException e) { 119 | log.error("Encountered an exception while consuming delete deployment by deploymentName: {} , exceptionMessage : {}", deploymentName, e.getResponseBody()); 120 | throw new BusinessException("Encountered an exception while consuming delete deployment"); 121 | } 122 | 123 | if (SUCCESS.equals(status.getStatus())) { 124 | log.info("Finished delete deployment by deploymentName: {}", deploymentName); 125 | } else { 126 | log.error("An error occurred while deleting deployment for deploymentName: {} ", deploymentName); 127 | } 128 | } 129 | 130 | @Override 131 | @RetryOperation(retryCount = 5, waitSeconds = 1) 132 | public void deleteService(String dataCenterName, String serviceName) { 133 | log.info("Started delete service by serviceName: {}", serviceName); 134 | 135 | DataCenterSettings settings = kubernetesClient.getClient(dataCenterName); 136 | ApiClient apiClient = settings.getApiClient(); 137 | Configuration.setDefaultApiClient(apiClient); 138 | 139 | CoreV1Api coreV1Api = new CoreV1Api(apiClient); 140 | try { 141 | coreV1Api.deleteNamespacedService(serviceName, NAMESPACE, null, null, null, null, null, null); 142 | } catch (JsonSyntaxException ignored) { 143 | /* 144 | Ignored due to issue of k8 client. 145 | https://github.com/kubernetes-client/java/issues/2307.If the issue is resolved this try-catch should be removed 146 | */ 147 | } catch (ApiException e) { 148 | log.error("Encountered an exception while consuming delete component by serviceName: {} , exceptionMessage: {}", serviceName, e.getResponseBody()); 149 | throw new BusinessException("Encountered an exception while consuming delete component"); 150 | } 151 | 152 | log.info("Finished delete service by serviceName: {}", serviceName); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/test/java/com/trendyol/kubernetesoperatorapi/application/CreateOperatorFacadeTest.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.application; 2 | 3 | import com.trendyol.kubernetesoperatorapi.application.port.OperatorApiPort; 4 | import com.trendyol.kubernetesoperatorapi.base.OrderTest; 5 | import com.trendyol.kubernetesoperatorapi.domain.command.CreateDeploymentCommand; 6 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.DataCenter; 7 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.RollBackLevel; 8 | import com.trendyol.kubernetesoperatorapi.domain.exception.BusinessException; 9 | import com.trendyol.kubernetesoperatorapi.domain.exception.RollBackBusinessException; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.mockito.ArgumentCaptor; 13 | import org.mockito.Captor; 14 | import org.mockito.Mock; 15 | 16 | import static com.trendyol.kubernetesoperatorapi.infra.constant.Constant.*; 17 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 18 | import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable; 19 | import static org.mockito.Mockito.doThrow; 20 | 21 | class CreateOperatorFacadeTest extends OrderTest { 22 | 23 | private CreateOperatorFacade createOperatorFacade; 24 | 25 | @Mock 26 | private OperatorApiPort operatorApiPort; 27 | 28 | @Captor 29 | private ArgumentCaptor createDeploymentCommandArgumentCaptor; 30 | 31 | @BeforeEach 32 | void setUp() { 33 | createOperatorFacade = new CreateOperatorFacade(operatorApiPort); 34 | } 35 | 36 | @Test 37 | void should_create_deployment() { 38 | //given 39 | CreateDeploymentCommand command = CreateDeploymentCommand.builder() 40 | .runId("runid") 41 | .dataCenter(DataCenter.AWS) 42 | .workerCount(3) 43 | .imageName("imagename") 44 | .build(); 45 | 46 | //when 47 | createOperatorFacade.createDeployment(command); 48 | 49 | //then 50 | inOrder.verify(operatorApiPort).createDeployment(createDeploymentCommandArgumentCaptor.capture()); 51 | inOrder.verifyNoMoreInteractions(); 52 | 53 | CreateDeploymentCommand value = createDeploymentCommandArgumentCaptor.getValue(); 54 | assertThat(value.getRunId()).isEqualTo("runid"); 55 | assertThat(value.getDataCenter()).isEqualTo(DataCenter.AWS); 56 | assertThat(value.getWorkerCount()).isEqualByComparingTo(3); 57 | assertThat(value.getImageName()).isEqualTo("imagename"); 58 | } 59 | 60 | @Test 61 | void throw_rollback_business_exception_for_all_flow() { 62 | //given 63 | CreateDeploymentCommand command = CreateDeploymentCommand.builder() 64 | .runId("runid") 65 | .dataCenter(DataCenter.AWS) 66 | .workerCount(3) 67 | .imageName("imagename") 68 | .build(); 69 | 70 | doThrow(new RollBackBusinessException("All flow exception", RollBackLevel.ALL_FLOW)) 71 | .doNothing() 72 | .when(operatorApiPort).createDeployment(command); 73 | 74 | //when 75 | Throwable throwable = catchThrowable(() -> createOperatorFacade.createDeployment(command)); 76 | 77 | //then 78 | assertThat(throwable) 79 | .isInstanceOf(BusinessException.class) 80 | .hasMessageContaining("Encountered an exception while creating deployment"); 81 | 82 | inOrder.verify(operatorApiPort).createDeployment(command); 83 | inOrder.verify(operatorApiPort).deleteService(command.getDataCenter().name(), MASTER_SERVICE_UI_NAME_PREFIX + command.getRunId()); 84 | inOrder.verify(operatorApiPort).deleteService(command.getDataCenter().name(), MASTER_SERVICE_NAME_PREFIX + command.getRunId()); 85 | inOrder.verify(operatorApiPort).deleteDeployment(command.getDataCenter().name(), WORKER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 86 | inOrder.verify(operatorApiPort).deleteDeployment(command.getDataCenter().name(), MASTER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 87 | inOrder.verifyNoMoreInteractions(); 88 | } 89 | 90 | @Test 91 | void throw_rollback_business_exception_for_before_service_ui() { 92 | //given 93 | CreateDeploymentCommand command = CreateDeploymentCommand.builder() 94 | .runId("runid") 95 | .dataCenter(DataCenter.AWS) 96 | .workerCount(3) 97 | .imageName("imagename") 98 | .build(); 99 | 100 | doThrow(new RollBackBusinessException("Before service ui exception", RollBackLevel.BEFORE_SERVICE_UI)) 101 | .doNothing() 102 | .when(operatorApiPort).createDeployment(command); 103 | 104 | //when 105 | Throwable throwable = catchThrowable(() -> createOperatorFacade.createDeployment(command)); 106 | 107 | //then 108 | assertThat(throwable) 109 | .isInstanceOf(BusinessException.class) 110 | .hasMessageContaining("Encountered an exception while creating deployment"); 111 | 112 | inOrder.verify(operatorApiPort).createDeployment(command); 113 | inOrder.verify(operatorApiPort).deleteService(command.getDataCenter().name(), MASTER_SERVICE_NAME_PREFIX + command.getRunId()); 114 | inOrder.verify(operatorApiPort).deleteDeployment(command.getDataCenter().name(), WORKER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 115 | inOrder.verify(operatorApiPort).deleteDeployment(command.getDataCenter().name(), MASTER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 116 | inOrder.verifyNoMoreInteractions(); 117 | } 118 | 119 | @Test 120 | void throw_rollback_business_exception_for_before_service() { 121 | //given 122 | CreateDeploymentCommand command = CreateDeploymentCommand.builder() 123 | .runId("runid") 124 | .dataCenter(DataCenter.AWS) 125 | .workerCount(3) 126 | .imageName("imagename") 127 | .build(); 128 | 129 | doThrow(new RollBackBusinessException("Before service exception", RollBackLevel.BEFORE_SERVICE)) 130 | .doNothing() 131 | .when(operatorApiPort).createDeployment(command); 132 | 133 | //when 134 | Throwable throwable = catchThrowable(() -> createOperatorFacade.createDeployment(command)); 135 | 136 | //then 137 | assertThat(throwable) 138 | .isInstanceOf(BusinessException.class) 139 | .hasMessageContaining("Encountered an exception while creating deployment"); 140 | 141 | inOrder.verify(operatorApiPort).createDeployment(command); 142 | inOrder.verify(operatorApiPort).deleteDeployment(command.getDataCenter().name(), WORKER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 143 | inOrder.verify(operatorApiPort).deleteDeployment(command.getDataCenter().name(), MASTER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 144 | inOrder.verifyNoMoreInteractions(); 145 | } 146 | 147 | @Test 148 | void throw_rollback_business_exception_for_before_worker_deployment() { 149 | //given 150 | CreateDeploymentCommand command = CreateDeploymentCommand.builder() 151 | .runId("runid") 152 | .dataCenter(DataCenter.AWS) 153 | .workerCount(3) 154 | .imageName("imagename") 155 | .build(); 156 | 157 | doThrow(new RollBackBusinessException("Before worker deployment exception", RollBackLevel.BEFORE_WORKER_DEPLOYMENT)) 158 | .doNothing() 159 | .when(operatorApiPort).createDeployment(command); 160 | 161 | //when 162 | Throwable throwable = catchThrowable(() -> createOperatorFacade.createDeployment(command)); 163 | 164 | //then 165 | assertThat(throwable) 166 | .isInstanceOf(BusinessException.class) 167 | .hasMessageContaining("Encountered an exception while creating deployment"); 168 | 169 | inOrder.verify(operatorApiPort).createDeployment(command); 170 | inOrder.verify(operatorApiPort).deleteDeployment(command.getDataCenter().name(), MASTER_DEPLOYMENT_NAME_PREFIX + command.getRunId()); 171 | inOrder.verifyNoMoreInteractions(); 172 | } 173 | 174 | @Test 175 | void throw_rollback_business_exception_for_before_master_deployment() { 176 | //given 177 | CreateDeploymentCommand command = CreateDeploymentCommand.builder() 178 | .runId("runid") 179 | .dataCenter(DataCenter.AWS) 180 | .workerCount(3) 181 | .imageName("imagename") 182 | .build(); 183 | 184 | doThrow(new RollBackBusinessException("Before master deployment exception", RollBackLevel.BEFORE_MASTER_DEPLOYMENT)) 185 | .doNothing() 186 | .when(operatorApiPort).createDeployment(command); 187 | 188 | //when 189 | Throwable throwable = catchThrowable(() -> createOperatorFacade.createDeployment(command)); 190 | 191 | //then 192 | assertThat(throwable) 193 | .isInstanceOf(BusinessException.class) 194 | .hasMessageContaining("Encountered an exception while creating deployment"); 195 | 196 | inOrder.verify(operatorApiPort).createDeployment(command); 197 | inOrder.verifyNoMoreInteractions(); 198 | } 199 | } -------------------------------------------------------------------------------- /src/main/java/com/trendyol/kubernetesoperatorapi/adapter/kubernetes/component/KubernetesDeploymentComponent.java: -------------------------------------------------------------------------------- 1 | package com.trendyol.kubernetesoperatorapi.adapter.kubernetes.component; 2 | 3 | import com.trendyol.kubernetesoperatorapi.domain.enumtype.RollBackLevel; 4 | import com.trendyol.kubernetesoperatorapi.domain.exception.RollBackBusinessException; 5 | import com.trendyol.kubernetesoperatorapi.infra.annotation.RetryOperation; 6 | import io.kubernetes.client.openapi.ApiClient; 7 | import io.kubernetes.client.openapi.ApiException; 8 | import io.kubernetes.client.openapi.apis.AppsV1Api; 9 | import io.kubernetes.client.openapi.apis.CoreV1Api; 10 | import io.kubernetes.client.openapi.models.*; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Map; 17 | import java.util.Objects; 18 | import java.util.Optional; 19 | 20 | import static com.trendyol.kubernetesoperatorapi.infra.constant.Constant.*; 21 | 22 | @Slf4j 23 | @Component 24 | @RequiredArgsConstructor 25 | public class KubernetesDeploymentComponent { 26 | 27 | private final KubernetesYamlConverterComponent kubernetesYamlConverterComponent; 28 | 29 | @RetryOperation(retryCount = 3, waitSeconds = 1) 30 | public String deployMaster(ApiClient apiClient, Map variables) { 31 | AppsV1Api appsV1Api = new AppsV1Api(apiClient); 32 | String runId = variables.get("RUN_ID"); 33 | String deploymentName = MASTER_DEPLOYMENT_NAME_PREFIX + runId; 34 | 35 | V1Deployment deployment = (V1Deployment) kubernetesYamlConverterComponent.convertKubernetesYml(variables, MASTER_DEPLOYMENT_YAML_PATH, V1Deployment.class); 36 | 37 | try { 38 | appsV1Api.createNamespacedDeployment(NAMESPACE, deployment, null, null, null, null); 39 | } catch (ApiException e) { 40 | log.error("Encountered an exception while consuming create name spaced deployment by deploymentName: {} , exceptionMessage: {}", deploymentName, e.getResponseBody()); 41 | throw new RollBackBusinessException("Encountered an exception while consuming create name spaced deployment for master", RollBackLevel.BEFORE_MASTER_DEPLOYMENT); 42 | } 43 | return deploymentName; 44 | } 45 | 46 | @RetryOperation(retryCount = 3, waitSeconds = 1) 47 | public String deployWorker(ApiClient apiClient, Map variables, int workerCount) { 48 | AppsV1Api appsV1Api = new AppsV1Api(apiClient); 49 | String runId = variables.get("RUN_ID"); 50 | String deploymentName = WORKER_DEPLOYMENT_NAME_PREFIX + runId; 51 | 52 | V1Deployment deployment = (V1Deployment) kubernetesYamlConverterComponent.convertKubernetesYml(variables, WORKER_DEPLOYMENT_YAML_PATH, V1Deployment.class); 53 | deployment.getSpec().replicas(workerCount); 54 | 55 | try { 56 | appsV1Api.createNamespacedDeployment(NAMESPACE, deployment, null, null, null, null); 57 | } catch (ApiException e) { 58 | log.error("Encountered an exception while consuming create name spaced deployment by deploymentName: {} , exceptionMessage: {}", deploymentName, e.getMessage(), e); 59 | throw new RollBackBusinessException("Encountered an exception while consuming create name spaced deployment for worker", RollBackLevel.BEFORE_WORKER_DEPLOYMENT); 60 | } 61 | return deploymentName; 62 | } 63 | 64 | @RetryOperation(retryCount = 3, waitSeconds = 1) 65 | public boolean rollOutStatus(String runId, ApiClient apiClient, String deploymentName, String appName, int replicaCount) { 66 | AppsV1Api appsV1Api = new AppsV1Api(apiClient); 67 | CoreV1Api coreV1Api = new CoreV1Api(apiClient); 68 | 69 | long start = System.currentTimeMillis(); 70 | boolean deploymentReady = false; 71 | while (!deploymentReady) { 72 | if (System.currentTimeMillis() - start > TIMEOUT_VALUE_FOR_ROLLOUT) { 73 | break; 74 | } 75 | try { 76 | V1DeploymentStatus status = appsV1Api.readNamespacedDeployment(deploymentName, NAMESPACE, null).getStatus(); 77 | 78 | int readyReplicas = Objects.nonNull(status) ? Optional.ofNullable(status.getReadyReplicas()).orElse(0) : 0; 79 | 80 | String labelSelector = String.format("id=%s,app=%s", runId, appName); 81 | V1PodList list = coreV1Api.listNamespacedPod(NAMESPACE, null, null, null, "status.phase=Running", labelSelector, Integer.MAX_VALUE, null, null, null, Boolean.FALSE); 82 | 83 | log.info("Waiting for deployment {} to finish. {}/{} replicas are available. {}/{} pods are running", deploymentName, readyReplicas, replicaCount, list.getItems().size(), replicaCount); 84 | deploymentReady = readyReplicas >= replicaCount && list.getItems().size() >= replicaCount; 85 | 86 | if (!deploymentReady) { 87 | Thread.sleep(INTERVAL_VALUE_FOR_ROLLOUT); 88 | } 89 | } catch (Exception ignored) { 90 | //This exception ignored, this method will try again without waiting. 91 | } 92 | } 93 | 94 | if (deploymentReady) { 95 | log.info("Created deployment successfully: {}", deploymentName); 96 | return true; 97 | } else { 98 | log.error("An error occurred while creating deployment: {}", deploymentName); 99 | return false; 100 | } 101 | } 102 | 103 | @RetryOperation(retryCount = 5, waitSeconds = 1) 104 | public void createService(ApiClient apiClient, String runId) { 105 | Map variables = Map.of("RUN_ID", runId); 106 | 107 | CoreV1Api coreV1Api = new CoreV1Api(apiClient); 108 | V1Service masterService = (V1Service) kubernetesYamlConverterComponent.convertKubernetesYml(variables, MASTER_SERVICE_YAML_PATH, V1Service.class); 109 | 110 | try { 111 | coreV1Api.createNamespacedService(NAMESPACE, masterService, null, null, null, null); 112 | } catch (ApiException e) { 113 | log.error("Encountered an exception while consuming create name spaced component by serviceName: {} , exceptionMessage: {}", MASTER_SERVICE, e.getResponseBody()); 114 | throw new RollBackBusinessException("Encountered an exception while consuming create name spaced component for " + MASTER_SERVICE, RollBackLevel.BEFORE_SERVICE); 115 | } 116 | log.info("component/{}-{} created", MASTER_SERVICE, runId); 117 | } 118 | 119 | @RetryOperation(retryCount = 5, waitSeconds = 1) 120 | public void createServiceUi(ApiClient apiClient, String runId) { 121 | Map variables = Map.of("RUN_ID", runId); 122 | 123 | CoreV1Api coreV1Api = new CoreV1Api(apiClient); 124 | V1Service masterService = (V1Service) kubernetesYamlConverterComponent.convertKubernetesYml(variables, MASTER_SERVICE_UI_YAML_PATH, V1Service.class); 125 | 126 | try { 127 | coreV1Api.createNamespacedService(NAMESPACE, masterService, null, null, null, null); 128 | } catch (ApiException e) { 129 | log.error("Encountered an exception while consuming create name spaced component by serviceName: {} , exceptionMessage: {}", MASTER_SERVICE_UI, e.getResponseBody()); 130 | throw new RollBackBusinessException("Encountered an exception while consuming create name spaced component for " + MASTER_SERVICE_UI, RollBackLevel.BEFORE_SERVICE_UI); 131 | } 132 | log.info("component/{}-{} created", MASTER_SERVICE_UI, runId); 133 | } 134 | 135 | public void printPodStatus(ApiClient apiClient, String runId) { 136 | CoreV1Api coreV1Api = new CoreV1Api(apiClient); 137 | try { 138 | V1PodList list = coreV1Api.listNamespacedPod(NAMESPACE, null, null, null, null, "id=" + runId, Integer.MAX_VALUE, null, null, null, Boolean.FALSE); 139 | var podInfos = new ArrayList(); 140 | podInfos.add(new String[]{"NAME", "STATUS", "AGE"}); 141 | long nowAsSecond = System.currentTimeMillis() / 1000; 142 | for (V1Pod pod : list.getItems()) { 143 | final long age = nowAsSecond - pod.getStatus().getStartTime().toEpochSecond(); 144 | podInfos.add(new Object[]{pod.getMetadata().getName(), pod.getStatus().getPhase(), age + "s"}); 145 | } 146 | 147 | for (Object[] row : podInfos) { 148 | System.out.format("%-55s%-15s%-15s%n", row); 149 | } 150 | } catch (ApiException ignored) { 151 | //This exception ignored. 152 | } 153 | } 154 | 155 | @RetryOperation(retryCount = 5, waitSeconds = 1) 156 | public V1ServiceList retrieveNamespacedServiceList(ApiClient apiClient, String runId) { 157 | CoreV1Api coreV1Api = new CoreV1Api(apiClient); 158 | V1ServiceList serviceList; 159 | try { 160 | serviceList = coreV1Api.listNamespacedService(NAMESPACE, null, null, null, null, "id=" + runId, Integer.MAX_VALUE, null, null, null, Boolean.FALSE); 161 | } catch (ApiException e) { 162 | log.error("Encountered an exception while listing name spaced service by exceptionMessage: {}", e.getResponseBody()); 163 | throw new RollBackBusinessException("Encountered an exception while listing name spaced service by runId: {}" + runId, RollBackLevel.ALL_FLOW); 164 | } 165 | 166 | return serviceList; 167 | } 168 | 169 | public void printServiceStatus(V1ServiceList list) { 170 | var serviceInfos = new ArrayList(); 171 | serviceInfos.add(new String[]{"NAME", "TYPE", "CLUSTER-IP", "PORT(S)", "AGE"}); 172 | long nowAsSecond = System.currentTimeMillis() / 1000; 173 | for (V1Service service : list.getItems()) { 174 | 175 | var ports = service.getSpec().getPorts(); 176 | StringBuilder sb = new StringBuilder(); 177 | for (int i = 0; i < ports.size(); i++) { 178 | sb.append(ports.get(i).getPort()); 179 | sb.append(Objects.nonNull(ports.get(i).getNodePort()) ? ":" + ports.get(i).getNodePort() : ""); 180 | sb.append("/"); 181 | sb.append(ports.get(i).getProtocol()); 182 | 183 | if (i + 1 < ports.size()) { 184 | sb.append(","); 185 | } 186 | } 187 | 188 | long age = nowAsSecond - service.getMetadata().getCreationTimestamp().toEpochSecond(); 189 | serviceInfos.add(new Object[]{service.getMetadata().getName(), service.getSpec().getType(), service.getSpec().getClusterIP(), sb.toString(), age + "s"}); 190 | } 191 | 192 | for (Object[] row : serviceInfos) { 193 | System.out.format("%-40s%-15s%-15s%-25s%-15s%n", row); 194 | } 195 | } 196 | } 197 | --------------------------------------------------------------------------------