ソースを参照

Merge branch 'feat/devcloud_linkage' of LiuYiYe/DevManage into master

LiuYiYe 4 年 前
コミット
fac2a70c83
25 ファイル変更770 行追加24 行削除
  1. 14 0
      src/main/java/cn/seecoder/devmanage/DevmanageApplication.java
  2. 28 7
      src/main/java/cn/seecoder/devmanage/controller/DeploymentController.java
  3. 16 3
      src/main/java/cn/seecoder/devmanage/controller/HelloController.java
  4. 61 0
      src/main/java/cn/seecoder/devmanage/controller/LimitRangeRequestController.java
  5. 34 0
      src/main/java/cn/seecoder/devmanage/controller/LocalLogController.java
  6. 14 2
      src/main/java/cn/seecoder/devmanage/log/LocalLogger.java
  7. 47 0
      src/main/java/cn/seecoder/devmanage/model/po/LimitRequest.java
  8. 12 0
      src/main/java/cn/seecoder/devmanage/model/vo/DeploymentVO.java
  9. 33 0
      src/main/java/cn/seecoder/devmanage/model/vo/LimitRequestVO.java
  10. 3 3
      src/main/java/cn/seecoder/devmanage/model/vo/LocalLogVO.java
  11. 2 0
      src/main/java/cn/seecoder/devmanage/model/vo/Response.java
  12. 42 0
      src/main/java/cn/seecoder/devmanage/model/vo/UserIdentity.java
  13. 16 0
      src/main/java/cn/seecoder/devmanage/model/vo/UserVO.java
  14. 9 0
      src/main/java/cn/seecoder/devmanage/repository/LimitRequestRepository.java
  15. 21 2
      src/main/java/cn/seecoder/devmanage/security/JwtAuthenticationFilter.java
  16. 2 0
      src/main/java/cn/seecoder/devmanage/security/JwtHelper.java
  17. 13 0
      src/main/java/cn/seecoder/devmanage/service/DevcloudLinkageService.java
  18. 2 2
      src/main/java/cn/seecoder/devmanage/service/HelloService.java
  19. 13 0
      src/main/java/cn/seecoder/devmanage/service/LimitRequestService.java
  20. 10 0
      src/main/java/cn/seecoder/devmanage/service/LocalLogService.java
  21. 123 0
      src/main/java/cn/seecoder/devmanage/service/impl/DevcloudLinkageServiceImpl.java
  22. 2 5
      src/main/java/cn/seecoder/devmanage/service/impl/HelloServiceImpl.java
  23. 157 0
      src/main/java/cn/seecoder/devmanage/service/impl/LimitRequestServiceImpl.java
  24. 93 0
      src/main/java/cn/seecoder/devmanage/service/impl/LocalLogServiceImpl.java
  25. 3 0
      src/main/resources/application-lyy.yaml

+ 14 - 0
src/main/java/cn/seecoder/devmanage/DevmanageApplication.java

@@ -3,6 +3,10 @@ package cn.seecoder.devmanage;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
 
 @SpringBootApplication(exclude= {UserDetailsServiceAutoConfiguration.class})
 public class DevmanageApplication {
@@ -11,4 +15,14 @@ public class DevmanageApplication {
 		SpringApplication.run(DevmanageApplication.class, args);
 	}
 
+	@Bean
+	public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
+		return new RestTemplate(factory);
+	}
+
+	@Bean
+	public ClientHttpRequestFactory clientHttpRequestFactory() {
+		return new SimpleClientHttpRequestFactory();
+	}
+
 }

+ 28 - 7
src/main/java/cn/seecoder/devmanage/controller/DeploymentController.java

@@ -1,12 +1,11 @@
 package cn.seecoder.devmanage.controller;
 
-import cn.seecoder.api.k8s.DeploymentApi;
-import cn.seecoder.api.k8s.model.Deployment;
-import cn.seecoder.api.k8s.model.K8sObjectRequest;
-import cn.seecoder.common.util.SpringUtil;
 import cn.seecoder.devmanage.log.LocalLogger;
+import cn.seecoder.devmanage.model.vo.DeploymentVO;
 import cn.seecoder.devmanage.model.vo.Response;
+import cn.seecoder.devmanage.model.vo.UserVO;
 import cn.seecoder.devmanage.service.DeploymentService;
