Azure / azure-cli

Azure Command-Line Interface
MIT License
3.99k stars 2.98k forks source link

Need Documentation on passing --tags as single argument #13170

Closed ghstahl closed 4 years ago

ghstahl commented 4 years ago

This is autogenerated. Please review and update as needed.

Describe the bug

Need instructions on how to pass in --tags as a single argument to az cli

Command line

Please note the "c=a dog"
The following works from the command line

az group create --name "rg-terraform-lionking"  --tags "a=b" "c=a dog" --location "eastus2"
{
  "id": "/subscriptions/**REDACTED**/resourceGroups/rg-terraform-lionking",
  "location": "eastus2",
  "managedBy": null,
  "name": "rg-terraform-lionking",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": {
    "a": "b",
    "c": "a dog"
  },
  "type": "Microsoft.Resources/resourceGroups"
}

The following does not work;

tags='"a=b" "c=a dog"'
echo $tags
"a=b" "c=a dog"
az group create --name "rg-terraform-lionking"  --tags $tags --location "eastus2"
{
  "id": "/subscriptions/39ac48fb-fea0-486a-ba84-e0ae9b06c663/resourceGroups/rg-terraform-lionking",
  "location": "eastus2",
  "managedBy": null,
  "name": "rg-terraform-lionking",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": {
    "\"a": "b\"",
    "\"c": "a",
    "dog\"": ""
  },
  "type": "Microsoft.Resources/resourceGroups"
}
az group create --name "rg-terraform-lionking"  --tags "${tags}" --location "eastus2"
{
  "id": "/subscriptions/39ac48fb-fea0-486a-ba84-e0ae9b06c663/resourceGroups/rg-terraform-lionking",
  "location": "eastus2",
  "managedBy": null,
  "name": "rg-terraform-lionking",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": {
    "\"a": "b\" \"c=a dog\""
  },
  "type": "Microsoft.Resources/resourceGroups"
}

Oddly enough, the following works if there is no spaces, but chokes on quotes:

tags='a=b c=dog'
echo $tags
a=b c=dog
az group create --name "rg-terraform-lionking"  --tags $tags --location "eastus2"
{
  "id": "/subscriptions/39ac48fb-fea0-486a-ba84-e0ae9b06c663/resourceGroups/rg-terraform-lionking",
  "location": "eastus2",
  "managedBy": null,
  "name": "rg-terraform-lionking",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": {
    "a": "b",
    "c": "dog"
  },
  "type": "Microsoft.Resources/resourceGroups"
}

# FAILS - adds quotes
tags='"a=b" "c=dog"'
echo $tags
"a=b" "c=dog"
{
  "id": "/subscriptions/39ac48fb-fea0-486a-ba84-e0ae9b06c663/resourceGroups/rg-terraform-lionking",
  "location": "eastus2",
  "managedBy": null,
  "name": "rg-terraform-lionking",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": {
    "\"a": "b\"",
    "\"c": "dog\""
  },
  "type": "Microsoft.Resources/resourceGroups"
}

Test Case

Pass in tags to a bash script.

tags='"a=b" "c=a dog"'
bash ./az-create-group.sh rg-terraform-lionking eastus2 "${tags}"                                                           

My Script

#!/bin/sh -l
die () {
    echo >&2 "$@"
    echo "die"
    exit 1
}
# This works
# az group create --name "rg-terraform-lionking" --location "eastus2" --tags "a=b" "c=a dog"

REQUIRED_ARGS=3
[ "$#" -eq $REQUIRED_ARGS ] || die "$REQUIRED_ARGS argument required, $# provided"
 bash --version

resource_group_name=$1  
location=$2
tags=$3

echo "resource_group_name:" $resource_group_name
echo "location:"            $location
echo "tags"                 $tags

az group create --name "$resource_group_name" --location "$location"  --tags "${tags}"

Output

{
  "id": "/subscriptions/**REDACTED**/resourceGroups/rg-terraform-lionking",
  "location": "eastus2",
  "managedBy": null,
  "name": "rg-terraform-lionking",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": {
    "\"a": "b\" \"c=a dog\""
  },
  "type": "Microsoft.Resources/resourceGroups"
}

So I don't know if this is a defect or documentation request on how to get this working.

Environment Summary

Linux-4.4.0-18362-Microsoft-x86_64-with-debian-buster-sid
Python 3.6.5
Installer: DEB

azure-cli 2.4.0

Extensions:
azure-devops 0.16.0

Additional Context

yonzhan commented 4 years ago

add to S170

zhoxing-ms commented 4 years ago

@ghstahl Hi, my suggestion is to change the parameter variable of the tags to an array, and modify IFS distinguish between the spaces in the array and the spaces in each tag. Can this method solve your problem?

Example:

