Merge two nested JSON files using JQ - json

I'm trying to merge two JSON files. The main destination is to overwrite environment variables in the 1st file with environment variables in the 2nd.
1st file:
{
"containerDefinitions": [
{
"name": "foo",
"image": "nginx:latest",
"cpu": 1024,
"memory": 4096,
"memoryReservation": 2048,
"portMappings": [
{
"containerPort": 8080,
"hostPort": 0,
"protocol": "tcp"
}
],
"essential": true,
"environment": [
{
"name": "SERVER_PORT",
"value": "8080"
},
{
"name": "DB_NAME",
"value": "example_db"
}
],
"mountPoints": [],
"volumesFrom": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/dev/ecs/example",
"awslogs-region": "us-west-1",
"awslogs-stream-prefix": "ecs"
}
}
}
],
"family": "bar",
"taskRoleArn": "arn:aws:iam::111111111111:role/assume-ecs-role",
"executionRoleArn": "arn:aws:iam::111111111111:role/ecs-task-execution-role",
"networkMode": "bridge",
"volumes": [],
"placementConstraints": [],
"requiresCompatibilities": [
"EC2"
]
}
2nd file:
{
"containerDefinitions": [
{
"environment": [
{
"name": "SERVER_PORT",
"value": "8081"
}
]
}
]
}
The product has to be next:
{
"containerDefinitions": [
{
"name": "foo",
"image": "nginx:latest",
"cpu": 1024,
"memory": 4096,
"memoryReservation": 2048,
"portMappings": [
{
"containerPort": 8080,
"hostPort": 0,
"protocol": "tcp"
}
],
"essential": true,
"environment": [
{
"name": "SERVER_PORT",
"value": "8081"
},
{
"name": "DB_NAME",
"value": "example_db"
}
],
"mountPoints": [],
"volumesFrom": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/dev/ecs/example",
"awslogs-region": "us-west-1",
"awslogs-stream-prefix": "ecs"
}
}
}
],
"family": "bar",
"taskRoleArn": "arn:aws:iam::111111111111:role/assume-ecs-role",
"executionRoleArn": "arn:aws:iam::111111111111:role/ecs-task-execution-role",
"networkMode": "bridge",
"volumes": [],
"placementConstraints": [],
"requiresCompatibilities": [
"EC2"
]
}
I tried to do next:
jq -s 'reduce .[] as $item ({}; reduce ($item | keys_unsorted[]) as $key (.; $item[$key] as $val | ($val | type) as $type | .[$key] = if ($type == "array") then (.[$key] + $val | unique) elif ($type == "object") then (.[$key] + $val) else $val end))' 1.json 2.json
But the result is:
{
"containerDefinitions": [
{
"name": "foo",
"image": "nginx:latest",
"cpu": 1024,
"memory": 4096,
"memoryReservation": 2048,
"portMappings": [
{
"containerPort": 8080,
"hostPort": 0,
"protocol": "tcp"
}
],
"essential": true,
"environment": [
{
"name": "SERVER_PORT",
"value": "8080"
},
{
"name": "DB_NAME",
"value": "example_db"
}
],
"mountPoints": [],
"volumesFrom": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/dev/ecs/example",
"awslogs-region": "us-west-1",
"awslogs-stream-prefix": "ecs"
}
}
},
{
"environment": [
{
"name": "SERVER_PORT",
"value": "8081"
}
]
}
],
"family": "bar",
"taskRoleArn": "arn:aws:iam::111111111111:role/assume-ecs-role",
"executionRoleArn": "arn:aws:iam::111111111111:role/ecs-task-execution-role",
"networkMode": "bridge",
"volumes": [],
"placementConstraints": [],
"requiresCompatibilities": [
"EC2"
]
}
Could anyone help to find out how to reach the right result?

Something like this will do the trick:
(input | .containerDefinitions[0].environment | from_entries) as $new_env
| input | .containerDefinitions[].environment |= ((from_entries + $new_env) | to_entries)
Online demo
In case it's unclear, the invocation should look like so:
jq -n '...' 2.json 1.json

Here's a solution using tostream and has(1) to read the values from the second file, and setpath to set them in the first file:
jq 'reduce (input | tostream | select(has(1))) as $i (.; setpath($i[0]; $i[1]))' \
1.json 2.json
Demo
When providing the files in reversed order (2.json 1.json), the context . and input have to be swapped:
jq 'reduce (tostream | select(has(1))) as $i (input; setpath($i[0]; $i[1]))' \
2.json 1.json
Demo

Related

JQ: Merge all entries in an array without hardcoding the key

