CanonicalTemplateResolver.java

package com.cloudforgeci.api.deploy;

import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.awscdk.cxapi.CloudAssembly;
import software.amazon.awscdk.cxapi.CloudFormationStackArtifact;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

/**
 * Resolves the canonical synthesized CloudFormation template on disk.
 */
public final class CanonicalTemplateResolver {
    private static final ObjectMapper MAPPER = new ObjectMapper();

    private CanonicalTemplateResolver() {
    }

    /**
     * Return {@code outputDirectory/{stackName}.template.json} when it already exists.
     */
    public static Path resolveExisting(Path outputDirectory, String stackName) throws IOException {
        Path expected = outputDirectory.resolve(stackName + ".template.json");
        if (!Files.exists(expected)) {
            throw new IOException("Canonical template not found: " + expected
                + ". Synthesize first (menu option 1, then deploy).");
        }
        return expected;
    }

    /**
     * Prefer an on-disk template under {@code outputDirectory}; otherwise materialize from
     * {@code assembly}.
     */
    public static Path resolve(Path outputDirectory, String stackName, CloudAssembly assembly)
            throws IOException {
        Path expected = outputDirectory.resolve(stackName + ".template.json");
        if (Files.exists(expected)) {
            return expected;
        }
        if (assembly == null) {
            throw new IOException("No CloudAssembly available for stack '" + stackName + "'.");
        }
        CloudFormationStackArtifact stack = assembly.getStackByName(stackName);
        if (stack == null) {
            List<String> stacks = assembly.getStacks().stream()
                .map(CloudFormationStackArtifact::getStackName)
                .sorted()
                .toList();
            throw new IOException(
                "Stack '" + stackName + "' not found in cloud assembly."
                    + (stacks.isEmpty() ? "" : " Available stacks: " + stacks));
        }
        return materialize(stack, expected);
    }

    /**
     * Ensure the synthesized stack template exists at {@code expectedPath}.
     */
    public static Path materialize(CloudFormationStackArtifact stack, Path expectedPath)
            throws IOException {
        String fullPath = stack.getTemplateFullPath();
        if (fullPath != null && !fullPath.isBlank()) {
            Path onDisk = Path.of(fullPath);
            if (Files.exists(onDisk)) {
                return onDisk;
            }
        }

        Object template = stack.getTemplate();
        if (template == null) {
            throw new IOException(
                "No template available for stack '" + stack.getStackName()
                    + "'. Synthesize first.");
        }

        Files.createDirectories(expectedPath.getParent());
        MAPPER.writeValue(expectedPath.toFile(), template);
        return expectedPath;
    }
}