-
Notifications
You must be signed in to change notification settings - Fork 9.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Load Cloudstack configuration from file #13926
Conversation
@svanharmelen could you please review this? |
@mcanevet I must admit that I'm not in favour of adding this customization. The configuration file you refer to is an Exoscale specific config file that is not part of Apache CloudStack. There are more cloud providers out there that add their own magic sauce or add tools to standard Apache CloudStack, but of course this single provider cannot serve all those use different flavours. So my goal is to keep this provider as close the Apache CloudStack as possible. As an example you can think about CloudMonkey. CloudMonkey also uses an ini style config file, but with a different layout (different fields). So now we already have 2 different formats. And I'm very sure there are more, but they are all customized. So why should we take one or the other? If anything I would argue that the CloudMonkey format is probably the most official one as that is hosted as the Apache CloudStack CloudMonkey project. So I would very much stay away from adding something like this, sorry. Luckily there are already a couple of ways you can config the provider within TF itself (directly in the file, using cmdline vars, using a tfvars file or use env variables) so I hope one of those options can make it into your workflow instead. |
@svanharmelen sounds legit. However, I'm not a fan of any other way of configuring the provider:
For the moment we are fetching credentials from pass-password datasource (#11381) using that kind of declaration (we could also use vault provider the same way): data "pass_password" "cloudstack" {
path = "foo/bar"
}
provider "cloudstack" {
api_url = "${data.pass_password.cloudstack.data["api_url"]}"
api_key = "${data.pass_password.cloudstack.data["api_key"]}"
secret_key = "${data.pass_password.cloudstack.data["secret_key"]}"
} Using that, we don't have to commit any credentials in the manifests and there is no risk of using wrong API keys because we hardcode the location of it in a shared password store. BUT, all user of the team use the same API keys belonging to a specific user, not its personal API keys. This is really annoying, especially when a team member leaves and closes its Cloudstack account ; or when we want to look into Cloudstack logs to see who did what ; we definitely want to use one's personal API keys... So the idea behind this PR is to be able to hardcode the location of a file in every team member's personal workstation containing personal API keys to access Cloudstack using this declaration: provider "cloudstack" {
config = "${pathexpand("~/.cloudstack.ini")}"
region = "my_region"
} Like we do for example for the AWS provider: provider "aws" {
profile = "foo"
region = "eu-west-1"
} I totally agree the Exoscale's Cloustack API client CLI is not the most used, but I guess it works for every Cloudstack deployment and to be honest I knew that it cloud be criticized because of that. Would you be agree with a PR implementing the same thing but using CloudMonkey file format? |
c2ad232
to
00f78e9
Compare
@svanharmelen I'm now using CloudMonkey's config file. Is it OK for you now? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mcanevet I can life with adding this feature (while still not a fan of it), but I do have some comments we need to address first...
One thing that is not in the comments but should also be added is better restrictions on the schema fields (the params). Mainly the ConflictsWith
option should be added to make sure either the config
param if used or the api URL/key/secret and never both...
Also since now all fields are optional
the validation of the provider will not work anymore as no values at all would now also be valid.
So there needs to be a proper validation that will return any errors during runtime. Not nice, but for that one I don't see another option...
"github.com/hashicorp/terraform/helper/schema" | ||
"github.com/hashicorp/terraform/terraform" | ||
"os/user" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use goimports
to order the imports
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
) | ||
|
||
// Provider returns a terraform.ResourceProvider. | ||
func Provider() terraform.ResourceProvider { | ||
usr, _ := user.Current() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling this without checking the error is asking for problems...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
"config": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Default: fmt.Sprintf("%s/.cloudmonkey/config", usr.HomeDir), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here is your problem 😉 If the call to user.Current()
failed, usr
would be nil
and calling usr.HomeDir
would cause a panic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
Required: true, | ||
DefaultFunc: schema.EnvDefaultFunc("CLOUDSTACK_API_URL", nil), | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("CLOUDSTACK_API_URL", ""), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would you change this from nil
to ""
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought the validation would fail if neither the environment variable nor the parameter was set with nil
, but I tested and looks to be ok.
Required: true, | ||
DefaultFunc: schema.EnvDefaultFunc("CLOUDSTACK_SECRET_KEY", nil), | ||
Optional: true, | ||
DefaultFunc: schema.EnvDefaultFunc("CLOUDSTACK_SECRET_KEY", ""), | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please put the new options below the secret_key
, as the existing api_url
, api_key
and secret_key
would be the main way to use this provider, and alternatively you can point to a config file. So let's order the parameters in that same way...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
section, err := config.GetSection(d.Get("profile").(string)) | ||
if err != nil { | ||
return config, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please return a nil
instead of the config
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
} | ||
|
||
if apiURL == "" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you only check the URL? We need to check the key and secret as well right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
if secretKey == "" { | ||
secretKey = section.Key("secretkey").String() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you only assign the config values if the variables are empty? Either all the values come from the config file, or they all come from the individual api_url
, api_key
and secret_key
parameters. To prevent unexpected behaviour they should never be mixed...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to mimic the aws
provider behavior where you can override the parameters in the ~/.aws/credentials
on provider instantiation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As explained in the comment above, I don't think we should add this option here. Let's make this as clear as possible and either use one or the other...
apiKey := d.Get("api_key").(string) | ||
secretKey := d.Get("secret_key").(string) | ||
|
||
if configFile := d.Get("config").(string); configFile != "" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's cleaner to check for this like so:
if configFile, ok := d.GetOk("config"); ok {
config, err := ini.Load(configFile.(string))
<snip>
This would give an error if you did set the config
param, but assigned it an empty string (the ini.Load()
func would return an error in that case). I think that would be more clear for users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
"profile": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Default: "", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the default is an empty string, then please don't set a default...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
00f78e9
to
d893b14
Compare
@svanharmelen I fixed most of your comment. I'm not sure we should mark parameters as conflicts as we should be able to override the config file we either the environment variables or hardcoded parameters, just like the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my additional comments...
Additionally I still miss the more strict ConflictsWith
options, which I believe add value.
And a validation that checks if either both a config
and profile
are given, of the api url/key/secret trio.
Since we don't have the required
options set anymore, we need to validate the given info ourselves.
usr, err := user.Current() | ||
if err != nil { | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still dangerous as the calling side of this function does not expect nil
, but assumes a *schema.Provider
. So when you do this and the calling side calls anything on the returned value, it will again panic.
"config": &schema.Schema{ | ||
Type: schema.TypeString, | ||
Optional: true, | ||
Default: fmt.Sprintf("%s/.cloudmonkey/config", usr.HomeDir), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I more or less understand your take on this one, but I don't think we should mimic the AWS provider. This is a new feature that is not in any way something CloudStack users expect. I'm OK with adding it, but I would like to make sure the use of it is as clear as possible.
So either you use the file or the inline fields. But not a combination of both. I don't see the use case that would benefit from being able to do that. I only see the possible confusion this may cause users.
So please remove this default which will also solve the error handling thingy when calling user.Current()
as that can then be deleted as well...
|
||
if secretKey == "" { | ||
secretKey = section.Key("secretkey").String() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As explained in the comment above, I don't think we should add this option here. Let's make this as clear as possible and either use one or the other...
apiKey := d.Get("api_key").(string) | ||
secretKey := d.Get("secret_key").(string) | ||
|
||
if profile, ok := d.GetOk("profile"); ok { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would revert this to checking the config
field as that makes more sense (for people reading the code)
3fe02a4
to
fcfe465
Compare
@svanharmelen I added |
@svanharmelen do you have any idea for |
I've been pretty busy today with some other PR's. I'll have a look at this one again coming Monday! From the top of my head I have no idea why this wouldn't work. So have to take a closer look... |
@svanharmelen did you find time to check this PR? |
@mcanevet I just downloaded and compiled your branch and it works as expected: |
@svanharmelen cool, I must have do something wrong during my tests. |
I'll merge it and then add a small validation func to improve the UX just bit. Thanks! |
I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. |
This PR adds 2 new parameters to the CloudStack provider allowing to fetch credentials from a configuration file compatible with https://github.com/exoscale/cs CloudStack CLI.
With this patch, one can use this manifest to configure the CloudStack provider: