-
Notifications
You must be signed in to change notification settings - Fork 1
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
Expose provider config #164
Changes from all commits
d32b3be
b2a9cc3
e6eaeda
17ed559
c8f8b19
4d7cac8
d68e485
68c7136
9bf690a
f96880c
b445d1d
01fdbe9
6cdcd6c
449e1f3
d914003
73ec203
c3a3b1d
ffeaf56
146ae16
b2b7884
40c76ba
5f1aaf5
84740b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,6 +64,8 @@ func NewModuleComponentResource( | |
moduleInputs resource.PropertyMap, | ||
inferredModule *InferredModuleSchema, | ||
packageRef string, | ||
providerSelfURN pulumi.URN, | ||
providersConfig map[string]resource.PropertyMap, | ||
opts ...pulumi.ResourceOption, | ||
) (componentUrn *urn.URN, outputs pulumi.Input, finalError error) { | ||
component := ModuleComponentResource{} | ||
|
@@ -84,15 +86,28 @@ func NewModuleComponentResource( | |
planStore.Forget(urn) | ||
}() | ||
|
||
var providerSelfRef pulumi.ProviderResource | ||
if providerSelfURN != "" { | ||
providerSelfRef = newProviderSelfReference(ctx, providerSelfURN) | ||
} | ||
|
||
go func() { | ||
resourceOptions := []pulumi.ResourceOption{ | ||
pulumi.Parent(&component), | ||
} | ||
|
||
if providerSelfRef != nil { | ||
resourceOptions = append(resourceOptions, pulumi.Provider(providerSelfRef)) | ||
} | ||
|
||
_, err := newModuleStateResource(ctx, | ||
// Needs to be prefixed by parent to avoid "duplicate URN". | ||
fmt.Sprintf("%s-state", name), | ||
pkgName, | ||
urn, | ||
packageRef, | ||
moduleInputs, | ||
pulumi.Parent(&component), | ||
resourceOptions..., | ||
) | ||
|
||
contract.AssertNoErrorf(err, "newModuleStateResource failed") | ||
|
@@ -127,7 +142,10 @@ func NewModuleComponentResource( | |
Name: outputName, | ||
}) | ||
} | ||
err = tfsandbox.CreateTFFile(tfName, tfModuleSource, tfModuleVersion, tf.WorkingDir(), moduleInputs, outputSpecs) | ||
err = tfsandbox.CreateTFFile(tfName, tfModuleSource, | ||
tfModuleVersion, tf.WorkingDir(), | ||
moduleInputs, outputSpecs, providersConfig) | ||
|
||
if err != nil { | ||
return nil, nil, fmt.Errorf("Seed file generation failed: %w", err) | ||
} | ||
|
@@ -169,10 +187,19 @@ func NewModuleComponentResource( | |
return | ||
} | ||
|
||
resourceOptions := []pulumi.ResourceOption{ | ||
pulumi.Parent(&component), | ||
} | ||
|
||
if providerSelfRef != nil { | ||
resourceOptions = append(resourceOptions, pulumi.Provider(providerSelfRef)) | ||
} | ||
|
||
cr, err := newChildResource(ctx, urn, pkgName, | ||
rp, | ||
packageRef, | ||
pulumi.Parent(&component)) | ||
resourceOptions..., | ||
) | ||
|
||
errs = append(errs, err) | ||
if err == nil { | ||
|
@@ -211,10 +238,19 @@ func NewModuleComponentResource( | |
// so that we propagate outputs from module | ||
return | ||
} | ||
|
||
resourceOptions := []pulumi.ResourceOption{ | ||
pulumi.Parent(&component), | ||
} | ||
|
||
if providerSelfRef != nil { | ||
resourceOptions = append(resourceOptions, pulumi.Provider(providerSelfRef)) | ||
} | ||
|
||
cr, err := newChildResource(ctx, urn, pkgName, | ||
rp, | ||
packageRef, | ||
pulumi.Parent(&component)) | ||
resourceOptions...) | ||
|
||
errs = append(errs, err) | ||
if err == nil { | ||
|
@@ -242,3 +278,16 @@ func NewModuleComponentResource( | |
|
||
return &urn, marshalledOutputs, nil | ||
} | ||
|
||
func newProviderSelfReference(ctx *pulumi.Context, urn1 pulumi.URN) pulumi.ProviderResource { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Frassle if I could have your 5 min to eyeball this. We feed the result of this function to We could drop to gRPC level of calling RegisterResource to workaround this but that negates benefits of using the SDK. While this workaround seems to work, you were mentioning that self-identifying a provider instance requires both a self-URN and self-ID, and self-URN by itself is not specific enough. Ideally something like this maybe would be public? I found no way to call it though due to internal constraints. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It tried extending the struct and overriding ID() and URN() outputs. That worked EXCEPT I cannot seem to ever be able to set this highly private There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we could use what we use here, the code as is, and then
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
var prov pulumi.ProviderResourceState | ||
err := ctx.RegisterResource( | ||
string(urn.URN(urn1).Type()), | ||
urn.URN(urn1).Name(), | ||
pulumi.Map{}, | ||
&prov, | ||
pulumi.URN_(string(urn1)), | ||
) | ||
contract.AssertNoErrorf(err, "RegisterResource failed to hydrate a self-reference") | ||
return &prov | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,6 +41,9 @@ func pulumiSchemaForModule(pargs *ParameterizeArgs, inferredModule *InferredModu | |
Name: string(packageName), | ||
Version: string(pkgVer), | ||
Types: inferredModule.SupportingTypes, | ||
Provider: schema.ResourceSpec{ | ||
InputProperties: inferredModule.ProvidersConfig.Variables, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call, we forgot this before. |
||
}, | ||
Resources: map[string]schema.ResourceSpec{ | ||
mainResourceToken: { | ||
IsComponent: true, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ import ( | |
emptypb "google.golang.org/protobuf/types/known/emptypb" | ||
|
||
"github.com/pulumi/pulumi/pkg/v3/resource/provider" | ||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource" | ||
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" | ||
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens" | ||
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" | ||
|
@@ -61,6 +62,8 @@ type server struct { | |
packageVersion packageVersion | ||
componentTypeName componentTypeName | ||
inferredModuleSchema *InferredModuleSchema | ||
providerConfig resource.PropertyMap | ||
providerSelfURN pulumi.URN | ||
} | ||
|
||
func (s *server) Parameterize( | ||
|
@@ -203,10 +206,23 @@ func (*server) GetPluginInfo( | |
}, nil | ||
} | ||
|
||
func (*server) Configure( | ||
func (s *server) Configure( | ||
_ context.Context, | ||
_ *pulumirpc.ConfigureRequest, | ||
req *pulumirpc.ConfigureRequest, | ||
) (*pulumirpc.ConfigureResponse, error) { | ||
config, err := plugin.UnmarshalProperties(req.Args, plugin.MarshalOptions{ | ||
KeepUnknowns: true, | ||
RejectAssets: true, | ||
KeepSecrets: true, | ||
KeepOutputValues: false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What this does is rewires first-class outputs to Secret or Computed cases, but it also drops dependencies inherent in first-class outputs. Whether that messes things up for Pulumi or not, I am not sure. We could test this out later on. I'll add a ticket. |
||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("configure failed to parse inputs: %w", err) | ||
} | ||
|
||
s.providerConfig = config | ||
|
||
return &pulumirpc.ConfigureResponse{ | ||
AcceptSecrets: true, | ||
SupportsPreview: true, | ||
|
@@ -264,6 +280,61 @@ func (s *server) acquirePackageReference( | |
return response.Ref, nil | ||
} | ||
|
||
// cleanProvidersConfig takes config that was produced from provider inputs in the program: | ||
// | ||
// const provider = new vpc.Provider("my-provider", { | ||
// aws: { | ||
// "region": "us-west-2" | ||
// } | ||
// }) | ||
// | ||
// the input config here would look like sometimes where the provider config is a JSON string: | ||
// | ||
// { | ||
// propertyKey("version"): stringProperty("0.1.0"), | ||
// propertyKey("aws"): stringProperty("{\"region\": \"us-west-2\"}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain step by step, what's going on here? Are we affected by the JSON encoding again :/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes. Config values arrive to the provider JSON-stringified so here we change them back to an object format, if necessary. When they are already an object, we use that shape. If the value is a string, then it must be a stringified JSON object since the schema for each field specifies a free-form object so it is same to assume. |
||
// } | ||
// | ||
// notice how the value is a string that is a JSON stringified object due to legacy provider SDK behavior | ||
// see https://github.com/pulumi/home/issues/3705 for reference | ||
// we need to convert this to a map[string]resource.PropertyMap so that it can be used | ||
// in the Terraform JSON file | ||
func cleanProvidersConfig(config resource.PropertyMap) map[string]resource.PropertyMap { | ||
providersConfig := make(map[string]resource.PropertyMap) | ||
for propertyKey, serializedConfig := range config { | ||
if string(propertyKey) == "version" || string(propertyKey) == "pluginDownloadURL" { | ||
// skip the version and pluginDownloadURL properties | ||
continue | ||
} | ||
|
||
if serializedConfig.IsString() { | ||
value := serializedConfig.StringValue() | ||
deserialized := map[string]interface{}{} | ||
if err := json.Unmarshal([]byte(value), &deserialized); err != nil { | ||
contract.Failf("failed to deserialize provider config into a map: %v", err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this thinks if it's a string it's always JSON-encoded string. That's probably OK since the type of these values is |
||
} | ||
|
||
if len(deserialized) > 0 { | ||
providersConfig[string(propertyKey)] = resource.NewPropertyMapFromMap(deserialized) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing continue here? I don't think it should fall through to the next case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the |
||
continue | ||
} | ||
|
||
if serializedConfig.IsObject() { | ||
// we might later get the behaviour where all programs no longer send serialized JSON | ||
// but send the actual object instead | ||
// right now only YAML and Go programs send the actual object | ||
// see https://github.com/pulumi/home/issues/3705 for reference | ||
providersConfig[string(propertyKey)] = serializedConfig.ObjectValue() | ||
continue | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's assert here if we fall through and we failed to parse it, we fail loud that we don't understand the encoding. This would be helpful weeding out some edge cases. I think secrets are encoded funny in JSON config encoding and we will need to tweak this a bit to fix that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done ✅ |
||
|
||
contract.Failf("cleanProvidersConfig failed to parse unsupported type: %v", serializedConfig) | ||
} | ||
|
||
return providersConfig | ||
} | ||
|
||
func (s *server) Construct( | ||
ctx context.Context, | ||
req *pulumirpc.ConstructRequest, | ||
|
@@ -275,6 +346,8 @@ func (s *server) Construct( | |
// TODO[https://github.com/pulumi/pulumi-terraform-module/issues/151] support Outputs in Unmarshal | ||
KeepOutputValues: false, | ||
}) | ||
|
||
providersConfig := cleanProvidersConfig(s.providerConfig) | ||
if err != nil { | ||
return nil, fmt.Errorf("Construct failed to parse inputs: %s", err) | ||
} | ||
|
@@ -302,7 +375,9 @@ func (s *server) Construct( | |
name, | ||
inputProps, | ||
s.inferredModuleSchema, | ||
packageRef) | ||
packageRef, | ||
s.providerSelfURN, | ||
providersConfig) | ||
Zaid-Ajaj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if err != nil { | ||
return nil, fmt.Errorf("NewModuleComponentResource failed: %w", err) | ||
|
@@ -332,6 +407,33 @@ func (s *server) Check( | |
} | ||
} | ||
|
||
func (s *server) CheckConfig( | ||
_ context.Context, | ||
req *pulumirpc.CheckRequest, | ||
) (*pulumirpc.CheckResponse, error) { | ||
s.providerSelfURN = pulumi.URN(req.Urn) | ||
|
||
config, err := plugin.UnmarshalProperties(req.GetNews(), plugin.MarshalOptions{ | ||
KeepUnknowns: true, | ||
RejectAssets: true, | ||
KeepSecrets: true, | ||
KeepOutputValues: false, | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("CheckConfig failed to parse inputs: %w", err) | ||
} | ||
|
||
// keep provider config in memory for use later. | ||
// we keep one instance of provider configuration because each configuration is used | ||
// once per provider process. | ||
s.providerConfig = config | ||
|
||
return &pulumirpc.CheckResponse{ | ||
Inputs: req.News, | ||
}, nil | ||
} | ||
|
||
func (s *server) Diff( | ||
ctx context.Context, | ||
req *pulumirpc.DiffRequest, | ||
|
@@ -380,7 +482,8 @@ func (s *server) Delete( | |
) (*emptypb.Empty, error) { | ||
switch { | ||
case req.GetType() == string(moduleStateTypeToken(s.packageName)): | ||
return s.moduleStateHandler.Delete(ctx, req, s.params.TFModuleSource, s.params.TFModuleVersion) | ||
providersConfig := cleanProvidersConfig(s.providerConfig) | ||
return s.moduleStateHandler.Delete(ctx, req, s.params.TFModuleSource, s.params.TFModuleVersion, providersConfig) | ||
case isChildResourceType(req.GetType()): | ||
return s.childHandler.Delete(ctx, req) | ||
default: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -242,6 +242,7 @@ func (h *moduleStateHandler) Delete( | |
req *pulumirpc.DeleteRequest, | ||
moduleSource TFModuleSource, | ||
moduleVersion TFModuleVersion, | ||
providersConfig map[string]resource.PropertyMap, | ||
) (*emptypb.Empty, error) { | ||
oldState := moduleState{} | ||
oldState.Unmarshal(req.GetProperties()) | ||
|
@@ -273,6 +274,7 @@ func (h *moduleStateHandler) Delete( | |
tf.WorkingDir(), | ||
olds["moduleInputs"].ObjectValue(), /*inputs*/ | ||
[]tfsandbox.TFOutputSpec{}, /*outputs*/ | ||
providersConfig, | ||
) | ||
|
||
if err != nil { | ||
|
@@ -330,8 +332,9 @@ func (h *moduleStateHandler) Read( | |
// when refreshing, we do not require outputs to be exposed | ||
err = tfsandbox.CreateTFFile(tfName, moduleSource, moduleVersion, | ||
tf.WorkingDir(), | ||
inputs, /*inputs*/ | ||
[]tfsandbox.TFOutputSpec{}, /*outputs*/ | ||
inputs, /*inputs*/ | ||
[]tfsandbox.TFOutputSpec{}, /*outputs*/ | ||
map[string]resource.PropertyMap{}, /*providersConfig*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a TODO here right? When read executes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
) | ||
if err != nil { | ||
return nil, fmt.Errorf("Seed file generation failed: %w", err) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ type InferredModuleSchema struct { | |
Outputs map[string]schema.PropertySpec | ||
SupportingTypes map[string]schema.ComplexTypeSpec | ||
RequiredInputs []string | ||
ProvidersConfig schema.ConfigSpec | ||
} | ||
|
||
var stringType = schema.TypeSpec{Type: "string"} | ||
|
@@ -268,6 +269,18 @@ func InferModuleSchema( | |
Outputs: make(map[string]schema.PropertySpec), | ||
RequiredInputs: []string{}, | ||
SupportingTypes: map[string]schema.ComplexTypeSpec{}, | ||
ProvidersConfig: schema.ConfigSpec{ | ||
Variables: map[string]schema.PropertySpec{}, | ||
}, | ||
} | ||
|
||
if module.ProviderRequirements != nil { | ||
for providerName := range module.ProviderRequirements.RequiredProviders { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks reasonable! |
||
inferredModuleSchema.ProvidersConfig.Variables[providerName] = schema.PropertySpec{ | ||
Description: "provider configuration for " + providerName, | ||
TypeSpec: mapType(anyType), | ||
} | ||
} | ||
} | ||
|
||
for variableName, variable := range module.Variables { | ||
|
@@ -388,7 +401,8 @@ func resolveModuleSources( | |
|
||
inputs := resource.PropertyMap{} | ||
outputs := []tfsandbox.TFOutputSpec{} | ||
err = tfsandbox.CreateTFFile(key, source, version, tf.WorkingDir(), inputs, outputs) | ||
providerConfig := map[string]resource.PropertyMap{} | ||
err = tfsandbox.CreateTFFile(key, source, version, tf.WorkingDir(), inputs, outputs, providerConfig) | ||
if err != nil { | ||
return "", fmt.Errorf("tofu file creation failed: %w", 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.
Remind me why it's ever empty. Is it only empty in tests? If so can we add a comment to that purpose?
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.
It is empty if only
CheckConfig
is called until we can access the URN fromConfigure
. Right now, URN fromConfigure
is not strictly needed and the check here is a workaround forTestSavingModuleState
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 yeah TestSavingModuleState we might want to remove that, together with an incomplete engine, in favor of just trusting full blown integration tests.