From f233cc6a76bfe48802b72c4f2f06c1c69bff651d Mon Sep 17 00:00:00 2001 From: wangyu096 Date: Fri, 29 Mar 2024 22:15:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=B9=E5=99=A8=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20label=20selector=20=E8=A1=A8=E8=BE=BE?= =?UTF-8?q?=E5=BC=8F=20#2858?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/cc/model/filter/BaseRuleDTO.java | 23 ++ .../i18n/exception/message.properties | 1 + .../i18n/exception/message_en.properties | 1 + .../i18n/exception/message_en_US.properties | 1 + .../i18n/exception/message_zh.properties | 1 + .../i18n/exception/message_zh_CN.properties | 1 + .../bk/job/common/constant/ErrorCode.java | 1 + .../constant/LabelSelectorOperatorEnum.java | 67 ++++ .../openapi/v4/OpenApiKubePodFilterDTO.java | 3 + .../openapi/v4/OpenApiLabelSelectExprDTO.java | 3 +- .../execute/model/web/vo/StepExecutionVO.java | 10 +- .../execute/model/web/vo/StepOperationVO.java | 15 +- .../WebTaskExecutionResultResourceImpl.java | 1 + .../job/execute/model/ExecuteTargetDTO.java | 9 +- .../bk/job/execute/model/KubePodFilter.java | 9 +- .../execute/model}/LabelSelectExprDTO.java | 20 +- .../service/impl/ContainerServiceImpl.java | 29 +- .../label/selector/LabelSelectorParse.java | 86 +++++ .../selector/LabelSelectorParseException.java | 37 ++ .../util/label/selector/LabelValidator.java | 161 +++++++++ .../execute/util/label/selector/Lexer.java | 171 +++++++++ .../execute/util/label/selector/Operator.java | 59 ++++ .../execute/util/label/selector/Parser.java | 325 ++++++++++++++++++ .../util/label/selector/ParserContext.java | 36 ++ .../util/label/selector/Requirement.java | 208 +++++++++++ .../util/label/selector/ScannedItem.java | 51 +++ .../execute/util/label/selector/Token.java | 62 ++++ .../label/selector/K8sLabelSelectorTest.java | 170 +++++++++ 28 files changed, 1524 insertions(+), 37 deletions(-) create mode 100644 src/backend/commons/common/src/main/java/com/tencent/bk/job/common/constant/LabelSelectorOperatorEnum.java rename src/backend/{commons/cmdb-sdk/src/main/java/com/tencent/bk/job/common/cc/model/container => job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model}/LabelSelectExprDTO.java (83%) create mode 100644 src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelSelectorParse.java create mode 100644 src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelSelectorParseException.java create mode 100644 src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelValidator.java create mode 100644 src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Lexer.java create mode 100644 src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Operator.java create mode 100644 src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Parser.java create mode 100644 src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/ParserContext.java create mode 100644 src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Requirement.java create mode 100644 src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/ScannedItem.java create mode 100644 src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Token.java create mode 100644 src/backend/job-execute/service-job-execute/src/test/java/com/tencent/bk/job/execute/common/util/label/selector/K8sLabelSelectorTest.java diff --git a/src/backend/commons/cmdb-sdk/src/main/java/com/tencent/bk/job/common/cc/model/filter/BaseRuleDTO.java b/src/backend/commons/cmdb-sdk/src/main/java/com/tencent/bk/job/common/cc/model/filter/BaseRuleDTO.java index 4f67c002e6..e4dff18559 100644 --- a/src/backend/commons/cmdb-sdk/src/main/java/com/tencent/bk/job/common/cc/model/filter/BaseRuleDTO.java +++ b/src/backend/commons/cmdb-sdk/src/main/java/com/tencent/bk/job/common/cc/model/filter/BaseRuleDTO.java @@ -61,10 +61,18 @@ public static BaseRuleDTO in(String field, Object value) { return new BaseRuleDTO(field, RuleOperatorEnum.IN.getOperator(), value); } + public static BaseRuleDTO notIn(String field, Object value) { + return new BaseRuleDTO(field, RuleOperatorEnum.NOT_IN.getOperator(), value); + } + public static BaseRuleDTO equals(String field, Object value) { return new BaseRuleDTO(field, RuleOperatorEnum.EQUAL.getOperator(), value); } + public static BaseRuleDTO notEquals(String field, Object value) { + return new BaseRuleDTO(field, RuleOperatorEnum.NOT_EQUAL.getOperator(), value); + } + public static BaseRuleDTO contains(String field, Object value) { return new BaseRuleDTO(field, RuleOperatorEnum.CONTAINS.getOperator(), value); } @@ -73,5 +81,20 @@ public static BaseRuleDTO filterObject(String field, Object value) { return new BaseRuleDTO(field, RuleOperatorEnum.FILTER_OBJECT.getOperator(), value); } + public static BaseRuleDTO exists(String field) { + return new BaseRuleDTO(field, RuleOperatorEnum.EXIST.getOperator(), null); + } + + public static BaseRuleDTO notExists(String field) { + return new BaseRuleDTO(field, RuleOperatorEnum.NOT_EXIST.getOperator(), null); + } + + public static BaseRuleDTO greaterThan(String field, Object value) { + return new BaseRuleDTO(field, RuleOperatorEnum.GREATER.getOperator(), value); + } + + public static BaseRuleDTO lessThan(String field, Object value) { + return new BaseRuleDTO(field, RuleOperatorEnum.LESS.getOperator(), value); + } } diff --git a/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message.properties b/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message.properties index a711aba945..c5c75fae8d 100644 --- a/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message.properties +++ b/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message.properties @@ -180,6 +180,7 @@ 1244028=步骤 [{0}] 的目标执行对象为空 1244029=步骤 [{0}] 的源文件执行对象为空 1244030=作业引用的执行对象不存在。不存在的执行对象个数:{0},执行对象列表[{1}] +1244031=Label Selector 不合法 ## 业务错误-定时任务(job-crontab) 1245008=删除定时任务失败 diff --git a/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_en.properties b/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_en.properties index be9b1a17ef..7e7e59d17a 100644 --- a/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_en.properties +++ b/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_en.properties @@ -180,6 +180,7 @@ 1244028=Step [{0}] target execute object is empty 1244029=Step [{0}] source execute object is empty 1244030=Execute object referenced by the job does not exist. Number of non-existent execution objects: {0}, execution object list: [{1}] +1244031=Invalid Label Selector ## Business error - job-crontab 1245008=Failed to delete cron diff --git a/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_en_US.properties b/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_en_US.properties index be9b1a17ef..7e7e59d17a 100644 --- a/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_en_US.properties +++ b/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_en_US.properties @@ -180,6 +180,7 @@ 1244028=Step [{0}] target execute object is empty 1244029=Step [{0}] source execute object is empty 1244030=Execute object referenced by the job does not exist. Number of non-existent execution objects: {0}, execution object list: [{1}] +1244031=Invalid Label Selector ## Business error - job-crontab 1245008=Failed to delete cron diff --git a/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_zh.properties b/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_zh.properties index a711aba945..c5c75fae8d 100644 --- a/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_zh.properties +++ b/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_zh.properties @@ -180,6 +180,7 @@ 1244028=步骤 [{0}] 的目标执行对象为空 1244029=步骤 [{0}] 的源文件执行对象为空 1244030=作业引用的执行对象不存在。不存在的执行对象个数:{0},执行对象列表[{1}] +1244031=Label Selector 不合法 ## 业务错误-定时任务(job-crontab) 1245008=删除定时任务失败 diff --git a/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_zh_CN.properties b/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_zh_CN.properties index 59711c7f5c..dfb5558681 100644 --- a/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_zh_CN.properties +++ b/src/backend/commons/common-i18n/src/main/resources/i18n/exception/message_zh_CN.properties @@ -181,6 +181,7 @@ 1244028=步骤 [{0}] 的目标执行对象为空 1244029=步骤 [{0}] 的源文件执行对象为空 1244030=作业引用的执行对象不存在。不存在的执行对象个数:{0},执行对象列表[{1}] +1244031=Label Selector 不合法 ## 业务错误-定时任务(job-crontab) 1245008=删除定时任务失败 diff --git a/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/constant/ErrorCode.java b/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/constant/ErrorCode.java index 9bce6a35b9..c086ef50f3 100644 --- a/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/constant/ErrorCode.java +++ b/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/constant/ErrorCode.java @@ -340,6 +340,7 @@ public class ErrorCode { public static final int STEP_SOURCE_EXECUTE_OBJECT_EMPTY = 1244029; // 执行对象不存在。无效的{0}个执行对象:[{1}] public static final int EXECUTE_OBJECT_NOT_EXIST = 1244030; + public static final int INVALID_LABEL_SELECTOR = 1244031; // 作业执行 end // 定时作业 start diff --git a/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/constant/LabelSelectorOperatorEnum.java b/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/constant/LabelSelectorOperatorEnum.java new file mode 100644 index 0000000000..ec3a796b9d --- /dev/null +++ b/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/constant/LabelSelectorOperatorEnum.java @@ -0,0 +1,67 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.common.constant; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Label selector 操作符 + */ +public enum LabelSelectorOperatorEnum { + NOT_EXISTS("not_exists"), + EQUALS("equals"), + IN("in"), + NOT_EQUALS("not_equals"), + NOT_IN("not_in"), + EXISTS("exists"), + GREATER_THAN("gt"), + LESS_THAN("lt"); + + private final String op; + + LabelSelectorOperatorEnum(String op) { + this.op = op; + } + + public String getOp() { + return op; + } + + @JsonValue + public static LabelSelectorOperatorEnum valOf(String op) { + for (LabelSelectorOperatorEnum operatorEnum : values()) { + if (operatorEnum.getOp().equals(op)) { + return operatorEnum; + } + } + throw new IllegalArgumentException("No LabelSelectorOperatorEnum constant: " + op); + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static LabelSelectorOperatorEnum forOp(String op) { + return LabelSelectorOperatorEnum.valOf(op); + } +} diff --git a/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/model/openapi/v4/OpenApiKubePodFilterDTO.java b/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/model/openapi/v4/OpenApiKubePodFilterDTO.java index 24b1344204..b746bd765e 100644 --- a/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/model/openapi/v4/OpenApiKubePodFilterDTO.java +++ b/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/model/openapi/v4/OpenApiKubePodFilterDTO.java @@ -46,5 +46,8 @@ public class OpenApiKubePodFilterDTO { */ @JsonProperty("label_selector") private List labelSelector; + + @JsonProperty("label_selector_expr") + private String labelSelectorExpr; } diff --git a/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/model/openapi/v4/OpenApiLabelSelectExprDTO.java b/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/model/openapi/v4/OpenApiLabelSelectExprDTO.java index a7da0a4e86..91fe3cad4e 100644 --- a/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/model/openapi/v4/OpenApiLabelSelectExprDTO.java +++ b/src/backend/commons/common/src/main/java/com/tencent/bk/job/common/model/openapi/v4/OpenApiLabelSelectExprDTO.java @@ -25,6 +25,7 @@ package com.tencent.bk.job.common.model.openapi.v4; import com.fasterxml.jackson.annotation.JsonProperty; +import com.tencent.bk.job.common.constant.LabelSelectorOperatorEnum; import lombok.Data; import java.util.List; @@ -43,7 +44,7 @@ public class OpenApiLabelSelectExprDTO { /** * 计算操作符 */ - private String operator; + private LabelSelectorOperatorEnum operator; /** * Label value diff --git a/src/backend/job-execute/api-job-execute/src/main/java/com/tencent/bk/job/execute/model/web/vo/StepExecutionVO.java b/src/backend/job-execute/api-job-execute/src/main/java/com/tencent/bk/job/execute/model/web/vo/StepExecutionVO.java index 23aa3c9283..ddee2bb1f0 100644 --- a/src/backend/job-execute/api-job-execute/src/main/java/com/tencent/bk/job/execute/model/web/vo/StepExecutionVO.java +++ b/src/backend/job-execute/api-job-execute/src/main/java/com/tencent/bk/job/execute/model/web/vo/StepExecutionVO.java @@ -26,6 +26,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.tencent.bk.job.common.annotation.CompatibleImplementation; +import com.tencent.bk.job.common.constant.CompatibleType; import com.tencent.bk.job.common.util.json.DecimalFormatJsonSerializer; import com.tencent.bk.job.common.util.json.LongTimestampSerializer; import io.swagger.annotations.ApiModel; @@ -40,8 +42,14 @@ public class StepExecutionVO { @ApiModelProperty("步骤实例ID") private Long stepInstanceId; - @ApiModelProperty("执行次数,默认为0") + + @ApiModelProperty(value = "重试次数", hidden = true) + @CompatibleImplementation(name = "execute_object", deprecatedVersion = "3.9.x", type = CompatibleType.DEPLOY, + explain = "使用 executeCount 参数替换。发布完成后可以删除") private Integer retryCount; + + @ApiModelProperty("执行次数,默认为0") + private Integer executeCount; @ApiModelProperty("步骤名称") private String name; @ApiModelProperty("步骤类型,1-脚本,2-文件,3-人工确认") diff --git a/src/backend/job-execute/api-job-execute/src/main/java/com/tencent/bk/job/execute/model/web/vo/StepOperationVO.java b/src/backend/job-execute/api-job-execute/src/main/java/com/tencent/bk/job/execute/model/web/vo/StepOperationVO.java index fc8ed87ea0..26ab73d933 100644 --- a/src/backend/job-execute/api-job-execute/src/main/java/com/tencent/bk/job/execute/model/web/vo/StepOperationVO.java +++ b/src/backend/job-execute/api-job-execute/src/main/java/com/tencent/bk/job/execute/model/web/vo/StepOperationVO.java @@ -25,6 +25,8 @@ package com.tencent.bk.job.execute.model.web.vo; import com.fasterxml.jackson.annotation.JsonInclude; +import com.tencent.bk.job.common.annotation.CompatibleImplementation; +import com.tencent.bk.job.common.constant.CompatibleType; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -38,12 +40,19 @@ public class StepOperationVO { @ApiModelProperty("步骤实例ID") private Long stepInstanceId; - @ApiModelProperty("执行次数") + + @ApiModelProperty(value = "重试次数", hidden = true) + @CompatibleImplementation(name = "execute_object", deprecatedVersion = "3.9.x", type = CompatibleType.DEPLOY, + explain = "使用 executeCount 参数替换。发布完成后可以删除") private Integer retryCount; - public StepOperationVO(Long stepInstanceId, Integer retryCount) { + @ApiModelProperty("执行次数") + private Integer executeCount; + + public StepOperationVO(Long stepInstanceId, Integer executeCount) { this.stepInstanceId = stepInstanceId; - this.retryCount = retryCount; + this.retryCount = executeCount; + this.executeCount = executeCount; } } diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/api/web/impl/WebTaskExecutionResultResourceImpl.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/api/web/impl/WebTaskExecutionResultResourceImpl.java index 91877d5dea..f5c0e8b6b3 100644 --- a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/api/web/impl/WebTaskExecutionResultResourceImpl.java +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/api/web/impl/WebTaskExecutionResultResourceImpl.java @@ -429,6 +429,7 @@ private TaskExecuteResultVO convertToTaskExecuteResultVO(TaskExecuteResultDTO ta StepExecutionVO stepExecutionVO = new StepExecutionVO(); stepExecutionVO.setName(stepExecutionDTO.getName()); stepExecutionVO.setRetryCount(stepExecutionDTO.getExecuteCount()); + stepExecutionVO.setExecuteCount(stepExecutionDTO.getExecuteCount()); stepExecutionVO.setStepInstanceId(stepExecutionDTO.getStepInstanceId()); stepExecutionVO.setStartTime(stepExecutionDTO.getStartTime()); stepExecutionVO.setEndTime(stepExecutionDTO.getEndTime()); diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/ExecuteTargetDTO.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/ExecuteTargetDTO.java index 1d51d971a0..de539fe674 100644 --- a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/ExecuteTargetDTO.java +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/ExecuteTargetDTO.java @@ -27,7 +27,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.tencent.bk.job.common.annotation.PersistenceObject; -import com.tencent.bk.job.common.cc.model.container.LabelSelectExprDTO; import com.tencent.bk.job.common.esb.model.job.EsbIpDTO; import com.tencent.bk.job.common.esb.model.job.v3.EsbServerV3DTO; import com.tencent.bk.job.common.gse.util.AgentUtils; @@ -42,8 +41,10 @@ import com.tencent.bk.job.common.model.vo.TaskHostNodeVO; import com.tencent.bk.job.common.model.vo.TaskTargetVO; import com.tencent.bk.job.execute.engine.model.ExecuteObject; +import com.tencent.bk.job.execute.util.label.selector.LabelSelectorParse; import lombok.Data; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.Collection; @@ -579,10 +580,14 @@ private static List convertToKubeContainerFilter(OpenApiExe .map(labelSelectExpr -> new LabelSelectExprDTO( labelSelectExpr.getKey(), labelSelectExpr.getOperator(), - labelSelectExpr.getValue(), labelSelectExpr.getValues())) .collect(Collectors.toList())); + } else if (StringUtils.isNotBlank(originContainerFilter.getPodFilter().getLabelSelectorExpr())) { + // 优先解析 label_selector;如果 label_selector 不存在,那么解析 label_selector_expr + List labelSelectExprList = LabelSelectorParse.parseToLabelSelectExprList( + originContainerFilter.getPodFilter().getLabelSelectorExpr()); + podFilter.setLabelSelector(labelSelectExprList); } containerFilter.setPodFilter(podFilter); } diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/KubePodFilter.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/KubePodFilter.java index bf3a2a91d3..0d73135a8a 100644 --- a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/KubePodFilter.java +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/KubePodFilter.java @@ -24,9 +24,7 @@ package com.tencent.bk.job.execute.model; -import com.fasterxml.jackson.annotation.JsonProperty; import com.tencent.bk.job.common.annotation.PersistenceObject; -import com.tencent.bk.job.common.cc.model.container.LabelSelectExprDTO; import lombok.Data; import org.apache.commons.collections4.CollectionUtils; @@ -48,9 +46,13 @@ public class KubePodFilter implements Cloneable { /** * label selector */ - @JsonProperty("label_selector") private List labelSelector; + /** + * pod label selector expression + */ + private String labelSelectorExpr; + @Override public KubePodFilter clone() { KubePodFilter clone = new KubePodFilter(); @@ -62,6 +64,7 @@ public KubePodFilter clone() { labelSelector.forEach(labelSelectExpr -> cloneLabelSelectExprList.add(labelSelectExpr.clone())); clone.setLabelSelector(cloneLabelSelectExprList); } + clone.setLabelSelectorExpr(labelSelectorExpr); return clone; } diff --git a/src/backend/commons/cmdb-sdk/src/main/java/com/tencent/bk/job/common/cc/model/container/LabelSelectExprDTO.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/LabelSelectExprDTO.java similarity index 83% rename from src/backend/commons/cmdb-sdk/src/main/java/com/tencent/bk/job/common/cc/model/container/LabelSelectExprDTO.java rename to src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/LabelSelectExprDTO.java index 434bbccc8a..55987d2d5a 100644 --- a/src/backend/commons/cmdb-sdk/src/main/java/com/tencent/bk/job/common/cc/model/container/LabelSelectExprDTO.java +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/model/LabelSelectExprDTO.java @@ -22,10 +22,10 @@ * IN THE SOFTWARE. */ -package com.tencent.bk.job.common.cc.model.container; +package com.tencent.bk.job.execute.model; -import com.fasterxml.jackson.annotation.JsonProperty; import com.tencent.bk.job.common.annotation.PersistenceObject; +import com.tencent.bk.job.common.constant.LabelSelectorOperatorEnum; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.collections4.CollectionUtils; @@ -43,30 +43,21 @@ public class LabelSelectExprDTO implements Cloneable { /** * Label key */ - @JsonProperty("label_key") private String key; /** * 计算操作符 - */ - private String operator; - - /** - * Label value - */ - @JsonProperty("label_value") - private String value; + **/ + private LabelSelectorOperatorEnum operator; /** * Label values */ - @JsonProperty("label_values") private List values; - public LabelSelectExprDTO(String key, String operator, String value, List values) { + public LabelSelectExprDTO(String key, LabelSelectorOperatorEnum operator, List values) { this.key = key; this.operator = operator; - this.value = value; this.values = values; } @@ -74,7 +65,6 @@ public LabelSelectExprDTO(String key, String operator, String value, List(values)); diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/service/impl/ContainerServiceImpl.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/service/impl/ContainerServiceImpl.java index 2afbb8ce15..b2f9faa206 100644 --- a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/service/impl/ContainerServiceImpl.java +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/service/impl/ContainerServiceImpl.java @@ -31,18 +31,17 @@ import com.tencent.bk.job.common.cc.model.container.KubeNamespaceDTO; import com.tencent.bk.job.common.cc.model.container.KubeNodeID; import com.tencent.bk.job.common.cc.model.container.KubeWorkloadDTO; -import com.tencent.bk.job.common.cc.model.container.LabelSelectExprDTO; import com.tencent.bk.job.common.cc.model.container.PodDTO; import com.tencent.bk.job.common.cc.model.filter.BaseRuleDTO; import com.tencent.bk.job.common.cc.model.filter.ComposeRuleDTO; import com.tencent.bk.job.common.cc.model.filter.PropertyFilterDTO; import com.tencent.bk.job.common.cc.model.filter.RuleConditionEnum; -import com.tencent.bk.job.common.cc.model.filter.RuleOperatorEnum; import com.tencent.bk.job.common.cc.model.query.KubeClusterQuery; import com.tencent.bk.job.common.cc.model.query.NamespaceQuery; import com.tencent.bk.job.common.cc.model.query.WorkloadQuery; import com.tencent.bk.job.common.cc.model.req.ListKubeContainerByTopoReq; import com.tencent.bk.job.common.cc.sdk.BizCmdbClient; +import com.tencent.bk.job.common.constant.LabelSelectorOperatorEnum; import com.tencent.bk.job.common.model.dto.Container; import com.tencent.bk.job.common.model.dto.HostDTO; import com.tencent.bk.job.common.model.dto.ResourceScope; @@ -53,6 +52,7 @@ import com.tencent.bk.job.execute.model.KubeNamespaceFilter; import com.tencent.bk.job.execute.model.KubePodFilter; import com.tencent.bk.job.execute.model.KubeWorkloadFilter; +import com.tencent.bk.job.execute.model.LabelSelectExprDTO; import com.tencent.bk.job.execute.service.ContainerService; import com.tencent.bk.job.execute.service.HostService; import com.tencent.bk.job.manage.model.inner.ServiceListAppHostResultDTO; @@ -167,19 +167,24 @@ public List listContainerByContainerFilter(long appId, KubeC } private BaseRuleDTO buildLabelFilterRule(LabelSelectExprDTO labelSelectExpr) { - RuleOperatorEnum operator = RuleOperatorEnum.valOf(labelSelectExpr.getOperator()); + LabelSelectorOperatorEnum operator = labelSelectExpr.getOperator(); switch (operator) { - case EQUAL: - case NOT_EQUAL: - return new BaseRuleDTO(labelSelectExpr.getKey(), labelSelectExpr.getOperator(), - labelSelectExpr.getValue()); + case EQUALS: + return BaseRuleDTO.equals(labelSelectExpr.getKey(), labelSelectExpr.getValues().get(0)); + case NOT_EQUALS: + return BaseRuleDTO.notEquals(labelSelectExpr.getKey(), labelSelectExpr.getValues().get(0)); case IN: + return BaseRuleDTO.in(labelSelectExpr.getKey(), labelSelectExpr.getValues()); case NOT_IN: - return new BaseRuleDTO(labelSelectExpr.getKey(), labelSelectExpr.getOperator(), - labelSelectExpr.getValues()); - case EXIST: - case NOT_EXIST: - return new BaseRuleDTO(labelSelectExpr.getKey(), labelSelectExpr.getOperator(), null); + return BaseRuleDTO.notIn(labelSelectExpr.getKey(), labelSelectExpr.getValues()); + case EXISTS: + return BaseRuleDTO.exists(labelSelectExpr.getKey()); + case NOT_EXISTS: + return BaseRuleDTO.notExists(labelSelectExpr.getKey()); + case LESS_THAN: + return BaseRuleDTO.lessThan(labelSelectExpr.getKey(), labelSelectExpr.getValues().get(0)); + case GREATER_THAN: + return BaseRuleDTO.greaterThan(labelSelectExpr.getKey(), labelSelectExpr.getValues().get(0)); default: throw new IllegalArgumentException("Invalid label select operator: " + operator); } diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelSelectorParse.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelSelectorParse.java new file mode 100644 index 0000000000..7f6bcb4670 --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelSelectorParse.java @@ -0,0 +1,86 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.util.label.selector; + +import com.tencent.bk.job.common.constant.LabelSelectorOperatorEnum; +import com.tencent.bk.job.execute.model.LabelSelectExprDTO; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Kubernetes label selector 运算表达式解析。参考 Kubernetes 官方开源代码: + *