resource_group_name='ResourceGroupName'
location='westus'
tags=("key=val" "key2=val2 val3 val4")
IFS='*'
az group create --name "$resource_group_name" --location "$location" --tags ${tags[*]}
IFS=' '
ghstahl commented 4 years ago

@zhoxing-ms Getting closer.

The tags need to be passed in as a single string via a github action action-docker-azure-terraform-setup

- name: Terraform Setup Step
      id: tfsetup
      uses: ./
      with:
        shortName: 'lunchMeat'
        location: 'eastus2'
        creds: ${{ secrets.AZURE_CREDENTIALS }}
        tags: 'owner=Firstname_Lastname application=cool-name'

NOTE: The way github actions passes in arguments to my entrypoint.sh is probably the constraint here which I can't change.

Its a github docker actions and here is my Entry Point

I have a simple test script that takes three arguments az-create-group

#!/bin/sh -l
die () {
    echo >&2 "$@"
    echo "die"
    exit 1
}
# This works
# az group create --name "rg-terraform-lionking" --location "eastus2" --tags "a=b" "c=a dog"

REQUIRED_ARGS=3
[ "$#" -eq $REQUIRED_ARGS ] || die "$REQUIRED_ARGS argument required, $# provided"
 bash --version

resource_group_name=$1  
location=$2
tags=$3

echo "resource_group_name :" $resource_group_name
echo "location: "            $location
echo "tags: "                $tags

az group create --name "$resource_group_name" --location "$location"  --tags $tags
resource_group_name='rg-lionking'
location='eastus2'
tags='owner=Firstname_Lastname application=cool-name'
./az-create-group.sh $resource_group_name $location $tags

GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
resource_group_name : rg-lionking
location:  eastus2
tags:  owner=Firstname_Lastname application=cool-name
{
  "id": "/subscriptions/39ac48fb-fea0-486a-ba84-e0ae9b06c663/resourceGroups/rg-lionking",
  "location": "eastus2",
  "managedBy": null,
  "name": "rg-lionking",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": {
    "application": "cool-name",
    "owner": "Firstname_Lastname"
  },
  "type": "Microsoft.Resources/resourceGroups"
}

At the moment I have banned all tags that have spaces in their values 👎

I don't know yet if the problem is on the github actions side to provide different ways to pass in an array with spaces in its values, or I haven't found a simple and easily understandable way to pass space valued array so that az cli likes it.

zhoxing-ms commented 4 years ago

@ghstahl I suggest that you can pass the array data as a string, and modify IFS to define the separator of array elements to avoid confusion between the separator of array elements and the spaces contained in tag. For example:

# mock the input data from yaml file
tags='a=b#c=a dog#sad=a dsa'
bash test.sh "$tags"
resource_group_name='zhoxing-test'
location='westus'
IFS='#'
tags=($1)

az group create --name "$resource_group_name" --location "$location"  --tags ${tags[@]}
IFS=' '

What do you think of this solution?

jiasli commented 4 years ago

Found a very useful tutorial: http://mywiki.wooledge.org/BashFAQ/050

This doesn't work:

tags='"a=b" "c=a dog"'
az group create --name "rg-terraform-lionking" --tags $tags --location "eastus2"

Because space is used for field splitting (see this stackoverflow answer) and

Quotes inside the variable are literal, not syntactical.

In general, passing multiple arguments ("a=b" and "c=a dog") within a single variable (tags) can be very nasty when it becomes complicated:

$ tags='"a=b" "c=a dog"'

# This command is purely for showing what is going on inside Bash.
# $ help set
# -v  Print shell input lines as they are read.
# -x  Print commands and their arguments as they are executed.
$ set -vx

$ eval az $tags --debug
# This is the output from `-v`, which is the original command we typed
eval az $tags --debug

# This is the output from `-x`, showing spaces are used to separate arguments
# and double quotes are treated literally
+ eval az '"a=b"' '"c=a' 'dog"' --debug
           -----   ----   ----

# This is the output from `-v`, which is the command being executed by `eval`
# The arguments are concatenated by `eval` and double quotes will be parsed by Bash
az "a=b" "c=a dog" --debug  

# This is the output from `-x`, showing arguments are correctly sent to `az`
++ az a=b 'c=a dog' --debug

# This is the output from `--debug`, showing arguments are correctly received by `az`
Command arguments: ['a=b', 'c=a dog', '--debug']

Or

$ eval az "$tags" --debug  # Quote `$tags` this time
eval az "$tags" --debug
+ eval az '"a=b" "c=a dog"' --debug  # `"a=b" "c=a dog"` is received as a whole argument
           ---------------
az "a=b" "c=a dog" --debug
++ az a=b 'c=a dog' --debug
Command arguments: ['a=b', 'c=a dog', '--debug']
jiasli commented 4 years ago

I am actually thinking we can change the behavior of --tags to make it more user-friendly.

