Skip to content

Commit

Permalink
Fixes and more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
t0yv0 committed Sep 1, 2023
1 parent c972223 commit 953fed7
Show file tree
Hide file tree
Showing 4 changed files with 367 additions and 126 deletions.
121 changes: 121 additions & 0 deletions provider/pvutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2016-2023, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package provider

import (
"github.com/pulumi/pulumi-terraform-bridge/v3/unstable/propertyvalue"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
)

// Assits building transformations on PropertyValue that do not want to deal with Secret, Computed,
// or Output values.
//
// The usage pattern looks like this:
//
// composePropertyValue[T](func (c *pvComposer) (resource.PropertyValue, T, error) {
// x, _ := c.Simplify(pv1)
// y, _ := c.SimplifyPropertyMap(pv2) // etc
// return resource.NewArrayProperty([]resource.PropertyValue{x, y}), result, nil
// })
//
// User code accessing values that passed through Simplify is guaranteed to never observe Secret,
// Computed, or Output values. All the metadata bits about these is floated to top-level and
// re-applied to the value the user code receives out of composePropertyValue.
func composePropertyValue[T any](
f func(c *pvComposer) (resource.PropertyValue, T, error),
) (resource.PropertyValue, T, error) {
c := &pvComposer{}
v, r, err := f(c)
fv, err := c.finalize(v, err)
return fv, r, err
}

type pvComposer struct {
secret bool
deps []resource.URN
}

func (c *pvComposer) Simplify(
pv resource.PropertyValue,
) (resource.PropertyValue, error) {
return propertyvalue.TransformErr(c.simplifyOne, pv)
}

func (c *pvComposer) SimplifyPropertyMap(
pm resource.PropertyMap,
) (resource.PropertyMap, error) {
res := resource.PropertyMap{}
for k, v := range pm {
sv, err := c.Simplify(v)
if err != nil {
return nil, err
}
res[k] = sv
}
return res, nil
}

func (c *pvComposer) simplifyOne(
pv resource.PropertyValue,
) (resource.PropertyValue, error) {
for {
switch {
case pv.IsSecret():
pv = pv.SecretValue().Element
c.secret = true
case pv.IsComputed():
return resource.PropertyValue{}, &foundUnknownError{}
case pv.IsOutput():
if !pv.OutputValue().Known {
return resource.PropertyValue{}, &foundUnknownError{}
}
ov := pv.OutputValue()
c.secret = c.secret || ov.Secret
c.deps = append(c.deps, ov.Dependencies...)
pv = ov.Element
default:
return pv, nil
}
}
}

func (c *pvComposer) finalize(
pv resource.PropertyValue,
err error,
) (resource.PropertyValue, error) {
if _, unk := err.(*foundUnknownError); unk {
return resource.NewOutputProperty(resource.Output{
Known: false,
}), nil
}
if err != nil {
return pv, err
}
if c.deps != nil || c.secret {
return resource.NewOutputProperty(resource.Output{
Element: pv,
Known: true,
Secret: c.secret,
Dependencies: c.deps,
}), nil
}
return pv, nil
}

type foundUnknownError struct{}

func (m *foundUnknownError) Error() string {
return "foundUnknownError"
}
93 changes: 0 additions & 93 deletions provider/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -7152,96 +7152,3 @@ func Provider() *tfbridge.ProviderInfo {

return &prov
}

// Apply provider tags to an individual resource.
//
// Historically, Pulumi has struggles to handle the "tags" and "tags_all" fields correctly:
// - https://github.com/pulumi/pulumi-aws/issues/2633
// - https://github.com/pulumi/pulumi-aws/issues/1655
//
// terraform-provider-aws has also struggled with implementing their desired behavior:
// - https://github.com/hashicorp/terraform-provider-aws/issues/29747
// - https://github.com/hashicorp/terraform-provider-aws/issues/29842
// - https://github.com/hashicorp/terraform-provider-aws/issues/24449
//
// The Terraform lifecycle simply does not have a good way to map provider configuration
// onto resource values, so terraform-provider-aws is forced to work around limitations in
// unreliable ways. For example, terraform-provider-aws does not apply tags correctly with
// -refresh=false.
//
// This gives pulumi the same limitations by default. However, unlike Terraform, Pulumi
// does have a clear way to insert provider configuration into resource properties:
// Check. By writing a custom check function that applies "default_tags" to "tags" before
// the Terraform provider sees any resource configuration, we can give a consistent,
// reliable and good experience for Pulumi users.
func applyTags(
ctx context.Context, config resource.PropertyMap, meta resource.PropertyMap,
) (resource.PropertyMap, error) {
var defaultTags awsShim.TagConfig

unknown := func() (resource.PropertyMap, error) {
current := config["tags"]
if current.IsOutput() {
output := current.OutputValue()
output.Known = false
config["tags"] = resource.NewOutputProperty(output)
} else {
config["tags"] = resource.MakeOutput(current)
}
return config, nil
}

// awsShim.NewTagConfig accepts (context.Context, i interface{}) where i can be
// one of map[string]interface{} among other types. .Mappable() produces a
// map[string]interface{} where every value is of type string. This is well
// handled by awsShim.NewTagConfig.
//
// config values are guaranteed to be of the correct type because they have
// already been seen and approved of by the provider, which verifies its
// configuration is well typed.

if defaults, ok := meta["defaultTags"]; ok {
if defaults.ContainsUnknowns() {
return unknown()
}
if defaults.IsObject() {
defaults := defaults.ObjectValue()
tags, ok := defaults["tags"]
if ok {
defaultTags = awsShim.NewTagConfig(ctx, tags.Mappable())
}
}
}

ignoredTags := &awsShim.TagIgnoreConfig{}
if ignores, ok := meta["ignoreTags"]; ok {
if ignores.ContainsUnknowns() {
return unknown()
}
if keys, ok := ignores.ObjectValue()["keys"]; ok {
ignoredTags.Keys = awsShim.NewTagConfig(ctx, keys.Mappable()).Tags
}
if keys, ok := ignores.ObjectValue()["keyPrefixes"]; ok {
ignoredTags.KeyPrefixes = awsShim.NewTagConfig(ctx, keys.Mappable()).Tags
}
}

var resourceTags awsShim.TagConfig
if tags, ok := config["tags"]; ok {
resourceTags = awsShim.NewTagConfig(ctx, tags.Mappable().(map[string]interface{}))
}

allTags := defaultTags.MergeTags(resourceTags.Tags).IgnoreConfig(ignoredTags)

if len(allTags) > 0 {
allTagProperties := make(resource.PropertyMap, len(allTags))
for k, v := range allTags {
allTagProperties[resource.PropertyKey(k)] = resource.NewStringProperty(v.ValueString())
}
config["tags"] = resource.NewObjectProperty(allTagProperties)
} else {
delete(config, "tags")
}

return config, nil
}
134 changes: 134 additions & 0 deletions provider/tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2016-2023, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package provider