I have a json like this
[
{
"name": "hosts",
"ipaddress": "1.2.3.4",
"status": "UP",
"randomkey": "randomvalue"
},
{
"name": "hosts",
"ipaddress": "5.6.7.8",
"status": "DOWN",
"newkey": "newvalue"
},
{
"name": "hosts",
"ipaddress": "9.10.11.12",
"status": "RESTART",
"anotherkey": "anothervalue"
}
]
I want to merge the objects and looking for some output like this
[
{
"name": "hosts", //doesn't matter if it is ["hosts"]
"ipaddress": ["1.2.3.4", "5.6.7.8", "9.10.11.12"],
"status": ["UP", "DOWN", "RESTART"],
"randomkey": ["randomvalue"],
"newkey": ["newvalue"],
"anotherkey": ["anothervalue"]
}
]
I can hardcode each and every key and do something like this - { ipaddress: (map(.ipaddress) | unique ) } + { status: (map(.status) | unique ) } + { randomkey: (map(.randomkey) | unique ) }
The important ask here is the values are random and cannot be hardcoded.
Is there a way i can merge all the keys without hardcoding the key here?
Using reduce, then unique would be one way:
jq '[
reduce (.[] | to_entries[]) as {$key, $value} ({}; .[$key] += [$value])
| map_values(unique)
]'
[
{
"name": [
"hosts"
],
"ipaddress": [
"1.2.3.4",
"5.6.7.8",
"9.10.11.12"
],
"status": [
"DOWN",
"RESTART",
"UP"
],
"randomkey": [
"randomvalue"
],
"newkey": [
"newvalue"
],
"anotherkey": [
"anothervalue"
]
}
]
Demo
Using group_by and map, then unique again would be another:
jq '[
map(to_entries[]) | group_by(.key)
| map({key: first.key, value: map(.value) | unique})
| from_entries
]'
[
{
"anotherkey": [
"anothervalue"
],
"ipaddress": [
"1.2.3.4",
"5.6.7.8",
"9.10.11.12"
],
"name": [
"hosts"
],
"newkey": [
"newvalue"
],
"randomkey": [
"randomvalue"
],
"status": [
"DOWN",
"RESTART",
"UP"
]
}
]
Demo

JQ: group by into single object with groups as keys

I have the following data:
[
{
"company.u_ats_region": "Region1",
"hostname": "host1",
"install_status": "1",
"os": "Windows",
"os_domain": "test.com"
},
{
"company.u_ats_region": "Region2",
"hostname": "host2",
"install_status": "1",
"os": "Windows",
"os_domain": "test.com"
},
{
"company.u_ats_region": "Region3",
"hostname": "host3",
"install_status": "7",
"os": "Windows",
"os_domain": "test.com"
}
]
And I've been using this query
{count: length,
regions: [group_by(."company.u_ats_region")[] |
{( [0]."company.u_ats_region"): [.[] |
{name: (.hostname+"."+.os_domain),
os: .os}]}]}
to convert the data into the following:
{
"count": 3,
"regions": [
{
"Region1": [
{
"name": "host1.test.com",
"os": "Windows"
}
]
},
{
"Region2": [
{
"name": "host2.test.com",
"os": "Windows"
}
]
},
{
"Region3": [
{
"name": "host3.test.com",
"os": "Windows"
}
]
}
]
}
This is close to what I'm trying to achieve but I would like 'regions' to be a single object with each region being a key within that object like this:
{
"count": 3,
"regions": {
"Region1": [
{
"name": "host1.test.com",
"os": "Windows"
}
],
"Region2": [
{
"name": "host2.test.com",
"os": "Windows"
}
],
"Region3": [
{
"name": "host3.test.com",
"os": "Windows"
}
]
}
}
I have tried playing around with 'add' but that still didn't bring me any closer to the result I'm trying to achieve. Any help is appreciated!
Creating an object with key and value fields, then using from_entries would be one way:
{
count: length,
regions: group_by(."company.u_ats_region")
| map({
key: .[0]."company.u_ats_region",
value: map({name: "\(.hostname).\(.os_domain)", os})
})
| from_entries
}
{
"count": 3,
"regions": {
"Region1": [
{
"name": "host1.test.com",
"os": "Windows"
}
],
"Region2": [
{
"name": "host2.test.com",
"os": "Windows"
}
],
"Region3": [
{
"name": "host3.test.com",
"os": "Windows"
}
]
}
}
Demo
You can define a custom function which performs the grouping and then tranfsorm the result. Using a function avoids having to repeat the selector:
def group_to_obj(f):
group_by(f) | map({key:first|f, value:.}) | from_entries;
{
count: length,
regions: group_to_obj(."company.u_ats_region")
| map_values(map({name: "\(.hostname).\(.os_domain)", os}))
}
Output:
{
"count": 3,
"regions": {
"Region1": [
{
"name": "host1.test.com",
"os": "Windows"
}
],
"Region2": [
{
"name": "host2.test.com",
"os": "Windows"
}
],
"Region3": [
{
"name": "host3.test.com",
"os": "Windows"
}
]
}
}
Using reduce to iteratively build up the arrays would be another way:
{
count: length,
regions: (
reduce .[] as $i ({};
.[$i."company.u_ats_region"] += [$i | {name: "\(.hostname).\(.os_domain)", os}]
)
)
}
{
"count": 3,
"regions": {
"Region1": [
{
"name": "host1.test.com",
"os": "Windows"
}
],
"Region2": [
{
"name": "host2.test.com",
"os": "Windows"
}
],
"Region3": [
{
"name": "host3.test.com",
"os": "Windows"
}
]
}
}
Demo

