Skip to content

Commit

Permalink
Check schema of variables in templates and validate input variables
Browse files Browse the repository at this point in the history
  • Loading branch information
baixinsui committed Mar 26, 2024
1 parent cffba32 commit f7958c0
Show file tree
Hide file tree
Showing 28 changed files with 536 additions and 316 deletions.
13 changes: 0 additions & 13 deletions modules/api/src/test/resources/ocl_terraform_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,19 +137,6 @@ deployment:
default = "cn-southwest-2"
description = "The region to deploy the service instance."
}
terraform {
required_providers {
huaweicloud = {
source = "huaweicloud/huaweicloud"
version = "~> 1.61.0"
}
}
}
provider "huaweicloud" {
region = var.region
}
locals {
admin_passwd = var.admin_passwd == "" ? random_password.password.result : var.admin_passwd
Expand Down
25 changes: 12 additions & 13 deletions modules/database/src/test/resources/ocl_terraform_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,27 +65,39 @@ deployment:
minLength: 8
maxLength: 16
pattern: ^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,16}$
modificationImpact:
isDataLost: true
isServiceInterrupted: true
- name: vpc_name
description: The vpc name of the service instance. If the value is empty, will use the default value to find or create VPC.
kind: variable
dataType: string
example: "vpc-default"
mandatory: false
value: "vpc-default"
modificationImpact:
isDataLost: true
isServiceInterrupted: true
- name: subnet_name
description: The sub network name of the service instance. If the value is empty, will use the default value to find or create subnet.
kind: variable
dataType: string
example: "subnet-default"
mandatory: false
value: "subnet-default"
modificationImpact:
isDataLost: true
isServiceInterrupted: true
- name: secgroup_name
description: The security group name of the service instance. If the value is empty, will use the default value to find or create security group.
kind: variable
dataType: string
example: "secgroup-default"
mandatory: false
value: "secgroup-default"
modificationImpact:
isDataLost: true
isServiceInterrupted: true
deployer: |
variable "flavor_id" {
type = string
Expand Down Expand Up @@ -123,19 +135,6 @@ deployment:
description = "The region to deploy the service instance."
}
terraform {
required_providers {
huaweicloud = {
source = "huaweicloud/huaweicloud"
version = "~> 1.61.0"
}
}
}
provider "huaweicloud" {
region = var.region
}
locals {
admin_passwd = var.admin_passwd == "" ? random_password.password.result : var.admin_passwd
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@

package org.eclipse.xpanse.modules.deployment;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.xpanse.modules.models.service.deploy.exceptions.VariableInvalidException;
import org.eclipse.xpanse.modules.models.servicetemplate.AvailabilityZoneConfig;
import org.springframework.util.CollectionUtils;
Expand All @@ -34,30 +32,26 @@ public static void validateAvailabilityZones(Map<String, String> inputMap,
if (CollectionUtils.isEmpty(zoneConfigs)) {
return;
}
Set<String> requiredZoneVarNameSet =
List<String> requiredZoneVarNames =
zoneConfigs.stream().filter(AvailabilityZoneConfig::getMandatory)
.map(AvailabilityZoneConfig::getVarName).collect(Collectors.toSet());
if (CollectionUtils.isEmpty(requiredZoneVarNameSet)) {
.map(AvailabilityZoneConfig::getVarName).toList();
if (CollectionUtils.isEmpty(requiredZoneVarNames)) {
return;
}
List<String> errorMessages = new ArrayList<>();
if (CollectionUtils.isEmpty(inputMap)) {
String missingZonesMsg =
String.format("The required availability zones variables %s are missing.",
StringUtils.join(requiredZoneVarNameSet, ", "));
throw new VariableInvalidException(List.of(missingZonesMsg));
requiredZoneVarNames.forEach(varName -> errorMessages.add(getErrorMessage(varName)));
} else {
requiredZoneVarNames.stream().filter(varName -> !inputMap.containsKey(varName))
.forEach(varName -> errorMessages.add(getErrorMessage(varName)));
}
Set<String> missingZoneVarNames = requiredZoneVarNameSet.stream()
.filter(requiredZoneVarName -> !inputMap.containsKey(requiredZoneVarName))
.collect(Collectors.toSet());
if (!missingZoneVarNames.isEmpty()) {
String missingZonesMsg =
String.format("The required availability zones variable names %s are missing.",
StringUtils.join(missingZoneVarNames, ", "));
throw new VariableInvalidException(List.of(missingZonesMsg));
if (!errorMessages.isEmpty()) {
throw new VariableInvalidException(errorMessages);
}

Map<String, String> requiredZoneVarValues = new HashMap<>();
for (String zoneVarName : inputMap.keySet()) {
if (requiredZoneVarNameSet.contains(zoneVarName)) {
if (requiredZoneVarNames.contains(zoneVarName)) {
if (requiredZoneVarValues.containsValue(inputMap.get(zoneVarName))) {
String duplicatedValuesMessage = String.format("The values of required "
+ "availability zones variables %s are duplicated.",
Expand All @@ -68,6 +62,10 @@ public static void validateAvailabilityZones(Map<String, String> inputMap,
}
}
}
}

private static String getErrorMessage(String varName) {
return String.format("required availability zone property '%s' not found", varName);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ public class OpenTofuLocalExecutor {
* @param variables variables for the open tofu command line.
* @param workspace workspace for the open tofu command line.
*/
OpenTofuLocalExecutor(Map<String, String> env,
Map<String, Object> variables,
String workspace,
@Nullable String subDirectory) {
OpenTofuLocalExecutor(Map<String, String> env,
Map<String, Object> variables,
String workspace,
@Nullable String subDirectory) {
this.env = env;
this.variables = variables;
this.workspace =
Expand Down Expand Up @@ -277,12 +277,21 @@ public DeploymentScriptValidationResult tfValidate() {
throw new OpenTofuExecutorException("OpenTofuExecutor.tfInit failed.",
initResult.getCommandStdError());
}
SystemCmdResult systemCmdResult = execute("tofu validate -json -no-color");
SystemCmdResult validateResult = execute("tofu validate -json -no-color");

if (!validateResult.isCommandSuccessful()) {
log.error("OpenTofuExecutor get validate json failed.");
throw new OpenTofuExecutorException("OpenTofuExecutor get validate json failed.",
validateResult.getCommandStdError());
}
try {
return new ObjectMapper().readValue(systemCmdResult.getCommandStdOutput(),
String commandStdOutput = validateResult.getCommandStdOutput();
String cleanedJson = commandStdOutput.substring(commandStdOutput.indexOf('{'));
return new ObjectMapper().readValue(cleanedJson,
DeploymentScriptValidationResult.class);
} catch (JsonProcessingException ex) {
throw new IllegalStateException("Serialising string to object failed.", ex);
throw new IllegalStateException(
"Serialising command output to validate result object failed.", ex);
}
}

Expand Down
13 changes: 0 additions & 13 deletions modules/deployment/src/test/resources/ocl_opentofu_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,19 +122,6 @@ deployment:
default = "cn-southwest-2"
description = "The region to deploy the service instance."
}
terraform {
required_providers {
huaweicloud = {
source = "huaweicloud/huaweicloud"
version = "~> 1.61.0"
}
}
}
provider "huaweicloud" {
region = var.region
}
locals {
admin_passwd = var.admin_passwd == "" ? random_password.password.result : var.admin_passwd
Expand Down
25 changes: 12 additions & 13 deletions modules/deployment/src/test/resources/ocl_terraform_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,27 +65,39 @@ deployment:
minLength: 8
maxLength: 16
pattern: ^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,16}$
modificationImpact:
isDataLost: true
isServiceInterrupted: true
- name: vpc_name
description: The vpc name of the service instance. If the value is empty, will use the default value to find or create VPC.
kind: variable
dataType: string
example: "vpc-default"
mandatory: false
value: "vpc-default"
modificationImpact:
isDataLost: true
isServiceInterrupted: true
- name: subnet_name
description: The sub network name of the service instance. If the value is empty, will use the default value to find or create subnet.
kind: variable
dataType: string
example: "subnet-default"
mandatory: false
value: "subnet-default"
modificationImpact:
isDataLost: true
isServiceInterrupted: true
- name: secgroup_name
description: The security group name of the service instance. If the value is empty, will use the default value to find or create security group.
kind: variable
dataType: string
example: "secgroup-default"
mandatory: false
value: "secgroup-default"
modificationImpact:
isDataLost: true
isServiceInterrupted: true
deployer: |
variable "flavor_id" {
type = string
Expand Down Expand Up @@ -123,19 +135,6 @@ deployment:
description = "The region to deploy the service instance."
}
terraform {
required_providers {
huaweicloud = {
source = "huaweicloud/huaweicloud"
version = "~> 1.61.0"
}
}
}
provider "huaweicloud" {
region = var.region
}
locals {
admin_passwd = var.admin_passwd == "" ? random_password.password.result : var.admin_passwd
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.StringUtils;

/**
* Exception throw when variables validation fails.
Expand All @@ -18,7 +19,7 @@ public class VariableInvalidException extends RuntimeException {
private final List<String> errorReasons;

public VariableInvalidException(List<String> errorReasons) {
super("Variable validation failed: " + errorReasons);
super(String.format("Variable validation failed: %s", StringUtils.join(errorReasons)));
this.errorReasons = errorReasons;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.xpanse.modules.models.service.deploy.exceptions.VariableInvalidException;
Expand All @@ -43,29 +44,31 @@ public class ServiceVariablesJsonSchemaValidator {
public void validateDeployVariables(List<DeployVariable> deployVariables,
Map<String, Object> deployProperty,
JsonObjectSchema jsonObjectSchema) {
if (!CollectionUtils.isEmpty(deployVariables) && !CollectionUtils.isEmpty(deployProperty)) {
try {
String jsonObjectSchemaString = jsonMapper.writeValueAsString(jsonObjectSchema);
Locale.setDefault(Locale.ENGLISH);
JsonSchemaFactory factory =
JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
JsonSchema schema = factory.getSchema(jsonObjectSchemaString);
String propertyJson = jsonMapper.writeValueAsString(deployProperty);
JsonNode jsonNode = jsonMapper.readTree(propertyJson);
Set<ValidationMessage> validate = schema.validate(jsonNode);

if (!validate.isEmpty()) {
List<String> errors = new ArrayList<>();
for (ValidationMessage validationMessage : validate) {
errors.add(validationMessage.getMessage().substring(2));
}
throw new VariableInvalidException(errors);
}
} catch (JsonProcessingException e) {
if (CollectionUtils.isEmpty(deployVariables) || Objects.isNull(jsonObjectSchema)) {
return;
}
try {
String jsonObjectSchemaString = jsonMapper.writeValueAsString(jsonObjectSchema);
Locale.setDefault(Locale.ENGLISH);
JsonSchemaFactory factory =
JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
JsonSchema schema = factory.getSchema(jsonObjectSchemaString);
String propertyJson = jsonMapper.writeValueAsString(deployProperty);
JsonNode jsonNode = jsonMapper.readTree(propertyJson);
Set<ValidationMessage> validate = schema.validate(jsonNode);

if (!validate.isEmpty()) {
List<String> errors = new ArrayList<>();
errors.add(e.getMessage());
for (ValidationMessage validationMessage : validate) {
errors.add(validationMessage.getMessage().substring(3));
}
throw new VariableInvalidException(errors);
}
} catch (JsonProcessingException e) {
List<String> errors = new ArrayList<>();
errors.add(e.getMessage());
throw new VariableInvalidException(errors);
}
}
}

0 comments on commit f7958c0

Please sign in to comment.