+ * https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/labels/selector.go + */ +public class LabelSelectorParse { + + public static List parse(String selector) { + Parser parser = new Parser(selector); + return parser.parse(); + } + + public static List parseToLabelSelectExprList(String selector) { + Parser parser = new Parser(selector); + List requirements = parser.parse(); + + if (CollectionUtils.isNotEmpty(requirements)) { + return Collections.emptyList(); + } + + return requirements.stream() + .map(requirement -> new LabelSelectExprDTO( + requirement.getKey(), + mapToLabelSelectorOperatorEnum(requirement.getOperator()), + requirement.getValues())) + .collect(Collectors.toList()); + } + + private static LabelSelectorOperatorEnum mapToLabelSelectorOperatorEnum(Operator operator) { + switch (operator) { + case Equals: + case DoubleEquals: + return LabelSelectorOperatorEnum.EQUALS; + case In: + return LabelSelectorOperatorEnum.IN; + case NotIn: + return LabelSelectorOperatorEnum.NOT_IN; + case Exists: + return LabelSelectorOperatorEnum.EXISTS; + case DoesNotExist: + return LabelSelectorOperatorEnum.NOT_EXISTS; + case NotEquals: + return LabelSelectorOperatorEnum.NOT_EQUALS; + case GreaterThan: + return LabelSelectorOperatorEnum.GREATER_THAN; + case LessThan: + return LabelSelectorOperatorEnum.LESS_THAN; + default: + throw new IllegalArgumentException("Invalid operator: " + operator); + } + } +} diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelSelectorParseException.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelSelectorParseException.java new file mode 100644 index 0000000000..c50409098c --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelSelectorParseException.java @@ -0,0 +1,37 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.util.label.selector; + +import com.tencent.bk.job.common.exception.InternalException; + +/** + * Label selector 解析异常 + */ +public class LabelSelectorParseException extends InternalException { + + public LabelSelectorParseException(String message) { + super(message, (Throwable) null); + } +} diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelValidator.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelValidator.java new file mode 100644 index 0000000000..79193cbf7d --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/LabelValidator.java @@ -0,0 +1,161 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.util.label.selector; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class LabelValidator { + + private static final String dns1123LabelFmt = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"; + private static final String dns1123SubdomainFmt = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*"; + private static final String dns1123SubdomainErrorMsg = "a lowercase RFC 1123 subdomain must consist of lower case" + + " alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character"; + private static final int DNS1123SubdomainMaxLength = 253; + private static final Pattern dns1123SubdomainRegexp = Pattern.compile("^" + dns1123SubdomainFmt + "$"); + private static final int qualifiedNameMaxLength = 63; + private static final String qnameCharFmt = "[a-zA-Z0-9]"; + private static final String qnameExtCharFmt = "[-A-Za-z0-9_.]"; + private static final String qualifiedNameFmt = "(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt; + private static final String qualifiedNameErrMsg = "must consist of alphanumeric characters, '-', '_' or '.', and " + + "must start and end with an alphanumeric character"; + private static final Pattern qualifiedNameRegexp = Pattern.compile("^" + qualifiedNameFmt + "$"); + + + private static final int LabelValueMaxLength = 63; + private static final String labelValueFmt = "(" + qualifiedNameFmt + ")?"; + private static final Pattern labelValueRegexp = Pattern.compile("^" + labelValueFmt + "$"); + private static final String labelValueErrMsg = "a valid label must be an empty string or consist of alphanumeric " + + "characters, '-', '_' or '.', and must start and end with an alphanumeric character"; + + /** + * Validate label key + */ + public static List validateLabelKey(String labelKey) { + return isQualifiedName(labelKey); + } + + /** + * IsQualifiedName tests whether the value passed is what Kubernetes calls a + * "qualified name". This is a format used in various places throughout the + * system. If the value is not valid, a list of error strings is returned. + * Otherwise an empty list (or nil) is returned. + */ + private static List isQualifiedName(String value) { + List errs = new ArrayList<>(); + String[] parts = value.split("/"); + String name; + switch (parts.length) { + case 1: + name = parts[0]; + break; + case 2: + String prefix = parts[0]; + name = parts[1]; + if (prefix.length() == 0) { + errs.add("prefix part must be " + emptyError()); + } else { + List msgs = isDNS1123Subdomain(prefix); + if (msgs.size() != 0) { + errs.addAll(prefixEach(msgs, "prefix part ")); + } + } + break; + default: + errs.add("a qualified name " + regexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", + "123-abc") + + " with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')"); + return errs; + } + if (name.length() == 0) { + errs.add("name part " + emptyError()); + } else if (name.length() > qualifiedNameMaxLength) { + errs.add("name part " + maxLenError(qualifiedNameMaxLength)); + } + if (!qualifiedNameRegexp.matcher(name).matches()) { + errs.add("name part " + regexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc")); + } + return errs; + } + + + private static List isDNS1123Subdomain(String value) { + List errs = new ArrayList<>(); + if (value.length() > DNS1123SubdomainMaxLength) { + errs.add(maxLenError(DNS1123SubdomainMaxLength)); + } + if (!dns1123SubdomainRegexp.matcher(value).matches()) { + errs.add(regexError(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com")); + } + return errs; + } + + private static String maxLenError(int length) { + return String.format("must be no more than %d characters", length); + } + + private static String regexError(String msg, String fmt, String... examples) { + if (examples.length == 0) { + return msg + " (regex used for validation is '" + fmt + "')"; + } + msg += " (e.g. "; + StringBuilder msgBuilder = new StringBuilder(msg); + for (int i = 0; i < examples.length; i++) { + if (i > 0) { + msgBuilder.append(" or "); + } + msgBuilder.append("'").append(examples[i]).append("', "); + } + msg = msgBuilder.toString(); + msg += "regex used for validation is '" + fmt + "')"; + return msg; + } + + private static String emptyError() { + return "must be non-empty"; + } + + private static List prefixEach(List msgs, String prefix) { + for (int i = 0; i < msgs.size(); i++) { + msgs.set(i, prefix + msgs.get(i)); + } + return msgs; + } + + public static List validateLabelValue(String value) { + List errs = new ArrayList<>(); + + if (value.length() > LabelValueMaxLength) { + errs.add(maxLenError(LabelValueMaxLength)); + } + + if (!labelValueRegexp.matcher(value).matches()) { + errs.add(regexError(labelValueErrMsg, labelValueFmt, "MyValue", "my_value", "12345")); + } + + return errs; + } +} diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Lexer.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Lexer.java new file mode 100644 index 0000000000..3ef8559198 --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Lexer.java @@ -0,0 +1,171 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.util.label.selector; + +/** + * Lexer represents the Lexer struct for label selector. + * It contains necessary information to tokenize the input string + */ +public class Lexer { + /** + * s stores the string to be tokenized + */ + private final String s; + /** + * pos is the position currently tokenized + */ + private int pos; + + public Lexer(String s) { + this.s = s; + this.pos = 0; + } + + /** + * read returns the character currently lexed + * increment the position and check the buffer overflow + */ + private char read() { + char ch = 0; + if (pos < s.length()) { + ch = s.charAt(pos); + pos++; + } + return ch; + } + + /** + * unread 'undoes' the last read character + */ + private void unread() { + pos--; + } + + /** + * isWhitespace returns true if the rune is a space, tab, or newline + */ + private boolean isWhitespace(char ch) { + return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'; + } + + /** + * isSpecialSymbol detects if the character ch can be an operator + */ + private boolean isSpecialSymbol(char ch) { + switch (ch) { + case '=': + case '!': + case '(': + case ')': + case ',': + case '>': + case '<': + return true; + } + return false; + } + + public ScannedItem lex() { + char ch = skipWhiteSpaces(read()); + if (ch == 0) { + return new ScannedItem(Token.EndOfStringToken, ""); + } + if (isSpecialSymbol(ch)) { + unread(); + return scanSpecialSymbol(); + } else { + unread(); + return scanIDOrKeyword(); + } + } + + /** + * scanIDOrKeyword scans string to recognize literal token (for example 'in') or an identifier. + */ + private ScannedItem scanIDOrKeyword() { + StringBuilder buffer = new StringBuilder(); + while (true) { + char ch = read(); + if (ch == 0) { + break; + } + if (isSpecialSymbol(ch) || isWhitespace(ch)) { + unread(); + break; + } else { + buffer.append(ch); + } + } + String s = buffer.toString(); + if (Token.string2token.containsKey(s)) { + return new ScannedItem(Token.string2token.get(s), s); + } + return new ScannedItem(Token.IdentifierToken, s); + } + + /** + * scanSpecialSymbol scans string starting with special symbol. + * special symbol identify non literal operators. "!=", "==", "=" + */ + private ScannedItem scanSpecialSymbol() { + ScannedItem lastScannedItem = new ScannedItem(); + StringBuilder buffer = new StringBuilder(); + while (true) { + char ch = read(); + if (ch == 0) { + break; + } + if (isSpecialSymbol(ch)) { + buffer.append(ch); + if (Token.string2token.containsKey(buffer.toString())) { + lastScannedItem = new ScannedItem(Token.string2token.get(buffer.toString()), buffer.toString()); + } else if (lastScannedItem.getToken() != null) { + unread(); + break; + } + } else { + unread(); + break; + } + } + if (lastScannedItem.getToken() == null) { + return new ScannedItem(Token.ErrorToken, ""); + } + return lastScannedItem; + } + + /** + * skipWhiteSpaces consumes all blank characters + * returning the first non blank character + */ + private char skipWhiteSpaces(char ch) { + while (true) { + if (!isWhitespace(ch)) { + return ch; + } + ch = read(); + } + } +} diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Operator.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Operator.java new file mode 100644 index 0000000000..9a8113f465 --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Operator.java @@ -0,0 +1,59 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.util.label.selector; + +/** + * Label selector 操作符 + */ +public enum Operator { + DoesNotExist("!"), + Equals("="), + DoubleEquals("=="), + In("in"), + NotEquals("!="), + NotIn("notin"), + Exists("exists"), + GreaterThan("gt"), + LessThan("lt"); + + private final String symbol; + + Operator(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } + + public static Operator valOf(String operator) { + for (Operator operatorEnum : values()) { + if (operatorEnum.getSymbol().equals(operator)) { + return operatorEnum; + } + } + throw new IllegalArgumentException("No Operator constant: " + operator); + } +} diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Parser.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Parser.java new file mode 100644 index 0000000000..e349169554 --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Parser.java @@ -0,0 +1,325 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.util.label.selector; + +import lombok.Data; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static com.tencent.bk.job.execute.util.label.selector.ParserContext.Values; + +/** + * Kubernetes label selector 解析 + */ +public class Parser { + private final Lexer lexer; + private final List scannedItems; + private int position; + + public Parser(String selector) { + this.lexer = new Lexer(selector); + this.scannedItems = new ArrayList<>(); + this.position = 0; + } + + /** + * scan runs through the input string and stores the ScannedItem in an array + * Parser can now lookahead and consume the tokens + */ + private void scan() { + while (true) { + ScannedItem scannedItem = lexer.lex(); + scannedItems.add(scannedItem); + if (scannedItem.getToken() == Token.EndOfStringToken) { + break; + } + } + } + + /** + * lookahead func returns the current token and string. No increment of current position + */ + private ScannedItem lookahead(ParserContext context) { + Token token = scannedItems.get(position).getToken(); + String literal = scannedItems.get(position).getLiteral(); + if (context == Values) { + switch (token) { + case InToken: + case NotInToken: + token = Token.IdentifierToken; + } + } + return new ScannedItem(token, literal); + } + + /** + * consume returns current token and string. Increments the position + */ + private ScannedItem consume(ParserContext context) { + position++; + Token token = scannedItems.get(position - 1).getToken(); + String literal = scannedItems.get(position - 1).getLiteral(); + if (context == Values) { + switch (token) { + case InToken: + case NotInToken: + token = Token.IdentifierToken; + } + } + return new ScannedItem(token, literal); + } + + public List parse() throws LabelSelectorParseException { + scan(); + + List requirements = new ArrayList<>(); + while (true) { + ScannedItem scannedItem = lookahead(Values); + Token token = scannedItem.getToken(); + switch (token) { + case IdentifierToken: + case DoesNotExistToken: + Requirement requirement = parseRequirement(); + requirements.add(requirement); + ScannedItem nextScannedItem = consume(Values); + Token nextToken = nextScannedItem.getToken(); + switch (nextToken) { + case EndOfStringToken: + return requirements; + case CommaToken: + ScannedItem nextScannedItem2 = lookahead(Values); + Token nextToken2 = nextScannedItem2.getToken(); + if (nextToken2 != Token.IdentifierToken && nextToken2 != Token.DoesNotExistToken) { + throw new LabelSelectorParseException( + "found '" + nextScannedItem2.getLiteral() + "', expected: identifier after ','"); + } + break; + default: + throw new LabelSelectorParseException("found '" + nextScannedItem.getLiteral() + + "', expected: ',' or 'end of string'"); + } + break; + case EndOfStringToken: + return requirements; + default: + throw new LabelSelectorParseException("found '" + scannedItems.get(position).getLiteral() + + "', expected: !, identifier, or 'end of string'"); + } + } + } + + private Requirement parseRequirement() throws LabelSelectorParseException { + KeyAndOperator keyAndOperator = parseKeyAndInferOperator(); + String key = keyAndOperator.getKey(); + Operator operator = keyAndOperator.getOperator(); + + if (operator == Operator.Exists || operator == Operator.DoesNotExist) { + return Requirement.newRequirement(key, operator, Collections.emptyList()); + } + + Operator op = parseOperator(); + List values; + switch (op) { + case In: + case NotIn: + values = parseValues(); + break; + case Equals: + case DoubleEquals: + case NotEquals: + case GreaterThan: + case LessThan: + values = parseExactValue(); + break; + default: + throw new LabelSelectorParseException("found '" + scannedItems.get(position).getLiteral() + + "', expected: " + Arrays.stream(Operator.values()) + .map(Operator::getSymbol).collect(Collectors.joining(", "))); + } + return Requirement.newRequirement(key, op, values); + } + + private Operator parseOperator() throws LabelSelectorParseException { + ScannedItem scannedItem = consume(ParserContext.KeyAndOperator); + Token token = scannedItem.getToken(); + switch (token) { + case InToken: + return Operator.In; + case EqualsToken: + return Operator.Equals; + case DoubleEqualsToken: + return Operator.DoubleEquals; + case GreaterThanToken: + return Operator.GreaterThan; + case LessThanToken: + return Operator.LessThan; + case NotInToken: + return Operator.NotIn; + case NotEqualsToken: + return Operator.NotEquals; + default: + throw new LabelSelectorParseException("found '" + scannedItem.getLiteral() + + "', expected: " + Arrays.stream(Operator.values()) + .map(Operator::getSymbol).collect(Collectors.joining(", "))); + } + } + + private List parseValues() throws LabelSelectorParseException { + ScannedItem scannedItem = consume(Values); + Token token = scannedItem.getToken(); + if (token != Token.OpenParToken) { + throw new LabelSelectorParseException( + "found '" + scannedItem.getLiteral() + "' expected: '('"); + } + scannedItem = lookahead(Values); + token = scannedItem.getToken(); + switch (token) { + case IdentifierToken: + case CommaToken: + List values = parseIdentifiersList(); + token = consume(Values).getToken(); + if (token != Token.ClosedParToken) { + throw new LabelSelectorParseException( + "found '" + scannedItem.getLiteral() + "', expected: ')'"); + } + return values; + case ClosedParToken: + consume(Values); + return Collections.emptyList(); + default: + throw new LabelSelectorParseException( + "found '" + scannedItems.get(position).getLiteral() + "', expected: ',', ')' or identifier"); + } + } + + private List parseIdentifiersList() throws LabelSelectorParseException { + List values = new ArrayList<>(); + while (true) { + Token token = consume(Values).getToken(); + switch (token) { + case IdentifierToken: + values.add(scannedItems.get(position - 1).getLiteral()); + token = lookahead(Values).getToken(); + switch (token) { + case CommaToken: + continue; + case ClosedParToken: + return values; + default: + throw new LabelSelectorParseException("found '" + scannedItems.get(position).getLiteral() + + "', expected: ',' or ')'"); + } + case CommaToken: + if (values.isEmpty()) { + values.add(""); + } + Token token2 = lookahead(Values).getToken(); + if (token2 == Token.ClosedParToken) { + values.add(""); + return values; + } + if (token2 == Token.CommaToken) { + consume(Values); + values.add(""); + } + break; + default: + return values; + } + } + } + + private List parseExactValue() throws LabelSelectorParseException { + List values = new ArrayList<>(); + Token token = lookahead(Values).getToken(); + if (token == Token.EndOfStringToken || token == Token.CommaToken) { + values.add(""); + return values; + } + token = consume(Values).getToken(); + if (token == Token.IdentifierToken) { + values.add(scannedItems.get(position - 1).getLiteral()); + return values; + } + throw new LabelSelectorParseException("found '" + scannedItems.get(position).getLiteral() + + "', expected: identifier"); + } + + + /** + * parseKeyAndInferOperator parses literals. + * in case of no operator '!, in, notin, ==, =, !=' are found + * the 'exists' operator is inferred + */ + private KeyAndOperator parseKeyAndInferOperator() throws LabelSelectorParseException { + Operator operator = null; + ScannedItem scannedItem = consume(Values); + Token tok = scannedItem.getToken(); + String literal = scannedItem.getLiteral(); + + if (tok == Token.DoesNotExistToken) { + operator = Operator.DoesNotExist; + scannedItem = consume(Values); + tok = scannedItem.getToken(); + literal = scannedItem.getLiteral(); + } + + if (tok != Token.IdentifierToken) { + throw new LabelSelectorParseException(String.format("found '%s', expected: identifier", literal)); + } + + List errors = LabelValidator.validateLabelKey(literal); + if (CollectionUtils.isNotEmpty(errors)) { + throw new LabelSelectorParseException("Invalid label key, errors:" + errors); + } + + ScannedItem lookaheadScanItem = lookahead(Values); + Token t = lookaheadScanItem.getToken(); + + if (t == Token.EndOfStringToken || t == Token.CommaToken) { + if (operator != Operator.DoesNotExist) { + operator = Operator.Exists; + } + } + + return new KeyAndOperator(literal, operator); + } + + @Data + private static class KeyAndOperator { + private String key; + private Operator operator; + + public KeyAndOperator(String key, Operator operator) { + this.key = key; + this.operator = operator; + } + } +} diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/ParserContext.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/ParserContext.java new file mode 100644 index 0000000000..9b2425920e --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/ParserContext.java @@ -0,0 +1,36 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.util.label.selector; + +/** + * ParserContext represents context during parsing: + * some literal for example 'in' and 'notin' can be + * recognized as operator for example 'x in (a)' but + * it can be recognized as value for example 'value in (in)' + */ +public enum ParserContext { + KeyAndOperator, + Values +} diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Requirement.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Requirement.java new file mode 100644 index 0000000000..7b62d13a57 --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Requirement.java @@ -0,0 +1,208 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.util.label.selector; + +import org.apache.commons.collections4.CollectionUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class Requirement { + private final String key; + private final Operator operator; + private final List values; + + public Requirement(String key, Operator operator, List values) { + this.key = key; + this.operator = operator; + this.values = values; + } + + public String getKey() { + return key; + } + + public Operator getOperator() { + return operator; + } + + public List getValues() { + return values; + } + + public boolean matches(Map labels) { + if (!labels.containsKey(key)) { + return false; + } + String value = labels.get(key); + switch (operator) { + case In: + case Equals: + case DoubleEquals: + return values.contains(value); + case NotIn: + case NotEquals: + return !values.contains(value); + case Exists: + return true; + case GreaterThan: + case LessThan: + if (!values.isEmpty()) { + long labelValue = Long.parseLong(value); + long requirementValue = Long.parseLong(values.iterator().next()); + return (operator == Operator.GreaterThan && labelValue > requirementValue) || + (operator == Operator.LessThan && labelValue < requirementValue); + } + return false; + default: + return false; + } + } + + public static Requirement newRequirement(String key, Operator op, List vals) { + List allErrs = LabelValidator.validateLabelKey(key); + if (CollectionUtils.isNotEmpty(allErrs)) { + throw new LabelSelectorParseException("Invalid label key, errors:" + allErrs); + } + allErrs = new ArrayList<>(); + switch (op) { + case In: + case NotIn: + if (vals.size() == 0) { + allErrs.add("values:" + vals + "for 'in', 'notin' operators, values set can't be empty"); + } + break; + case Equals: + case DoubleEquals: + case NotEquals: + if (vals.size() != 1) { + allErrs.add("values:" + vals + "exact-match compatibility requires one single value"); + } + break; + case Exists: + case DoesNotExist: + if (vals.size() != 0) { + allErrs.add("values:" + vals + "values set must be empty for exists and does not exist"); + } + break; + case GreaterThan: + case LessThan: + if (vals.size() != 1) { + allErrs.add("values:" + vals + "for 'Gt', 'Lt' operators, exactly one value is required"); + } + for (String val : vals) { + try { + Long.parseLong(val); + } catch (NumberFormatException e) { + allErrs.add("Value for " + val + "for 'Gt', 'Lt' operators, the " + + "value must be an integer"); + } + } + break; + default: + allErrs.add("Not support operator: " + op); + } + for (String val : vals) { + List valueErrors = LabelValidator.validateLabelValue(val); + if (CollectionUtils.isNotEmpty(valueErrors)) { + allErrs.addAll(valueErrors); + } + } + + if (CollectionUtils.isNotEmpty(allErrs)) { + throw new LabelSelectorParseException("Validate label selector fail, errors:" + allErrs); + } + return new Requirement(key, op, vals); + } + + public static Requirement newRequirement(String key, Operator op, String val) { + return new Requirement(key, op, Collections.singletonList(val)); + } + + public static Requirement newRequirement(String key, Operator op, String[] vals) { + return new Requirement(key, op, Arrays.asList(vals)); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(key); + switch (operator) { + case Equals: + sb.append("="); + break; + case DoubleEquals: + sb.append("=="); + break; + case NotEquals: + sb.append("!="); + break; + case In: + sb.append(" in "); + break; + case NotIn: + sb.append(" notin "); + break; + case GreaterThan: + sb.append(">"); + break; + case LessThan: + sb.append("<"); + break; + case Exists: + case DoesNotExist: + return sb.toString(); + } + if (operator == Operator.In || operator == Operator.NotIn) { + sb.append("("); + } + List sortedValues = new ArrayList<>(values); + Collections.sort(sortedValues); + sb.append(String.join(",", sortedValues)); + if (operator == Operator.In || operator == Operator.NotIn) { + sb.append(")"); + } + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Requirement that = (Requirement) o; + return key.equals(that.key) && + operator == that.operator && + values.equals(that.values); + } + + @Override + public int hashCode() { + return Objects.hash(key, operator, values); + } +} diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/ScannedItem.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/ScannedItem.java new file mode 100644 index 0000000000..657897c697 --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/ScannedItem.java @@ -0,0 +1,51 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.util.label.selector; + +/** + * ScannedItem contains the Token and the literal produced by the lexer. + */ +public class ScannedItem { + private final Token token; + private final String literal; + + public ScannedItem() { + this.token = null; + this.literal = null; + } + + public ScannedItem(Token token, String literal) { + this.token = token; + this.literal = literal; + } + + public Token getToken() { + return token; + } + + public String getLiteral() { + return literal; + } +} diff --git a/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Token.java b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Token.java new file mode 100644 index 0000000000..c498795aa3 --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/main/java/com/tencent/bk/job/execute/util/label/selector/Token.java @@ -0,0 +1,62 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.util.label.selector; + +import java.util.HashMap; +import java.util.Map; + +/** + * lexical token + */ +public enum Token { + ErrorToken, + EndOfStringToken, + ClosedParToken, + CommaToken, + DoesNotExistToken, + DoubleEqualsToken, + EqualsToken, + GreaterThanToken, + IdentifierToken, + InToken, + LessThanToken, + NotEqualsToken, + NotInToken, + OpenParToken; + + public static final Map string2token = new HashMap() {{ + put(")", ClosedParToken); + put(",", CommaToken); + put("!", DoesNotExistToken); + put("==", DoubleEqualsToken); + put("=", EqualsToken); + put(">", GreaterThanToken); + put("in", InToken); + put("<", LessThanToken); + put("!=", NotEqualsToken); + put("notin", NotInToken); + put("(", OpenParToken); + }}; +} diff --git a/src/backend/job-execute/service-job-execute/src/test/java/com/tencent/bk/job/execute/common/util/label/selector/K8sLabelSelectorTest.java b/src/backend/job-execute/service-job-execute/src/test/java/com/tencent/bk/job/execute/common/util/label/selector/K8sLabelSelectorTest.java new file mode 100644 index 0000000000..398eafc5fa --- /dev/null +++ b/src/backend/job-execute/service-job-execute/src/test/java/com/tencent/bk/job/execute/common/util/label/selector/K8sLabelSelectorTest.java @@ -0,0 +1,170 @@ +/* + * Tencent is pleased to support the open source community by making BK-JOB蓝鲸智云作业平台 available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-JOB蓝鲸智云作业平台 is licensed under the MIT License. + * + * License for BK-JOB蓝鲸智云作业平台: + * -------------------------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +package com.tencent.bk.job.execute.common.util.label.selector; + +import com.tencent.bk.job.execute.util.label.selector.LabelSelectorParse; +import com.tencent.bk.job.execute.util.label.selector.LabelSelectorParseException; +import com.tencent.bk.job.execute.util.label.selector.Operator; +import com.tencent.bk.job.execute.util.label.selector.Requirement; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class K8sLabelSelectorTest { + + @Test + void testSelectorParse() { + String[] testGoodStrings = { + "x=a,y=b,z=c", + "", + "x!=a,y=b", + "x=", + "x= ", + "x=,z= ", + "x= ,z= ", + "!x", + "x>1", + "x>1,z<5", + }; + String[] testBadStrings = { + "x=a||y=b", + "x==a==b", + "!x=a", + "x LabelSelectorParse.parse(test)); + } + for (String test : testBadStrings) { + assertThrows(LabelSelectorParseException.class, () -> LabelSelectorParse.parse(test)); + } + } + + @Test + void testParseToRequirements() { + expectMatch(LabelSelectorParse.parse("app=nginx"), + Requirements.build(Requirement.newRequirement("app", Operator.Equals, "nginx"))); + expectMatch(LabelSelectorParse.parse("app==nginx"), + Requirements.build(Requirement.newRequirement("app", Operator.DoubleEquals, "nginx"))); + expectMatch(LabelSelectorParse.parse("version==1.0"), + Requirements.build(Requirement.newRequirement("version", Operator.DoubleEquals, "1.0"))); + expectMatch(LabelSelectorParse.parse("version=1.0"), + Requirements.build(Requirement.newRequirement("version", Operator.Equals, "1.0"))); + + + expectMatch(LabelSelectorParse.parse("app!=nginx"), + Requirements.build(Requirement.newRequirement("app", Operator.NotEquals, "nginx"))); + + expectMatch(LabelSelectorParse.parse("app in (nginx, redis)"), + Requirements.build(Requirement.newRequirement("app", Operator.In, new String[]{"nginx", "redis"}))); + expectMatch(LabelSelectorParse.parse("version in (1.0, 2.0)"), + Requirements.build(Requirement.newRequirement("version", Operator.In, new String[]{"1.0", "2.0"}))); + + expectMatch(LabelSelectorParse.parse("app notin (nginx, redis)"), + Requirements.build(Requirement.newRequirement("app", Operator.NotIn, new String[]{"nginx", "redis"}))); + + expectMatch(LabelSelectorParse.parse("app"), + Requirements.build(Requirement.newRequirement("app", Operator.Exists, Collections.emptyList()))); + expectMatch(LabelSelectorParse.parse("release-version"), + Requirements.build(Requirement.newRequirement("release-version", Operator.Exists, + Collections.emptyList()))); + + expectMatch(LabelSelectorParse.parse("!app"), + Requirements.build(Requirement.newRequirement("app", Operator.DoesNotExist, Collections.emptyList()))); + + expectMatch(LabelSelectorParse.parse("version > 10"), + Requirements.build(Requirement.newRequirement("version", Operator.GreaterThan, "10"))); + expectMatch(LabelSelectorParse.parse("version < 10"), + Requirements.build(Requirement.newRequirement("version", Operator.LessThan, "10"))); + + expectMatch(LabelSelectorParse.parse("app=nginx, tier=frontend"), + Requirements.build( + Requirement.newRequirement("app", Operator.Equals, "nginx"), + Requirement.newRequirement("tier", Operator.Equals, "frontend")) + ); + expectMatch(LabelSelectorParse.parse("env=production,version!=1.0"), + Requirements.build( + Requirement.newRequirement("env", Operator.Equals, "production"), + Requirement.newRequirement("version", Operator.NotEquals, "1.0")) + ); + expectMatch(LabelSelectorParse.parse("app in (nginx, redis),tier=backend"), + Requirements.build( + Requirement.newRequirement("app", Operator.In, new String[]{"nginx", "redis"}), + Requirement.newRequirement("tier", Operator.Equals, "backend")) + ); + expectMatch(LabelSelectorParse.parse("!beta-version,app!=mysql"), + Requirements.build( + Requirement.newRequirement("beta-version", Operator.DoesNotExist, Collections.emptyList()), + Requirement.newRequirement("app", Operator.NotEquals, "mysql")) + ); + + expectMatch(LabelSelectorParse.parse("val in (in, notin)"), + Requirements.build( + Requirement.newRequirement("val", Operator.In, new String[]{"in", "notin"})) + ); + } + + private void expectMatch(List actualRequirementList, + Requirements expectRequirements) { + actualRequirementList.sort(new ByKey()); + expectRequirements.requirements.sort(new ByKey()); + assertEquals(actualRequirementList.size(), expectRequirements.requirements.size()); + for (int i = 0; i < actualRequirementList.size(); i++) { + Requirement actual = actualRequirementList.get(i); + Requirement expect = expectRequirements.requirements.get(i); + assertEquals(actual, expect); + } + } + + private static class Requirements { + private final List requirements = new ArrayList<>(); + + public static Requirements build(Requirement... requirementArgs) { + Requirements requirements = new Requirements(); + for (Requirement requirement : requirementArgs) { + requirements.add(requirement); + } + return requirements; + } + + public void add(Requirement requirement) { + requirements.add(requirement); + } + } + + private static class ByKey implements Comparator { + @Override + public int compare(Requirement o1, Requirement o2) { + return o1.getKey().compareTo(o2.getKey()); + } + } +}