Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deploy a resources defined inside a module #53

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/main/java/io/cloudsoft/terraform/TerraformConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ enum TerraformStatus {
.description("URL of the file containing values for the Terraform variables.")
.build();

@SetFromFlag("tfWorkingDirectory")
ConfigKey<String> WORKING_DIRECTORY = ConfigKeys.builder(String.class)
.name("tf.working.directory")
.description("Working directory for Terraform commands.")
.defaultValue("")
.build();

@SetFromFlag("tfModuleDepthState")
ConfigKey<Integer> MODULE_DEPTH = ConfigKeys.builder(Integer.class)
.name("tf.module.depth")
.description("Module depth to be represented.")
.defaultValue(1)
.build();
AttributeSensor<String> CONFIGURATION_APPLIED = Sensors.newStringSensor("tf.configuration.applied",
"The most recent time a Terraform configuration has been successfully applied.");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ protected void disconnectSensors() {
*/
private void updateDeploymentState() {
final String result = getDriver().runShowTask();
Map<String, Object> state = StateParser.parseResources(result);
Map<String, Object> state = StateParser.parseResources(result, getConfig(TerraformConfiguration.MODULE_DEPTH));
sensors().set(TerraformConfiguration.STATE, state);
Map<String, Object> resources = new HashMap<>(state);
updateResources(resources, this, ManagedResource.class);
Expand All @@ -137,9 +137,11 @@ private void updateDeploymentState() {
}
}

private static Predicate<? super Entity> runningOrSync = c -> !c.sensors().getAll().containsKey(RESOURCE_STATUS) || (!c.sensors().get(RESOURCE_STATUS).equals("running") &&
c.getParent().sensors().get(DRIFT_STATUS).equals(TerraformStatus.SYNC));

private static Predicate<? super Entity> runningOrSync = c -> !c.sensors().getAll().containsKey(RESOURCE_STATUS) ||
(c.getParent().sensors().getAll().containsKey(DRIFT_STATUS) &&
!c.sensors().get(RESOURCE_STATUS).equals("running") &&
c.getParent().sensors().get(DRIFT_STATUS).equals(TerraformStatus.SYNC)
);
private void updateResources(Map<String, Object> resources, Entity parent, Class<? extends TerraformResource> clazz) {
List<Entity> childrenToRemove = new ArrayList<>();
parent.getChildren().stream().filter(c -> clazz.isAssignableFrom(c.getClass())).forEach(c -> {
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/io/cloudsoft/terraform/TerraformDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ default String destroyCommand() {
}

default String makeTerraformCommand(String argument) {
return format("cd %s && %s/terraform %s", getRunDir(), getInstallDir(), argument);
return format("cd %s && %s/terraform %s", getEnvironmentDir(), getInstallDir(), argument);
}

default String getConfigurationFilePath() {
Expand All @@ -65,11 +65,11 @@ default String getTfVarsFilePath() {
}

default String getStateFilePath() {
return getRunDir() + "/terraform.tfstate";
return getEnvironmentDir() + "/terraform.tfstate";
}

// these are just here to allow the terraform commands building methods to be default too :)
String getRunDir();
String getInstallDir();

String getEnvironmentDir();
}
28 changes: 25 additions & 3 deletions src/main/java/io/cloudsoft/terraform/TerraformSshDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ public String getOsTag() {
if (os == null) return "linux_amd64";
// If not Mac, assume Linux
String osType = os.isMac() ? "darwin" : "linux";
String archType = os.is64bit() ?
os.getArch().toLowerCase().contains("arm") ? "arm64" : "amd64":
os.getArch().toLowerCase().contains("arm") ? "arm" : "386";
String archType = os.is64bit() ? "amd64" : "386";

return osType + "_" + archType;
}
Expand Down Expand Up @@ -331,4 +329,28 @@ private Task refreshTaskWithName(final String name) {
.newTask()
.asTask();
}

@Override
public String getEnvironmentDir() {
String baseDir = super.getRunDir();
String workingDirectory = getWorkingDirectory();
return Strings.isEmpty(workingDirectory) ?
baseDir :
baseDir + "/" + workingDirectory;
}

public String getWorkingDirectory() {
String workingDir = entity.getConfig(TerraformConfiguration.WORKING_DIRECTORY);
return workingDir.startsWith("/") ?
removeInitialSlashes(workingDir) :
workingDir;
}

private String removeInitialSlashes(String workingDir) {
//todojd improve + test
while(workingDir.startsWith("/")){
workingDir = workingDir.substring(1);
}
return workingDir;
}
}
86 changes: 50 additions & 36 deletions src/main/java/io/cloudsoft/terraform/parser/StateParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
public final class StateParser {
private static final Logger LOG = LoggerFactory.getLogger(StateParser.class);
public static final ImmutableList blankItems = ImmutableList.of("[]", "", "null", "\"\"", "{}", "[{}]");
public static final String CHILD_MODULES = "child_modules";

private static Predicate<? super PlanLogEntry> providerPredicate = (Predicate<PlanLogEntry>) planLogEntry -> planLogEntry.getProvider() != PlanLogEntry.Provider.NOT_SUPPORTED;
private static Predicate<? super PlanLogEntry> changeSummaryPredicate = (Predicate<PlanLogEntry>) ple -> ple.type == PlanLogEntry.LType.CHANGE_SUMMARY;
Expand All @@ -36,7 +37,11 @@ public final class StateParser {


public static Map<String, Object> parseResources(final String state){
Map<String, Object> result = new HashMap<>();
return parseResources(state,0);
}
public static Map<String, Object> parseResources(final String state, int moduleDepth){

Map<String, Object> result = new HashMap<>();
ObjectMapper objectMapper = new ObjectMapper();
try {
JsonNode root = objectMapper.readTree(state);
Expand All @@ -51,48 +56,57 @@ public static Map<String, Object> parseResources(final String state){
if(!root.get("values").has("root_module")) {
throw new IllegalArgumentException ("A valid deployment state should have a root_module node!");
}
if(!root.get("values").get("root_module").has("resources")) {
JsonNode rootModule = root.get("values").get("root_module");
if(!rootModule.has("resources")) {
throw new IllegalArgumentException ("A valid deployment state should have a resources node!");
}
parseResources(result, rootModule, 0, moduleDepth);
} catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot parse Terraform state!", e);
}
return result;
}

JsonNode resourceNode = root.at("/values/root_module/resources");
resourceNode.forEach(resource -> {
Map<String, Object> resourceBody = new LinkedHashMap<>();

//if (resource.has("mode") && "managed".equals(resource.get("mode").asText())) {
result.put(resource.get("address").asText(), resourceBody);

resourceBody.put("resource.address", resource.get("address").asText());
resourceBody.put("resource.mode", resource.get("mode").asText());
resourceBody.put("resource.type", resource.get("type").asText());
resourceBody.put("resource.name", resource.get("name").asText());
resourceBody.put("resource.provider", resource.get("provider_name").asText());
if(resource.has("values")) {
Iterator<Map.Entry<String, JsonNode>> it = resource.get("values").fields();
while(it.hasNext()) {
Map.Entry<String,JsonNode> value = it.next();
if(isNotBlankPredicate.test(value.getValue())) {
resourceBody.put("value." + value.getKey(), value.getValue().asText());
}
}
private static void parseResources(Map<String, Object> result, JsonNode parsingModule, int moduleDepthCounter, final int maxModuleDepth) {
moduleDepthCounter++;
for (JsonNode resource : parsingModule.get("resources")) {
Map<String, Object> resourceBody = new LinkedHashMap<>();

//if (resource.has("mode") && "managed".equals(resource.get("mode").asText())) {
result.put(resource.get("address").asText(), resourceBody);

resourceBody.put("resource.address", resource.get("address").asText());
resourceBody.put("resource.mode", resource.get("mode").asText());
resourceBody.put("resource.type", resource.get("type").asText());
resourceBody.put("resource.name", resource.get("name").asText());
resourceBody.put("resource.provider", resource.get("provider_name").asText());
if (resource.has("values")) {
Iterator<Map.Entry<String, JsonNode>> it = resource.get("values").fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> value = it.next();
if (isNotBlankPredicate.test(value.getValue())) {
resourceBody.put("value." + value.getKey(), value.getValue().asText());
}
}
}

if(resource.has("sensitive_values")) {
Iterator<Map.Entry<String, JsonNode>> it = resource.get("sensitive_values").fields();
while(it.hasNext()) {
Map.Entry<String,JsonNode> value = it.next();
if(isNotBlankPredicate.test(value.getValue())) {
resourceBody.put("sensitive.value." + value.getKey(), value.getValue().asText());
}
}
if (resource.has("sensitive_values")) {
Iterator<Map.Entry<String, JsonNode>> it = resource.get("sensitive_values").fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> value = it.next();
if (isNotBlankPredicate.test(value.getValue())) {
resourceBody.put("sensitive.value." + value.getKey(), value.getValue().asText());
}
//}

});
} catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot parse Terraform state!", e);
}
}
//}
if ((maxModuleDepth >= moduleDepthCounter) && parsingModule.has(CHILD_MODULES)) {
JsonNode modules = parsingModule.get(CHILD_MODULES);
for (JsonNode module : modules) {
parseResources(result, module, moduleDepthCounter++, maxModuleDepth);
}
}
}
return result;
}

public static Map<String, Object> parsePlanLogEntries(final String planLogEntriesAsStr){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,33 @@ public void parseVsCreate() throws IOException {
assertEquals(resources.size(), 3);
assertEquals(resources.stream().filter(m -> m.containsValue("create")).count(), 3);
}

@Test
void testParsedStateWithModule() throws IOException {
String state = loadTestData("state/show-using-module.json");
Map<String, Object> result = StateParser.parseResources(state,0);
// Only one resource as root
assertEquals(result.size(),1);
// Nine resources reading the first level of modules
result = StateParser.parseResources(state,1);
assertEquals(result.size(),9);
// 21 resources reading the second level of modules
result = StateParser.parseResources(state,2);
assertEquals(result.size(),21);
// There is only two levels, passing more depth is ignored
result = StateParser.parseResources(state,10);
assertEquals(result.size(),21);
}

@Test
void testParsedStateWithoutModule() throws IOException {
String state = loadTestData("state/show-single-resource.json");
// If not depth is passed, defaults in 0
Map<String, Object> result = StateParser.parseResources(state);
assertEquals(result.size(),1);
result = StateParser.parseResources(state,1);
assertEquals(result.size(),1);
result = StateParser.parseResources(state,10);
assertEquals(result.size(),1);
}
}
102 changes: 102 additions & 0 deletions src/test/resources/state/show-single-resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{
"format_version": "1.0",
"terraform_version": "1.1.4",
"values": {
"outputs": {
"id": {
"sensitive": false,
"value": "sg-01484b93f40426a7f"
},
"name": {
"sensitive": false,
"value": "terraform-20220425083341393100000001"
}
},
"root_module": {
"resources": [
{
"address": "aws_security_group.allow_all",
"mode": "managed",
"type": "aws_security_group",
"name": "allow_all",
"provider_name": "registry.terraform.io/hashicorp/aws",
"schema_version": 1,
"values": {
"arn": "arn:aws:ec2:eu-west-2:304295633295:security-group/sg-01484b93f40426a7f",
"description": "test-security-group allowing all access",
"egress": [
{
"cidr_blocks": [
"0.0.0.0/0"
],
"description": "",
"from_port": 0,
"ipv6_cidr_blocks": [],
"prefix_list_ids": [],
"protocol": "-1",
"security_groups": [],
"self": false,
"to_port": 0
}
],
"id": "sg-01484b93f40426a7f",
"ingress": [
{
"cidr_blocks": [
"0.0.0.0/0"
],
"description": "",
"from_port": 0,
"ipv6_cidr_blocks": [],
"prefix_list_ids": [],
"protocol": "-1",
"security_groups": [],
"self": false,
"to_port": 0
}
],
"name": "terraform-20220425083341393100000001",
"name_prefix": "terraform-",
"owner_id": "304295633295",
"revoke_rules_on_delete": false,
"tags": {
"Name": "TestSG-KillMePlease"
},
"tags_all": {
"Name": "TestSG-KillMePlease",
"cloudsoft:group": "AMP",
"cloudsoft:workload": "Terraform",
"environment": "dev"
},
"timeouts": null,
"vpc_id": "vpc-619e7d08"
},
"sensitive_values": {
"egress": [
{
"cidr_blocks": [
false
],
"ipv6_cidr_blocks": [],
"prefix_list_ids": [],
"security_groups": []
}
],
"ingress": [
{
"cidr_blocks": [
false
],
"ipv6_cidr_blocks": [],
"prefix_list_ids": [],
"security_groups": []
}
],
"tags": {},
"tags_all": {}
}
}
]
}
}
}
Loading