withfig / fig

Public issue tracker for Fig.
https://fig.io
MIT License
2.05k stars 54 forks source link

Position-Dependent CLI Script Input Variable Interpolation Bug #2371

Open ghost opened 1 year ago

ghost commented 1 year ago

Checks

Operating system

macOS 13.2.1 (22D68)

Expected behaviour

The script is based on this blog post, which provides an example for how to use AWS SSM to connect to a remote EC2 instance through the CLI: SSM Session Manager - no bastion host necessary!. We already use Fig for this successfully, so I wanted to extend our scripts to cover SSH tunnels.

Our script takes a bunch of user inputs, does a few AWS CLI lookups, and then fills in all the information necessary to open an SSH tunnel to a remote host. The Fig script should take all these inputs and interpolate a well-formed SSH statement to open a remote tunnel.

Actual behaviour

Fig doesn't properly interpolate one of our variables, resulting in a malformed SSH statement.

Things we've tried:

Steps to reproduce

Script inputs:

Script code (Bash):

# set AWS_PROFILE globally for this subroutine
export AWS_PROFILE={{aws-profile}}

# use the instance name to query EC2 for matching instance ids (should return one)
export INSTANCE_ID=$(aws ec2 describe-instances --filters 'Name=tag:Name,Values={{instance-name}}' --query 'Reservations[*].Instances[*].InstanceId' --output text)

# derive the SSH public key from the selected SSH private key
export SSH_PUBLIC_KEY=$(ssh-keygen -y -f ~/.ssh/{{ssh-private-keyname}})

# add the public SSH key to EC2
# note: key is ephemeral and will be removed after 60 seconds
aws ec2-instance-connect send-ssh-public-key \
  --instance-id ${INSTANCE_ID} \
  --instance-os-user ec2-user \
  --ssh-public-key "${SSH_PUBLIC_KEY}" > /dev/null

# open the SSH tunnel without launching a terminal
ssh \
  -oStrictHostKeyChecking=no \
  -oUserKnownHostsFile=/dev/null \
  -o 'ProxyCommand aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters portNumber=%p' \
  -M -S ~/.ssh/ssh-tunnel-ctrl-socket \
  -fnNT \
  -L {{local-port}}:{{remote-host}}:{{remote-port}} \
  -i ~/.ssh/{{ssh-private-keyname}} \
  ec2-user@${INSTANCE_ID}

exit 0

Which returns: Bad local forwarding specification '{{local-port}}:localhost:1085'

Environment

fig-details:
  - 2.14.2
hardware-info:
  - model: 
  - model-id: 
  - chip-id: Apple M1 Max
  - cores: 10
  - mem: 32.00 GB
os-info:
  - macOS 13.2.1 (22D68)
environment:
  - shell: /bin/zsh
  - terminal: iterm
  - cwd: /Users/jonathanbackhaus/Downloads
  - exe-path: /Users/jonathanbackhaus/.fig/bin/fig
  - install-method: unknown
  - env-vars:
    - FIGTERM_SESSION_ID: bd339ea6-2bb4-4f38-9449-3af2c6e8162d
    - FIG_SET_PARENT_CHECK: 1
    - FIG_TERM: 2.14.2
    - PATH: /Users/jonathanbackhaus/.gloo-mesh/bin:/Users/jonathanbackhaus/.rd/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/jonathanbackhaus/.fig/bin:/Users/jonathanbackhaus/.local/bin
    - SHELL: /bin/zsh
    - TERM: xterm-256color
    - __CFBundleIdentifier: com.googlecode.iterm2
    - FIG_PID: 39139
    - FIG_SET_PARENT: bd339ea6-2bb4-4f38-9449-3af2c6e8162d
ghost commented 1 year ago

Redacted screenshot of input config: Screenshot 2023-02-24 at 3 42 24 PM

grant0417 commented 1 year ago

Hey @jonbackhaus-mt, if you are comfortable sharing here could you send the file at ~/Library/Caches/fig/scripts/{namespace}.{script-name}.json where {namespace} is the team username or the username of you have it under, and {script-name} is just the name.

grant0417 commented 1 year ago

Also tried recreating the script and it seems to template correctly for me:

Screenshot 2023-02-24 at 3 22 05 PM Screenshot 2023-02-24 at 3 22 11 PM

ghost commented 1 year ago

I think I see the issue in the tree:

