0

Currently I'm trying to create a docker image that will setup some things in our environment.

If the user pass the --interactive parameter the idea is to spin up the container and show an interactive sheel to select which options you want to run, for example

Ex:

  • Run Full Setup
  • Create Database
  • Create Database User

I've made the logic to check if is interactive mode or not, but when is interactive I'm not able to pass the desired option.

Even when I run the container in attached mode.

The only way I'm able to do that is connect to the container throught Docker Desktop terminal and run the script by hand using the function runInteractiveSetupUserNeedsToConnectToContainer

Is there anyway that i could run the interactive menu when user is connected to docker container terminal?

I mean, this command do the job

docker exec -it setup node /app/interactive.js

But I wanted the same behavior without needing to pass node /app/interactive.js

Commands that I've already tried to use

docker-compose up --attach  setup
docker-compose up
docker exec -it setup node /app/interactive.js #this way works but i need to pass the command to run

I'll attach here a simpler version of the code that I'm using the function

FROM node
WORKDIR /app
COPY ./package.json .
COPY ./interactive.js .
WORKDIR /app/modules
COPY ./modules .
WORKDIR /app
RUN apt update && apt install -y curl jq iputils-ping traceroute lsb-release && \
    npm install &&  \
    ln -sf /bin/bash /bin/sh]

ENTRYPOINT ["node","/app/modules/main.js"]
CMD [ "$*" ]
version: "3.9"

services:
  setup:
    image: setup
    build:
      context: .
      dockerfile: Dockerfile
    command: --interactive
    volumes:
      - ./interactive.js:/app/interactive.js
      - ./package.json:/app/package.json
      - ./modules:/app/modules
    stdin_open: true
    tty: true

modules

interactive menu

function createAndStartMenu(){
    const menu = require('node-menu');
    const setup = require('/app/modules/setup.js');
    menu.addDelimiter('-', 40, 'Setup')
    menu.addItem('Run Full Setup',setup.runFullSetup);
    menu.addItem('Create Database',setup.createDatabase);
    menu.addItem('Create Database User',setup.createDatabaseUser);
    menu.addItem('Stop Setup',async ()=>{
        menu.stop();
    });
    menu.addItem('Finish Interactive Setup Successfully', async ()=>{
            console.log("Finishing interactive setup...");
            console.log("The application will shutdown with exit code 0, it may take a few seconds...")
            process.exit(0);
    })
    menu.addItem('Finish Interactive Setup With Errors',
      async ()=>{
            console.log("Finishing interactive setup with errors...");
            console.log("The application will shutdown with exit code 1, it may take a few seconds...")
            process.exit(1);
        }
    );
    menu.start();
}
module.exports = {
    createAndStartMenu
}

main



async function runInteractiveSetupUserNeedsToConnectToContainer(){
    console.log("Running interactive setup...");
    console.log("To exit press CTRL+C");
    console.log("To cancel press CTRL+C twice");
    console.log("As you started the application in interactive mode, you may need to take some actions...")
    console.log("You can attach to the running container with the following command:")
    console.log("docker exec -it <container_name> /bin/bash")
    console.log("Or you can connect throught the docker integrated terminal, if so you may need to call the following command:")
    console.log("node interactive.js")
    let cancellationToken =false;
    process.on('SIGINT', function () {
        console.log("Cancellation token set to true throught SIGINT...")
        cancellationToken = true;

    });

        
        setInterval(()=>{
            if(cancellationToken) return process.exit(0);
            console.log("Waiting for cancellation token...");
        },1000);   

}
async function runInteractiveSetupWithoutNeedToConnectToContainer(){
    console.log("Running interactive setup...");
    const interactiveMenu = require('/app/modules/interactiveMenu.js');
    interactiveMenu.createAndStartMenu();
}
async function runNonInteractiveSetup(){
    console.log("Running non-interactive setup...");
    const setup = require('/app/modules/setup.js');
    await setup.runFullSetup();
}
async function run(){
    const commandLineArgs = require('command-line-args')
    const optionsDefinitions=[
        {name:'interactive',alias:"i", type:Boolean,defaultOption:false}
    ];
    const options = commandLineArgs(optionsDefinitions)
    const interactive = options.interactive;
    
    if(interactive) await runInteractiveSetupUserNeedsToConnectToContainer();
    else await runNonInteractiveSetup();
}
run().then(()=>console.log("Setup finished with no errors...")).catch((err)=>console.log(`Setup finished with errors: ${err}`));



setup