@zhoxing-ms, how about we expand the syntax of --tags to support JSON to save our customers from this mess?

$ set -vx

$ tags='{"a": "b", "c": "a dog"}'
tags='{"a": "b", "c": "a dog"}'
+ tags='{"a": "b", "c": "a dog"}'

$ echo "$tags"
echo "$tags"
+ echo '{"a": "b", "c": "a dog"}'
{"a": "b", "c": "a dog"}

$ az "$tags" --debug
az "$tags" --debug
+ az '{"a": "b", "c": "a dog"}' --debug
Command arguments: ['{"a": "b", "c": "a dog"}', '--debug']

I think this will be extremely helpful.


Another possible solution is to use this form and parse spaces and double quotes " by az itself.

$ az "$tags" --debug
az "$tags" --debug
+ az '"a=b" "c=a dog"' --debug
Command arguments: ['"a=b" "c=a dog"', '--debug']
                     ---------------

But I think this is a bad solution as az is doing jobs belonging to Bash.

jiasli commented 4 years ago

FYI, a good post explaining the difference between ${metadata[*]}, "${metadata[*]}", ${metadata[@]} and "${metadata[@]}": https://stackoverflow.com/a/3355375/2199657

Also see the bash manual:

https://www.gnu.org/software/bash/manual/bash.html#Arrays

If the subscript is ‘@’ or ‘*’, the word expands to all members of the array name. These subscripts differ only when the word appears within double quotes. If the word is double-quoted, ${name[*]} expands to a single word with the value of each array member separated by the first character of the IFS variable, and ${name[@]} expands each element of name to a separate word.

$ metadata=("wiritten=1934" "author=R K")

$ python3 -c "import sys; print(sys.argv)" ${metadata[*]}
['-c', 'wiritten=1934', 'author=R', 'K']

$ python3 -c "import sys; print(sys.argv)" "${metadata[*]}"
['-c', 'wiritten=1934 author=R K']

$ python3 -c "import sys; print(sys.argv)" ${metadata[@]}
['-c', 'wiritten=1934', 'author=R', 'K']

$ python3 -c "import sys; print(sys.argv)" "${metadata[@]}"
['-c', 'wiritten=1934', 'author=R K']
zhoxing-ms commented 4 years ago

@ghstahl Do you have any other questions?

ghstahl commented 4 years ago

@zhoxing-ms As long as you give me something that can bridge the gap between github actions string parameters, and the az cli command than I am good. I would hope that an earlier suggestion of sending in an json string for this would be the best approach. As I said earlier, I have mandated "no space" tags just so I can move on.

zhoxing-ms commented 4 years ago

@ghstahl Yes, we will support JSON format input for tags in the future~ I have created another issue #13659 to follow it up

jiasli commented 1 year ago

After 3 years, I would like to add some more personal understanding of this issue.

Let's look at the help message for --tabs:

$ az group create -h
...
    --tags                                   : Space-separated tags: key[=value] [key[=value] ...].

We are actually leveraging shell's capability to pass multiple arguments as --tags. That means, each tag must be one argument and the number of tags is equal to the number of arguments following --tags:

--tags "a=b" "c=a dog"
        ^^^   ^^^^^^^
         1       2

To verify this:

$ python3 -c "import sys; print(sys.argv)" "a=b" "c=a dog"
['-c', 'a=b', 'c=a dog']

When a variable is used in a command such as --tags $tags, the only shell functionality that works is using space to separate arguments; double quotes aren't parsed by shell in such context. Bash internally processes --tags $tags as

--tags "a=b" "c=a dog"
       ^^^^^ ^^^^ ^^^^
         1    2    3

To verify this:

$ tags='"a=b" "c=a dog"'

$ python3 -c "import sys; print(sys.argv)" $tags
['-c', '"a=b"', '"c=a', 'dog"']

$ python3 -c "import sys; print(sys.argv)" "$tags"
['-c', '"a=b" "c=a dog"']

So, we have to use eval to make double quotes work. However, I now feel eval may not be the preferred solution as it can easily lead to shell injection.


The best way for saving multiple arguments in one variable and expanding that variable during command execution is to use an array:

$ tags=("a=b" "c=a dog")
$ python3 -c "import sys; print(sys.argv)" "${tags[@]}"
['-c', 'a=b', 'c=a dog']

This is actually how az entry script is written - with "$@":

https://github.com/Azure/azure-cli/blob/868deccc88529c6cbba31df8456c08d5988167a0/build_scripts/windows/scripts/az#L3

The tags need to be passed in as a single string via a github action

Because of this limitation, passing $tags as an array in the format of tags=("a=b" "c=a dog") is not possible here, so https://github.com/Azure/azure-cli/issues/13659 is still the best way for passing --tags as a single argument.

bjh1977 commented 1 year ago

What an embarrassing situation this is. Absolutely pathetic.