{
  "uuid": "a5f9d77c-81a6-46d9-bcd1-47b5c35de4a0",
  "name": "ssm-tunnel",
  "displayName": null,
  "description": null,
  "templateVersion": 4,
  "tags": [],
  "rules": [],
  "steps": [
    {
      "name": null,
      "parameters": [
        {
          "name": "aws-profile",
          "displayName": null,
          "description": "Description...",
          "dependsOn": [],
          "required": null,
          "type": "selector",
          "typeData": {
            "placeholder": null,
            "suggestions": null,
            "generators": [
              {
                "type": "script",
                "script": "aws configure list-profiles",
                "tree": [
                  "aws configure list-profiles"
                ]
              }
            ],
            "allowRawTextInput": null,
            "multi": null
          },
          "cli": {
            "short": null,
            "long": "aws-profile",
            "required": true,
            "require_equals": false,
            "type": {
              "String": {
                "default": null
              }
            },
            "raw": false
          }
        },
        {
          "name": "instance-name",
          "displayName": null,
          "description": null,
          "dependsOn": [],
          "required": null,
          "type": "selector",
          "typeData": {
            "placeholder": null,
            "suggestions": null,
            "generators": [
              {
                "type": "script",
                "script": "aws --profile {{aws-profile}} ec2 describe-instances | jq '.Reservations[] | .Instances[] | select(.State.Name == \"running\") | ( .Tags | map( { (.Key): .Value } ) | add | .Name)' | tr -d '\"' | sort",
                "tree": [
                  "aws --profile ",
                  {
                    "name": "aws-profile"
                  },
                  " ec2 describe-instances | jq '.Reservations[] | .Instances[] | select(.State.Name == \"running\") | ( .Tags | map( { (.Key): .Value } ) | add | .Name)' | tr -d '\"' | sort"
                ]
              }
            ],
            "allowRawTextInput": null,
            "multi": null
          },
          "cli": {
            "short": null,
            "long": "instance-name",
            "required": true,
            "require_equals": false,
            "type": {
              "String": {
                "default": null
              }
            },
            "raw": false
          }
        },
        {
          "name": "ssh-private-keyname",
          "displayName": null,
          "description": null,
          "dependsOn": [],
          "required": null,
          "type": "selector",
          "typeData": {
            "placeholder": null,
            "suggestions": null,
            "generators": [
              {
                "type": "script",
                "script": "ls ~/.ssh/",
                "tree": [
                  "ls ~/.ssh/"
                ]
              }
            ],
            "allowRawTextInput": null,
            "multi": null
          },
          "cli": {
            "short": null,
            "long": "ssh-private-keyname",
            "required": true,
            "require_equals": false,
            "type": {
              "String": {
                "default": null
              }
            },
            "raw": false
          }
        },
        {
          "name": "local-port",
          "displayName": null,
          "description": null,
          "dependsOn": [],
          "required": null,
          "type": "text",
          "typeData": {
            "placeholder": "Enter value here"
          },
          "cli": {
            "short": null,
            "long": "local-port",
            "required": true,
            "require_equals": false,
            "type": {
              "String": {
                "default": null
              }
            },
            "raw": false
          }
        },
        {
          "name": "remote-host",
          "displayName": null,
          "description": null,
          "dependsOn": [],
          "required": null,
          "type": "text",
          "typeData": {
            "placeholder": "Enter value here"
          },
          "cli": {
            "short": null,
            "long": "remote-host",
            "required": true,
            "require_equals": false,
            "type": {
              "String": {
                "default": "localhost"
              }
            },
            "raw": false
          }
        },
        {
          "name": "remote-port",
          "displayName": null,
          "description": null,
          "dependsOn": [],
          "required": null,
          "type": "text",
          "typeData": {
            "placeholder": "Enter value here"
          },
          "cli": {
            "short": null,
            "long": "remote-port",
            "required": true,
            "require_equals": false,
            "type": {
              "String": {
                "default": null
              }
            },
            "raw": false
          }
        }
      ]
    },
    {
      "name": null,
      "runtime": "BASH",
      "tree": [
        "# set AWS_PROFILE globally for this subroutine\nexport AWS_PROFILE=",
        {
          "name": "aws-profile"
        },
        "\n\n# use the instance name to query EC2 for matching instance ids (should return one)\nexport INSTANCE_ID=$(aws ec2 describe-instances --filters 'Name=tag:Name,Values=",
        {
          "name": "instance-name"
        },
        "' --query 'Reservations[*].Instances[*].InstanceId' --output text)\n\n# derive the SSH public key from the selected SSH private key\nexport SSH_PUBLIC_KEY=$(ssh-keygen -y -f ~/.ssh/",
        {
          "name": "ssh-private-keyname"
        },
        ")\n\n# add the public SSH key to EC2\n# note: key is ephemeral and will be removed after 60 seconds\naws ec2-instance-connect send-ssh-public-key --instance-id ${INSTANCE_ID} --instance-os-user ec2-user --ssh-public-key \"${SSH_PUBLIC_KEY}\" > /dev/null\n\n# open the SSH tunnel without launching a terminal\nssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -o 'ProxyCommand aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters portNumber=%p' -M -S ~/.ssh/ssh-tunnel-ctrl-socket -fnNT -L {{local-port}}:",
        {
          "name": "remote-host"
        },
        ":",
        {
          "name": "remote-port"
        },
        " -i ~/.ssh/",
        {
          "name": "ssh-private-keyname"
        },
        " ec2-user@${INSTANCE_ID}\n\n# print results\necho \"SSH Tunnel Established!\"\necho \"localhost:{{local-port}} --> ",
        {
          "name": "instance-name"
        },
        " --> ",
        {
          "name": "remote-host"
        },
        ":",
        {
          "name": "remote-port"
        },
        "\"\necho \"\"\nread -p \"Press enter to close the tunnel\"\n\n# close the tunnel\nssh -S ~/.ssh/ssh-tunnel-ctrl-socket -O exit ec2-user@${INSTANCE_ID}"
      ]
    }
  ],
  "namespace": "ise-dee",
  "isOwnedByUser": false,
  "lastInvokedAt": "2023-02-24T20:31:50.952Z",
  "lastInvokedAtByUser": "2023-02-24T20:31:50.952Z",
  "invocationTrackStderr": false,
  "invocationTrackStdout": false,
  "invocationTrackInputs": false,
  "invocationDisableTrack": false,
  "shouldCache": true
}
grant0417 commented 1 year ago

