diff --git a/examples/examples_go_test.go b/examples/examples_go_test.go index 2960e9609a0..b274957e692 100644 --- a/examples/examples_go_test.go +++ b/examples/examples_go_test.go @@ -237,7 +237,7 @@ func (st tagsState) validateStateResult(phase int) func( return func(t *testing.T, stack integration.RuntimeValidationStackInfo) { for k, v := range stack.Outputs { switch k { - case "bucket-name", "legacy-bucket-name", "appconfig-app-arn": + case "bucket-name", "legacy-bucket-name", "appconfig-app-arn", "appconfig-env-arn", "get-appconfig-env": continue } @@ -251,12 +251,18 @@ func (st tagsState) validateStateResult(phase int) func( t.Logf("key=%s tags are as expected: %v", k, actualTagsJSON) if k == "bucket" { + // TODO: uncomment when https://github.com/pulumi/pulumi-aws/issues/4258 is fixed + // getTags := stack.Outputs["get-bucket"].(string) + // assert.Equal(t, v.(string), getTags) bucketName := stack.Outputs["bucket-name"].(string) st.assertTagsEqualWithRetry(t, fetchBucketTags(bucketName), "bad bucket tags") } if k == "legacy-bucket" { + // TODO: uncomment when https://github.com/pulumi/pulumi-aws/issues/4258 is fixed + // getTags := stack.Outputs["get-legacy-bucket"].(string) + // assert.Equal(t, v.(string), getTags) bucketName := stack.Outputs["legacy-bucket-name"].(string) st.assertTagsEqualWithRetry(t, fetchBucketTags(bucketName), @@ -268,10 +274,28 @@ func (st tagsState) validateStateResult(phase int) func( fetchAppConfigTags(arn), "bad appconfig app tags") } + if k == "appconfig-env" { + getTags := stack.Outputs["get-appconfig-env"].(string) + isEqual(t, v.(string), getTags) + arn := stack.Outputs["appconfig-env-arn"].(string) + st.assertTagsEqualWithRetry(t, + fetchAppConfigTags(arn), + "bad appconfig app tags") + } } } } +func isEqual(t *testing.T, a, b string) { + if a == "null" { + a = "{}" + } + if b == "null" { + b = "{}" + } + assert.Equal(t, a, b) +} + func fetchBucketTags(awsBucket string) tagsFetcher { return func() (map[string]string, error) { sess := session.Must(session.NewSessionWithOptions(session.Options{ diff --git a/examples/tags-combinations-go/main.go b/examples/tags-combinations-go/main.go index 967892922e1..9d51556e52c 100644 --- a/examples/tags-combinations-go/main.go +++ b/examples/tags-combinations-go/main.go @@ -71,12 +71,41 @@ func main() { return err } + env, err := appconfig.NewEnvironment(ctx, "testappconfigenv"+testIdent, &appconfig.EnvironmentArgs{ + ApplicationId: app.ID(), + Tags: tagsMap, + }, pulumi.Provider(p)) + if err != nil { + return err + } + + getEnv, err := appconfig.GetEnvironment(ctx, "get-testappconfigenv"+testIdent, env.ID(), &appconfig.EnvironmentState{}, pulumi.Provider(p)) + if err != nil { + return err + } + + // TODO: uncomment when https://github.com/pulumi/pulumi-aws/issues/4258 is fixed + // refresh doesn't work for `forceDelete` & `acl` + // getBucket, err := s3.GetBucketV2(ctx, "get-bucketv2"+testIdent, bucket.ID(), &s3.BucketV2State{}, pulumi.Provider(p), pulumi.IgnoreChanges([]string{"forceDestroy", "acl"})) + // if err != nil { + // return err + // } + // getLegacyBucket, err := s3.GetBucket(ctx, "get-legacybucket"+testIdent, legacyBucket.ID(), &s3.BucketState{}, pulumi.Provider(p), pulumi.IgnoreChanges([]string{"forceDestroy", "acl"})) + // if err != nil { + // return err + // } + ctx.Export("bucket", exportTags(bucket.Tags)) + // ctx.Export("get-bucket", exportTags(getBucket.Tags)) ctx.Export("legacy-bucket", exportTags(legacyBucket.Tags)) + // ctx.Export("get-legacy-bucket", exportTags(getLegacyBucket.Tags)) ctx.Export("bucket-name", bucket.Bucket) ctx.Export("legacy-bucket-name", legacyBucket.Bucket) ctx.Export("appconfig-app", exportTags(app.Tags)) ctx.Export("appconfig-app-arn", app.Arn) + ctx.Export("appconfig-env", exportTags(env.Tags)) + ctx.Export("get-appconfig-env", exportTags(getEnv.Tags)) + ctx.Export("appconfig-env-arn", env.Arn) return nil }) diff --git a/examples/tags-combinations-go/step1/main.go b/examples/tags-combinations-go/step1/main.go index 7f4463a7335..19483c72909 100644 --- a/examples/tags-combinations-go/step1/main.go +++ b/examples/tags-combinations-go/step1/main.go @@ -71,12 +71,41 @@ func main() { return err } + env, err := appconfig.NewEnvironment(ctx, "testappconfigenv"+testIdent, &appconfig.EnvironmentArgs{ + ApplicationId: app.ID(), + Tags: tagsMap, + }, pulumi.Provider(p)) + if err != nil { + return err + } + + getEnv, err := appconfig.GetEnvironment(ctx, "get-testappconfigenv"+testIdent, env.ID(), &appconfig.EnvironmentState{}, pulumi.Provider(p)) + if err != nil { + return err + } + + // TODO: uncomment when https://github.com/pulumi/pulumi-aws/issues/4258 is fixed + // refresh doesn't work for `forceDelete` & `acl` + // getBucket, err := s3.GetBucketV2(ctx, "get-bucketv2"+testIdent, bucket.ID(), &s3.BucketV2State{}, pulumi.Provider(p)) + // if err != nil { + // return err + // } + // getLegacyBucket, err := s3.GetBucket(ctx, "get-legacybucket"+testIdent, legacyBucket.ID(), &s3.BucketState{}, pulumi.Provider(p)) + // if err != nil { + // return err + // } + ctx.Export("bucket", exportTags(bucket.Tags)) + // ctx.Export("get-bucket", exportTags(getBucket.Tags)) ctx.Export("legacy-bucket", exportTags(legacyBucket.Tags)) + // ctx.Export("get-legacy-bucket", exportTags(getLegacyBucket.Tags)) ctx.Export("bucket-name", bucket.Bucket) ctx.Export("legacy-bucket-name", legacyBucket.Bucket) ctx.Export("appconfig-app", exportTags(app.Tags)) ctx.Export("appconfig-app-arn", app.Arn) + ctx.Export("appconfig-env", exportTags(env.Tags)) + ctx.Export("get-appconfig-env", exportTags(getEnv.Tags)) + ctx.Export("appconfig-env-arn", env.Arn) return nil }) diff --git a/provider/go.mod b/provider/go.mod index ebcfcf48fee..a695b673e23 100644 --- a/provider/go.mod +++ b/provider/go.mod @@ -3,9 +3,12 @@ module github.com/pulumi/pulumi-aws/provider/v6 go 1.22.5 require ( + github.com/aws/aws-sdk-go-v2 v1.30.3 github.com/aws/aws-sdk-go-v2/config v1.27.26 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 + github.com/aws/aws-sdk-go-v2/service/appconfig v1.31.3 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.33.3 + github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.23.3 github.com/aws/aws-sdk-go-v2/service/s3 v1.58.2 github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.54 github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 @@ -65,7 +68,6 @@ require ( github.com/armon/go-radix v1.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go v1.54.18 // indirect - github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.26 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.7 // indirect @@ -81,7 +83,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/amplify v1.23.3 // indirect github.com/aws/aws-sdk-go-v2/service/apigateway v1.25.3 // indirect github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.22.3 // indirect - github.com/aws/aws-sdk-go-v2/service/appconfig v1.31.3 // indirect github.com/aws/aws-sdk-go-v2/service/appfabric v1.9.3 // indirect github.com/aws/aws-sdk-go-v2/service/appflow v1.43.3 // indirect github.com/aws/aws-sdk-go-v2/service/appintegrations v1.27.3 // indirect @@ -229,7 +230,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/rekognition v1.43.2 // indirect github.com/aws/aws-sdk-go-v2/service/resourceexplorer2 v1.12.3 // indirect github.com/aws/aws-sdk-go-v2/service/resourcegroups v1.24.3 // indirect - github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.23.3 // indirect github.com/aws/aws-sdk-go-v2/service/rolesanywhere v1.13.3 // indirect github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 // indirect github.com/aws/aws-sdk-go-v2/service/route53domains v1.25.3 // indirect diff --git a/provider/provider_python_test.go b/provider/provider_python_test.go index 353c7acff4e..7c59091820e 100644 --- a/provider/provider_python_test.go +++ b/provider/provider_python_test.go @@ -6,16 +6,12 @@ package provider import ( - "bytes" - "context" - "fmt" "path/filepath" "strings" "testing" "time" "github.com/pulumi/pulumi/pkg/v3/testing/integration" - "github.com/stretchr/testify/require" ) func TestRegress3196(t *testing.T) { @@ -61,7 +57,6 @@ func TestRegress3887(t *testing.T) { // Make sure that importing an AWS targetGroup succeeds. func TestRegress2534(t *testing.T) { - ctx := context.Background() ptest := pulumiTest(t, filepath.Join("test-programs", "regress-2534")) upResult := ptest.Up() targetGroupArn := upResult.Outputs["targetGroupArn"].Value.(string) @@ -71,24 +66,8 @@ func TestRegress2534(t *testing.T) { workdir := workspace.WorkDir() t.Logf("workdir = %s", workdir) - exec := func(args ...string) { - var env []string - for k, v := range workspace.GetEnvVars() { - env = append(env, fmt.Sprintf("%s=%s", k, v)) - } - stdin := bytes.NewReader([]byte{}) - var arguments []string - arguments = append(arguments, args...) - arguments = append(arguments, "-s", ptest.CurrentStack().Name()) - s1, s2, code, err := workspace.PulumiCommand().Run(ctx, workdir, stdin, nil, nil, env, arguments...) - t.Logf("import stdout: %s", s1) - t.Logf("import stderr: %s", s2) - t.Logf("code=%v", code) - require.NoError(t, err) - } - - exec("import", "aws:lb/targetGroup:TargetGroup", "newtg", targetGroupArn, "--yes") - exec("state", "unprotect", strings.ReplaceAll(targetGroupUrn, "::test", "::newtg"), "--yes") + execPulumi(t, ptest, workdir, "import", "aws:lb/targetGroup:TargetGroup", "newtg", targetGroupArn, "--yes") + execPulumi(t, ptest, workdir, "state", "unprotect", strings.ReplaceAll(targetGroupUrn, "::test", "::newtg"), "--yes") } func getPythonBaseOptions(t *testing.T) integration.ProgramTestOptions { diff --git a/provider/provider_test.go b/provider/provider_test.go index 34ef879fcdc..37b3cfa2a27 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -2,6 +2,8 @@ package provider import ( + "bytes" + "context" "encoding/json" "fmt" "os" @@ -34,6 +36,24 @@ func getEnvRegion(t *testing.T) string { return envRegion } +func execPulumi(t *testing.T, ptest *pulumitest.PulumiTest, workdir string, args ...string) { + ctx := context.Background() + var env []string + workspace := ptest.CurrentStack().Workspace() + for k, v := range workspace.GetEnvVars() { + env = append(env, fmt.Sprintf("%s=%s", k, v)) + } + stdin := bytes.NewReader([]byte{}) + var arguments []string + arguments = append(arguments, args...) + arguments = append(arguments, "-s", ptest.CurrentStack().Name()) + s1, s2, code, err := workspace.PulumiCommand().Run(ctx, workdir, stdin, nil, nil, env, arguments...) + t.Logf("stdout: %s", s1) + t.Logf("stderr: %s", s2) + t.Logf("code=%v", code) + require.NoError(t, err) +} + type testProviderUpgradeOptions struct { baselineVersion string linkNodeSDK bool diff --git a/provider/provider_yaml_test.go b/provider/provider_yaml_test.go index 571fec4aa22..10207b80151 100644 --- a/provider/provider_yaml_test.go +++ b/provider/provider_yaml_test.go @@ -14,16 +14,26 @@ import ( "os/exec" "path/filepath" "runtime" + "sort" "strings" "testing" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" + appconfigsdk "github.com/aws/aws-sdk-go-v2/service/appconfig" + tagsdk "github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi" s3sdk "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/pulumi/providertest/pulumitest" "github.com/pulumi/providertest/pulumitest/assertpreview" "github.com/pulumi/providertest/pulumitest/opttest" + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/pulumi/pulumi/sdk/v3/go/auto/optpreview" + "github.com/pulumi/pulumi/sdk/v3/go/auto/optrefresh" + "github.com/pulumi/pulumi/sdk/v3/go/auto/optup" "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -145,6 +155,7 @@ func TestSecretVersionUpgrade(t *testing.T) { } func TestRdsParameterGroupUnclearDiff(t *testing.T) { + t.Parallel() if testing.Short() { t.Skipf("Skipping in testing.Short() mode, assuming this is a CI run without credentials") } @@ -234,6 +245,7 @@ resources: tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() workdir := t.TempDir() err := os.WriteFile(filepath.Join(workdir, "Pulumi.yaml"), []byte(tc.file1), 0o600) @@ -294,6 +306,7 @@ func randSeq(n int) string { } func TestNonIdempotentSnsTopic(t *testing.T) { + t.Parallel() ptest := pulumiTest(t, filepath.Join("test-programs", "non-idempotent-sns-topic")) ptest.InstallStack("test") @@ -306,6 +319,7 @@ func TestNonIdempotentSnsTopic(t *testing.T) { } func TestOpenZfsFileSystemUpgrade(t *testing.T) { + t.Parallel() if testing.Short() { t.Skipf("Skipping in testing.Short() mode, assuming this is a CI run without credentials") } @@ -369,6 +383,7 @@ resources: // we use a test with a snapshot since this test is only useful the first time, once // we know it works it should continue to work. t.Run("new-version", func(t *testing.T) { + t.Parallel() err = os.WriteFile(filepath.Join(workdir, "Pulumi.yaml"), secondProgram, 0o600) assert.NoError(t, err) pulumiTest := pulumitest.NewPulumiTest(t, workdir, @@ -384,6 +399,7 @@ resources: // Make sure that legacy Bucket supports deleting tags out of band and detecting drift. func TestRegress3674(t *testing.T) { + t.Parallel() ptest := pulumiTest(t, filepath.Join("test-programs", "regress-3674"), opttest.SkipInstall()) upResult := ptest.Up() bucketName := upResult.Outputs["bucketName"].Value.(string) @@ -399,6 +415,7 @@ func TestRegress3674(t *testing.T) { // Ensure that pulumi-aws can authenticate using IMDS API when Pulumi is running in a context where that is made // available such as an EC2 instance. func TestIMDSAuth(t *testing.T) { + t.Parallel() var localProviderBuild string actual := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) expected := "linux/amd64" @@ -437,6 +454,7 @@ func TestIMDSAuth(t *testing.T) { } } t.Run("IDMSv2", func(t *testing.T) { + t.Parallel() ptest := pulumiTest(t, filepath.Join("test-programs", "imds-auth", "imds-v2"), opttest.SkipInstall()) ptest.SetConfig("localProviderBuild", localProviderBuild) result := ptest.Up() @@ -452,6 +470,7 @@ func TestIMDSAuth(t *testing.T) { // // See https://github.com/pulumi/pulumi-aws/issues/2796 func TestS3BucketObjectDeprecation(t *testing.T) { + t.Parallel() ptest := pulumiTest(t, filepath.Join("test-programs", "regress-2796"), opttest.SkipInstall()) result := ptest.Up() t.Logf("STDOUT: %v", result.StdOut) @@ -459,7 +478,431 @@ func TestS3BucketObjectDeprecation(t *testing.T) { require.NotContains(t, result.StdOut+result.StdErr, "aws_s3_object") } -func configureS3() *s3sdk.Client { +type tagsTestStep struct { + // The name of the Pulumi program + name string + + // The type token of the resource, i.e. aws:s3:Bucket + token string + + // The pulumi type of the resource, i.e. aws:s3/bucket:Bucket + typ string + + // Constant properties for the primary resource under test. + // + // This cannot include the tags property, which will be adjusted by the test. + properties map[string]interface{} + + // List of tags to add to the resource + tags map[string]interface{} + + // List of default tags to add to the provider + defaultTags map[string]interface{} + + // List of tag keys to add to the provider `ignoreTags.Keys` property + ignoreTagKeys []string + + // Function to run prior to _any_ import step + preImportHook func(t *testing.T, outputs auto.OutputMap) + + // Function to run after running the first up. This can be used to + // run extra validation + postUpHook func(t *testing.T, outputs auto.OutputMap) + + // Other is a string that is inserted into the test program. It is intended to be + // used to provision supporting resources in tests. + other string + + // If skip is non-empty, the test will be skipped with `skip` as the given reason. + skip string +} + +// TestAccDefaultTags tries to test all the scenarios that might affect provider defaultTags / resource tags +// i.e. up, refresh, preview, import, etc +func TestAccDefaultTags(t *testing.T) { + t.Parallel() + if testing.Short() { + t.Skipf("Skipping in testing.Short() mode, assuming this is a CI run without credentials") + } + + isNil := func(val interface{}) bool { + if val == nil { + return true + } + v, ok := val.(map[string]interface{}) + return ok && len(v) == 0 + } + validateOutputTags := func(outputs auto.OutputMap, expectedTags map[string]interface{}) { + stackOutputTags := outputs["actual"] + if !isNil(expectedTags) || !isNil(stackOutputTags) { + assert.Equal(t, expectedTags, stackOutputTags.Value) + } + } + + steps := []tagsTestStep{ + // Pulumi maintains it's own version of aws:s3:Bucket in + // `s3legacy/bucket_legacy.go`. Because we don't have any + // terraform-provider-aws maintainers to ensure our tagging works the same + // way as other resource's tagging, we give our own bucket special testing + // to make sure that tags work. + { + name: "legacy", token: "aws:s3:Bucket", typ: "aws:s3/bucket:Bucket", + tags: map[string]interface{}{ + "LocalTag": "foo", + }, + defaultTags: map[string]interface{}{ + "GlobalTag": "bar", + }, + postUpHook: func(t *testing.T, outputs auto.OutputMap) { + validateOutputTags(outputs, map[string]interface{}{ + "LocalTag": "foo", + "GlobalTag": "bar", + }) + bucketName := outputs["id"].Value.(string) + tags := getBucketTagging(context.Background(), bucketName) + assert.Equal(t, tags, []types.Tag{ + { + Key: pulumi.StringRef("LocalTag"), + Value: pulumi.StringRef("foo"), + }, + { + Key: pulumi.StringRef("GlobalTag"), + Value: pulumi.StringRef("bar"), + }, + }) + }, + }, + { + name: "legacy_ignore_tags", token: "aws:s3:Bucket", typ: "aws:s3/bucket:Bucket", + tags: map[string]interface{}{ + "LocalTag": "foo", + }, + ignoreTagKeys: []string{"IgnoreKey"}, + preImportHook: func(t *testing.T, outputs auto.OutputMap) { + t.Helper() + resArn := outputs["resArn"].Value.(string) + addResourceTags(context.Background(), resArn, map[string]string{ + "IgnoreKey": "foo", + }) + }, + defaultTags: map[string]interface{}{ + "GlobalTag": "bar", + }, + postUpHook: func(t *testing.T, outputs auto.OutputMap) { + validateOutputTags(outputs, map[string]interface{}{ + "LocalTag": "foo", + "GlobalTag": "bar", + }) + }, + }, + + // Both aws:cognito:UserPool and aws:s3:BucketV2 are full SDKv2 resources managed + // by Terraform, but they have different requirements for successful tag + // interactions. That is why we have tests for both resources. + { + name: "bucket", token: "aws:s3:BucketV2", typ: "aws:s3/bucketV2:BucketV2", + tags: map[string]interface{}{ + "LocalTag": "foo", + }, + postUpHook: func(t *testing.T, outputs auto.OutputMap) { + validateOutputTags(outputs, map[string]interface{}{ + "LocalTag": "foo", + "GlobalTag": "bar", + }) + }, + defaultTags: map[string]interface{}{ + "GlobalTag": "bar", + }, + }, + { + name: "bucket_ignore_tags", token: "aws:s3:BucketV2", typ: "aws:s3/bucketV2:BucketV2", + tags: map[string]interface{}{ + "LocalTag": "foo", + }, + postUpHook: func(t *testing.T, outputs auto.OutputMap) { + validateOutputTags(outputs, map[string]interface{}{ + "LocalTag": "foo", + "GlobalTag": "bar", + }) + }, + defaultTags: map[string]interface{}{ + "GlobalTag": "bar", + }, + ignoreTagKeys: []string{"IgnoreKey"}, + preImportHook: func(t *testing.T, outputs auto.OutputMap) { + t.Helper() + resArn := outputs["resArn"].Value.(string) + addResourceTags(context.Background(), resArn, map[string]string{ + "IgnoreKey": "foo", + }) + }, + }, + { + name: "sdkv2", token: "aws:cognito:UserPool", typ: "aws:cognito/userPool:UserPool", + tags: map[string]interface{}{ + "LocalTag": "foo", + }, + defaultTags: map[string]interface{}{ + "GlobalTag": "bar", + }, + postUpHook: func(t *testing.T, outputs auto.OutputMap) { + validateOutputTags(outputs, map[string]interface{}{ + "LocalTag": "foo", + "GlobalTag": "bar", + }) + }, + properties: map[string]interface{}{ + // aliasAttributes is necessary because otherwise we don't + // see a clean initial refresh + "aliasAttributes": []interface{}{"email"}, + }, + }, + + // A PF resource (appconfig:Environment) + // PF resources deal with tags differently + { + name: "pf", token: "aws:appconfig:Environment", typ: "aws:appconfig/environment:Environment", + tags: map[string]interface{}{ + "LocalTag": "foo", + }, + defaultTags: map[string]interface{}{ + "GlobalTag": "bar", + }, + postUpHook: func(t *testing.T, outputs auto.OutputMap) { + validateOutputTags(outputs, map[string]interface{}{ + "LocalTag": "foo", + "GlobalTag": "bar", + }) + }, + other: ` + app: + type: aws:appconfig:Application`, + properties: map[string]interface{}{ + "applicationId": "${app.id}", + }, + }, + { + name: "pf_ignore_tags", token: "aws:appconfig:Environment", typ: "aws:appconfig/environment:Environment", + tags: map[string]interface{}{ + "LocalTag": "foo", + }, + ignoreTagKeys: []string{"IgnoreKey"}, + preImportHook: func(t *testing.T, outputs auto.OutputMap) { + t.Helper() + resArn := outputs["resArn"].Value.(string) + addResourceTags(context.Background(), resArn, map[string]string{ + "IgnoreKey": "foo", + }) + }, + defaultTags: map[string]interface{}{ + "GlobalTag": "bar", + }, + postUpHook: func(t *testing.T, outputs auto.OutputMap) { + validateOutputTags(outputs, map[string]interface{}{ + "LocalTag": "foo", + "GlobalTag": "bar", + }) + }, + other: ` + app: + type: aws:appconfig:Application`, + properties: map[string]interface{}{ + "applicationId": "${app.id}", + }, + }, + } + + for _, step := range steps { + step := step + t.Run(step.name, func(t *testing.T) { + t.Parallel() + if reason := step.skip; reason != "" { + t.Skipf(reason) + } + testTagsPulumiLifecycle(t, step) + }) + } +} + +// testTagsPulumiLifecycle tests the complete lifecycle of a pulumi program +// Scenarios that this tests: +// 1. `Up` with both provider `defaultTags`/`ignoreTags` and resource level `tags` +// 1a. Run validations on result +// 2. `Refresh` with no changes +// 3. `Import` using the resource option. Ensures resource can be successfully imported +// 3a. Allows for a hook to be run prior to import being run. e.g. Add tags remotely +// 4. `Import` using the CLI. Ensures resources can be successfully imported +// 4a. Allows for a hook to be run prior to import being run. e.g. Add tags remotely +// 5. `Refresh` with no changes +func testTagsPulumiLifecycle(t *testing.T, step tagsTestStep) { + t.Helper() + ctx := context.Background() + + stepDir, err := os.MkdirTemp(os.TempDir(), step.name) + assert.NoError(t, err) + fpath := filepath.Join(stepDir, "Pulumi.yaml") + + generateTagsTest(t, step, fpath, "") + ptest := pulumiTest(t, stepDir, opttest.TestInPlace()) + stack := ptest.CurrentStack() + + t.Log("Initial deployment...") + upRes, err := stack.Up(ctx) + assert.NoError(t, err) + outputs := upRes.Outputs + urn := outputs["urn"].Value.(string) + id := outputs["id"].Value.(string) + providerUrn := outputs["providerUrn"].Value.(string) + if step.postUpHook != nil { + step.postUpHook(t, outputs) + } + + t.Log("refresh...") + _, err = stack.Refresh(ctx, optrefresh.ExpectNoChanges()) + assert.NoError(t, err) + + t.Log("delete state...") + execPulumi(t, ptest, stepDir, "state", "delete", urn) + + // import using the import resource option + t.Log("up with import...") + if step.preImportHook != nil { + step.preImportHook(t, outputs) + } + generateTagsTest(t, step, fpath, id) + upRes, err = stack.Up(ctx, optup.Diff()) + assert.NoError(t, err) + changes := *upRes.Summary.ResourceChanges + assert.Equal(t, 1, changes["import"]) + + t.Log("delete state...") + execPulumi(t, ptest, stepDir, "state", "delete", urn) + + t.Log("import from cli...") + if step.preImportHook != nil { + step.preImportHook(t, outputs) + } + generateTagsTest(t, step, fpath, "") + execPulumi(t, ptest, stepDir, "import", step.typ, "res", id, "--provider", fmt.Sprintf("aws-provider=%s", providerUrn), "--yes") + execPulumi(t, ptest, stepDir, "state", "unprotect", urn, "--yes") + + // need to run an up to fix the state. It should be a no-op + // re https://github.com/pulumi/pulumi-aws/issues/4204 + upRes, err = stack.Up(ctx) + assert.NoError(t, err) + for k := range *upRes.Summary.ResourceChanges { + if k != "same" { + t.Fatal("expected no changes") + } + } + + t.Log("preview with refresh...") + _, err = stack.Preview(ctx, optpreview.Refresh(), optpreview.ExpectNoChanges()) + assert.NoError(t, err) +} + +// generateTagsTest generates a pulumi program for the given test step +// and writes it to the test directory +func generateTagsTest(t *testing.T, step tagsTestStep, testPath string, importId string) { + template := `name: test-aws-%s +runtime: yaml +resources: + aws-provider: + type: pulumi:providers:aws%s%s + res: + type: %s%s%s +outputs: + actual: ${res.tags} + urn: ${res.urn} + id: ${res.id} + resArn: ${res.arn} + providerUrn: ${aws-provider.urn}` + + options := map[string]interface{}{ + "provider": "${aws-provider}", + } + + if importId != "" { + options["import"] = importId + } + + var expandMap func(level int, v interface{}) string + expandMap = func(level int, v interface{}) string { + indent := "\n" + strings.Repeat(" ", level) + + var body string + switch v := v.(type) { + case nil: + return "" + case string: + body = v + case []string: + for _, v := range v { + body += indent + "- " + strings.TrimSpace(expandMap(level+1, v)) + } + case []interface{}: + for _, v := range v { + body += indent + "- " + strings.TrimSpace(expandMap(level+1, v)) + } + case map[string]interface{}: + sortedKeys := make([]string, len(v)) + for k := range v { + sortedKeys = append(sortedKeys, k) + } + sort.Strings(sortedKeys) + for _, k := range sortedKeys { + v := v[k] + + val := expandMap(level+1, v) + if val == "" { + continue + } + body += indent + k + ": " + val + } + default: + t.Logf("Unknown value type %T", v) + t.FailNow() + } + + return body + } + + expandProps := func(key string, props ...map[string]interface{}) string { + a := map[string]interface{}{} + for _, arg := range props { + for k, v := range arg { + a[k] = v + } + } + + return expandMap(2, map[string]interface{}{ + key: a, + }) + } + + providerProps := map[string]interface{}{ + "defaultTags": map[string]interface{}{ + "tags": step.defaultTags, + }, + } + if step.ignoreTagKeys != nil { + providerProps["ignoreTags"] = map[string]interface{}{ + "keys": step.ignoreTagKeys, + } + } + + body := fmt.Sprintf(template, step.name, + expandProps("properties", providerProps), step.other, step.token, + expandProps("options", options), + expandProps("properties", map[string]interface{}{ + "tags": step.tags, + }, step.properties)) + + t.Logf("template for %s: \n%s", step.name, body) + require.NoError(t, os.WriteFile(testPath, []byte(body), 0600)) +} + +func loadAwsDefaultConfig() aws.Config { loadOpts := []func(*config.LoadOptions) error{} if p, ok := os.LookupEnv("AWS_PROFILE"); ok { loadOpts = append(loadOpts, config.WithSharedConfigProfile(p)) @@ -469,9 +912,91 @@ func configureS3() *s3sdk.Client { } cfg, err := config.LoadDefaultConfig(context.TODO(), loadOpts...) contract.AssertNoErrorf(err, "failed to load AWS config") + + return cfg +} + +func configureS3() *s3sdk.Client { + cfg := loadAwsDefaultConfig() return s3sdk.NewFromConfig(cfg) } +func configureAppconfig() *appconfigsdk.Client { + cfg := loadAwsDefaultConfig() + return appconfigsdk.NewFromConfig(cfg) +} + +func configureTagSdk() *tagsdk.Client { + cfg := loadAwsDefaultConfig() + return tagsdk.NewFromConfig(cfg) +} + +func addResourceTags(ctx context.Context, arn string, tags map[string]string) { + tag := configureTagSdk() + _, err := tag.TagResources(ctx, &tagsdk.TagResourcesInput{ + ResourceARNList: []string{arn}, + Tags: tags, + }) + contract.AssertNoErrorf(err, "error tagging resource") +} + +func addAppconfigEnvironmentTags(ctx context.Context, envArn string, tags map[string]string) { + appconfig := configureAppconfig() + existingTags, err := appconfig.ListTagsForResource(ctx, &appconfigsdk.ListTagsForResourceInput{ + ResourceArn: &envArn, + }) + contract.AssertNoErrorf(err, "failed to list tags for appconfig env") + + for k, v := range existingTags.Tags { + if _, exists := tags[k]; !exists { + tags[k] = v + } + } + + _, err = appconfig.TagResource(ctx, &appconfigsdk.TagResourceInput{ + ResourceArn: &envArn, + Tags: tags, + }) + contract.AssertNoErrorf(err, "error tagging appconfig env") +} + +func getBucketTagging(ctx context.Context, awsBucket string) []types.Tag { + s3 := configureS3() + tagging, err := s3.GetBucketTagging(ctx, &s3sdk.GetBucketTaggingInput{ + Bucket: &awsBucket, + }) + contract.AssertNoErrorf(err, "failed to get bucket tagging") + return tagging.TagSet +} + +func addBucketTags(ctx context.Context, bucketName string, tags map[string]string) { + s3 := configureS3() + existingTags := getBucketTagging(ctx, bucketName) + + newTags := []types.Tag{} + + for k, v := range tags { + newTags = append(newTags, types.Tag{ + Key: &k, + Value: &v, + }) + } + + for _, v := range existingTags { + if _, exists := tags[*v.Key]; !exists { + newTags = append(newTags, v) + } + } + + _, err := s3.PutBucketTagging(ctx, &s3sdk.PutBucketTaggingInput{ + Bucket: &bucketName, + Tagging: &types.Tagging{ + TagSet: newTags, + }, + }) + contract.AssertNoErrorf(err, "error putting bucket tags") +} + func deleteBucketTagging(ctx context.Context, awsBucket string) { s3 := configureS3() _, err := s3.DeleteBucketTagging(ctx, &s3sdk.DeleteBucketTaggingInput{ @@ -479,3 +1004,12 @@ func deleteBucketTagging(ctx context.Context, awsBucket string) { }) contract.AssertNoErrorf(err, "failed to delete bucket tagging") } + +func getCwd(t *testing.T) string { + cwd, err := os.Getwd() + if err != nil { + t.FailNow() + } + + return cwd +} diff --git a/provider/resources.go b/provider/resources.go index 782e7a1ec00..eb8ba3e80f2 100644 --- a/provider/resources.go +++ b/provider/resources.go @@ -5955,6 +5955,22 @@ compatibility shim in favor of the new "name" field.`) prov.Resources[key].PreCheckCallback = applyTags } + // also override read so that it works during import + // as a side effect this will also run during create and update, but since + // `tags` and `defaultTags` should always be equal then it doesn't really matter. + // One extra place that we make sure `tags=defaultTags` is fine. + if transform := prov.Resources[key].TransformOutputs; transform != nil { + prov.Resources[key].TransformOutputs = func(ctx context.Context, pm resource.PropertyMap) (resource.PropertyMap, error) { + config, err := transform(ctx, pm) + if err != nil { + return nil, err + } + return applyTagsOutputs(ctx, config) + } + } else { + prov.Resources[key].TransformOutputs = applyTagsOutputs + } + if prov.Resources[key].GetFields() == nil { prov.Resources[key].Fields = map[string]*tfbridge.SchemaInfo{} } diff --git a/provider/tags.go b/provider/tags.go index 40b1ef8307a..9925af8807e 100644 --- a/provider/tags.go +++ b/provider/tags.go @@ -57,6 +57,40 @@ func applyTags( } if allTags.IsNull() { delete(ret, "tags") + delete(ret, "tagsAll") + return ret, nil + } + ret["tags"] = allTags + ret["tagsAll"] = allTags + + return ret, nil +} + +// similar to applyTags, but applied to the `TransformOutputs` method that is run +// during Read +func applyTagsOutputs( + ctx context.Context, config resource.PropertyMap, +) (resource.PropertyMap, error) { + ret := config.Copy() + configTags := resource.NewObjectProperty(resource.PropertyMap{}) + if t, ok := config["tags"]; ok { + configTags = t + } + + meta := resource.PropertyMap{} + if at, ok := config["tagsAll"]; ok { + meta["defaultTags"] = resource.NewObjectProperty(resource.PropertyMap{ + "tags": at, + }) + } + + allTags, err := mergeTags(ctx, configTags, meta) + if err != nil { + return nil, err + } + if allTags.IsNull() { + delete(ret, "tags") + delete(ret, "tagsAll") return ret, nil } ret["tags"] = allTags diff --git a/provider/tags_test.go b/provider/tags_test.go index 6612883ba1f..f7d511eb19c 100644 --- a/provider/tags_test.go +++ b/provider/tags_test.go @@ -140,7 +140,7 @@ func TestApplyTags(t *testing.T) { }, }, { - name: "provier sets a tag", + name: "provider sets a tag", config: resource.PropertyMap{}, meta: resource.PropertyMap{ "defaultTags": resource.NewObjectProperty(resource.PropertyMap{ @@ -220,6 +220,187 @@ func TestApplyTags(t *testing.T) { } } +func TestApplyTagsOutputs(t *testing.T) { + ctx := context.Background() + + type gen = *rapid.Generator[resource.PropertyValue] + type pk = resource.PropertyKey + type pv = resource.PropertyValue + type pm = resource.PropertyMap + + maybeNullOrUnknown := func(x gen) gen { + return rapid.OneOf( + rapid.Just(resource.NewNullProperty()), + x, + rapid.Map(x, resource.MakeComputed), + ) + } + + str := maybeNullOrUnknown(rapid.OneOf( + rapid.Just(resource.NewStringProperty("")), + rapid.Just(resource.NewStringProperty("foo")), + rapid.Just(resource.NewStringProperty("bar")), + )) + + keys := rapid.Map(rapid.OneOf[string]( + rapid.Just(""), + rapid.Just("a"), + rapid.Just("b"), + ), func(s string) pk { + return resource.PropertyKey(s) + }) + + makeObj := func(m map[pk]resource.PropertyValue) resource.PropertyValue { + return resource.NewObjectProperty(resource.PropertyMap(m)) + } + + keyValueTags := maybeNullOrUnknown( + rapid.Map(rapid.MapOfN[pk, pv](keys, str, 0, 3), makeObj)) + + config := rapid.Map(keyValueTags, func(tags pv) pm { + return resource.PropertyMap{ + "tags": tags, + } + }) + + defaultConfig := maybeNullOrUnknown(rapid.Map(config, + resource.NewObjectProperty)) + + ignoreConfig := rapid.Custom[pv](func(t *rapid.T) pv { + keys := keyValueTags.Draw(t, "keys") + keyPrefixes := keyValueTags.Draw(t, "keyPrefixes") + m := resource.PropertyMap{} + if !keys.IsNull() { + m["keys"] = keys + } + if !keyPrefixes.IsNull() { + m["keyPrefixes"] = keyPrefixes + } + return resource.NewObjectProperty(m) + }) + + meta := rapid.Custom[pm](func(t *rapid.T) pm { + i := ignoreConfig.Draw(t, "ignoreConfig") + d := defaultConfig.Draw(t, "defaultConfig") + m := resource.PropertyMap{} + if !i.IsNull() { + m["ignoreConfig"] = i + } + if !d.IsNull() { + m["defaultConfig"] = d + } + return m + }) + + type args struct { + meta resource.PropertyMap + config resource.PropertyMap + } + + argsGen := rapid.Custom[args](func(t *rapid.T) args { + m := meta.Draw(t, "meta") + c := config.Draw(t, "config") + return args{meta: m, config: c} + }) + + t.Run("no panics", func(t *testing.T) { + rapid.Check(t, func(t *rapid.T) { + args := argsGen.Draw(t, "args") + _, err := applyTags(ctx, args.config, args.meta) + require.NoError(t, err) + }) + }) + + type testCase struct { + name string + config resource.PropertyMap + expect resource.PropertyMap + } + + testCases := []testCase{ + { + name: "resource tags propagate", + config: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{ + "tag1": resource.NewStringProperty("tag1v"), + }), + }, + expect: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{ + "tag1": resource.NewStringProperty("tag1v"), + }), + }, + }, + { + name: "provider sets a tag", + config: resource.PropertyMap{ + "tagsAll": resource.NewObjectProperty(resource.PropertyMap{ + "tag2": resource.NewStringProperty("tag2v"), + }), + }, + expect: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{ + "tag2": resource.NewStringProperty("tag2v"), + }), + }, + }, + { + name: "provider adds a tag to resource tags", + config: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{ + "tag1": resource.NewStringProperty("tag1v"), + }), + "tagsAll": resource.NewObjectProperty(resource.PropertyMap{ + "tag2": resource.NewStringProperty("tag2v"), + }), + }, + expect: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{ + "tag1": resource.NewStringProperty("tag1v"), + "tag2": resource.NewStringProperty("tag2v"), + }), + }, + }, + { + name: "provider cannot change a resource tag", + config: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{ + "tag1": resource.NewStringProperty("tag1v"), + }), + "tagsAll": resource.NewObjectProperty(resource.PropertyMap{ + "tag1": resource.NewStringProperty("tag2v"), + }), + }, + expect: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{ + "tag1": resource.NewStringProperty("tag1v"), + }), + }, + }, + { + name: "unknowns mark the entire computation unknown", + config: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{ + "tag1": resource.MakeComputed(resource.PropertyValue{}), + }), + }, + expect: resource.PropertyMap{ + "tags": resource.NewOutputProperty(resource.Output{Known: false}), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r, err := applyTagsOutputs(ctx, tc.config) + require.NoError(t, err) + // Expect tagsAll to be copied from tags. + tc.expect["tagsAll"] = tc.expect["tags"] + require.Equal(t, tc.expect, r) + }) + } +} + func TestAddingEmptyTagProducesChangeDiff(t *testing.T) { replayEvent := ` [ diff --git a/sdk/go.mod b/sdk/go.mod index 3342945c3c1..64aed864149 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -31,6 +31,7 @@ require ( github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/djherbis/times v1.5.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect @@ -46,7 +47,8 @@ require ( github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect @@ -64,13 +66,13 @@ require ( github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect github.com/pulumi/esc v0.9.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/spf13/cast v1.4.1 // indirect - github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect github.com/tweekmonster/luser v0.0.0-20161003172636-3fa38070dbd7 // indirect @@ -88,10 +90,11 @@ require ( golang.org/x/term v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/tools v0.22.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 // indirect google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.34.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/frand v1.4.2 // indirect + pgregory.net/rapid v0.6.1 // indirect ) diff --git a/sdk/go.sum b/sdk/go.sum index 101ac2d232f..9275ce15106 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -39,7 +39,7 @@ github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vc github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -52,8 +52,8 @@ github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/El github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -105,12 +105,13 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -156,8 +157,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= @@ -170,8 +171,8 @@ github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= @@ -254,6 +255,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -291,8 +293,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 h1:8EeVk1VKMD+GD/neyEHGmz7pFblqPjHoi+PGQIlLx2s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= @@ -311,5 +313,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= -pgregory.net/rapid v0.5.5 h1:jkgx1TjbQPD/feRoK+S/mXw9e1uj6WilpHrXJowi6oA= -pgregory.net/rapid v0.5.5/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +pgregory.net/rapid v0.6.1 h1:4eyrDxyht86tT4Ztm+kvlyNBLIk071gR+ZQdhphc9dQ= +pgregory.net/rapid v0.6.1/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=