+import cn.seecoder.devmanage.service.DevcloudLinkageService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -21,10 +20,32 @@ import java.util.List;
 @RequestMapping("/deployment")
 public class DeploymentController {
     private final DeploymentService deploymentService;
+    private final DevcloudLinkageService devcloudLinkageService;
     private final LocalLogger localLogger;
 
-    @GetMapping
-    public ResponseEntity<Response<List<Deployment>>> getDeployments(@RequestParam String namespace, @RequestParam(required = false) String name) {
-        return ResponseEntity.ok().body(Response.buildSuccess(deploymentService.getDeployments(namespace, name)));
+//    @GetMapping
+//    public ResponseEntity<Response<List<Deployment>>> getDeployments(@RequestParam String namespace, @RequestParam(required = false) String name) {
+//        return ResponseEntity.ok().body(Response.buildSuccess(deploymentService.getDeployments(namespace, name)));
+//    }
+
+    @GetMapping("/page")
+    public ResponseEntity<Response<List<DeploymentVO>>> getPagedDeployment(@RequestParam Integer pageNo, @RequestParam Integer pageSize) {
+        return ResponseEntity.ok().body(Response.buildSuccess(devcloudLinkageService.getPagedDeployments(pageNo, pageSize)));
+    }
+
+    @GetMapping("/search")
+    public ResponseEntity<Response<List<DeploymentVO>>> searchPagedDeployments(@RequestParam String keyword, @RequestParam Integer pageNo, @RequestParam Integer pageSize) {
+        return ResponseEntity.ok().body(Response.buildSuccess(devcloudLinkageService.searchPagedDeployments(keyword, pageNo, pageSize)));
+    }
+
+    @GetMapping("/owner/namespace")
+    public ResponseEntity<Response<UserVO>> getOwnerOfNamespace(@RequestParam String namespace) {
+        return ResponseEntity.ok().body(Response.buildSuccess(devcloudLinkageService.getOwnerOfNamespace(namespace)));
+    }
+
+    @GetMapping("/pages")
+    public ResponseEntity<Response<Integer>> getTotalPages(@RequestParam Integer pageSize) {
+        return ResponseEntity.ok().body(Response.buildSuccess(devcloudLinkageService.getTotalPageCount(pageSize)));
     }
+
 }

+ 16 - 3
src/main/java/cn/seecoder/devmanage/controller/HelloController.java

@@ -9,7 +9,7 @@ import cn.seecoder.api.k8s.model.LimitRange;
 import cn.seecoder.api.k8s.model.Pod;
 import cn.seecoder.common.util.SpringUtil;
 import cn.seecoder.devmanage.model.po.LocalLog;
-import cn.seecoder.devmanage.model.vo.LocalLogVo;
+import cn.seecoder.devmanage.model.vo.LocalLogVO;
 import cn.seecoder.devmanage.model.vo.Response;
 import cn.seecoder.devmanage.service.HelloService;
 import lombok.RequiredArgsConstructor;
@@ -17,7 +17,11 @@ import org.springframework.context.annotation.Profile;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.time.ZoneId;
 import java.util.List;
+import java.util.TimeZone;
 
 /**
  * For test purpose only.
@@ -36,15 +40,24 @@ public class HelloController {
         return ResponseEntity.ok().body(Response.buildSuccess("Hello Seecoder"));
     }
 
+    @GetMapping("/timestamp")
+    public ResponseEntity<Response<String>> ts() {
+        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+        simpleDateFormat.setTimeZone(TimeZone.getTimeZone(ZoneId.of("+8")));
+
+        return ResponseEntity.ok().body(Response.buildSuccess(simpleDateFormat.format(timestamp)));
+    }
+
     @PutMapping("/log")
-    public ResponseEntity<Response<LocalLogVo>> putLog(@RequestBody LocalLogVo localLogVo) {
+    public ResponseEntity<Response<LocalLogVO>> putLog(@RequestBody LocalLogVO localLogVo) {
         LocalLog localLog = helloService.putLog(localLogVo);
         if (localLog == null) {
             return  ResponseEntity.badRequest().body(
                     Response.buildFailure(400, "Error: invalid date format.", null)
             );
         } else {
-            return ResponseEntity.ok().body(Response.buildSuccess(new LocalLogVo(localLog)));
+            return ResponseEntity.ok().body(Response.buildSuccess(new LocalLogVO(localLog)));
         }
     }
 

+ 61 - 0
src/main/java/cn/seecoder/devmanage/controller/LimitRangeRequestController.java

@@ -0,0 +1,61 @@
+package cn.seecoder.devmanage.controller;
+
+import cn.seecoder.api.k8s.model.LimitRange;
+import cn.seecoder.devmanage.model.vo.Response;
+import cn.seecoder.devmanage.model.vo.UserVO;
+import cn.seecoder.devmanage.service.DevcloudLinkageService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Objects;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/limitrequest")
+public class LimitRangeRequestController {
+
+    private final int ALL_REQUEST = 0;
+    private final int ONLY_HANDLED = 1;
+    private final int ONLY_UNHANDLED = 2;
+    private final DevcloudLinkageService devcloudLinkageService;
+
+    /**
+     * This is an API for students and not for administrators.
+     * Vip students can have their LimitRange changed in a certain permitted range.
+     * Other students' requests and Vip students' request beyond limits should be added
+     * into the database waiting for administrators' permission.
+     * @param namespace
+     * @param name
+     * @param limitRange
+     * @return
+     */
+    @PostMapping("/student/request")
+    public ResponseEntity<Response<LimitRange>> requestLimitRangeChange(@RequestParam String namespace, @RequestParam String name, @RequestBody LimitRange limitRange) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        String username = (String) authentication.getPrincipal();
+        UserVO ownerOfNamespace = devcloudLinkageService.getOwnerOfNamespace(namespace);
+        if (!Objects.equals(username, ownerOfNamespace.getUsername())) {
+            return ResponseEntity.badRequest().body(Response.buildFailure(400, String.format("Requested namespace \"%s\" does not belong to %s", namespace, username), null));
+        }
+        // TODO
+        return null;
+    }
+
+    /**
+     * This is for student to check their LimitRanges
+     * In this case, we need to verify if requested namespace actually belongs to the
+     * student, and should deny if foreign namespaces were requested.
+     * @param namespace
+     * @param name
+     * @return requested LimitRange
+     */
+    @GetMapping("/student/query")
+    public ResponseEntity<Response<LimitRange>> queryMyLimitRange(@RequestParam String namespace, @RequestParam String name) {
+        // TODO
+        return null;
+    }
+
+}