Replace \r from json in shell

I have below JSON in a variable name TASK_DEFINTIION
It has a \r character at the end of "image": "700707367057.dkr.ecr.us-east-1.amazonaws.com/php-demo:feature-feature01\r" under containerDefinitions
I am using TASK_DEFINITION_AFTER= 'echo $TASK_DEFINTIION | sed "s/\\r//g"' to remove the \r but seems it is removing all the hidden carriage return but not removing the one which is visible as regular character.
Any help would be highly appriciated.
{
"memory": "1024",
"networkMode": "awsvpc",
"family": "ecs-php-demo",
"placementConstraints": [],
"cpu": "512",
"executionRoleArn": "arn:aws:iam::700707367057:role/ecsTaskExecutionRole",
"volumes": [],
"requiresCompatibilities": [
"FARGATE"
],
"taskRoleArn": "arn:aws:iam::700707367057:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"memoryReservation": 256,
"environment": [],
"name": "ecs-php-demo",
"mountPoints": [],
"image": "700707367057.dkr.ecr.us-east-1.amazonaws.com/php-demo:feature-feature01\r",
"cpu": 0,
"portMappings": [
{
"protocol": "tcp",
"containerPort": 8080,
"hostPort": 8080
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs",
"awslogs-group": "/ecs/ecs-php-demo"
}
},
"essential": true,
"volumesFrom": []
}
]
}
Using jq rtrimstr to stay conform with the JSON syntax:
#!/usr/bin/bash
TASK_DEFINTIION="$(
jq '.containerDefinitions[].image|=rtrimstr("\r")' <<<"$TASK_DEFINTIION"
)"
echo "$TASK_DEFINTIION"
man jq:
rtrimstr(str)
Outputs its input with the given suffix string removed, if it ends with it.
jq ´[.[]|rtrimstr("foo")]´
["fo", "foo", "barfoo", "foobar", "foob"]
=> ["fo","","bar","foobar","foob"]
In your command, first \ escapes the second \, so sed sees only one \
You need :
TASK_DEFINITION_AFTER="$(echo $TASK_DEFINTIION | sed "s/\\\\r//g")"
you can use jq to replace key value:
jq '.containerDefinitions[].image="700707367057.dkr.ecr.us-east-1.amazonaws.com/php-demo:feature-feature01"' file.json
but unfortunately, jq does not support in-place editing, so you must redirect to a temporary file first and then replace your original file with it, or use sponge utility from the more utils package, like that:
jq '.containerDefinitions[].image="700707367057.dkr.ecr.us-east-1.amazonaws.com/php-demo:feature-feature01"' file.json|sponge file.json
A pure jq solution to remove \r with gsub :
jq '.containerDefinitions[].image|=gsub("[\r]"; "")' file.json|sponge file.json
output sample :
{
"memory": "1024",
"networkMode": "awsvpc",
"family": "ecs-php-demo",
"placementConstraints": [],
"cpu": "512",
"executionRoleArn": "arn:aws:iam::700707367057:role/ecsTaskExecutionRole",
"volumes": [],
"requiresCompatibilities": [
"FARGATE"
],
"taskRoleArn": "arn:aws:iam::700707367057:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"memoryReservation": 256,
"environment": [],
"name": "ecs-php-demo",
"mountPoints": [],
"image": "700707367057.dkr.ecr.us-east-1.amazonaws.com/php-demo:feature-feature01",
"cpu": 0,
"portMappings": [
{
"protocol": "tcp",
"containerPort": 8080,
"hostPort": 8080
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs",
"awslogs-group": "/ecs/ecs-php-demo"
}
},
"essential": true,
"volumesFrom": []
}
]
}

Using jq to find and replace elements from Json sub collection