import (
"context"

awsShim "github.com/hashicorp/terraform-provider-aws/shim"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
)

// Apply provider tags to an individual resource.
//
// Historically, Pulumi has struggles to handle the "tags" and "tags_all" fields correctly:
// - https://github.com/pulumi/pulumi-aws/issues/2633
// - https://github.com/pulumi/pulumi-aws/issues/1655
//
// terraform-provider-aws has also struggled with implementing their desired behavior:
// - https://github.com/hashicorp/terraform-provider-aws/issues/29747
// - https://github.com/hashicorp/terraform-provider-aws/issues/29842
// - https://github.com/hashicorp/terraform-provider-aws/issues/24449
//
// The Terraform lifecycle simply does not have a good way to map provider configuration
// onto resource values, so terraform-provider-aws is forced to work around limitations in
// unreliable ways. For example, terraform-provider-aws does not apply tags correctly with
// -refresh=false.
//
// This gives pulumi the same limitations by default. However, unlike Terraform, Pulumi
// does have a clear way to insert provider configuration into resource properties:
// Check. By writing a custom check function that applies "default_tags" to "tags" before
// the Terraform provider sees any resource configuration, we can give a consistent,
// reliable and good experience for Pulumi users.
func applyTags(
ctx context.Context, config resource.PropertyMap, meta resource.PropertyMap,
) (resource.PropertyMap, error) {
ret := config.Copy()

configTags := resource.NewObjectProperty(resource.PropertyMap{})
if t, ok := config["tags"]; ok {
configTags = t
}
allTags, hasTags, err := mergeTags(ctx, configTags, meta)
if err != nil {
return nil, err
}
if !hasTags {
delete(ret, "tags")
return ret, nil
}
ret["tags"] = allTags
return ret, nil
}

// Wrap mergeTagsSimple with taking care of unknowns, secrets and outputs.
func mergeTags(
ctx context.Context, tags resource.PropertyValue, meta resource.PropertyMap,
) (resource.PropertyValue, bool, error) {
return composePropertyValue(
func(c *pvComposer) (resource.PropertyValue, bool, error) {
stags, err := c.Simplify(tags)
if err != nil {
return resource.PropertyValue{}, false, err
}
smeta, err := c.SimplifyPropertyMap(meta)
if err != nil {
return resource.PropertyValue{}, false, err
}
return mergeTagsSimple(ctx, stags, smeta)
})
}

// At this level we do not need to track secret or unknown anymore.
func mergeTagsSimple(
ctx context.Context, tags resource.PropertyValue, meta resource.PropertyMap,
) (resource.PropertyValue, bool, error) {
var defaultTags awsShim.TagConfig

// awsShim.NewTagConfig accepts (context.Context, i interface{}) where i can be one of
// map[string]interface{} among other types. .Mappable() produces a map[string]interface{}
// where every value is of type string. This is well handled by awsShim.NewTagConfig.
//
// config values are guaranteed to be of the correct type because they have already been
// seen and approved of by the provider, which verifies its configuration is well typed.

if defaults, ok := meta["defaultTags"]; ok {
if defaults.IsObject() {
defaults := defaults.ObjectValue()
tags, ok := defaults["tags"]
if ok {
defaultTags = awsShim.NewTagConfig(ctx, tags.Mappable())
}
}
}

ignoredTags := &awsShim.TagIgnoreConfig{}
if ignores, ok := meta["ignoreTags"]; ok {
if keys, ok := ignores.ObjectValue()["keys"]; ok {
ignoredTags.Keys = awsShim.NewTagConfig(ctx, keys.Mappable()).Tags
}
if keys, ok := ignores.ObjectValue()["keyPrefixes"]; ok {
ignoredTags.KeyPrefixes = awsShim.NewTagConfig(ctx, keys.Mappable()).Tags
}
}

var resourceTags awsShim.TagConfig
if tags.IsObject() {
resourceTags = awsShim.NewTagConfig(ctx, tags.Mappable())
}

allTags := defaultTags.MergeTags(resourceTags.Tags).IgnoreConfig(ignoredTags)

if len(allTags) > 0 {
allTagProperties := make(resource.PropertyMap, len(allTags))
for k, v := range allTags {
pk := resource.PropertyKey(k)
allTagProperties[pk] = resource.NewStringProperty(v.ValueString())
}
return resource.NewObjectProperty(allTagProperties), true, nil
} else {
return resource.PropertyValue{}, false, nil
}
}
Loading

0 comments on commit 953fed7

Please sign in to comment.