+ 34 - 0
src/main/java/cn/seecoder/devmanage/controller/LocalLogController.java

@@ -0,0 +1,34 @@
+package cn.seecoder.devmanage.controller;
+
+import cn.seecoder.devmanage.model.vo.LocalLogVO;
+import cn.seecoder.devmanage.model.vo.Response;
+import cn.seecoder.devmanage.service.LocalLogService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/locallog")
+public class LocalLogController {
+
+    private final LocalLogService localLogService;
+
+    @GetMapping("/page")
+    public ResponseEntity<Response<List<LocalLogVO>>> getPagedLogs(@RequestParam Integer pageNo, @RequestParam Integer pageSize) {
+        List<LocalLogVO> pagedLogs = localLogService.getPagedLogs(pageNo, pageSize);
+        return ResponseEntity.ok().body(Response.buildSuccess(pagedLogs));
+    }
+
+    @GetMapping("/search")
+    public ResponseEntity<Response<List<LocalLogVO>>> searchPagedLogs(@RequestParam Integer pageNo, @RequestParam Integer pageSize, @RequestParam String keyword) {
+        List<LocalLogVO> pagedLogs = localLogService.searchPagedLogs(pageNo, pageSize, keyword);
+        return ResponseEntity.ok().body(Response.buildSuccess(pagedLogs));
+    }
+
+}

+ 14 - 2
src/main/java/cn/seecoder/devmanage/log/LocalLogger.java

@@ -4,6 +4,7 @@ import cn.seecoder.devmanage.model.po.LocalLog;
 import cn.seecoder.devmanage.repository.LocalLogRepository;
 import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