I am trying to parse convert Json Task definition with new image.
I want to change the "image": "docker.org/alpha/alpha-app-newgen:12.2.3" value in Json to "image": "docker.org/alpha/alpha-app-newgen:12.5.0" or any other version dynamically.
below is my Json task def:
{
"taskDefinition": {
"family": "ing-stack",
"volumes": [
{
"host": {
"sourcePath": "/tmp/nginx/elb.conf"
},
"name": "volume-0"
}
],
"containerDefinitions": [
{
"dnsSearchDomains": [],
"environment": [
{
"name": "API_SECRET",
"value": "ING-SECRET"
},
{
"name": "API_KEY",
"value": "AVERA-CADA-VERA-KEY"
}
],
"readonlyRootFilesystem": false,
"name": "ing-stg",
"links": [],
"mountPoints": [],
"image": "docker.org/alpha/alpha-app-newgen:12.2.3",
"privileged": false,
"essential": true,
"portMappings": [
{
"protocol": "tcp",
"containerPort": 19000,
"hostPort": 19000
}
],
"dockerLabels": {}
},
{
"dnsSearchDomains": [],
"environment": [
{
"name": "NG_PROXY",
"value": "ing"
}
],
"readonlyRootFilesystem": false,
"name": "web",
"links": [
"identity-ng"
],
"mountPoints": [
{
"sourceVolume": "volume-0",
"readOnly": false,
"containerPath": "/etc/nginx/conf.d/default.conf"
}
],
"image": "docker.org/alpha/alpha-ui:6.4.7",
"portMappings": [
{
"protocol": "tcp",
"containerPort": 443,
"hostPort": 443
},
{
"protocol": "tcp",
"containerPort": 80,
"hostPort": 80
}
],
"memory": 512,
"command": [
"sh",
"prep-run-nginx.sh"
],
"dockerLabels": {}
}
],
"revision": 136
}
}
I need to get the same structure back with new value for the image.
I tried the following
jq '. | select(.containerDefinitions[].image | contains("'$new_img_no_ver'") ) | .image |= "my new image"', but its adding to the end of the JSON.
Can anybody tell me how to achieve this.
Here are two potential solutions. Other variants are of course possible.
walk/1
If you don't want to be bothered with the details of exactly where the relevant "image" tag is located, consider using walk/1:
Invocation:
jq --arg old "docker.org/alpha/alpha-app-newgen:12.2.3" --arg new "HELLO" -f update.jq input.json
update.jq:
walk(if type == "object" and .image == $old then .image=$new else . end)
If your jq does not have walk/1, then consider updating or getting its jq definition by googling: jq def walk
Targeted update
Invocation: as above
update.jq:
.taskDefinition.containerDefinitions[].image |= (if . == $old then $new else . end)

Jq update JSON key:value based on value

I'm pretty new to jq and wanted to use it to update an AWS ECS task definition with a new value. AWS cli returns the following json response and i would like to modify the object with name property CONFIG_URL with value "this is atest".
{
"family": "contentpublishing-task",
"volumes": [],
"containerDefinitions": [
{
"environment": [
{
"name": "TEST_ENV",
"value": "TEST"
},
{
"name": "CONFIG_URL",
"value": "s3://stg-appcfg/config-20160729-1130.json"
}
],
"name": "contentpublishing",
"mountPoints": [],
"image": "contentpublishing:blah",
"cpu": 512,
"portMappings": [
{
"protocol": "tcp",
"containerPort": 8081,
"hostPort": 8080
}
],
"memory": 256,
"essential": true,
"volumesFrom": []
}
]
}
Tried the following query
cat test.json | jq 'select(.containerDefinitions[0].environment[].name=="CONFIG_URL").value|="this is atest"' 2>&1
But following has been returned. As you can see an additional value key is added the outer most json object.
{
"family": "contentpublishing-task",
"volumes": [],
"containerDefinitions": [
{
"environment": [
{
"name": "TEST_ENV",
"value": "TEST"
},
{
"name": "CONFIG_URL",
"value": "s3://stg-appcfg/config-20160729-1130.json"
}
],
"name": "contentpublishing",
"mountPoints": [],
"image": "contentpublishing:blah",
"cpu": 512,
"portMappings": [
{
"protocol": "tcp",
"containerPort": 8081,
"hostPort": 8080
}
],
"memory": 256,
"essential": true,
"volumesFrom": []
}
],
"value": "this is atest"
}
You have to select the corresponding environment node first before setting the value. Your query doesn't change the context so it's still on the root item so you end up adding the new value to the root.
$ jq --arg update_name "CONFIG_URL" --arg update_value "this is a test" \
'(.containerDefinitions[].environment[] | select(.name == $update_name)).value = $update_value' input.json
Here is a solution which uses jq Complex assignments
(
.containerDefinitions[]
| .environment[]
| select(.name == "CONFIG_URL")
| .value
) |= "this is atest"