async function runFullSetup(){
    await createDatabase();
    await createDatabaseUser();
}
async function createDatabase(){
    console.log('creating database');
};
async function createDatabaseUser(){
    console.log('creating user');
};
module.exports= {
    runFullSetup,
    createDatabase,
    createDatabaseUser
}

interactive

const { createAndStartMenu } = require('/app/modules/interactiveMenu.js');
createAndStartMenu();

package.json

{
  "dependencies": {
    "axios": "^1.4.0",
    "command-line-args": "^5.2.1",
    "install": "^0.13.0",
    "mongodb": "^5.7.0",
    "mysql": "^2.18.1",
    "node-menu": "^1.3.2",
    "npm": "^9.8.0",
    "pg": "^8.11.1"
  }
}

Vinicius Andrade
  • 151
  • 1
  • 4
  • 22
  • 1
    A Docker container runs a single process in an isolated environment. The process can be any process, subject to the limits Docker and Linux impose. `docker exec` is a debugging tool that doesn't seem like it should be part of your core workflow for what you describe. Are you running into problems with Compose mostly being oriented around running non-interactive processes; [Interactive shell using Docker Compose](https://stackoverflow.com/questions/36249744/interactive-shell-using-docker-compose)? – David Maze Jul 19 '23 at 01:31
  • Do you know if I can add into .profile or .bashrc? – Vinicius Andrade Jul 19 '23 at 04:53
  • Most paths that run containers don't run a shell inside the container and don't read shell dotfiles. Many containers don't even contain bash. – David Maze Jul 19 '23 at 10:23
  • Do you have any suggestions to accomplish that? – Vinicius Andrade Jul 19 '23 at 18:48

1 Answers1

0

I've found out a solution for it According to this answer from apug

By default docker start a non-login shell. To read .profile file you need a login shell docker exec -it <container> ash -l.

He alsos sais that

To read /etc/profile (or another startup file) everytime, you've to set ENV variable

So in my docker file ended up like:

FROM node
WORKDIR /app
COPY ./package.json .
COPY ./interactive.js .
WORKDIR /app/modules
COPY ./modules .
RUN apt update && apt install -y curl jq iputils-ping traceroute lsb-release && \
    npm install &&  \
    ln -sf /bin/bash /bin/sh 
WORKDIR /app
ENV ENV="/root/.profile" # <-- new line added
ENTRYPOINT ["node","/app/modules/main.js"]

And in my main.js i'm a content in ~/.profile

// Changed this function to make more clear
function appendInFile(filePath, content){
    const fs = require('fs');
    fs.appendFile(filePath, content, (err) => {
        if (err) {
          console.error('Error appending to file:', err);
        } else {
          console.log('Text appended to file successfully.');
        }
    });
}
async function runInteractiveSetupUserNeedsToConnectToContainer(){
    console.log("Running interactive setup...");
    console.log("To exit press CTRL+C");
    console.log("To cancel press CTRL+C twice");
    console.log("As you started the application in interactive mode, you may need to take some actions...")
    console.log("You can attach to the running container with the following command:")
    console.log("docker exec -it <container_name> /bin/bash -c \"node interactive.js\"")
    console.log("Or you can connect throught the docker integrated terminal, if so you may need to call the following command:")
    
    let cancellationToken =false;
    process.on('SIGINT', function () {
        console.log("Cancellation token set to true throught SIGINT...")
        cancellationToken = true;

    });
//added this 2 lines above
    const os = require('os');
    appendInFile(`${os.homedir()}/.profile`,`node /app/interactive.js`);
        
    setInterval(()=>{
        if(cancellationToken) return;
        console.log("Waiting for cancellation token...");
    },1000);   

}
async function runInteractiveSetupWithoutNeedToConnectToContainer(){
    console.log("Running interactive setup...");
    const interactiveMenu = require('/app/modules/interactiveMenu.js');
    interactiveMenu.createAndStartMenu();
}
async function runNonInteractiveSetup(){
    console.log("Running non-interactive setup...");
    const setup = require('/app/modules/setup.js');
    await setup.runFullSetup();
}
async function run(){
    const commandLineArgs = require('command-line-args')
    const optionsDefinitions=[
        {name:'interactive',alias:"i", type:Boolean,defaultOption:false}
    ];
    const options = commandLineArgs(optionsDefinitions)
    const interactive = options.interactive;
    
    if(interactive) await runInteractiveSetupUserNeedsToConnectToContainer();
    else await runNonInteractiveSetup();
}
run().then(()=>console.log("Setup is running...")).catch((err)=>console.log(`Setup finished with errors: ${err}`));



Vinicius Andrade
  • 151
  • 1
  • 4
  • 22