@@ -33,18 +34,29 @@ public class LocalLogger {
 
     @Transactional
     public void recordSuccess(String message) {
-        String currentUser = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        String currentUser = getCurrentUser();
         LocalLog localLog = new LocalLog(currentUser, message, System.currentTimeMillis(), true);
         localLogRepository.save(localLog);
     }
 
     @Transactional
     public void recordFailure(String message) {
-        String currentUser = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        String currentUser = getCurrentUser();
         LocalLog localLog = new LocalLog(currentUser, message, System.currentTimeMillis(), false);
         localLogRepository.save(localLog);
     }
 
+    private String getCurrentUser() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        String currentUser;
+        if (authentication != null) {
+            currentUser = (String) authentication.getPrincipal();
+        } else {
+            currentUser = "Unauthorized";
+        }
+        return currentUser;
+    }
+
     public static String convertTime(Long timestamp) {
         Date date = new Date(timestamp);
         return dateFormat.format(date);

+ 47 - 0
src/main/java/cn/seecoder/devmanage/model/po/LimitRequest.java

@@ -0,0 +1,47 @@
+package cn.seecoder.devmanage.model.po;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.persistence.*;
+
+import static javax.persistence.GenerationType.AUTO;
+
+@Entity
+@Getter
+@Setter
+public class LimitRequest {
+    @Id
+    @GeneratedValue(strategy = AUTO)
+    private Long id;
+    @Column(nullable = false)
+    private String requester; // user principle
+    @Column(nullable = false)
+    private String namespace;
+    @Column(nullable = false)
+    private String name; // name of LimitRange
+    @Column(nullable = false)
+    private Integer maxMem;
+    @Column(nullable = false)
+    private Integer maxCpu;
+    @Column(nullable = false)
+    private Boolean handled;
+    @Column(nullable = false)
+    private Boolean permitted;
+    @Column(nullable = false)
+    private Long timestamp;
+
+    public LimitRequest() {
+    }
+
+    public LimitRequest(String requester, String namespace, String name, Integer maxMem, Integer maxCpu, Boolean handled, Boolean permitted, Long timestamp) {
+        this.requester = requester;
+        this.namespace = namespace;
+        this.name = name;
+        this.maxMem = maxMem;
+        this.maxCpu = maxCpu;
+        this.handled = handled;
+        this.permitted = permitted;
+        this.timestamp = timestamp;
+    }
+}

+ 12 - 0
src/main/java/cn/seecoder/devmanage/model/vo/DeploymentVO.java

@@ -0,0 +1,12 @@
+package cn.seecoder.devmanage.model.vo;
+
+import lombok.Data;
+
+@Data
+public class DeploymentVO {
+    private Integer id;
+    private Integer pipelineId;
+    private String namespace;
+    private String deployName;
+    private String deployedTime;
+}

+ 33 - 0
src/main/java/cn/seecoder/devmanage/model/vo/LimitRequestVO.java

@@ -0,0 +1,33 @@
+package cn.seecoder.devmanage.model.vo;
+
+import cn.seecoder.devmanage.log.LocalLogger;
+import cn.seecoder.devmanage.model.po.LimitRequest;
+import lombok.Data;
+
+@Data
+public class LimitRequestVO {
+    private Long id;
+    private String requester;
+    private String namespace;
+    private String name;
+    private Integer maxMem;
+    private Integer maxCpu;
+    private Boolean handled;
+    private Boolean permitted;
+    private String time;
+
+    public LimitRequestVO() {
+    }
+
+    public LimitRequestVO(LimitRequest limitRequest) {
+        id = limitRequest.getId();
+        requester = limitRequest.getRequester();
+        namespace = limitRequest.getNamespace();
+        name = limitRequest.getName();
+        maxMem = limitRequest.getMaxMem();
+        maxCpu = limitRequest.getMaxCpu();
+        handled = limitRequest.getHandled();
+        permitted = limitRequest.getPermitted();
+        time = LocalLogger.convertTime(limitRequest.getTimestamp());
+    }
+}

+ 3 - 3
src/main/java/cn/seecoder/devmanage/model/vo/LocalLogVo.java → src/main/java/cn/seecoder/devmanage/model/vo/LocalLogVO.java

@@ -5,16 +5,16 @@ import cn.seecoder.devmanage.model.po.LocalLog;
 import lombok.Data;
 
 @Data
-public class LocalLogVo {
+public class LocalLogVO {
     private String operator;
     private String message;
     private String time;
     private Boolean success;
 
-    public LocalLogVo() {
+    public LocalLogVO() {
     }
 
-    public LocalLogVo(LocalLog localLog) {
+    public LocalLogVO(LocalLog localLog) {
         operator = localLog.getOperator();
         message = localLog.getMessage();
         time = LocalLogger.convertTime(localLog.getTimestamp());

+ 2 - 0
src/main/java/cn/seecoder/devmanage/model/vo/Response.java

@@ -2,11 +2,13 @@ package cn.seecoder.devmanage.model.vo;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.NoArgsConstructor;
 
 import java.io.Serializable;
 
 @Data
 @AllArgsConstructor
+@NoArgsConstructor
 public class Response<T> implements Serializable {
     private Integer code;
     private String msg;

+ 42 - 0
src/main/java/cn/seecoder/devmanage/model/vo/UserIdentity.java

@@ -0,0 +1,42 @@
+package cn.seecoder.devmanage.model.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public enum UserIdentity {
+
+    @JsonProperty("学生")
+    STUDENT("学生"),
+
+    @JsonProperty("教师")
+    TEACHER("教师"),
+
+    @JsonProperty("管理员")
+    ADMIN("管理员"),
+
+    @JsonProperty("职员")
+    STAFF("职员"),
+
+    @JsonProperty("HR")
+    HR("HR"),
+
+    @JsonProperty("SEEC")
+    SEEC("SEEC"),
+
+    @JsonProperty("SEECX")
+    SEECX("SEECX"),
+
+    @JsonProperty("ASSISTANT")
+    ASSISTANT("ASSISTANT");
+
+    private final String name;
+
+    UserIdentity(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+}
+

+ 16 - 0
src/main/java/cn/seecoder/devmanage/model/vo/UserVO.java

@@ -0,0 +1,16 @@
+package cn.seecoder.devmanage.model.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserVO {
+    private Integer id;
+    private String username;
+    private String phone;
+    private String email;
+    private UserIdentity role;
+}

+ 9 - 0
src/main/java/cn/seecoder/devmanage/repository/LimitRequestRepository.java

@@ -0,0 +1,9 @@
+package cn.seecoder.devmanage.repository;
+
+import cn.seecoder.devmanage.model.po.LimitRequest;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional(readOnly = true)
+public interface LimitRequestRepository extends JpaRepository<LimitRequest, Long> {
+}

+ 21 - 2
src/main/java/cn/seecoder/devmanage/security/JwtAuthenticationFilter.java

@@ -1,9 +1,13 @@
 package cn.seecoder.devmanage.security;
 
+import cn.seecoder.devmanage.log.LocalLogger;
 import cn.seecoder.devmanage.model.vo.Response;
 import com.auth0.jwt.JWT;
 import com.auth0.jwt.JWTVerifier;
 import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.exceptions.SignatureVerificationException;
+import com.auth0.jwt.exceptions.TokenExpiredException;
 import com.auth0.jwt.interfaces.DecodedJWT;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
@@ -34,10 +38,23 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
 @Slf4j
 public class JwtAuthenticationFilter extends OncePerRequestFilter {
     private final JwtHelper jwtHelper;
+    private final LocalLogger localLogger;
 
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         String authorizationHeader = request.getHeader(AUTHORIZATION);
+        /*
+        X-Forwarded-For format
+        203.0.113.195, 70.41.3.18, 150.172.238.178
+        client ip, proxy1, proxy2
+         */
+        String ipAddressList = request.getHeader("X-Forwarded-For");
+        String clientIpAddress;
+        if (ipAddressList == null) {
+            clientIpAddress = "[lacks X-Forwarded-For header]";
+        } else {
+            clientIpAddress = ipAddressList.split(",")[0];
+        }
         if (authorizationHeader != null) {
             try {
                 Algorithm algorithm = jwtHelper.getAlgorithm();
@@ -51,8 +68,9 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
                         new UsernamePasswordAuthenticationToken(username, null, authorities);
                 SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                 filterChain.doFilter(request, response);
-            } catch (Exception e) {
-                log.error("Error JWT parsing or verifying: {}", e.getMessage());
+            } catch (JWTDecodeException | SignatureVerificationException | TokenExpiredException e) {
+                log.error("Error JWT parsing: {}", e.getMessage());
+                localLogger.recordFailure(String.format("Invalid JWT from %s: %s (JWT: %s)", clientIpAddress, e.getMessage(), authorizationHeader));
                 response.setStatus(FORBIDDEN.value());
                 response.setContentType(APPLICATION_JSON_VALUE);
                 new ObjectMapper().writeValue(response.getOutputStream(),
@@ -60,6 +78,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
             }
         } else {
             log.error("Request lacks Authorization header.");
+            localLogger.recordFailure(String.format("Request from %s lacks Authorization header.", clientIpAddress));
             response.setStatus(FORBIDDEN.value());
             response.setContentType(APPLICATION_JSON_VALUE);
             new ObjectMapper().writeValue(response.getOutputStream(),

+ 2 - 0
src/main/java/cn/seecoder/devmanage/security/JwtHelper.java

@@ -12,6 +12,8 @@ import javax.annotation.PostConstruct;
 public class JwtHelper {
     @Value("${seecoder.jwt.secret}")
     private String secret;
+    @Value("${seecoder.devcloud.jwt}")
+    private String jwtForDevcloud;
     private Algorithm algorithm;
 
     @PostConstruct

+ 13 - 0
src/main/java/cn/seecoder/devmanage/service/DevcloudLinkageService.java

@@ -0,0 +1,13 @@
+package cn.seecoder.devmanage.service;
+
+import cn.seecoder.devmanage.model.vo.DeploymentVO;
+import cn.seecoder.devmanage.model.vo.UserVO;
+
+import java.util.List;
+
+public interface DevcloudLinkageService {
+    List<DeploymentVO> getPagedDeployments(Integer pageNo, Integer pageSize);
+    List<DeploymentVO> searchPagedDeployments(String keyword, Integer pageNo, Integer pageSize);
+    UserVO getOwnerOfNamespace(String namespace);
+    Integer getTotalPageCount(Integer pageSize);
+}

+ 2 - 2
src/main/java/cn/seecoder/devmanage/service/HelloService.java

@@ -1,8 +1,8 @@
 package cn.seecoder.devmanage.service;
 
 import cn.seecoder.devmanage.model.po.LocalLog;
-import cn.seecoder.devmanage.model.vo.LocalLogVo;
+import cn.seecoder.devmanage.model.vo.LocalLogVO;
 
 public interface HelloService {
-    public LocalLog putLog(LocalLogVo localLogVo);
+    public LocalLog putLog(LocalLogVO localLogVo);
 }

+ 13 - 0
src/main/java/cn/seecoder/devmanage/service/LimitRequestService.java

@@ -0,0 +1,13 @@
+package cn.seecoder.devmanage.service;
+
+import cn.seecoder.devmanage.model.vo.LimitRequestVO;
+
+import java.util.List;
+
+public interface LimitRequestService {
+    LimitRequestVO newLimitRequest(String namespace, String name, Integer maxCpu, Integer maxMem);
+    List<LimitRequestVO> getPagedLimitRequests(Integer pageNo, Integer pageSize, boolean descByTime, int handledSwitch);
+    List<LimitRequestVO> searchPagedLimitRequests(Integer pageNo, Integer pageSize, String keyword, boolean descByTime);
+    LimitRequestVO permitRequest(Long limitRequestId);
+    void denyRequest(Long limitRequest);
+}

+ 10 - 0
src/main/java/cn/seecoder/devmanage/service/LocalLogService.java

@@ -0,0 +1,10 @@
+package cn.seecoder.devmanage.service;
+
+import cn.seecoder.devmanage.model.vo.LocalLogVO;
+
+import java.util.List;
+
+public interface LocalLogService {
+    List<LocalLogVO> getPagedLogs(Integer pageNo, Integer pageSize);
+    List<LocalLogVO> searchPagedLogs(Integer pageNo, Integer pageSize, String keyword);
+}

+ 123 - 0
src/main/java/cn/seecoder/devmanage/service/impl/DevcloudLinkageServiceImpl.java

@@ -0,0 +1,123 @@
+package cn.seecoder.devmanage.service.impl;
+
+import cn.seecoder.devmanage.model.vo.DeploymentVO;
+import cn.seecoder.devmanage.model.vo.Response;
+import cn.seecoder.devmanage.model.vo.UserVO;
+import cn.seecoder.devmanage.security.JwtHelper;
+import cn.seecoder.devmanage.service.DevcloudLinkageService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.springframework.http.HttpHeaders.ACCEPT;
+import static org.springframework.http.HttpHeaders.AUTHORIZATION;
+import static org.springframework.http.HttpMethod.GET;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+
+@RequiredArgsConstructor
+@Service
+public class DevcloudLinkageServiceImpl implements DevcloudLinkageService {
+
+    @Value("${seecoder.devcloud.url}")
+    private String DEVCLOUD_API_BASE_URL;
+    private final String SUFFIX_DEPLOYMENT = "/deployment";
+    private final String SUFFIX_DEPLOYMENT_SEARCH = "/deployment/search";
+    private final String SUFFIX_OWNER_NAMESPACE = "/owner/namespace";
+    private final String SUFFIX_PAGES = "/pages";
+    private final JwtHelper jwtHelper;
+    private final RestTemplate restTemplate;
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Override
+    public List<DeploymentVO> getPagedDeployments(Integer pageNo, Integer pageSize) {
+        HttpEntity<?> request = getDefaultRequest();
+        Map<String, Integer> params = new HashMap<>();
+        params.put("pageNo", pageNo);
+        params.put("pageSize", pageSize);
+        String url = UriComponentsBuilder.fromHttpUrl(DEVCLOUD_API_BASE_URL + SUFFIX_DEPLOYMENT)
+                .queryParam("pageNo", "{pageNo}")
+                .queryParam("pageSize", "{pageSize}")
+                .encode().toUriString();
+        ResponseEntity<Response> response = restTemplate.exchange(url, GET, request, Response.class, params);
+        Response<List<DeploymentVO>> body = response.getBody();
+        if (body != null) {
+            return body.getData();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public List<DeploymentVO> searchPagedDeployments(String keyword, Integer pageNo, Integer pageSize) {
+        HttpEntity<?> request = getDefaultRequest();
+        Map<String, String> params = new HashMap<>();
+        params.put("keyword", keyword);
+        params.put("pageNo", pageNo.toString());
+        params.put("pageSize", pageSize.toString());
+        String url = UriComponentsBuilder.fromHttpUrl(DEVCLOUD_API_BASE_URL + SUFFIX_DEPLOYMENT_SEARCH)
+                .queryParam("pageNo", "{pageNo}")
+                .queryParam("pageSize", "{pageSize}")
+                .queryParam("keyword", "{keyword}")
+                .encode().toUriString();
+        ResponseEntity<Response> response = restTemplate.exchange(url, GET, request, Response.class, params);
+        Response<List<DeploymentVO>> body = response.getBody();
+        if (body != null) {
+            return body.getData();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public UserVO getOwnerOfNamespace(String namespace) {
+        HttpEntity<?> request = getDefaultRequest();
+        Map<String, String> params = new HashMap<>();
+        params.put("namespace", namespace);
+        String url = UriComponentsBuilder.fromHttpUrl(DEVCLOUD_API_BASE_URL + SUFFIX_OWNER_NAMESPACE)
+                .queryParam("namespace", "{namespace}")
+                .encode().toUriString();
+        ResponseEntity<Response> response = restTemplate.exchange(url, GET, request, Response.class, params);
+        Response body = response.getBody();
+        if (body != null) {
+            // By default, body.getData() is of LinkedHashMap type
+            return mapper.convertValue(body.getData(), UserVO.class);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public Integer getTotalPageCount(Integer pageSize) {
+        HttpEntity<?> request = getDefaultRequest();
+        Map<String, Integer> params = new HashMap<>();
+        params.put("pageSize", pageSize);
+        String url = UriComponentsBuilder.fromHttpUrl(DEVCLOUD_API_BASE_URL + SUFFIX_PAGES)
+                .queryParam("pageSize", "{pageSize}")
+                .encode().toUriString();
+        ResponseEntity<Response> response = restTemplate.exchange(url, GET, request, Response.class, params);
+        Response<Integer> body = response.getBody();
+        if (body != null) {
+            return body.getData();
+        } else {
+            return null;
+        }
+    }
+
+    private HttpEntity<?> getDefaultRequest() {
+        HttpHeaders headers = new HttpHeaders();
+        headers.add(AUTHORIZATION, jwtHelper.getJwtForDevcloud());
+        headers.add(ACCEPT, APPLICATION_JSON_VALUE);
+        return new HttpEntity<>(headers);
+    }
+
+}

+ 2 - 5
src/main/java/cn/seecoder/devmanage/service/impl/HelloServiceImpl.java

@@ -1,8 +1,7 @@
 package cn.seecoder.devmanage.service.impl;
 
-import cn.seecoder.devmanage.log.LocalLogger;
 import cn.seecoder.devmanage.model.po.LocalLog;
-import cn.seecoder.devmanage.model.vo.LocalLogVo;
+import cn.seecoder.devmanage.model.vo.LocalLogVO;
 import cn.seecoder.devmanage.repository.LocalLogRepository;
 import cn.seecoder.devmanage.service.HelloService;
 import lombok.RequiredArgsConstructor;
@@ -10,8 +9,6 @@ import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.text.ParseException;
-
 @Service
 @Profile("lyy")
 @RequiredArgsConstructor
@@ -21,7 +18,7 @@ public class HelloServiceImpl implements HelloService {
 
     @Transactional
     @Override
-    public LocalLog putLog(LocalLogVo localLogVo) {
+    public LocalLog putLog(LocalLogVO localLogVo) {
 //        try {
 //            LocalLog localLog = new LocalLog(localLogVo.getOperator(),
 //                    localLogVo.getMessage(),

+ 157 - 0
src/main/java/cn/seecoder/devmanage/service/impl/LimitRequestServiceImpl.java

@@ -0,0 +1,157 @@
+package cn.seecoder.devmanage.service.impl;
+
+import cn.seecoder.devmanage.model.po.LimitRequest;
+import cn.seecoder.devmanage.model.vo.LimitRequestVO;
+import cn.seecoder.devmanage.repository.LimitRequestRepository;
+import cn.seecoder.devmanage.service.LimitRequestService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.*;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class LimitRequestServiceImpl implements LimitRequestService {
+
+    private final LimitRequestRepository limitRequestRepository;
+    private final int[] validPageSizes = new int[]{5, 10, 20};
+    private final String[] HANDLED_MATCHES = {"已处理", "handled", "handle", "已审阅", "已操作"};
+    private final String[] UNHANDLED_MATCHES = {"未处理", "unhandled", "unhandle", "未审阅", "未操作"};
+    private final String[] PERMITTED_MATCHES = {"已允许", "允许", "permitted", "permit", "已获准", "获准", "已准许", "准许"};
+    private final String[] DENIED_MATCHES = {"未允许", "未获准", "未准许", "deny", "denied", "unpermitted", "unpermit"};
+    private final int ALL_REQUEST = 0;
+    private final int ONLY_HANDLED = 1;
+    private final int ONLY_UNHANDLED = 2;
+
+    @Override
+    public LimitRequestVO newLimitRequest(String namespace, String name, Integer maxCpu, Integer maxMem) {
+        String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        LimitRequest limitRequest =
+                new LimitRequest(username, namespace, name, maxMem, maxCpu, false, false, System.currentTimeMillis());
+        LimitRequest save = limitRequestRepository.save(limitRequest);
+        return new LimitRequestVO(save);
+    }
+
+    @Override
+    public List<LimitRequestVO> getPagedLimitRequests(Integer pageNo, Integer pageSize, boolean descByTime, int handledSwitch) {
+        if (isInvalidPageSize(pageSize)) {
+            return new ArrayList<>();
+        }
+        Page<LimitRequest> results;
+        Sort sort = descByTime ? Sort.by("timestamp").descending() : Sort.by("timestamp").ascending();
+        if (handledSwitch == ALL_REQUEST) {
+            results = limitRequestRepository.findAll(PageRequest.of(pageNo, pageSize, sort));
+        } else if (handledSwitch == ONLY_HANDLED || handledSwitch == ONLY_UNHANDLED) {
+            boolean handled = handledSwitch == ONLY_HANDLED;
+            LimitRequest probe = new LimitRequest();
+            probe.setHandled(handled);
+            ExampleMatcher matcher = ExampleMatcher.matchingAll()
+                    .withMatcher("handled", ExampleMatcher.GenericPropertyMatchers.exact());
+            Example<LimitRequest> example = Example.of(probe, matcher);
+            results = limitRequestRepository.findAll(example, PageRequest.of(pageNo, pageSize, sort));
+        } else {
+            return new ArrayList<>();
+        }
+        List<LimitRequestVO> ans = new ArrayList<>(pageSize);
+        for (LimitRequest po: results) {
+            ans.add(new LimitRequestVO(po));
+        }
+        return ans;
+    }
+
+    @Override
+    public List<LimitRequestVO> searchPagedLimitRequests(Integer pageNo, Integer pageSize, String keyword, boolean descByTime) {
+        if (isInvalidPageSize(pageSize) || keyword == null) {
+            return new ArrayList<>();
+        }
+        Sort sort = descByTime ? Sort.by("timestamp").descending() : Sort.by("timestamp").ascending();
+        LimitRequest probe = new LimitRequest();
+        probe.setRequester(keyword);
+        probe.setNamespace(keyword);
+        probe.setName(keyword);
+        // String keywords
+        ExampleMatcher matcher = ExampleMatcher.matchingAny()
+                .withIgnoreCase()
+                .withMatcher("requester", ExampleMatcher.GenericPropertyMatchers.contains())
+                .withMatcher("namespace", ExampleMatcher.GenericPropertyMatchers.contains())
+                .withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains());
+        // Long keyword
+        if (isParsableLong(keyword)) {
+            probe.setId(Long.parseLong(keyword));
+            matcher = matcher.withMatcher("id", ExampleMatcher.GenericPropertyMatchers.exact());
+        }
+        // Integer keywords
+        if (isParsableInteger(keyword)) {
+            int intKey = Integer.parseInt(keyword);
+            probe.setMaxMem(intKey);
+            probe.setMaxCpu(intKey);
+            matcher = matcher
+                    .withMatcher("maxMem", ExampleMatcher.GenericPropertyMatchers.exact())
+                    .withMatcher("maxCpu", ExampleMatcher.GenericPropertyMatchers.exact());
+        }
+        // Handled
+        if (Arrays.asList(HANDLED_MATCHES).contains(keyword)) {
+            probe.setHandled(true);
+            matcher = matcher.withMatcher("handled", ExampleMatcher.GenericPropertyMatchers.exact());
+        } else if (Arrays.asList(UNHANDLED_MATCHES).contains(keyword)) {
+            probe.setHandled(false);
+            matcher = matcher.withMatcher("handled", ExampleMatcher.GenericPropertyMatchers.exact());
+        }
+        // Permitted
+        if (Arrays.asList(PERMITTED_MATCHES).contains(keyword)) {
+            probe.setPermitted(true);
+            matcher = matcher.withMatcher("permitted", ExampleMatcher.GenericPropertyMatchers.exact());
+        } else if (Arrays.asList(DENIED_MATCHES).contains(keyword)) {
+            probe.setPermitted(false);
+            matcher = matcher.withMatcher("permitted", ExampleMatcher.GenericPropertyMatchers.exact());
+        }
+        Example<LimitRequest> example = Example.of(probe, matcher);
+        Page<LimitRequest> results = limitRequestRepository.findAll(example, PageRequest.of(pageNo, pageSize, sort));
+        List<LimitRequestVO> ans = new ArrayList<>();
+        for (LimitRequest po: results) {
+            ans.add(new LimitRequestVO(po));
+        }
+        return ans;
+    }
+
+    @Override
+    public LimitRequestVO permitRequest(Long limitRequestId) {
+        return null;
+    }
+
+    @Override
+    public void denyRequest(Long limitRequest) {
+
+    }
+
+    private boolean isParsableLong(String keyword) {
+        try {
+            Long.parseLong(keyword);
+            return true;
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+
+    private boolean isParsableInteger(String keyword) {
+        try {
+            Integer.parseInt(keyword);
+            return true;
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+
+    private boolean isInvalidPageSize(Integer pageSize) {
+        for (int validPageSize: validPageSizes) {
+            if (validPageSize == pageSize) {
+                return false;
+            }
+        }
+        return true;
+    }
+}

+ 93 - 0
src/main/java/cn/seecoder/devmanage/service/impl/LocalLogServiceImpl.java

@@ -0,0 +1,93 @@
+package cn.seecoder.devmanage.service.impl;
+
+import cn.seecoder.devmanage.model.po.LocalLog;
+import cn.seecoder.devmanage.model.vo.LocalLogVO;
+import cn.seecoder.devmanage.repository.LocalLogRepository;
+import cn.seecoder.devmanage.service.LocalLogService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Example;
+import org.springframework.data.domain.ExampleMatcher;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Transactional(readOnly = true)
+@RequiredArgsConstructor
+@Service
+public class LocalLogServiceImpl implements LocalLogService {
+
+    private final LocalLogRepository localLogRepository;
+    private static final int[] validPageSizes = new int[]{5, 10, 20};
+
+    @Override
+    public List<LocalLogVO> getPagedLogs(Integer pageNo, Integer pageSize) {
+        if (isInvalidPageSize(pageSize)) {
+            return new ArrayList<>();
+        } else {
+            Page<LocalLog> results = localLogRepository.findAll(PageRequest.of(pageNo, pageSize));
+            List<LocalLogVO> ans = new ArrayList<>(pageSize);
+            for (LocalLog localLog: results) {
+                ans.add(new LocalLogVO(localLog));
+            }
+            return ans;
+        }
+    }
+
+    @Override
+    public List<LocalLogVO> searchPagedLogs(Integer pageNo, Integer pageSize, String keyword) {
+        if (isInvalidPageSize(pageSize)) {
+            return new ArrayList<>();
+        }
+        // does not support time based searches
+        // Step 1: construct probe entity
+        LocalLog probe = new LocalLog();
+        if (isParsableLong(keyword)) {
+            probe.setId(Long.parseLong(keyword));
+        }
+        probe.setMessage(keyword);
+        if (keyword.equalsIgnoreCase("true")) {
+            probe.setSuccess(true);
+        } else if (keyword.equalsIgnoreCase("false")) {
+            probe.setSuccess(false);
+        }
+        probe.setOperator(keyword);
+        // Step 2: construct ExampleMatcher
+        ExampleMatcher matcher = ExampleMatcher.matchingAny()
+                .withMatcher("id", ExampleMatcher.GenericPropertyMatchers.exact())
+                .withMatcher("operator", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
+                .withMatcher("message", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
+                .withMatcher("success", ExampleMatcher.GenericPropertyMatchers.exact())
+                .withIgnorePaths("timestamp");
+        // Step 3: construct Example
+        Example<LocalLog> example = Example.of(probe, matcher);
+        // Step 4: query by Example
+        Page<LocalLog> results = localLogRepository.findAll(example, PageRequest.of(pageNo, pageSize));
+        List<LocalLogVO> ans = new ArrayList<>(pageSize);
+        for (LocalLog localLog: results) {
+            ans.add(new LocalLogVO(localLog));
+        }
+        return ans;
+    }
+
+    private boolean isParsableLong(String keyword) {
+        try {
+            Long.parseLong(keyword);
+            return true;
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+
+    private boolean isInvalidPageSize(Integer pageSize) {
+        for (int validPageSize: validPageSizes) {
+            if (validPageSize == pageSize) {
+                return false;
+            }
+        }
+        return true;
+    }
+}

+ 3 - 0
src/main/resources/application-lyy.yaml

@@ -9,6 +9,9 @@ spring:
       ddl-auto: update
     show-sql: true
 seecoder:
+  devcloud:
+    jwt: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJEZXZtYW5hZ2UiLCJleHAiOjIwNDgzODAxMjQsImF1dGgiOiJTRUVDIiwidXNlcl9pbmZvIjp7ImlkIjowLCJwaG9uZSI6IiIsInVzZXJuYW1lIjoiRGV2bWFuYWdlIiwibmFtZSI6IkRldm1hbmFnZSIsImVtYWlsIjoiIiwicGhvdG8iOm51bGwsInJvbGUiOiJTRUVDIiwic2lnbmF0dXJlIjoi5pegIiwiY3JlYXRlZFRpbWUiOm51bGwsInZhbGlkIjp0cnVlfX0.BKVMLQsbr9Rx0G3OWzW8PZXgVLvu8XEev1F2OdTtpQI
+    url: https://devcloud.seec.seecoder.cn/api/devmanage
   jwt:
     secret: 127e5883-6e59-4868-becf-7fc0617194f2
   k8s: