Writing dynamically created YAML - Creating the Docker compose file for an ecosystem (using js-yaml)

160 Views Asked by At

Question: Has anyone been able to work with the js-yaml library to write a docker compose like this?

Context:

I am working on dotnet templates and have some pre-defined application architecture patterns for our teams. I would like to take it a step further and create the docker compose for the entirety of the ecosystem.

The main issue is on this line that I need to change the tokens and write to the YAML.

I have tried both of the following in the template and cannot have valid YAML output.

command: "./daprd -app-id ecosystemtemplate-${application} -app-port ${EXPOSED_PORT:-4443} --resources-path /app/config"
command: [ "./daprd", "-app-id", "ecosystemtemplate-${application}", "-app-port", "${EXPOSED_PORT:-4443}", "--resources-path", "/app/config"

How do I have the js-yaml lib respect the items that are quoted and it should preserve the quotes?

Taking the above command YAML and running it through json2yaml.com, the YAML->json looks correct, as does the json->YAML. To recreate this, I first copy the template YAML into the UI, then I copy the json and paste it again, thus updating the YAML output, which looks correct.

enter image description here

If I run this using the nodejs, I have incorrect output in the YAML. the image, command, and network_mode all need quotes. I have hacked at this too many ways to list.

enter image description here

The PoC will become much more involved when this moves to product-ready enterprise-scale applications.

enter image description here

Example:

From the template, I create a solution.json file that contains information that the ecosystem template uses.

enter image description here

The following code is used to read the solution.json

async function createDockerComposeYaml () {
    console.log("Creating Docker Compose");
    const fileName = 'docker-compose.yml';
    const applicationDescriptor = '${application}';

    await (async () => {
        try {
          await fs.unlink(fileName);
        } catch (e) {
          // Swallow the error.
          // Normally happens when the file does not exist the first time.
        }
      })();

    try {
        const dockerComposeYaml = await fs.readFile('./docker-templates/compose/docker-compose-template.yml', 'utf-8');
        const serviceYamlTokenized = await fs.readFile('./docker-templates/compose/docker-compose-service-template.yml', 'utf-8');
        const daprSidecarYamlTokenized = await fs.readFile('./docker-templates/compose/docker-compose-service-dapr-sidecar-template.yml', 'utf-8');

        let dockerCompose = YAML.load(dockerComposeYaml);

        solutions.forEach(solution => {
            // Build the service for the application.
            let serviceYaml = serviceYamlTokenized.replaceAll('${application}', solution.name.toLowerCase());
            serviceYaml = serviceYaml.replace('${dockerfile}', solution.dockerfile);
            const service = YAML.load(serviceYaml);

            // Build the service for the application dapr sidecar.
            const serviceSidecarYaml = daprSidecarYamlTokenized.replaceAll('${application}', solution.name.toLowerCase());
            const serviceSidecar = YAML.load(serviceSidecarYaml);

            if (dockerCompose.services === null) {
                dockerCompose.services = [];
            }

            dockerCompose.services.push(service);
            dockerCompose.services.push(serviceSidecar);
        });

        writeFile(fileName, YAML.dump(dockerCompose, { 'forceQuotes': true }));
    } catch(e) {
        console.log(e);
    }
}

Content:

solution.json

{
  "name": "DddTemplate",
  "template": "ddd",
  "dockerfile": "DddTemplate/src/DddTemplate.Api/Dockerfile",
  "launchSettings": "DddTemplate/src/DddTemplate.Api/Properties/launchSettings.json"
}

docker-compose-template.yml

version: '3.7'
name: EcosystemTemplate

volumes:

services:

docker-compose-service-template.yml

  ${application}:
    image: ${DOCKER_REGISTRY-}${application}:${IMAGE_TAG}
    build:
      context: .
      dockerfile: "${dockerfile}"
      args:
        - IMG_REGISTRY=${REGISTRY:-mcr.microsoft.com}
    environment:
      - ASPNETCORE_URLS=${ASPNETCORE_URLS:-https://*:4443}
    ports:
      - "43301:${EXPOSED_PORT:-4443}"
    volumes:
      - ~/.aspnet/https:/https:ro

docker-compose-service-dapr-sidecar-template.yml

  ${application}-dapr:
    image: "daprio/daprd:latest"
    command: "./daprd -app-id ${application} -app-port ${EXPOSED_PORT:-4443} --resources-path /app/config"
    depends_on:
      - ${application}
    network_mode: "service:${application}"
    volumes_from:
      - ${application}
1

There are 1 best solutions below

7
traynor On BEST ANSWER

Here's a quick-and-dirty suggestion:

You could disable quotes, and use the replacer function, and then add some placeholder only on fields and values you want to modify (you can modify the code accordingly), and then those values will be quoted, and then you create YAML file, but remove the placeholder from it just before you write the file.

Try this:

const somePlaceholder = '@@';

// add fields that you want to process
const fields = ['image', 'command', 'network_mode'];

function myReplacer(key, value) {

    if (typeof value === 'string') {

        if (fields.includes(key)) {
        // or
        // if (!value.includes('${')) {

            // handle command field not being quoted by adding quotes
            if(key === 'command') {

                return `'${somePlaceholder}${value}${somePlaceholder}'`
            } else {

                return `${somePlaceholder}${value}${somePlaceholder}`;
            }

        } else {
            return value;
        }
    } else {
        return value;
    }
}


// replace placeholder
const replaced = YAML.dump(dockerCompose, { replacer: myReplacer }).replaceAll('@@', '');

writeFile(fileName, replaced);