I am trying to append many variables that are zeep objects to a list. However, when I print out the list at the end, each variable in the list has the same data.
I have tried isolating when the variable gets overwritten, and it's happening when I put a new value into the variable with the same name, but I do this after I append the variable to the list. So I am confused as to why this is happening. I've tried to use different variable names, but it results in the same problem.
def create_json_list(json_template):
newVar = json_template
newVar['name'] = unique_name
list.append(newVar)
newVar = json_template
newVar['name'] = unique_name2
list.append(newVar)
print(list[0]['name'])
print(list[1]['name'])
# At this point, the same name gets printed twice
You are appending the same object to the list multiple times. If you notice, the template is getting modified:
import json
template = {
"field1": 1,
"field2": "two"
}
json_list = list()
def create_json_list(json_template):
newVar = json_template
newVar['name'] = "unique_name"
json_list.append(newVar)
newVar = json_template
newVar['name'] = 'another_unique_name'
json_list.append(newVar)
print(json.dumps(json_template, indent=4))
create_json_list(template)
Output
{
"field2": "two",
"field1": 1,
"name": "another_unique_name"
}
You need to create a new template for each entry:
newVar = dict(json_template)
From the documenation:
Assignment statements in Python do not copy objects, they create
bindings between a target and an object.
If you want to copy an object, you need to let Python know. In the case of a dict, you can just use the constructor as shown above.
For a zeep object, you should be able to use a factory:
factory = client.type_factory('ns0')
newVar = factory.JsonTemplate({"field1": 1, "field2": "two"})
Related
I want to allow any key to be set within a dictionary object and require Name to be set. Im passing this object into a variable that forces Name to be set but its ignoring all the other keys
tags = {
"Name" = "EC2_Name_Value" # Required
"AnyKey1" = "value1"
"AnyKey2" = "value2"
...
}
variable "tags" {
type = object({
Name = string
})
}
> var.tags
{
"Name" = "EC2_Name_Value"
}
I know that I'm able to use key = optional(string) however, i want to accept all extra keys and not have to define only the keys i want to accept.
What I would suggest is leaving the tags variable as is (maybe renaming it), and then using either an additional variable or local variables, e.g.:
variable "required_tag" {
type = object({
Name = string
})
}
variable "additional_tags" {
type = object(any)
}
Then, you would use the merge built-in function [1] to bring them all together:
tags = merge(var.required_tag, var.additional_tags)
Alternatively, since you know you will always need that one tag, you could switch it up a bit and remove the required_tag (or tags in your example) variable, and do something like:
tags = merge({ Name = "EC2_Name_Value" }, var.additional_tags)
Last, but not the least, there is an option to use default_tags on the provider level [2], but I am not sure if that fits your case.
[1] https://developer.hashicorp.com/terraform/language/functions/merge
[2] https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/resource-tagging#propagating-tags-to-all-resources
I am trying to copy contents of a list and map into a file in terraform.
Here is my variables.tf file.
variable "fruits" {
type = list
description = "List Of Fruits"
default = [ "Apple" , "Banana" , "Mango" ]
}
variable "animals" {
type = map
description = "Map of animals"
default = {
herbivores = "sheep"
carnovore = "lion"
omnivore = "bear"
}
}
variable "birds" {
type = string
description = "A string for Bird"
default = "parrot"
}
variable "bool" {
default = "true"
}
variable "filenames" {
default = "Terraform_Basics.txt"
}
I am trying to create a resource type local_file which will have contents of the map and list values. Able to copy single value but not able to capture all the values at once to be copied to the file.
resource "local_file" "pet" {
filename = var.filenames
content = var.fruits[0]
}
I am new to terraform, can someone help in how I can achieve this.
To do that, there needs to be one very important change. Even if you manage to get it to work for all the elements, you would get the following error:
var.fruits is a list of dynamic, known only after apply
var.pets is a map of dynamic, known only after apply
For these kinds of tests, I usually suggest using local variables [1]. Their values are not dynamic and are known. So for the first part, I would change variable definitions to local variables:
locals {
fruits = ["Apple", "Banana", "Mango"]
pets = {
herbivores = "sheep"
carnovore = "lion"
omnivore = "bear"
}
filenames = "Terraform_Basics.txt"
}
Now, another thing to note is that you gave the file in the example you posted a logical name of pets while you were trying to output the values of fruits, which could lead to a lot of confusion down the road. Here is how your resource blocks should look like:
resource "local_file" "fruits" {
...
}
resource "local_file" "pets" {
...
}
I will explain the content that needs to be added to the resource blocks. You want to fetch all the elements of a map/list. For that, you can use the splat expression [2].If it is a list, then you can simply do:
resource "local_file" "fruits" {
filename = local.filenames
content = join(", ", local.fruits[*])
}
For a map, there is a slightly different syntax as you would use the values built-in function [3] which also returns a list, hence why the splat expression is required in this case as well:
resource "local_file" "pets" {
filename = local.filenames
content = join(", ", values(local.pets)[*])
}
Note that there is an additional built-in function you need to use and that is join [4]. If you were to try to pass only the local variable value in the content, you would get the following error:
Inappropriate value for attribute "content": string required.
If you are looking to further improve the code, my suggestion would be to create a different filename for pets and fruits because otherwise it will get overridden constantly:
locals {
fruits = ["Apple", "Banana", "Mango"]
pets = {
herbivores = "sheep"
carnovore = "lion"
omnivore = "bear"
}
filename_pets = "Terraform_Basics_Pets.txt"
filename_fruits = "Terraform_Basics_Fruits.txt"
}
And then, in the resources blocks:
resource "local_file" "fruits" {
filename = local.filename_fruits
content = join(", ", local.fruits[*])
}
resource "local_file" "pets" {
filename = local.filename_pets
content = join(", ", values(local.pets)[*])
}
[1] https://www.terraform.io/language/values/locals
[2] https://www.terraform.io/language/expressions/splat
[3] https://www.terraform.io/language/functions/values
[4] https://www.terraform.io/language/functions/join
I'm not sure why I'm having such a hard time finding an answer for this, but I have a list that I need to get the value from where the key matches certain criteria. The keys are all unique. In the example below, I want to get the color where the name equals "headache". Result should be "4294930176".
//Example list
String trendName = 'headache';
List trendsList = [{name: fatigue, color: 4284513675}, {name: headache, color: 4294930176}];
//What I'm trying
int trendIndex = trendsList.indexWhere((f) => f.name == trendName);
Color trendColor = Color(int.parse(trendsList[trendIndex].color));
print(trendColor);
Error I get: Class '_InternalLinkedHashMap' has no instance getter 'name'. Any suggestions?
EDIT:
Here's how I'm adding the data to the list, where userDocuments is taken from a Firestore collection:
for (int i = 0; i < userDocument.length; i++) {
var trendColorMap = {
'name': userDocument[i]['name'],
'color': userDocument[i]['color'].toString(),
};
trendsList.add(trendColorMap);
}
I guess, I got what the problem was. You were making a little mistake, and that was, you're trying to call the Map element as an object value.
A HashMap element cannot be called as f.name, it has to be called f['name']. So taking your code as a reference, do this, and you are good to go.
String trendName = 'headache';
List trendsList = [{'name': 'fatigue', 'color': 4284513675}, {'name': headache, 'color': 4294930176}];
//What I'm trying
// You call the name as f['name']
int trendIndex = trendsList.indexWhere((f) => f['name'] == trendName);
print(trendIndex) // Output you will get is 1
Color trendColor = Color(int.parse(trendsList[trendIndex]['color'])); //same with this ['color'] not x.color
print(trendColor);
Check that out, and let me know if that helps you, I am sure it will :)
I'm looking for way to define a list of ssh keys in a variables file so that I could retrieve them in the tf module code for my compute instance like this :
metadata = {
ssh-keys = join("\n", [for user, key in var.ssh_keys : "${user}:${key}"])
}
Here is the content of the variables file I wrote to achieve that :
variable "ssh_keys" {
type = "map"
default = {
{
user = "amary"
key = "${file("/Users/nixmind/.ssh/amary.pub")}"
}
{
user = "nixmind"
key = "${file("/Users/nixmind/.ssh/nixmind.pub")}"
}
}
}
But I'm having this error :
Error: Missing attribute value
on variables.tf line 8, in variable "ssh_keys":
4:
5:
6:
7:
8:
9:
Expected an attribute value, introduced by an equals sign ("=").
I'm not sure to really get what to do there.
There are a few different problems here. I'll talk about them one at a time.
The first is that your default expression is not using correct map syntax. Here's a corrected version:
variable "ssh_keys" {
type = map(string)
default = {
amary = file("/Users/nixmind/.ssh/amary.pub")
nixmind = file("/Users/nixmind/.ssh/nixmind.pub")
}
}
The second problem is that a variable default value cannot include function calls, so the calls to file above are invalid. There are a few different options here about how to deal with this, but if this is a variable in a root module then I expect it would be most convenient to have the variable be a map of filenames rather than a map of the contents of those files, and then the module itself can read the contents of those files in a later step:
variable "ssh_key_files" {
type = map(string)
default = {
amary = "/Users/nixmind/.ssh/amary.pub"
nixmind = "/Users/nixmind/.ssh/nixmind.pub"
}
}
Your for expression for building the list of "user:key" strings was correct with how you had the variable defined before, but with the adjustment I've made above to use filenames instead of contents we'll need an extra step to actually read the files:
locals {
ssh_keys = { for u, fn in var.ssh_key_files : u => file(fn) }
}
We can then use local.ssh_keys to get the map from username to key needed for the metadata expression:
metadata = {
ssh-keys = join("\n", [for user, key in local.ssh_keys : "${user}:${key}"])
}
If you do want this module to accept already-loaded SSH key data rather than filenames then that is possible but the variable will need to be required rather than having a default, because it'll be up to the calling module to load the files.
The definition without the default value will look like this:
variable "ssh_keys" {
type = map(string)
}
Then your calling module, if there is one (that is, if this isn't a root module) can be the one to call file to load those in:
module "example" {
source = "./modules/example"
ssh_keys = {
amary = file("/Users/nixmind/.ssh/amary.pub")
nixmind = file("/Users/nixmind/.ssh/nixmind.pub")
}
}
The above is a reasonable interface for a shared module that will be called from another module like this, but it's not a convenient design for a root module because the "caller" in that case is the person or script running the terraform program, and so providing the data from those files would require reading them outside of Terraform and passing in the results.
My problem was semantic not syntactic, cause I can't use a map of map as tried to. Instead I used a liste and the error desapeared.
variable ssh_keys {
type = list(object({
user=string
key=string
}))
default = [
{
"user" = "amaret93"
"key" = "/Users/valerietchala/.ssh/amaret93.pub"
},
{
"user" = "nixmind"
"key" = "/Users/valerietchala/.ssh/nixmind.pub"
}
]
}
But the Martin's answer above is also a more good approach
I'm trying to pass context into a dynamic expression that I evaluate every iteration of a for loop. I understand that the load string only evaluates within a global context meaning local variables are inaccessible. In my case I work around this limitation by converting a local into a global for the purpose of the string evaluation. Here's what I have:
require 'cosmo'
model = { { player = "Cliff", age = 35, gender = "male" }, { player = "Ally", age = 36, gender = "female" }, { player = "Jasmine", age = 13, gender = "female" }, { player = "Lauren", age = 6.5, gender = "female" } }
values = { eval = function(args)
output = ''
condition = assert(loadstring('return ' .. args.condition))
for _, it in ipairs(model) do
each = it
if condition() then
output = output .. each.player .. ' age: ' .. each.age .. ' ' .. '\n'
end
end
return output
end }
template = "$eval{ condition = 'each.age < 30' }"
result = cosmo.fill(template, values)
print (result)
My ultimate goal (other than mastering Lua) is to build out an XSLT like tempting engine where I could do something like:
apply_templates{ match = each.age > 30}[[<parent-player>$each.player</parent-player>]]
apply_templates{ match = each.age > 30}[[<child-player>$each.player</child-player>]]
...And generate different outputs. Currently I'm stuck on my above hawkish means of sharing a local context thru a global. Does anyone here have better insight on how I'd go about doing what I'm attempting to do?
It's worth noting that setfenv was removed from Lua 5.2 and loadstring is deprecated. 5.2 is pretty new so you won't have to worry about it for a while, but it is possible to write a load routine that works for both versions:
local function load_code(code, environment)
if setfenv and loadstring then
local f = assert(loadstring(code))
setfenv(f,environment)
return f
else
return assert(load(code, nil,"t",environment))
end
end
local context = {}
context.string = string
context.table = table
-- etc. add libraries/functions that are safe for your application.
-- see: http://lua-users.org/wiki/SandBoxes
local condition = load_code("return " .. args.condition, context)
Version 5.2's load handles both the old loadstring behavior and sets the environment (context, in your example). Version 5.2 also changes the concept of environments, so loadstring may be the least of your worries. Still, it's something to consider to possibly save yourself some work down the road.
You can change the context of a function with setfenv(). This allows you to basically sandbox the loaded function into its own private environment. Something like the following should work:
local context = {}
local condition = assert(loadstring('return ' .. args.condition))
setfenv(condition, context)
for _, it in ipairs(model) do
context['each'] = it
if condition() then
-- ...
This will also prevent the condition value from being able to access any data you don't want it to, or more crucially, modifying any data you don't want it to. Note, however, that you'll need to expose any top-level bindings into the context table that you want condition to be able to access (e.g. if you want it to have access to the math package then you'll need to stick that into context). Alternatively, if you don't have any problem with condition having global access and you simply want to deal with not making your local a global, you can use a metatable on context to have it pass unknown indexes through to _G:
setmetatable(context, { __index = _G })