I am quite confused by this one!

I added the script specifically to our unit tests and everything is fine, manually recreated the script and it works fine, the cache file looks 100% correct besides the 2 instances where only {{local-port}} isn't templated.

ghost commented 1 year ago

@grant0417 When/how do you re-generate the template? Given your results, the only thing that is likely different is how we originally wrote the script. (I know I refactored my code a bunch as I tested each step.) Maybe it's a caching issue?

I modified the JSON tree manually and the script now works. (And Fig didn't change anything on the UI side as a result of my updates.) So that's maybe another data point.

UPDATE: I modified the script and ran into the same templating issue again (even after manually correcting the JSON). So it's repeatable, at least within my instance of the script.

grant0417 commented 1 year ago

Have you tried making a new Fig Script and copying the text to it? Really struggling to figure out what is going on here!

ghost commented 1 year ago

Okay. It just happened again in a brand new script -- no relation to the original script.

Here's the bash source:

# the directory of the script
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# the temp directory used, within $DIR
TEMP_PATH=$(mktemp -d)

# check if tmp dir was created
if [[ ! "$TEMP_PATH" || ! -d "$TEMP_PATH" ]]; then
  echo "Could not create temp dir"
  exit 1
fi

# deletes the temp directory
function cleanup {      
  rm -rf "$TEMP_PATH"
  echo "Deleted temp working directory $TEMP_PATH"
}

# register the cleanup function to be called on the EXIT signal
trap cleanup EXIT

# extract path parts
export RDZIP_FILENAME=$(basename "{{rdzip-filepath}}")
export RDZIP_DIRPATH=$(dirname "{{rdzip-filepath}}")
export RDZIP_DIRNAME=$(basename "${RDZIP_DIRPATH}")
export RDZIP_VERSION=${RDZIP_DIRNAME}

# extract 
cp {{rdzip-filepath}} "${TEMP_PATH}/"

unzip "${TEMP_PATH}/${RDZIP_FILENAME}" -d "${TEMP_PATH}"

# end
read -p "Press enter to terminate script."

JSON attached: ise-dee.rdzip-unpacker.json.zip

The cp {{rdzip-filepath}} "${TEMP_PATH}/" line does not properly template/render in the JSON, which causes the Fig variable to be incorrectly exposed in the Bash call.

ghost commented 1 year ago

FYI -- this is still happening. I'm trying to debug another script. I'm watching the json file from ~/Library/Caches/fig/scripts in the background and can see Fig not properly unpacking a variable in the script tree.

Is there anything else I can do to help debug/resolve this issue?

grant0417 commented 1 year ago

@mschrage any ideas what to do here, I tried to debug this pretty deeply back when it was reported but could not figure out what was going on.

ghost commented 1 year ago

@mschrage -- I've got this happening across multiple scripts, all within our organization. I've tried everything I can think of to fix it without some kind of nuclear option (like start a new organization). This bug is already blocking some of our more advanced workflows -- like using AWS SSM to open an SSH tunnel. We're open to continuing the debugging conversation if you all can support the activity.