Add exclude flag support (#1900)

Signed-off-by: RLRabinowitz <rlrabinowitz2@gmail.com>
This commit is contained in:
Arel Rabinowitz 2024-11-05 17:16:00 +02:00 committed by GitHub
parent e802b23200
commit 3d4bf29c56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 4428 additions and 628 deletions

View File

@ -35,6 +35,9 @@ linters-settings:
gocognit:
min-complexity: 50
goconst:
ignore-tests: true # Is documented to be the default behaviour, but that doesn't seem to be the case
issues:
exclude-rules:
- path: (.+)_test.go

View File

@ -5,6 +5,7 @@ UPGRADE NOTES:
* Using the `ghcr.io/opentofu/opentofu` image as a base image for custom images is deprecated and this will be removed in OpenTofu 1.10. Please see https://opentofu.org/docs/intro/install/docker/ for instructions on building your own image.
NEW FEATURES:
* Add support for `-exclude` flag, to allow excluding specific resources and modules with resource targeting ([#426](https://github.com/opentofu/opentofu/issues/426))
ENHANCEMENTS:
* State encryption key providers now support customizing the metadata key via `encrypted_metadata_alias` ([#1605](https://github.com/opentofu/opentofu/issues/1605))

View File

@ -282,6 +282,7 @@ type Operation struct {
PlanMode plans.Mode
AutoApprove bool
Targets []addrs.Targetable
Excludes []addrs.Targetable
ForceReplace []addrs.AbsResourceInstance
// Injected by the command creating the operation (plan/apply/refresh/etc...)
Variables map[string]UnparsedVariableValue

View File

@ -203,6 +203,7 @@ func (b *Local) localRunDirect(op *backend.Operation, run *backend.LocalRun, cor
planOpts := &tofu.PlanOpts{
Mode: op.PlanMode,
Targets: op.Targets,
Excludes: op.Excludes,
ForceReplace: op.ForceReplace,
SetVariables: variables,
SkipRefresh: op.Type != backend.OperationTypeRefresh && !op.PlanRefresh,

View File

@ -161,6 +161,14 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati
}
}
if len(op.Excludes) != 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"-exclude option is not supported",
"The -exclude option is not currently supported for remote plans.",
))
}
// Return if there are any errors.
if diags.HasErrors() {
return nil, diags.Err()

View File

@ -466,6 +466,45 @@ func TestRemote_applyWithTarget(t *testing.T) {
}
}
// Applying with an exclude flag should error
func TestRemote_applyWithExclude(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup()
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
op.Workspace = backend.DefaultStateName
op.Excludes = []addrs.Targetable{addr}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail")
}
if !run.PlanEmpty {
t.Fatalf("expected plan to be empty")
}
errOutput := output.Stderr()
if !strings.Contains(errOutput, "-exclude option is not supported") {
t.Fatalf("expected -exclude option is not supported error, got: %v", errOutput)
}
stateMgr, _ := b.StateMgr(backend.DefaultStateName)
// An error suggests that the state was not unlocked after apply
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Fatalf("unexpected error locking state after failed apply: %s", err.Error())
}
}
func TestRemote_applyWithTargetIncompatibleAPIVersion(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()

View File

@ -27,8 +27,9 @@ func (b *Remote) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Fu
var diags tfdiags.Diagnostics
ret := &backend.LocalRun{
PlanOpts: &tofu.PlanOpts{
Mode: op.PlanMode,
Targets: op.Targets,
Mode: op.PlanMode,
Targets: op.Targets,
Excludes: op.Excludes,
},
}

View File

@ -128,6 +128,14 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio
}
}
if len(op.Excludes) != 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"-exclude option is not supported",
"The -exclude option is not currently supported for remote plans.",
))
}
if !op.PlanRefresh {
desiredAPIVersion, _ := version.NewVersion("2.4")

View File

@ -537,6 +537,39 @@ func TestRemote_planWithTargetIncompatibleAPIVersion(t *testing.T) {
}
}
// Planning with an exclude flag should error
func TestRemote_planWithExclude(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
op.Workspace = backend.DefaultStateName
op.Excludes = []addrs.Targetable{addr}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail")
}
if !run.PlanEmpty {
t.Fatalf("expected plan to be empty")
}
errOutput := output.Stderr()
if !strings.Contains(errOutput, "-exclude option is not supported") {
t.Fatalf("expected -exclude option is not supported error, got: %v", errOutput)
}
}
func TestRemote_planWithReplace(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()

View File

@ -78,6 +78,14 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio
))
}
if len(op.Excludes) != 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"-exclude option is not supported",
"The -exclude option is not currently supported for remote plans.",
))
}
// Return if there are any errors.
if diags.HasErrors() {
return nil, diags.Err()

View File

@ -623,6 +623,45 @@ func TestCloud_applyWithTarget(t *testing.T) {
}
}
// Applying with an exclude flag should error
func TestCloud_applyWithExclude(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()
op, configCleanup, done := testOperationApply(t, "./testdata/apply")
defer configCleanup()
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
op.Workspace = testBackendSingleWorkspaceName
op.Excludes = []addrs.Targetable{addr}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail")
}
if !run.PlanEmpty {
t.Fatalf("expected plan to be empty")
}
errOutput := output.Stderr()
if !strings.Contains(errOutput, "-exclude option is not supported") {
t.Fatalf("expected -exclude option is not supported error, got: %v", errOutput)
}
stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
// An error suggests that the state was not unlocked after apply
if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
t.Fatalf("unexpected error locking state after failed apply: %s", err.Error())
}
}
func TestCloud_applyWithReplace(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()

View File

@ -27,8 +27,9 @@ func (b *Cloud) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Ful
var diags tfdiags.Diagnostics
ret := &backend.LocalRun{
PlanOpts: &tofu.PlanOpts{
Mode: op.PlanMode,
Targets: op.Targets,
Mode: op.PlanMode,
Targets: op.Targets,
Excludes: op.Excludes,
},
}

View File

@ -79,6 +79,14 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation
))
}
if len(op.Excludes) != 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"-exclude option is not supported",
"The -exclude option is not currently supported for remote plans.",
))
}
if len(op.GenerateConfigOut) > 0 {
diags = diags.Append(genconfig.ValidateTargetFile(op.GenerateConfigOut))
}

View File

@ -565,6 +565,39 @@ func TestCloud_planWithTarget(t *testing.T) {
}
}
// Planning with an exclude flag should error
func TestCloud_planWithExclude(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()
op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
defer configCleanup()
addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
op.Workspace = testBackendSingleWorkspaceName
op.Excludes = []addrs.Targetable{addr}
run, err := b.Operation(context.Background(), op)
if err != nil {
t.Fatalf("error starting operation: %v", err)
}
<-run.Done()
output := done(t)
if run.Result == backend.OperationSuccess {
t.Fatal("expected apply operation to fail")
}
if !run.PlanEmpty {
t.Fatalf("expected plan to be empty")
}
errOutput := output.Stderr()
if !strings.Contains(errOutput, "-exclude option is not supported") {
t.Fatalf("expected -exclude option is not supported error, got: %v", errOutput)
}
}
func TestCloud_planWithReplace(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()

View File

@ -284,6 +284,7 @@ func (c *ApplyCommand) OperationRequest(
opReq.PlanFile = planFile
opReq.PlanRefresh = args.Refresh
opReq.Targets = args.Targets
opReq.Excludes = args.Excludes
opReq.ForceReplace = args.ForceReplace
opReq.Type = backend.OperationTypeApply
opReq.View = view.Operation()

View File

@ -7,6 +7,7 @@ package command
import (
"os"
"path/filepath"
"strings"
"testing"
@ -386,290 +387,222 @@ func TestApply_destroyPath(t *testing.T) {
}
}
// Config with multiple resources with dependencies, targeting destroy of a
// root node, expecting all other resources to be destroyed due to
// dependencies.
func TestApply_destroyTargetedDependencies(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
testCopyDir(t, testFixturePath("apply-destroy-targeted"), td)
defer testChdir(t, td)()
originalState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"i-ab123"}`),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_load_balancer",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"i-abc123"}`),
Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")},
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
})
statePath := testStateFile(t, originalState)
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
func TestApply_targetedDestroy(t *testing.T) {
testCases := []struct {
name string
flagName string
flagValue string
wantStatFunc func(s *states.SyncState)
}{
{
// Config with multiple resources with dependencies, targeting destroy of a
// leaf node, expecting the other resources to remain.
name: "Targeted Destroy",
flagName: "-target",
flagValue: "test_load_balancer.foo",
wantStatFunc: func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"i-ab123"}`),
Status: states.ObjectReady,
},
},
},
"test_load_balancer": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"instances": {Type: cty.List(cty.String), Optional: true},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
},
)
},
},
{
// Config with multiple resources with dependencies, targeting destroy of a
// root node, expecting all other resources to be destroyed due to
// dependencies.
name: "Targeted Destroy of root",
flagName: "-target",
flagValue: "test_instance.foo",
// No wantStatFunc, expecting empty state
},
{
// Config with multiple resources with dependencies, destroy excluding a
// non-existent node, expecting all other resources to be destroyed.
name: "Targeted Destroy excluding non-existent resource",
flagName: "-exclude",
flagValue: "test_load_balancer.foo-nonexistent",
// No wantStatFunc, expecting empty state
},
{
// Config with multiple resources with dependencies, destroy excluding the root node,
// expecting other resources to remain
name: "Targeted Destroy with exclude of root",
flagName: "-exclude",
flagValue: "test_instance.foo",
wantStatFunc: func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"i-ab123"}`),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
},
},
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
view, done := testView(t)
c := &ApplyCommand{
Destroy: true,
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a temporary working directory that is empty
td := filepath.Join(t.TempDir(), t.Name())
testCopyDir(t, testFixturePath("apply-destroy-targeted"), td)
defer testChdir(t, td)()
// Run the apply command pointing to our existing state
args := []string{
"-auto-approve",
"-target", "test_instance.foo",
"-state", statePath,
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Log(output.Stdout())
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
originalState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"i-ab123"}`),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_load_balancer",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"i-abc123"}`),
Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")},
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
})
// Verify a new state exists
if _, err := os.Stat(statePath); err != nil {
t.Fatalf("err: %s", err)
}
statePath := testStateFile(t, originalState)
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
stateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
if err != nil {
t.Fatalf("err: %s", err)
}
if stateFile == nil || stateFile.State == nil {
t.Fatal("state should not be nil")
}
spew.Config.DisableMethods = true
if !stateFile.State.Empty() {
t.Fatalf("unexpected final state\ngot: %s\nwant: empty state", spew.Sdump(stateFile.State))
}
// Should have a backup file
f, err = os.Open(statePath + DefaultBackupExtension)
if err != nil {
t.Fatalf("err: %s", err)
}
backupStateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
actualStr := strings.TrimSpace(backupStateFile.State.String())
expectedStr := strings.TrimSpace(originalState.String())
if actualStr != expectedStr {
t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr)
}
}
// Config with multiple resources with dependencies, targeting destroy of a
// leaf node, expecting the other resources to remain.
func TestApply_destroyTargeted(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()
testCopyDir(t, testFixturePath("apply-destroy-targeted"), td)
defer testChdir(t, td)()
originalState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"i-ab123"}`),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_load_balancer",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"i-abc123"}`),
Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")},
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
})
wantState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"i-ab123"}`),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
})
statePath := testStateFile(t, originalState)
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
"test_load_balancer": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"instances": {Type: cty.List(cty.String), Optional: true},
},
},
},
},
},
"test_load_balancer": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"instances": {Type: cty.List(cty.String), Optional: true},
},
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
view, done := testView(t)
c := &ApplyCommand{
Destroy: true,
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
},
},
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
}
view, done := testView(t)
c := &ApplyCommand{
Destroy: true,
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
// Run the apply command pointing to our existing state
args := []string{
"-auto-approve",
tc.flagName, tc.flagValue,
"-state", statePath,
}
// Run the apply command pointing to our existing state
args := []string{
"-auto-approve",
"-target", "test_load_balancer.foo",
"-state", statePath,
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Log(output.Stdout())
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Log(output.Stdout())
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
// Verify a new state exists
if _, err := os.Stat(statePath); err != nil {
t.Fatalf("err: %s", err)
}
// Verify a new state exists
if _, err := os.Stat(statePath); err != nil {
t.Fatalf("err: %s", err)
}
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
f, err := os.Open(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
defer f.Close()
stateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
if err != nil {
t.Fatalf("err: %s", err)
}
if stateFile == nil || stateFile.State == nil {
t.Fatal("state should not be nil")
}
stateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
if err != nil {
t.Fatalf("err: %s", err)
}
if stateFile == nil || stateFile.State == nil {
t.Fatal("state should not be nil")
}
actualStr := strings.TrimSpace(stateFile.State.String())
expectedStr := strings.TrimSpace(wantState.String())
if actualStr != expectedStr {
t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr)
}
if tc.wantStatFunc != nil {
wantState := states.BuildState(tc.wantStatFunc)
actualStr := strings.TrimSpace(stateFile.State.String())
expectedStr := strings.TrimSpace(wantState.String())
if actualStr != expectedStr {
t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", actualStr, expectedStr)
}
} else if !stateFile.State.Empty() {
// Missing wantStatFunc means expected empty state
t.Fatalf("unexpected final state\ngot: %s\nwant: empty state", spew.Sdump(stateFile.State))
}
// Should have a backup file
f, err = os.Open(statePath + DefaultBackupExtension)
if err != nil {
t.Fatalf("err: %s", err)
}
// Should have a backup file
f, err = os.Open(statePath + DefaultBackupExtension)
if err != nil {
t.Fatalf("err: %s", err)
}
backupStateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
backupStateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled())
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
backupActualStr := strings.TrimSpace(backupStateFile.State.String())
backupExpectedStr := strings.TrimSpace(originalState.String())
if backupActualStr != backupExpectedStr {
t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", backupActualStr, backupExpectedStr)
backupActualStr := strings.TrimSpace(backupStateFile.State.String())
backupExpectedStr := strings.TrimSpace(originalState.String())
if backupActualStr != backupExpectedStr {
t.Fatalf("bad:\n\nactual:\n%s\n\nexpected:\nb%s", backupActualStr, backupExpectedStr)
}
})
}
}

View File

@ -2020,6 +2020,94 @@ func TestApply_targetFlagsDiags(t *testing.T) {
}
}
// Config with multiple resources, targeted apply with exclude
func TestApply_excluded(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("apply-excluded"), td)
defer testChdir(t, td)()
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
},
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
view, done := testView(t)
c := &ApplyCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
args := []string{
"-auto-approve",
"-exclude", "test_instance.bar",
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
if got, want := output.Stdout(), "3 added, 0 changed, 0 destroyed"; !strings.Contains(got, want) {
t.Fatalf("bad change summary, want %q, got:\n%s", want, got)
}
}
// Diagnostics for invalid -exclude flags
func TestApply_excludeFlagsDiags(t *testing.T) {
testCases := map[string]string{
"test_instance.": "Dot must be followed by attribute name.",
"test_instance": "Resource specification must include a resource type and name.",
}
for exclude, wantDiag := range testCases {
t.Run(exclude, func(t *testing.T) {
td := testTempDir(t)
defer os.RemoveAll(td)
defer testChdir(t, td)()
view, done := testView(t)
c := &ApplyCommand{
Meta: Meta{
View: view,
},
}
args := []string{
"-auto-approve",
"-exclude", exclude,
}
code := c.Run(args)
output := done(t)
if code != 1 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
got := output.Stderr()
if !strings.Contains(got, exclude) {
t.Fatalf("bad error output, want %q, got:\n%s", exclude, got)
}
if !strings.Contains(got, wantDiag) {
t.Fatalf("bad error output, want %q, got:\n%s", wantDiag, got)
}
})
}
}
func TestApply_replace(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("apply-replace"), td)

View File

@ -9,6 +9,8 @@ import (
"strings"
"testing"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/opentofu/opentofu/internal/addrs"
@ -202,9 +204,11 @@ func TestParseApply_targets(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParseApply(tc.args)
if len(diags) > 0 {
if tc.wantErr == "" {
t.Fatalf("unexpected diags: %v", diags)
if tc.wantErr == "" && len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
} else if tc.wantErr != "" {
if len(diags) == 0 {
t.Fatalf("expected diags but got none")
} else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, tc.wantErr)
}
@ -216,6 +220,81 @@ func TestParseApply_targets(t *testing.T) {
}
}
func TestParseApply_excludes(t *testing.T) {
foobarbaz, _ := addrs.ParseTargetStr("foo_bar.baz")
boop, _ := addrs.ParseTargetStr("module.boop")
testCases := map[string]struct {
args []string
want []addrs.Targetable
wantErr string
}{
"no excludes by default": {
args: nil,
want: nil,
},
"one exclude": {
args: []string{"-exclude=foo_bar.baz"},
want: []addrs.Targetable{foobarbaz.Subject},
},
"two excludes": {
args: []string{"-exclude=foo_bar.baz", "-exclude", "module.boop"},
want: []addrs.Targetable{foobarbaz.Subject, boop.Subject},
},
"invalid traversal": {
args: []string{"-exclude=foo."},
want: nil,
wantErr: "Dot must be followed by attribute name",
},
"invalid target": {
args: []string{"-exclude=data[0].foo"},
want: nil,
wantErr: "A data source name is required",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParseApply(tc.args)
if tc.wantErr == "" && len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
} else if tc.wantErr != "" {
if len(diags) == 0 {
t.Fatalf("expected diags but got none")
} else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, tc.wantErr)
}
}
if !cmp.Equal(got.Operation.Excludes, tc.want) {
t.Fatalf("unexpected result\n%s", cmp.Diff(got.Operation.Targets, tc.want))
}
})
}
}
func TestParseApply_excludeAndTarget(t *testing.T) {
got, gotDiags := ParseApply([]string{"-exclude=foo_bar.baz", "-target=foo_bar.bar"})
if len(gotDiags) == 0 {
t.Fatalf("expected error, but there was none")
}
wantDiags := tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Invalid combination of arguments",
"-target and -exclude flags cannot be used together. Please remove one of the flags",
),
}
if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
if len(got.Operation.Targets) > 0 {
t.Errorf("Did not expect operation to parse targets, but it parsed %d targets", len(got.Operation.Targets))
}
if len(got.Operation.Excludes) > 0 {
t.Errorf("Did not expect operation to parse excludes, but it parsed %d targets", len(got.Operation.Excludes))
}
}
func TestParseApply_replace(t *testing.T) {
foobarbaz, _ := addrs.ParseAbsResourceInstanceStr("foo_bar.baz")
foobarbeep, _ := addrs.ParseAbsResourceInstanceStr("foo_bar.beep")
@ -261,15 +340,17 @@ func TestParseApply_replace(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParseApply(tc.args)
if len(diags) > 0 {
if tc.wantErr == "" {
t.Fatalf("unexpected diags: %v", diags)
if tc.wantErr == "" && len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
} else if tc.wantErr != "" {
if len(diags) == 0 {
t.Fatalf("expected diags but got none")
} else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, tc.wantErr)
}
}
if !cmp.Equal(got.Operation.ForceReplace, tc.want) {
t.Fatalf("unexpected result\n%s", cmp.Diff(got.Operation.Targets, tc.want))
t.Fatalf("unexpected result\n%s", cmp.Diff(got.Operation.ForceReplace, tc.want))
}
})
}

View File

@ -69,6 +69,10 @@ type Operation struct {
// their dependencies.
Targets []addrs.Targetable
// Excludes allow limiting an operation to execute on all resources other
// than a set of excluded resource addresses and resources dependent on them.
Excludes []addrs.Targetable
// ForceReplace addresses cause OpenTofu to force a particular set of
// resource instances to generate "replace" actions in any plan where they
// would normally have generated "no-op" or "update" actions.
@ -85,25 +89,25 @@ type Operation struct {
// method Parse to populate the exported fields from these, validating
// the raw values in the process.
targetsRaw []string
excludesRaw []string
forceReplaceRaw []string
destroyRaw bool
refreshOnlyRaw bool
}
// Parse must be called on Operation after initial flag parse. This processes
// the raw target flags into addrs.Targetable values, returning diagnostics if
// invalid.
func (o *Operation) Parse() tfdiags.Diagnostics {
// parseTargetables gets a list of strings, each representing a targetable object, and returns a list of
// addrs.Targetable
// This is used for parsing the input of -target and -exclude flags
func parseTargetables(rawTargetables []string, flag string) ([]addrs.Targetable, tfdiags.Diagnostics) {
var targetables []addrs.Targetable
var diags tfdiags.Diagnostics
o.Targets = nil
for _, tr := range o.targetsRaw {
for _, tr := range rawTargetables {
traversal, syntaxDiags := hclsyntax.ParseTraversalAbs([]byte(tr), "", hcl.Pos{Line: 1, Column: 1})
if syntaxDiags.HasErrors() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Invalid target %q", tr),
fmt.Sprintf("Invalid %s %q", flag, tr),
syntaxDiags[0].Detail,
))
continue
@ -113,14 +117,50 @@ func (o *Operation) Parse() tfdiags.Diagnostics {
if targetDiags.HasErrors() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Invalid target %q", tr),
fmt.Sprintf("Invalid %s %q", flag, tr),
targetDiags[0].Description().Detail,
))
continue
}
o.Targets = append(o.Targets, target.Subject)
targetables = append(targetables, target.Subject)
}
return targetables, diags
}
func parseRawTargetsAndExcludes(targets []string, excludes []string) ([]addrs.Targetable, []addrs.Targetable, tfdiags.Diagnostics) {
var parsedTargets []addrs.Targetable
var parsedExcludes []addrs.Targetable
var diags tfdiags.Diagnostics
if len(targets) > 0 && len(excludes) > 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid combination of arguments",
"-target and -exclude flags cannot be used together. Please remove one of the flags",
))
return parsedTargets, parsedExcludes, diags
}
var parseDiags tfdiags.Diagnostics
parsedTargets, parseDiags = parseTargetables(targets, "target")
diags = diags.Append(parseDiags)
parsedExcludes, parseDiags = parseTargetables(excludes, "exclude")
diags = diags.Append(parseDiags)
return parsedTargets, parsedExcludes, diags
}
// Parse must be called on Operation after initial flag parse. This processes
// the raw target flags into addrs.Targetable values, returning diagnostics if
// invalid.
func (o *Operation) Parse() tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
var parseDiags tfdiags.Diagnostics
o.Targets, o.Excludes, parseDiags = parseRawTargetsAndExcludes(o.targetsRaw, o.excludesRaw)
diags = diags.Append(parseDiags)
for _, raw := range o.forceReplaceRaw {
traversal, syntaxDiags := hclsyntax.ParseTraversalAbs([]byte(raw), "", hcl.Pos{Line: 1, Column: 1})
@ -230,6 +270,7 @@ func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars
f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy")
f.BoolVar(&operation.refreshOnlyRaw, "refresh-only", false, "refresh-only")
f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target")
f.Var((*flagStringSlice)(&operation.excludesRaw), "exclude", "exclude")
f.Var((*flagStringSlice)(&operation.forceReplaceRaw), "replace", "replace")
}

View File

@ -9,6 +9,8 @@ import (
"strings"
"testing"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/opentofu/opentofu/internal/addrs"
@ -134,25 +136,33 @@ func TestParsePlan_targets(t *testing.T) {
"invalid traversal": {
args: []string{"-target=foo."},
want: nil,
wantErr: "Dot must be followed by attribute name",
wantErr: "Invalid target \"foo.\": Dot must be followed by attribute name",
},
"invalid target": {
args: []string{"-target=data[0].foo"},
want: nil,
wantErr: "A data source name is required",
wantErr: "Invalid target \"data[0].foo\": A data source name is required",
},
"empty target": {
args: []string{"-target="},
want: nil,
wantErr: "Invalid target \"\": Must begin with a variable name.", // The error is `Invalid target "": Must begin with a variable name.`
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParsePlan(tc.args)
if len(diags) > 0 {
if tc.wantErr == "" {
t.Fatalf("unexpected diags: %v", diags)
if tc.wantErr == "" && len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
} else if tc.wantErr != "" {
if len(diags) == 0 {
t.Fatalf("expected diags but got none")
} else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, tc.wantErr)
}
}
if !cmp.Equal(got.Operation.Targets, tc.want) {
t.Fatalf("unexpected result\n%s", cmp.Diff(got.Operation.Targets, tc.want))
}
@ -160,6 +170,86 @@ func TestParsePlan_targets(t *testing.T) {
}
}
func TestParsePlan_excludes(t *testing.T) {
foobarbaz, _ := addrs.ParseTargetStr("foo_bar.baz")
boop, _ := addrs.ParseTargetStr("module.boop")
testCases := map[string]struct {
args []string
want []addrs.Targetable
wantErr string
}{
"no excludes by default": {
args: nil,
want: nil,
},
"one exclude": {
args: []string{"-exclude=foo_bar.baz"},
want: []addrs.Targetable{foobarbaz.Subject},
},
"two excludes": {
args: []string{"-exclude=foo_bar.baz", "-exclude", "module.boop"},
want: []addrs.Targetable{foobarbaz.Subject, boop.Subject},
},
"invalid traversal": {
args: []string{"-exclude=foo."},
want: nil,
wantErr: "Invalid exclude \"foo.\": Dot must be followed by attribute name",
},
"invalid exclude": {
args: []string{"-exclude=data[0].foo"},
want: nil,
wantErr: "Invalid exclude \"data[0].foo\": A data source name is required",
},
"empty exclude": {
args: []string{"-exclude="},
want: nil,
wantErr: "Invalid exclude \"\": Must begin with a variable name.",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParsePlan(tc.args)
if tc.wantErr == "" && len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
} else if tc.wantErr != "" {
if len(diags) == 0 {
t.Fatalf("expected diags but got none")
} else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, tc.wantErr)
}
}
if !cmp.Equal(got.Operation.Excludes, tc.want) {
t.Fatalf("unexpected result\n%s", cmp.Diff(got.Operation.Excludes, tc.want))
}
})
}
}
func TestParsePlan_excludeAndTarget(t *testing.T) {
got, gotDiags := ParsePlan([]string{"-exclude=foo_bar.baz", "-target=foo_bar.bar"})
if len(gotDiags) == 0 {
t.Fatalf("expected error, but there was none")
}
wantDiags := tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Invalid combination of arguments",
"-target and -exclude flags cannot be used together. Please remove one of the flags",
),
}
if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
if len(got.Operation.Targets) > 0 {
t.Errorf("Did not expect operation to parse targets, but it parsed %d targets", len(got.Operation.Targets))
}
if len(got.Operation.Excludes) > 0 {
t.Errorf("Did not expect operation to parse excludes, but it parsed %d targets", len(got.Operation.Excludes))
}
}
func TestParsePlan_vars(t *testing.T) {
testCases := map[string]struct {
args []string

View File

@ -9,6 +9,8 @@ import (
"strings"
"testing"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/google/go-cmp/cmp"
"github.com/opentofu/opentofu/internal/addrs"
)
@ -119,13 +121,16 @@ func TestParseRefresh_targets(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParseRefresh(tc.args)
if len(diags) > 0 {
if tc.wantErr == "" {
t.Fatalf("unexpected diags: %v", diags)
if tc.wantErr == "" && len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
} else if tc.wantErr != "" {
if len(diags) == 0 {
t.Fatalf("expected diags but got none")
} else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, tc.wantErr)
}
}
if !cmp.Equal(got.Operation.Targets, tc.want) {
t.Fatalf("unexpected result\n%s", cmp.Diff(got.Operation.Targets, tc.want))
}
@ -133,6 +138,80 @@ func TestParseRefresh_targets(t *testing.T) {
}
}
func TestParseRefresh_excludes(t *testing.T) {
foobarbaz, _ := addrs.ParseTargetStr("foo_bar.baz")
boop, _ := addrs.ParseTargetStr("module.boop")
testCases := map[string]struct {
args []string
want []addrs.Targetable
wantErr string
}{
"no excludes by default": {
args: nil,
want: nil,
},
"one exclude": {
args: []string{"-exclude=foo_bar.baz"},
want: []addrs.Targetable{foobarbaz.Subject},
},
"two excludes": {
args: []string{"-exclude=foo_bar.baz", "-exclude", "module.boop"},
want: []addrs.Targetable{foobarbaz.Subject, boop.Subject},
},
"invalid traversal": {
args: []string{"-exclude=foo."},
want: nil,
wantErr: "Dot must be followed by attribute name",
},
"invalid target": {
args: []string{"-exclude=data[0].foo"},
want: nil,
wantErr: "A data source name is required",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, diags := ParseRefresh(tc.args)
if tc.wantErr == "" && len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
} else if tc.wantErr != "" {
if len(diags) == 0 {
t.Fatalf("expected diags but got none")
} else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, tc.wantErr)
}
}
if !cmp.Equal(got.Operation.Excludes, tc.want) {
t.Fatalf("unexpected result\n%s", cmp.Diff(got.Operation.Excludes, tc.want))
}
})
}
}
func TestParseRefresh_excludeAndTarget(t *testing.T) {
got, gotDiags := ParseRefresh([]string{"-exclude=foo_bar.baz", "-target=foo_bar.bar"})
if len(gotDiags) == 0 {
t.Fatalf("expected error, but there was none")
}
wantDiags := tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Invalid combination of arguments",
"-target and -exclude flags cannot be used together. Please remove one of the flags",
),
}
if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
if len(got.Operation.Targets) > 0 {
t.Errorf("Did not expect operation to parse targets, but it parsed %d targets", len(got.Operation.Targets))
}
if len(got.Operation.Excludes) > 0 {
t.Errorf("Did not expect operation to parse excludes, but it parsed %d targets", len(got.Operation.Excludes))
}
}
func TestParseRefresh_vars(t *testing.T) {
testCases := map[string]struct {
args []string

View File

@ -213,6 +213,10 @@ type Meta struct {
targets []addrs.Targetable
targetFlags []string
// Excludes for this context (private)
excludes []addrs.Targetable
excludeFlags []string
// Internal fields
color bool
oldUi cli.Ui
@ -618,6 +622,7 @@ func (m *Meta) extendedFlagSet(n string) *flag.FlagSet {
f.BoolVar(&m.input, "input", true, "input")
f.Var((*FlagStringSlice)(&m.targetFlags), "target", "resource to target")
f.Var((*FlagStringSlice)(&m.excludeFlags), "exclude", "resource to exclude")
f.BoolVar(&m.compactWarnings, "compact-warnings", false, "use compact warnings")
f.BoolVar(&m.consolidateWarnings, "consolidate-warnings", true, "consolidate warnings")
f.BoolVar(&m.consolidateErrors, "consolidate-errors", false, "consolidate errors")

View File

@ -443,6 +443,7 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType, enc encryptio
Encryption: enc,
PlanOutBackend: planOutBackend,
Targets: m.targets,
Excludes: m.excludes,
UIIn: m.UIInput(),
UIOut: m.Ui,
Workspace: workspace,

View File

@ -166,6 +166,7 @@ func (c *PlanCommand) OperationRequest(
opReq.PlanOutPath = planOutPath
opReq.GenerateConfigOut = generateConfigOut
opReq.Targets = args.Targets
opReq.Excludes = args.Excludes
opReq.ForceReplace = args.ForceReplace
opReq.Type = backend.OperationTypePlan
opReq.View = view.Operation()
@ -238,7 +239,14 @@ Plan Customization Options:
resource, or resource instance and all of its
dependencies. You can use this option multiple times to
include more than one object. This is for exceptional
use only.
use only. Cannot be used alongside the -exclude flag
-exclude=resource Limit the planning operation to not operate on the given
module, resource, or resource instance and all of the
resources and modules that depend on it. You can use this
option multiple times to exclude more than one object.
This is for exceptional use only. Cannot be used alongside
the -target flag
-var 'foo=bar' Set a value for one of the input variables in the root
module of the configuration. Use this option more than

View File

@ -1401,6 +1401,92 @@ func TestPlan_targetFlagsDiags(t *testing.T) {
}
}
// Config with multiple resources, targeted plan with exclude
func TestPlan_excluded(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("apply-excluded"), td)
defer testChdir(t, td)()
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
},
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
view, done := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
args := []string{
"-exclude", "test_instance.bar",
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
if got, want := output.Stdout(), "3 to add, 0 to change, 0 to destroy"; !strings.Contains(got, want) {
t.Fatalf("bad change summary, want %q, got:\n%s", want, got)
}
}
// Diagnostics for invalid -exclude flags
func TestPlan_excludeFlagsDiags(t *testing.T) {
testCases := map[string]string{
"test_instance.": "Dot must be followed by attribute name.",
"test_instance": "Resource specification must include a resource type and name.",
}
for exclude, wantDiag := range testCases {
t.Run(exclude, func(t *testing.T) {
td := testTempDir(t)
defer os.RemoveAll(td)
defer testChdir(t, td)()
view, done := testView(t)
c := &PlanCommand{
Meta: Meta{
View: view,
},
}
args := []string{
"-exclude", exclude,
}
code := c.Run(args)
output := done(t)
if code != 1 {
t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
}
got := output.Stderr()
if !strings.Contains(got, exclude) {
t.Fatalf("bad error output, want %q, got:\n%s", exclude, got)
}
if !strings.Contains(got, wantDiag) {
t.Fatalf("bad error output, want %q, got:\n%s", wantDiag, got)
}
})
}
}
func TestPlan_replace(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("plan-replace"), td)

View File

@ -150,6 +150,7 @@ func (c *RefreshCommand) OperationRequest(be backend.Enhanced, view views.Refres
opReq.ConfigDir = "."
opReq.Hooks = view.Hooks()
opReq.Targets = args.Targets
opReq.Excludes = args.Excludes
opReq.Type = backend.OperationTypeRefresh
opReq.View = view.Operation()
@ -205,6 +206,11 @@ Options:
will be performed. All locations, for all errors
will be listed. Disabled by default
-exclude=resource Resource to exclude. Operation will be limited to all
resources that are not excluded or dependent on excluded
resources. This flag can be used multiple times. Cannot
be used alongside the -target flag.
-input=true Ask for input for variables if not directly set.
-lock=false Don't hold a state lock during the operation. This is
@ -219,7 +225,8 @@ Options:
-target=resource Resource to target. Operation will be limited to this
resource and its dependencies. This flag can be used
multiple times.
multiple times. Cannot be used alongside the -exclude
flag.
-var 'foo=bar' Set a variable in the OpenTofu configuration. This
flag can be set multiple times.

View File

@ -852,6 +852,100 @@ func TestRefresh_targetFlagsDiags(t *testing.T) {
}
}
// Config with multiple resources, targeted refresh with exclude
func TestRefresh_excluded(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("refresh-targeted"), td)
defer testChdir(t, td)()
state := testState()
statePath := testStateFile(t, state)
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
},
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
view, done := testView(t)
c := &RefreshCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
args := []string{
"-exclude", "test_instance.bar",
"-state", statePath,
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
got := output.Stdout()
if want := "test_instance.foo: Refreshing"; !strings.Contains(got, want) {
t.Fatalf("expected output to contain %q, got:\n%s", want, got)
}
if doNotWant := "test_instance.bar: Refreshing"; strings.Contains(got, doNotWant) {
t.Fatalf("expected output not to contain %q, got:\n%s", doNotWant, got)
}
}
// Diagnostics for invalid -exclude flags
func TestRefresh_excludeFlagsDiags(t *testing.T) {
testCases := map[string]string{
"test_instance.": "Dot must be followed by attribute name.",
"test_instance": "Resource specification must include a resource type and name.",
}
for exclude, wantDiag := range testCases {
t.Run(exclude, func(t *testing.T) {
td := testTempDir(t)
defer os.RemoveAll(td)
defer testChdir(t, td)()
view, done := testView(t)
c := &RefreshCommand{
Meta: Meta{
View: view,
},
}
args := []string{
"-exclude", exclude,
}
code := c.Run(args)
output := done(t)
if code != 1 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
got := output.Stderr()
if !strings.Contains(got, exclude) {
t.Fatalf("bad error output, want %q, got:\n%s", exclude, got)
}
if !strings.Contains(got, wantDiag) {
t.Fatalf("bad error output, want %q, got:\n%s", wantDiag, got)
}
})
}
}
func TestRefresh_warnings(t *testing.T) {
// Create a temporary working directory that is empty
td := t.TempDir()

View File

@ -1024,25 +1024,26 @@ func TestTest_PartialUpdates(t *testing.T) {
Warning: Resource targeting is in effect
You are creating a plan with the -target option, which means that the result
of this plan may not represent all of the changes requested by the current
configuration.
You are creating a plan with either the -target option or the -exclude
option, which means that the result of this plan may not represent all of the
changes requested by the current configuration.
The -target option is not for routine use, and is provided only for
exceptional situations such as recovering from errors or mistakes, or when
OpenTofu specifically suggests to use it as part of an error message.
The -target and -exclude options are not for routine use, and are provided
only for exceptional situations such as recovering from errors or mistakes,
or when OpenTofu specifically suggests to use it as part of an error message.
Warning: Applied changes may be incomplete
The plan was created with the -target option in effect, so some changes
requested in the configuration may have been ignored and the output values
may not be fully updated. Run the following command to verify that no other
changes are pending:
The plan was created with the -target or the -exclude option in effect, so
some changes requested in the configuration may have been ignored and the
output values may not be fully updated. Run the following command to verify
that no other changes are pending:
tofu plan
Note that the -target option is not suitable for routine use, and is provided
only for exceptional situations such as recovering from errors or mistakes,
or when OpenTofu specifically suggests to use it as part of an error message.
Note that the -target and -exclude options are not suitable for routine use,
and are provided only for exceptional situations such as recovering from
errors or mistakes, or when OpenTofu specifically suggests to use it as part
of an error message.
run "second"... pass
Success! 2 passed, 0 failed.
@ -1055,25 +1056,26 @@ Success! 2 passed, 0 failed.
Warning: Resource targeting is in effect
You are creating a plan with the -target option, which means that the result
of this plan may not represent all of the changes requested by the current
configuration.
You are creating a plan with either the -target option or the -exclude
option, which means that the result of this plan may not represent all of the
changes requested by the current configuration.
The -target option is not for routine use, and is provided only for
exceptional situations such as recovering from errors or mistakes, or when
OpenTofu specifically suggests to use it as part of an error message.
The -target and -exclude options are not for routine use, and are provided
only for exceptional situations such as recovering from errors or mistakes,
or when OpenTofu specifically suggests to use it as part of an error message.
Warning: Applied changes may be incomplete
The plan was created with the -target option in effect, so some changes
requested in the configuration may have been ignored and the output values
may not be fully updated. Run the following command to verify that no other
changes are pending:
The plan was created with the -target or the -exclude option in effect, so
some changes requested in the configuration may have been ignored and the
output values may not be fully updated. Run the following command to verify
that no other changes are pending:
tofu plan
Note that the -target option is not suitable for routine use, and is provided
only for exceptional situations such as recovering from errors or mistakes,
or when OpenTofu specifically suggests to use it as part of an error message.
Note that the -target and -exclude options are not suitable for routine use,
and are provided only for exceptional situations such as recovering from
errors or mistakes, or when OpenTofu specifically suggests to use it as part
of an error message.
Failure! 0 passed, 1 failed.
`,

View File

@ -0,0 +1,9 @@
resource "test_instance" "foo" {
count = 2
}
resource "test_instance" "bar" {
}
resource "test_instance" "baz" {
}

View File

@ -387,6 +387,10 @@ type Plan struct {
// target addresses are present, the plan applies to the whole
// configuration.
TargetAddrs []string `protobuf:"bytes,5,rep,name=target_addrs,json=targetAddrs,proto3" json:"target_addrs,omitempty"`
// An unordered set of exclude addresses to exclude when applying. If no
// target addresses are present, the plan applies to the whole
// configuration.
ExcludeAddrs []string `protobuf:"bytes,6,rep,name=exclude_addrs,json=excludeAddrs,proto3" json:"exclude_addrs,omitempty"`
// An unordered set of force-replace addresses to include when applying.
// This must match the set of addresses that was used when creating the
// plan, or else applying the plan will fail when it reaches a different
@ -499,6 +503,13 @@ func (x *Plan) GetTargetAddrs() []string {
return nil
}
func (x *Plan) GetExcludeAddrs() []string {
if x != nil {
return x.ExcludeAddrs
}
return nil
}
func (x *Plan) GetForceReplaceAddrs() []string {
if x != nil {
return x.ForceReplaceAddrs
@ -1348,7 +1359,7 @@ var File_planfile_proto protoreflect.FileDescriptor
var file_planfile_proto_rawDesc = []byte{
0x0a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x12, 0x06, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x22, 0xdf, 0x06, 0x0a, 0x04, 0x50, 0x6c, 0x61,
0x12, 0x06, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x22, 0x84, 0x07, 0x0a, 0x04, 0x50, 0x6c, 0x61,
0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01,
0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x07, 0x75,
0x69, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x74,
@ -1377,178 +1388,181 @@ var file_planfile_proto_rawDesc = []byte{
0x6c, 0x74, 0x73, 0x52, 0x0c, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74,
0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72,
0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41,
0x64, 0x64, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65,
0x70, 0x6c, 0x61, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28,
0x09, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x41,
0x64, 0x64, 0x72, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72,
0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52,
0x10, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
0x6e, 0x12, 0x29, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x18, 0x0d, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x42, 0x61, 0x63, 0x6b,
0x65, 0x6e, 0x64, 0x52, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x4b, 0x0a, 0x13,
0x72, 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75,
0x74, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x70, 0x6c,
0x61, 0x6e, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x5f, 0x61, 0x74, 0x74, 0x72, 0x52, 0x12, 0x72, 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x41,
0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x1a, 0x52, 0x0a, 0x0e, 0x56, 0x61, 0x72, 0x69, 0x61,
0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70,
0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x4d, 0x0a, 0x0d, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x12, 0x1a, 0x0a, 0x08,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x04, 0x61, 0x74, 0x74, 0x72,
0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e,
0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x61, 0x74, 0x74, 0x72, 0x22, 0x69, 0x0a, 0x07, 0x42, 0x61,
0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x06, 0x63, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c,
0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52,
0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73,
0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x77, 0x6f, 0x72, 0x6b,
0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xc0, 0x02, 0x0a, 0x06, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61,
0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x42, 0x0a, 0x16, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65,
0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73,
0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e,
0x50, 0x61, 0x74, 0x68, 0x52, 0x14, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x53, 0x65, 0x6e, 0x73,
0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12, 0x40, 0x0a, 0x15, 0x61, 0x66,
0x74, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61,
0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c,
0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x13, 0x61, 0x66, 0x74, 0x65, 0x72, 0x53, 0x65,
0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12, 0x2f, 0x0a, 0x09,
0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69,
0x6e, 0x67, 0x52, 0x09, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x29, 0x0a,
0x10, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74,
0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xd3, 0x02, 0x0a, 0x16, 0x52, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61,
0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0d, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x76, 0x5f,
0x72, 0x75, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
0x70, 0x72, 0x65, 0x76, 0x52, 0x75, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x64,
0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08,
0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e,
0x67, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61,
0x6e, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65,
0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x10, 0x72, 0x65,
0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x0b,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61,
0x74, 0x68, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6c,
0x61, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65,
0x61, 0x73, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x74, 0x66, 0x70,
0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74,
0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e,
0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x68,
0x0a, 0x0c, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x61, 0x6e,
0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65,
0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73,
0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x22, 0xfc, 0x03, 0x0a, 0x0c, 0x43, 0x68, 0x65,
0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x04, 0x6b, 0x69, 0x6e,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e,
0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x4f, 0x62,
0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x1f,
0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x41, 0x64, 0x64, 0x72, 0x12,
0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65,
0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74,
0x61, 0x74, 0x75, 0x73, 0x12, 0x3b, 0x0a, 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18,
0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43,
0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x4f, 0x62, 0x6a, 0x65,
0x63, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74,
0x73, 0x1a, 0x8f, 0x01, 0x0a, 0x0c, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x75,
0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x64, 0x64,
0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41,
0x64, 0x64, 0x72, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65,
0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x66, 0x61, 0x69, 0x6c,
0x75, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03,
0x28, 0x09, 0x52, 0x0f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x73, 0x22, 0x34, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a,
0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x41,
0x53, 0x53, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x41, 0x49, 0x4c, 0x10, 0x02, 0x12, 0x09,
0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x22, 0x5c, 0x0a, 0x0a, 0x4f, 0x62, 0x6a,
0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45,
0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x53, 0x4f,
0x55, 0x52, 0x43, 0x45, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x4f, 0x55, 0x54, 0x50, 0x55, 0x54,
0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x48, 0x45, 0x43,
0x4b, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x50, 0x55, 0x54, 0x5f, 0x56, 0x41, 0x52,
0x49, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x04, 0x22, 0x28, 0x0a, 0x0c, 0x44, 0x79, 0x6e, 0x61, 0x6d,
0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61,
0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63,
0x6b, 0x22, 0xa5, 0x01, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x27, 0x0a, 0x05, 0x73, 0x74,
0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c,
0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65, 0x70, 0x52, 0x05, 0x73, 0x74,
0x65, 0x70, 0x73, 0x1a, 0x74, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70, 0x12, 0x27, 0x0a, 0x0e, 0x61,
0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f,
0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c,
0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48,
0x00, 0x52, 0x0a, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x42, 0x0a, 0x0a,
0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x1b, 0x0a, 0x09, 0x49, 0x6d, 0x70,
0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x2a, 0x31, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0a,
0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45,
0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x46, 0x52, 0x45,
0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x02, 0x2a, 0x7c, 0x0a, 0x06, 0x41, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4f, 0x50, 0x10, 0x00, 0x12, 0x0a, 0x0a,
0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41,
0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12,
0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x05, 0x12, 0x16, 0x0a, 0x12, 0x44,
0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54,
0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x48,
0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x07, 0x12, 0x0a, 0x0a, 0x06, 0x46,
0x4f, 0x52, 0x47, 0x45, 0x54, 0x10, 0x08, 0x2a, 0xc8, 0x03, 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45,
0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45,
0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, 0x41, 0x49, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12,
0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x52, 0x45,
0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x50, 0x4c, 0x41,
0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x4e, 0x4f,
0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x25, 0x0a, 0x21, 0x44, 0x45,
0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f,
0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10,
0x04, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41,
0x55, 0x53, 0x45, 0x5f, 0x57, 0x52, 0x4f, 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x50, 0x45, 0x54, 0x49,
0x54, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45,
0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x49,
0x4e, 0x44, 0x45, 0x58, 0x10, 0x06, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45,
0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x45, 0x41, 0x43, 0x48, 0x5f, 0x4b, 0x45,
0x59, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45,
0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x44, 0x55, 0x4c, 0x45, 0x10,
0x08, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f,
0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x53, 0x10, 0x09, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45,
0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49,
0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x0a, 0x12, 0x23, 0x0a, 0x1f, 0x52,
0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x44, 0x45, 0x50, 0x45,
0x4e, 0x44, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x0b,
0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45,
0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, 0x10, 0x0d, 0x12,
0x21, 0x0a, 0x1d, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53,
0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x56, 0x45, 0x5f, 0x54, 0x41, 0x52, 0x47, 0x45, 0x54,
0x10, 0x0c, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x6f, 0x66, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x6f,
0x66, 0x75, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e,
0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x64, 0x64, 0x72, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f,
0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x63,
0x6c, 0x75, 0x64, 0x65, 0x41, 0x64, 0x64, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x6f, 0x72,
0x63, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x73,
0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70,
0x6c, 0x61, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x65, 0x72,
0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0e,
0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x56,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e,
0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e,
0x2e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x52, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e,
0x64, 0x12, 0x4b, 0x0a, 0x13, 0x72, 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x5f, 0x61, 0x74,
0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a,
0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x52, 0x12, 0x72, 0x65, 0x6c, 0x65,
0x76, 0x61, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1c,
0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x15, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x1a, 0x52, 0x0a, 0x0e,
0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63,
0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
0x1a, 0x4d, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74,
0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x20, 0x0a,
0x04, 0x61, 0x74, 0x74, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66,
0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x61, 0x74, 0x74, 0x72, 0x22,
0x69, 0x0a, 0x07, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79,
0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c,
0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14,
0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56,
0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09,
0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xc0, 0x02, 0x0a, 0x06, 0x43,
0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18,
0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x41,
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a,
0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e,
0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61,
0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x42, 0x0a, 0x16, 0x62,
0x65, 0x66, 0x6f, 0x72, 0x65, 0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f,
0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66,
0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x14, 0x62, 0x65, 0x66, 0x6f, 0x72,
0x65, 0x53, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12,
0x40, 0x0a, 0x15, 0x61, 0x66, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69,
0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c,
0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x13, 0x61, 0x66,
0x74, 0x65, 0x72, 0x53, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68,
0x73, 0x12, 0x2f, 0x0a, 0x09, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x05,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x49, 0x6d,
0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x09, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69,
0x6e, 0x67, 0x12, 0x29, 0x0a, 0x10, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x5f,
0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x67, 0x65,
0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xd3, 0x02,
0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e,
0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72,
0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x22, 0x0a, 0x0d,
0x70, 0x72, 0x65, 0x76, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0e, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x76, 0x52, 0x75, 0x6e, 0x41, 0x64, 0x64, 0x72,
0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18,
0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x4b, 0x65,
0x79, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x08, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a,
0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e,
0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63,
0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65,
0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12,
0x37, 0x0a, 0x10, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x70, 0x6c,
0x61, 0x63, 0x65, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c,
0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65,
0x64, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61,
0x73, 0x6f, 0x6e, 0x22, 0x68, 0x0a, 0x0c, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x43, 0x68, 0x61,
0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e,
0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12,
0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01,
0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x22, 0xfc, 0x03,
0x0a, 0x0c, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x33,
0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x74,
0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c,
0x74, 0x73, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x04, 0x6b,
0x69, 0x6e, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x61, 0x64,
0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x41, 0x64, 0x64, 0x72, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68,
0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75,
0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3b, 0x0a, 0x07, 0x6f, 0x62, 0x6a,
0x65, 0x63, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x66, 0x70,
0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73,
0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x07, 0x6f,
0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x1a, 0x8f, 0x01, 0x0a, 0x0c, 0x4f, 0x62, 0x6a, 0x65, 0x63,
0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x62, 0x6a, 0x65, 0x63,
0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x62,
0x6a, 0x65, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61,
0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a,
0x10, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65,
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x34, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74,
0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12,
0x08, 0x0a, 0x04, 0x50, 0x41, 0x53, 0x53, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x41, 0x49,
0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x22, 0x5c,
0x0a, 0x0a, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x0f, 0x0a, 0x0b,
0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a,
0x08, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x4f,
0x55, 0x54, 0x50, 0x55, 0x54, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x02, 0x12, 0x09, 0x0a,
0x05, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x50, 0x55,
0x54, 0x5f, 0x56, 0x41, 0x52, 0x49, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x04, 0x22, 0x28, 0x0a, 0x0c,
0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07,
0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d,
0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x22, 0xa5, 0x01, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12,
0x27, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11,
0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65,
0x70, 0x52, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x74, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70,
0x12, 0x27, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61,
0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72,
0x69, 0x62, 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x65, 0x6c, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14,
0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56,
0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b,
0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x1b,
0x0a, 0x09, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x2a, 0x31, 0x0a, 0x04, 0x4d,
0x6f, 0x64, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x00, 0x12,
0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c,
0x52, 0x45, 0x46, 0x52, 0x45, 0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x02, 0x2a, 0x7c,
0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4f, 0x50,
0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x08,
0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41,
0x54, 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x05,
0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f,
0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45, 0x41,
0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x07,
0x12, 0x0a, 0x0a, 0x06, 0x46, 0x4f, 0x52, 0x47, 0x45, 0x54, 0x10, 0x08, 0x2a, 0xc8, 0x03, 0x0a,
0x1c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63,
0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x08, 0x0a,
0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x50, 0x4c, 0x41,
0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, 0x41, 0x49, 0x4e, 0x54,
0x45, 0x44, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f,
0x42, 0x59, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d,
0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f,
0x43, 0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12,
0x25, 0x0a, 0x21, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53,
0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f,
0x4e, 0x46, 0x49, 0x47, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45,
0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x57, 0x52, 0x4f, 0x4e, 0x47, 0x5f, 0x52,
0x45, 0x50, 0x45, 0x54, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x44,
0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f,
0x55, 0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x06, 0x12, 0x1b, 0x0a, 0x17, 0x44,
0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x45, 0x41,
0x43, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, 0x44, 0x45, 0x4c, 0x45,
0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f,
0x44, 0x55, 0x4c, 0x45, 0x10, 0x08, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43,
0x45, 0x5f, 0x42, 0x59, 0x5f, 0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x53, 0x10, 0x09, 0x12,
0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f,
0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x0a,
0x12, 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45,
0x5f, 0x44, 0x45, 0x50, 0x45, 0x4e, 0x44, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x50, 0x45, 0x4e, 0x44,
0x49, 0x4e, 0x47, 0x10, 0x0b, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45,
0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x4e, 0x45, 0x53, 0x54,
0x45, 0x44, 0x10, 0x0d, 0x12, 0x21, 0x0a, 0x1d, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42,
0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x56, 0x45, 0x5f, 0x54,
0x41, 0x52, 0x47, 0x45, 0x54, 0x10, 0x0c, 0x42, 0x40, 0x5a, 0x3e, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x74, 0x6f, 0x66, 0x75, 0x2f, 0x6f,
0x70, 0x65, 0x6e, 0x74, 0x6f, 0x66, 0x75, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c,
0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f,
0x70, 0x6c, 0x61, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
}
var (

View File

@ -66,6 +66,11 @@ message Plan {
// configuration.
repeated string target_addrs = 5;
// An unordered set of exclude addresses to exclude when applying. If no
// target addresses are present, the plan applies to the whole
// configuration.
repeated string exclude_addrs = 6;
// An unordered set of force-replace addresses to include when applying.
// This must match the set of addresses that was used when creating the
// plan, or else applying the plan will fail when it reaches a different

View File

@ -46,6 +46,7 @@ type Plan struct {
Changes *Changes
DriftedResources []*ResourceInstanceChangeSrc
TargetAddrs []addrs.Targetable
ExcludeAddrs []addrs.Targetable
ForceReplaceAddrs []addrs.AbsResourceInstance
Backend Backend

View File

@ -223,6 +223,14 @@ func readTfplan(r io.Reader) (*plans.Plan, error) {
plan.TargetAddrs = append(plan.TargetAddrs, target.Subject)
}
for _, rawExcludeAddr := range rawPlan.ExcludeAddrs {
exclude, diags := addrs.ParseTargetStr(rawExcludeAddr)
if diags.HasErrors() {
return nil, fmt.Errorf("plan contains invalid exclude address %q: %w", exclude, diags.Err())
}
plan.ExcludeAddrs = append(plan.ExcludeAddrs, exclude.Subject)
}
for _, rawReplaceAddr := range rawPlan.ForceReplaceAddrs {
addr, diags := addrs.ParseAbsResourceInstanceStr(rawReplaceAddr)
if diags.HasErrors() {
@ -610,6 +618,10 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error {
rawPlan.TargetAddrs = append(rawPlan.TargetAddrs, targetAddr.String())
}
for _, excludeAddr := range plan.ExcludeAddrs {
rawPlan.ExcludeAddrs = append(rawPlan.ExcludeAddrs, excludeAddr.String())
}
for _, replaceAddr := range plan.ForceReplaceAddrs {
rawPlan.ForceReplaceAddrs = append(rawPlan.ForceReplaceAddrs, replaceAddr.String())
}

View File

@ -93,14 +93,14 @@ func (c *Context) Apply(plan *plans.Plan, config *configs.Config) (*states.State
newState.PruneResourceHusks()
}
if len(plan.TargetAddrs) > 0 {
if len(plan.TargetAddrs) > 0 || len(plan.ExcludeAddrs) > 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Applied changes may be incomplete",
`The plan was created with the -target option in effect, so some changes requested in the configuration may have been ignored and the output values may not be fully updated. Run the following command to verify that no other changes are pending:
`The plan was created with the -target or the -exclude option in effect, so some changes requested in the configuration may have been ignored and the output values may not be fully updated. Run the following command to verify that no other changes are pending:
tofu plan
Note that the -target option is not suitable for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
Note that the -target and -exclude options are not suitable for routine use, and are provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
))
}
@ -181,6 +181,7 @@ func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate
RootVariableValues: variables,
Plugins: c.plugins,
Targets: plan.TargetAddrs,
Excludes: plan.ExcludeAddrs,
ForceReplace: plan.ForceReplaceAddrs,
Operation: operation,
ExternalReferences: plan.ExternalReferences,

File diff suppressed because it is too large Load Diff

View File

@ -7149,52 +7149,51 @@ func TestContext2Apply_targetedDestroy(t *testing.T) {
m := testModule(t, "destroy-targeted")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.a").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
)
root.SetOutputValue("out", cty.StringVal("bar"), false)
var state *states.State
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.b").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-bcd345"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
)
// First plan and apply a create operation
if diags := ctx.Validate(m); diags.HasErrors() {
t.Fatalf("validate errors: %s", diags.Err())
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
assertNoErrors(t, diags)
if diags := ctx.Validate(m); diags.HasErrors() {
t.Fatalf("validate errors: %s", diags.Err())
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply err: %s", diags.Err())
}
}
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "a",
),
},
})
assertNoErrors(t, diags)
{
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "a",
),
},
})
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("diags: %s", diags.Err())
}
}
mod := state.RootModule()
@ -7223,10 +7222,10 @@ func TestContext2Apply_targetedDestroy(t *testing.T) {
t.Fatalf("expected 1 outputs, got: %#v", mod.OutputValues)
}
// the module instance should remain
// the module instance should not remain
mod = state.Module(addrs.RootModuleInstance.Child("child", addrs.NoKey))
if len(mod.Resources) != 1 {
t.Fatalf("expected 1 resources, got: %#v", mod.Resources)
if mod != nil {
t.Fatalf("expected child module to not exist in state, but it does")
}
}
@ -7554,7 +7553,8 @@ func TestContext2Apply_targetedModuleUnrelatedOutputs(t *testing.T) {
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
_ = state.EnsureModule(addrs.RootModuleInstance.Child("child2", addrs.NoKey))
child1 := state.EnsureModule(addrs.RootModuleInstance.Child("child1", addrs.NoKey))
child1.SetOutputValue("instance_id", cty.StringVal("something"), false)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
@ -7643,6 +7643,7 @@ func TestContext2Apply_targetedResourceOrphanModule(t *testing.T) {
m := testModule(t, "apply-targeted-resource-orphan-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
p.ApplyResourceChangeFn = testApplyFn
state := states.NewState()
child := state.EnsureModule(addrs.RootModuleInstance.Child("parent", addrs.NoKey))
@ -7650,7 +7651,7 @@ func TestContext2Apply_targetedResourceOrphanModule(t *testing.T) {
mustResourceInstanceAddr("aws_instance.bar").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"type":"aws_instance"}`),
AttrsJSON: []byte(`{"id":"abc","type":"aws_instance"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
)
@ -7671,9 +7672,24 @@ func TestContext2Apply_targetedResourceOrphanModule(t *testing.T) {
})
assertNoErrors(t, diags)
if _, diags := ctx.Apply(plan, m); diags.HasErrors() {
state, diags = ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatalf("apply errors: %s", diags.Err())
}
checkStateString(t, state, `
aws_instance.foo:
ID = foo
provider = provider["registry.opentofu.org/hashicorp/aws"]
type = aws_instance
module.parent:
aws_instance.bar:
ID = abc
provider = provider["registry.opentofu.org/hashicorp/aws"]
type = aws_instance
`)
}
func TestContext2Apply_unknownAttribute(t *testing.T) {

View File

@ -67,6 +67,16 @@ type PlanOpts struct {
// warnings as part of the planning result.
Targets []addrs.Targetable
// If Excludes has a non-zero length then it activates targeted planning
// mode, where OpenTofu will take actions only for resource instances
// that are not mentioned in this set and are not dependent on targets
// mentioned in this set.
//
// Targeted planning mode is intended for exceptional use only,
// and so populating this field will cause OpenTofu to generate extra
// warnings as part of the planning result.
Excludes []addrs.Targetable
// ForceReplace is a set of resource instance addresses whose corresponding
// objects should be forced planned for replacement if the provider's
// plan would otherwise have been to either update the object in-place or
@ -186,13 +196,13 @@ func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts
varDiags := checkInputVariables(config.Module.Variables, opts.SetVariables)
diags = diags.Append(varDiags)
if len(opts.Targets) > 0 {
if len(opts.Targets) > 0 || len(opts.Excludes) > 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Resource targeting is in effect",
`You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
`You are creating a plan with either the -target option or the -exclude option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
The -target and -exclude options are not for routine use, and are provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
))
}
@ -241,6 +251,7 @@ The -target option is not for routine use, and is provided only for exceptional
if plan != nil {
plan.VariableValues = varVals
plan.TargetAddrs = opts.Targets
plan.ExcludeAddrs = opts.Excludes
} else if !diags.HasErrors() {
panic("nil plan but no errors")
}
@ -460,7 +471,7 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State
return destroyPlan, diags
}
func (c *Context) prePlanFindAndApplyMoves(config *configs.Config, prevRunState *states.State, targets []addrs.Targetable) ([]refactoring.MoveStatement, refactoring.MoveResults) {
func (c *Context) prePlanFindAndApplyMoves(config *configs.Config, prevRunState *states.State) ([]refactoring.MoveStatement, refactoring.MoveResults) {
explicitMoveStmts := refactoring.FindMoveStatements(config)
implicitMoveStmts := refactoring.ImpliedMoveStatements(config, prevRunState, explicitMoveStmts)
var moveStmts []refactoring.MoveStatement
@ -473,11 +484,17 @@ func (c *Context) prePlanFindAndApplyMoves(config *configs.Config, prevRunState
return moveStmts, moveResults
}
func (c *Context) prePlanVerifyTargetedMoves(moveResults refactoring.MoveResults, targets []addrs.Targetable) tfdiags.Diagnostics {
if len(targets) < 1 {
return nil // the following only matters when targeting
func (c *Context) prePlanVerifyTargetedMoves(moveResults refactoring.MoveResults, targets []addrs.Targetable, excludes []addrs.Targetable) tfdiags.Diagnostics {
if len(targets) > 0 {
return c.prePlanVerifyMovesWithTargetFlag(moveResults, targets)
}
if len(excludes) > 0 {
return c.prePlanVerifyMovesWithExcludeFlag(moveResults, excludes)
}
return nil
}
func (c *Context) prePlanVerifyMovesWithTargetFlag(moveResults refactoring.MoveResults, targets []addrs.Targetable) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
var excluded []addrs.AbsResourceInstance
@ -541,6 +558,70 @@ func (c *Context) prePlanVerifyTargetedMoves(moveResults refactoring.MoveResults
return diags
}
func (c *Context) prePlanVerifyMovesWithExcludeFlag(moveResults refactoring.MoveResults, excludes []addrs.Targetable) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
var excluded []addrs.AbsResourceInstance
for _, result := range moveResults.Changes.Values() {
fromExcluded := false
toExcluded := false
for _, excludeAddr := range excludes {
if excludeAddr.TargetContains(result.From) {
fromExcluded = true
}
if excludeAddr.TargetContains(result.To) {
toExcluded = true
}
}
if fromExcluded {
excluded = append(excluded, result.From)
}
if toExcluded {
excluded = append(excluded, result.To)
}
}
if len(excluded) > 0 {
sort.Slice(excluded, func(i, j int) bool {
return excluded[i].Less(excluded[j])
})
var listBuf strings.Builder
var prevResourceAddr addrs.AbsResource
for _, instAddr := range excluded {
// Targeting generally ends up selecting whole resources rather
// than individual instances, because we don't factor in
// individual instances until DynamicExpand, so we're going to
// always show whole resource addresses here, excluding any
// instance keys. (This also neatly avoids dealing with the
// different quoting styles required for string instance keys
// on different shells, which is handy.)
//
// To avoid showing duplicates when we have multiple instances
// of the same resource, we'll remember the most recent
// resource we rendered in prevResource, which is sufficient
// because we sorted the list of instance addresses above, and
// our sort order always groups together instances of the same
// resource.
resourceAddr := instAddr.ContainingResource()
if resourceAddr.Equal(prevResourceAddr) {
continue
}
fmt.Fprintf(&listBuf, "\n -exclude=%q", resourceAddr.String())
prevResourceAddr = resourceAddr
}
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Moved resource instances excluded by targeting",
fmt.Sprintf(
"Resource instances in your current state have moved to new addresses in the latest configuration. OpenTofu must include those resource instances while planning in order to ensure a correct result, but your -exclude=... options exclude some of those resource instances.\n\nTo create a valid plan, either remove your -exclude=... options altogether or just specifically remove the following options:%s\n\nNote that removing these options may include further additional resource instances in your plan, in order to respect object dependencies.",
listBuf.String(),
),
))
}
return diags
}
func (c *Context) postPlanValidateMoves(config *configs.Config, stmts []refactoring.MoveStatement, allInsts instances.Set) tfdiags.Diagnostics {
return refactoring.ValidateMoves(stmts, config, allInsts)
}
@ -662,11 +743,11 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o
log.Printf("[DEBUG] Building and walking plan graph for %s", opts.Mode)
prevRunState = prevRunState.DeepCopy() // don't modify the caller's object when we process the moves
moveStmts, moveResults := c.prePlanFindAndApplyMoves(config, prevRunState, opts.Targets)
moveStmts, moveResults := c.prePlanFindAndApplyMoves(config, prevRunState)
// If resource targeting is in effect then it might conflict with the
// move result.
diags = diags.Append(c.prePlanVerifyTargetedMoves(moveResults, opts.Targets))
diags = diags.Append(c.prePlanVerifyTargetedMoves(moveResults, opts.Targets, opts.Excludes))
if diags.HasErrors() {
// We'll return early here, because if we have any moved resource
// instances excluded by targeting then planning is likely to encounter
@ -765,6 +846,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
RootVariableValues: opts.SetVariables,
Plugins: c.plugins,
Targets: opts.Targets,
Excludes: opts.Excludes,
ForceReplace: opts.ForceReplace,
skipRefresh: opts.SkipRefresh,
preDestroyRefresh: opts.PreDestroyRefresh,
@ -778,16 +860,16 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
return graph, walkPlan, diags
case plans.RefreshOnlyMode:
graph, diags := (&PlanGraphBuilder{
Config: config,
State: prevRunState,
RootVariableValues: opts.SetVariables,
Plugins: c.plugins,
Targets: opts.Targets,
skipRefresh: opts.SkipRefresh,
skipPlanChanges: true, // this activates "refresh only" mode.
Operation: walkPlan,
ExternalReferences: opts.ExternalReferences,
ProviderFunctionTracker: providerFunctionTracker,
Config: config,
State: prevRunState,
RootVariableValues: opts.SetVariables,
Plugins: c.plugins,
Targets: opts.Targets,
Excludes: opts.Excludes,
skipRefresh: opts.SkipRefresh,
skipPlanChanges: true, // this activates "refresh only" mode.
Operation: walkPlan,
ExternalReferences: opts.ExternalReferences,
}).Build(addrs.RootModuleInstance)
return graph, walkPlan, diags
case plans.DestroyMode:
@ -797,6 +879,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
RootVariableValues: opts.SetVariables,
Plugins: c.plugins,
Targets: opts.Targets,
Excludes: opts.Excludes,
skipRefresh: opts.SkipRefresh,
Operation: walkPlanDestroy,
ProviderFunctionTracker: providerFunctionTracker,

View File

@ -1574,9 +1574,9 @@ func TestContext2Plan_movedResourceUntargeted(t *testing.T) {
tfdiags.Sourceless(
tfdiags.Warning,
"Resource targeting is in effect",
`You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
`You are creating a plan with either the -target option or the -exclude option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
The -target and -exclude options are not for routine use, and are provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
),
tfdiags.Sourceless(
tfdiags.Error,
@ -1614,9 +1614,9 @@ Note that adding these options may include further additional resource instances
tfdiags.Sourceless(
tfdiags.Warning,
"Resource targeting is in effect",
`You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
`You are creating a plan with either the -target option or the -exclude option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
The -target and -exclude options are not for routine use, and are provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
),
tfdiags.Sourceless(
tfdiags.Error,
@ -1654,9 +1654,9 @@ Note that adding these options may include further additional resource instances
tfdiags.Sourceless(
tfdiags.Warning,
"Resource targeting is in effect",
`You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
`You are creating a plan with either the -target option or the -exclude option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
The -target and -exclude options are not for routine use, and are provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
),
tfdiags.Sourceless(
tfdiags.Error,
@ -1702,9 +1702,167 @@ Note that adding these options may include further additional resource instances
tfdiags.Sourceless(
tfdiags.Warning,
"Resource targeting is in effect",
`You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
`You are creating a plan with either the -target option or the -exclude option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
The -target and -exclude options are not for routine use, and are provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
),
// ...but now we have no error about test_object.a
}.ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
})
t.Run("excluding instance A", func(t *testing.T) {
_, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
// NOTE: addrA isn't excluded, and it's pending move to addrB
// and so this plan request is invalid.
addrA,
},
})
diags.Sort()
// We're semi-abusing "ForRPC" here just to get diagnostics that are
// more easily comparable than the various different diagnostics types
// tfdiags uses internally. The RPC-friendly diagnostics are also
// comparison-friendly, by discarding all of the dynamic type information.
gotDiags := diags.ForRPC()
wantDiags := tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Warning,
"Resource targeting is in effect",
`You are creating a plan with either the -target option or the -exclude option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
The -target and -exclude options are not for routine use, and are provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
),
tfdiags.Sourceless(
tfdiags.Error,
"Moved resource instances excluded by targeting",
`Resource instances in your current state have moved to new addresses in the latest configuration. OpenTofu must include those resource instances while planning in order to ensure a correct result, but your -exclude=... options exclude some of those resource instances.
To create a valid plan, either remove your -exclude=... options altogether or just specifically remove the following options:
-exclude="test_object.a"
Note that removing these options may include further additional resource instances in your plan, in order to respect object dependencies.`,
),
}.ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
})
t.Run("excluding instance B", func(t *testing.T) {
_, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
addrB,
// NOTE: addrB is excluded here, and it's pending move from
// addrA and so this plan request is invalid.
},
})
diags.Sort()
// We're semi-abusing "ForRPC" here just to get diagnostics that are
// more easily comparable than the various different diagnostics types
// tfdiags uses internally. The RPC-friendly diagnostics are also
// comparison-friendly, by discarding all of the dynamic type information.
gotDiags := diags.ForRPC()
wantDiags := tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Warning,
"Resource targeting is in effect",
`You are creating a plan with either the -target option or the -exclude option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
The -target and -exclude options are not for routine use, and are provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
),
tfdiags.Sourceless(
tfdiags.Error,
"Moved resource instances excluded by targeting",
`Resource instances in your current state have moved to new addresses in the latest configuration. OpenTofu must include those resource instances while planning in order to ensure a correct result, but your -exclude=... options exclude some of those resource instances.
To create a valid plan, either remove your -exclude=... options altogether or just specifically remove the following options:
-exclude="test_object.b"
Note that removing these options may include further additional resource instances in your plan, in order to respect object dependencies.`,
),
}.ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
})
t.Run("excluding both addresses", func(t *testing.T) {
_, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
// NOTE: both addrA nor addrB are excluded here, but there's
// a pending move between them and so this is invalid.
addrA,
addrB,
},
})
diags.Sort()
// We're semi-abusing "ForRPC" here just to get diagnostics that are
// more easily comparable than the various different diagnostics types
// tfdiags uses internally. The RPC-friendly diagnostics are also
// comparison-friendly, by discarding all of the dynamic type information.
gotDiags := diags.ForRPC()
wantDiags := tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Warning,
"Resource targeting is in effect",
`You are creating a plan with either the -target option or the -exclude option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
The -target and -exclude options are not for routine use, and are provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
),
tfdiags.Sourceless(
tfdiags.Error,
"Moved resource instances excluded by targeting",
`Resource instances in your current state have moved to new addresses in the latest configuration. OpenTofu must include those resource instances while planning in order to ensure a correct result, but your -exclude=... options exclude some of those resource instances.
To create a valid plan, either remove your -exclude=... options altogether or just specifically remove the following options:
-exclude="test_object.a"
-exclude="test_object.b"
Note that removing these options may include further additional resource instances in your plan, in order to respect object dependencies.`,
),
}.ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
})
t.Run("without excluding either instance", func(t *testing.T) {
// The error messages in the other subtests above suggest removing
// addresses to the set of excludes. This additional test makes sure that
// following that advice actually leads to a valid result.
_, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
mustResourceInstanceAddr("test_object.unrelated"),
// This time we're excluding neither address,
// to get the same effect an end-user would get if following
// the advice in our error message in the other subtests.
},
})
diags.Sort()
// We're semi-abusing "ForRPC" here just to get diagnostics that are
// more easily comparable than the various different diagnostics types
// tfdiags uses internally. The RPC-friendly diagnostics are also
// comparison-friendly, by discarding all of the dynamic type information.
gotDiags := diags.ForRPC()
wantDiags := tfdiags.Diagnostics{
// Still get the warning about the -target option...
tfdiags.Sourceless(
tfdiags.Warning,
"Resource targeting is in effect",
`You are creating a plan with either the -target option or the -exclude option, which means that the result of this plan may not represent all of the changes requested by the current configuration.
The -target and -exclude options are not for routine use, and are provided only for exceptional situations such as recovering from errors or mistakes, or when OpenTofu specifically suggests to use it as part of an error message.`,
),
// ...but now we have no error about test_object.a
}.ForRPC()
@ -1759,7 +1917,7 @@ resource "test_object" "b" {
},
})
_, diags := ctx.Plan(m, state, &PlanOpts{
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
Targets: []addrs.Targetable{
addrA,
@ -1767,6 +1925,79 @@ resource "test_object" "b" {
})
//
assertNoErrors(t, diags)
if len(plan.Changes.Resources) != 1 {
t.Fatalf("expected 1 resource change, but got %d", len(plan.Changes.Resources))
}
instPlan := plan.Changes.ResourceInstance(addrA)
if instPlan == nil {
t.Fatalf("expected plan for %s; but got none", addrA)
}
}
func TestContext2Plan_excludedResourceSchemaChange(t *testing.T) {
// an excluded resource which requires a schema migration should not
// block planning due external changes in the plan.
addrA := mustResourceInstanceAddr("test_object.a")
addrB := mustResourceInstanceAddr("test_object.b")
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "a" {
}
resource "test_object" "b" {
}`,
})
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{}`),
Status: states.ObjectReady,
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{
// old_list is no longer in the schema
AttrsJSON: []byte(`{"old_list":["used to be","a list here"]}`),
Status: states.ObjectReady,
}, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`))
})
p := simpleMockProvider()
// external changes trigger a "drift report", but because test_object.b was
// excluded, the state was not fixed to match the schema and cannot be
// deocded for the report.
p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
var resp providers.ReadResourceResponse
obj := req.PriorState.AsValueMap()
// test_number changed externally
obj["test_number"] = cty.NumberIntVal(1)
resp.NewState = cty.ObjectVal(obj)
return resp
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
addrB,
},
})
//
assertNoErrors(t, diags)
if len(plan.Changes.Resources) != 1 {
t.Fatalf("expected 1 resource change, but got %d", len(plan.Changes.Resources))
}
instPlan := plan.Changes.ResourceInstance(addrA)
if instPlan == nil {
t.Fatalf("expected plan for %s; but got none", addrA)
}
}
func TestContext2Plan_movedResourceRefreshOnly(t *testing.T) {

View File

@ -4068,6 +4068,57 @@ func TestContext2Plan_targeted(t *testing.T) {
}
}
// All exclude flag tests in this file are inspired by a counterpart target flag test
// Usually that test exists right before the exclude flag test
func TestContext2Plan_excluded(t *testing.T) {
m := testModule(t, "plan-targeted")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Resource(addrs.ManagedResourceMode, "aws_instance", "foo"),
},
})
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
ty := schema.ImpliedType()
if len(plan.Changes.Resources) != 1 {
t.Fatal("expected 1 changes, got", len(plan.Changes.Resources))
}
for _, res := range plan.Changes.Resources {
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
switch i := ric.Addr.String(); i {
case "module.mod[0].aws_instance.foo":
if res.Action != plans.Create {
t.Fatalf("resource %s should be created", i)
}
checkVals(t, objectVal(t, schema, map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"num": cty.NumberIntVal(2),
"type": cty.UnknownVal(cty.String),
}), ric.After)
default:
t.Fatal("unknown instance:", i)
}
}
}
// Test that targeting a module properly plans any inputs that depend
// on another module.
func TestContext2Plan_targetedCrossModule(t *testing.T) {
@ -4123,6 +4174,33 @@ func TestContext2Plan_targetedCrossModule(t *testing.T) {
}
}
// Test that excluding a module properly plans and excludes any
// dependent modules.
func TestContext2Plan_excludedCrossModule(t *testing.T) {
m := testModule(t, "plan-targeted-cross-module")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Child("A", addrs.NoKey),
},
})
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
if len(plan.Changes.Resources) != 0 {
t.Fatal("expected 0 changes, got", len(plan.Changes.Resources))
}
}
func TestContext2Plan_targetedModuleWithProvider(t *testing.T) {
m := testModule(t, "plan-targeted-module-with-provider")
p := testProvider("null")
@ -4173,6 +4251,56 @@ func TestContext2Plan_targetedModuleWithProvider(t *testing.T) {
}
}
func TestContext2Plan_excludedModuleWithProvider(t *testing.T) {
m := testModule(t, "plan-targeted-module-with-provider")
p := testProvider("null")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
Provider: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"key": {Type: cty.String, Optional: true},
},
},
ResourceTypes: map[string]*configschema.Block{
"null_resource": {
Attributes: map[string]*configschema.Attribute{},
},
},
})
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("null"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Child("child1", addrs.NoKey),
},
})
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
schema := p.GetProviderSchemaResponse.ResourceTypes["null_resource"].Block
ty := schema.ImpliedType()
if len(plan.Changes.Resources) != 1 {
t.Fatal("expected 1 changes, got", len(plan.Changes.Resources))
}
res := plan.Changes.Resources[0]
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
if ric.Addr.String() != "module.child2.null_resource.foo" {
t.Fatalf("unexpected resource: %s", ric.Addr)
}
}
func TestContext2Plan_targetedOrphan(t *testing.T) {
m := testModule(t, "plan-targeted-orphan")
p := testProvider("aws")
@ -4238,6 +4366,71 @@ func TestContext2Plan_targetedOrphan(t *testing.T) {
}
}
func TestContext2Plan_excludedOrphan(t *testing.T) {
m := testModule(t, "plan-targeted-orphan")
p := testProvider("aws")
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.orphan").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-789xyz"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.nottargeted").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "nottargeted",
),
},
})
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
ty := schema.ImpliedType()
if len(plan.Changes.Resources) != 1 {
t.Fatal("expected 1 changes, got", len(plan.Changes.Resources))
}
for _, res := range plan.Changes.Resources {
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
switch i := ric.Addr.String(); i {
case "aws_instance.orphan":
if res.Action != plans.Delete {
t.Fatalf("resource %s should be destroyed", ric.Addr)
}
default:
t.Fatal("unknown instance:", i)
}
}
}
// https://github.com/hashicorp/terraform/issues/2538
func TestContext2Plan_targetedModuleOrphan(t *testing.T) {
m := testModule(t, "plan-targeted-module-orphan")
@ -4301,6 +4494,68 @@ func TestContext2Plan_targetedModuleOrphan(t *testing.T) {
}
}
func TestContext2Plan_excludedModuleOrphan(t *testing.T) {
m := testModule(t, "plan-targeted-module-orphan")
p := testProvider("aws")
state := states.NewState()
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.orphan").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-789xyz"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
)
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("aws_instance.nottargeted").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"i-abc123"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.DestroyMode,
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Child("child", addrs.NoKey).Resource(
addrs.ManagedResourceMode, "aws_instance", "nottargeted",
),
},
})
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
ty := schema.ImpliedType()
if len(plan.Changes.Resources) != 1 {
t.Fatal("expected 1 changes, got", len(plan.Changes.Resources))
}
res := plan.Changes.Resources[0]
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
if ric.Addr.String() != "module.child.aws_instance.orphan" {
t.Fatalf("unexpected resource :%s", ric.Addr)
}
if res.Action != plans.Delete {
t.Fatalf("resource %s should be deleted", ric.Addr)
}
}
func TestContext2Plan_targetedModuleUntargetedVariable(t *testing.T) {
m := testModule(t, "plan-targeted-module-untargeted-variable")
p := testProvider("aws")
@ -4356,9 +4611,64 @@ func TestContext2Plan_targetedModuleUntargetedVariable(t *testing.T) {
}
}
func TestContext2Plan_excludedModuleUntargetedVariable(t *testing.T) {
m := testModule(t, "plan-targeted-module-untargeted-variable")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Excludes: []addrs.Targetable{
// Exclude green instance, which should also exclude the dependent green module
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "green",
),
},
})
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
ty := schema.ImpliedType()
if len(plan.Changes.Resources) != 2 {
t.Fatal("expected 2 changes, got", len(plan.Changes.Resources))
}
for _, res := range plan.Changes.Resources {
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
if res.Action != plans.Create {
t.Fatalf("resource %s should be created", ric.Addr)
}
switch i := ric.Addr.String(); i {
case "aws_instance.blue":
checkVals(t, objectVal(t, schema, map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"type": cty.UnknownVal(cty.String),
}), ric.After)
case "module.blue_mod.aws_instance.mod":
checkVals(t, objectVal(t, schema, map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"value": cty.UnknownVal(cty.String),
"type": cty.UnknownVal(cty.String),
}), ric.After)
default:
t.Fatal("unknown instance:", i)
}
}
}
// ensure that outputs missing references due to targeting are removed from
// the graph.
func TestContext2Plan_outputContainsTargetedResource(t *testing.T) {
func TestContext2Plan_outputContainsUntargetedResource(t *testing.T) {
m := testModule(t, "plan-untargeted-resource-output")
p := testProvider("aws")
ctx := testContext2(t, &ContextOpts{
@ -4367,13 +4677,14 @@ func TestContext2Plan_outputContainsTargetedResource(t *testing.T) {
},
})
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Child("mod", addrs.NoKey).Resource(
addrs.ManagedResourceMode, "aws_instance", "a",
),
},
})
if diags.HasErrors() {
t.Fatalf("err: %s", diags)
}
@ -4386,6 +4697,77 @@ func TestContext2Plan_outputContainsTargetedResource(t *testing.T) {
if got, want := diags[0].Description().Summary, "Resource targeting is in effect"; got != want {
t.Errorf("wrong diagnostic summary %#v; want %#v", got, want)
}
if len(plan.Changes.Outputs) != 0 {
t.Fatalf("expected 0 output changes, but got %d", len(plan.Changes.Outputs))
}
schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
ty := schema.ImpliedType()
res := plan.Changes.Resources[0]
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
if ric.Addr.String() != "module.mod.aws_instance.a[0]" {
t.Fatalf("unexpected resource :%s", ric.Addr)
}
if res.Action != plans.Create {
t.Fatalf("resource %s should be deleted", ric.Addr)
}
}
func TestContext2Plan_outputContainsExcludedResource(t *testing.T) {
m := testModule(t, "plan-untargeted-resource-output")
p := testProvider("aws")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Child("mod", addrs.NoKey).Resource(
addrs.ManagedResourceMode, "aws_instance", "b",
),
},
})
if diags.HasErrors() {
t.Fatalf("err: %s", diags)
}
if len(diags) != 1 {
t.Fatalf("got %d diagnostics; want 1", diags)
}
if got, want := diags[0].Severity(), tfdiags.Warning; got != want {
t.Errorf("wrong diagnostic severity %#v; want %#v", got, want)
}
if got, want := diags[0].Description().Summary, "Resource targeting is in effect"; got != want {
t.Errorf("wrong diagnostic summary %#v; want %#v", got, want)
}
if len(plan.Changes.Outputs) != 0 {
t.Fatalf("expected 0 output changes, but got %d", len(plan.Changes.Outputs))
}
schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
ty := schema.ImpliedType()
res := plan.Changes.Resources[0]
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
if ric.Addr.String() != "module.mod.aws_instance.a[0]" {
t.Fatalf("unexpected resource :%s", ric.Addr)
}
if res.Action != plans.Create {
t.Fatalf("resource %s should be deleted", ric.Addr)
}
}
// https://github.com/hashicorp/terraform/issues/4515
@ -4442,6 +4824,60 @@ func TestContext2Plan_targetedOverTen(t *testing.T) {
}
}
// https://github.com/hashicorp/terraform/issues/4515 - Making sure it doesn't happen with exclude flag
func TestContext2Plan_excludedOverTen(t *testing.T) {
m := testModule(t, "plan-targeted-over-ten")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
for i := 0; i < 13; i++ {
key := fmt.Sprintf("aws_instance.foo[%d]", i)
id := fmt.Sprintf("i-abc%d", i)
attrs := fmt.Sprintf(`{"id":"%s","type":"aws_instance"}`, id)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr(key).Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(attrs),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
)
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, state, &PlanOpts{
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.IntKey(1),
),
},
})
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
ty := schema.ImpliedType()
for _, res := range plan.Changes.Resources {
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
if res.Action != plans.NoOp {
t.Fatalf("unexpected action %s for %s", res.Action, ric.Addr)
}
}
}
func TestContext2Plan_provider(t *testing.T) {
m := testModule(t, "plan-provider")
p := testProvider("aws")
@ -5975,6 +6411,72 @@ resource "aws_instance" "foo" {
}
}
func TestContext2Plan_excludeExpandedAddress(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
module "mod" {
count = 3
source = "./mod"
}
`,
"mod/main.tf": `
resource "aws_instance" "foo" {
count = 2
}
`,
})
p := testProvider("aws")
excludes := []addrs.Targetable{}
exclude, diags := addrs.ParseTargetStr("module.mod[1].aws_instance.foo[0]")
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
excludes = append(excludes, exclude.Subject)
exclude, diags = addrs.ParseTargetStr("module.mod[2]")
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
excludes = append(excludes, exclude.Subject)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Excludes: excludes,
})
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
expected := map[string]plans.Action{
// the whole mod[0], which was not excluded
`module.mod[0].aws_instance.foo[0]`: plans.Create,
`module.mod[0].aws_instance.foo[1]`: plans.Create,
// the unexcluded mod[1] instance
`module.mod[1].aws_instance.foo[1]`: plans.Create,
// the whole of mod[2] was excluded
}
for _, res := range plan.Changes.Resources {
want := expected[res.Addr.String()]
if res.Action != want {
t.Fatalf("expected %s action, got: %q %s", want, res.Addr, res.Action)
}
delete(expected, res.Addr.String())
}
for res, action := range expected {
t.Errorf("missing %s change for %s", action, res)
}
}
func TestContext2Plan_targetResourceInModuleInstance(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
@ -6030,6 +6532,62 @@ resource "aws_instance" "foo" {
}
}
func TestContext2Plan_excludeResourceInModuleInstance(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
module "mod" {
count = 3
source = "./mod"
}
`,
"mod/main.tf": `
resource "aws_instance" "foo" {
}
`,
})
p := testProvider("aws")
exclude, diags := addrs.ParseTargetStr("module.mod[1].aws_instance.foo")
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
excludes := []addrs.Targetable{exclude.Subject}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Excludes: excludes,
})
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
expected := map[string]plans.Action{
// the unexcluded instances from mod[0] and mod[2]
`module.mod[0].aws_instance.foo`: plans.Create,
`module.mod[2].aws_instance.foo`: plans.Create,
}
for _, res := range plan.Changes.Resources {
want := expected[res.Addr.String()]
if res.Action != want {
t.Fatalf("expected %s action, got: %q %s", want, res.Addr, res.Action)
}
delete(expected, res.Addr.String())
}
for res, action := range expected {
t.Errorf("missing %s change for %s", action, res)
}
}
func TestContext2Plan_moduleRefIndex(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
@ -6265,6 +6823,57 @@ func TestContext2Plan_targetedModuleInstance(t *testing.T) {
}
}
func TestContext2Plan_excludedModuleInstance(t *testing.T) {
m := testModule(t, "plan-targeted")
p := testProvider("aws")
p.PlanResourceChangeFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "bar",
),
addrs.RootModuleInstance.Child("mod", addrs.IntKey(0)),
},
})
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block
ty := schema.ImpliedType()
if len(plan.Changes.Resources) != 1 {
t.Fatal("expected 1 changes, got", len(plan.Changes.Resources))
}
for _, res := range plan.Changes.Resources {
ric, err := res.Decode(ty)
if err != nil {
t.Fatal(err)
}
switch i := ric.Addr.String(); i {
case "aws_instance.foo":
if res.Action != plans.Create {
t.Fatalf("resource %s should be created", i)
}
checkVals(t, objectVal(t, schema, map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"num": cty.NumberIntVal(2),
"type": cty.UnknownVal(cty.String),
}), ric.After)
default:
t.Fatal("unknown instance:", i)
}
}
}
func TestContext2Plan_dataRefreshedInPlan(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `

View File

@ -291,6 +291,95 @@ func TestContext2Refresh_targeted(t *testing.T) {
}
expected := []string{"vpc-abc123", "i-abc123"}
sort.Strings(expected)
sort.Strings(refreshedResources)
if !reflect.DeepEqual(refreshedResources, expected) {
t.Fatalf("expected: %#v, got: %#v", expected, refreshedResources)
}
}
// All exclude flag tests in this file are inspired by a counterpart target flag test
// Usually that test exists right before the exclude flag test
func TestContext2Refresh_excluded(t *testing.T) {
p := testProvider("aws")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
Provider: &configschema.Block{},
ResourceTypes: map[string]*configschema.Block{
"aws_elb": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"instances": {
Type: cty.Set(cty.String),
Optional: true,
},
},
},
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"vpc_id": {
Type: cty.String,
Optional: true,
},
},
},
"aws_vpc": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
},
},
},
})
// This test uses the same setup as TestContext2Refresh_targeted, but here the resources that should be refreshed
// are aws_vpc.metoo and aws_instance.notme. These are resources that are not aws_instance.me or dependent on it
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
testSetResourceInstanceCurrent(root, "aws_vpc.metoo", `{"id":"vpc-abc123"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_instance.notme", `{"id":"i-bcd345"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_instance.me", `{"id":"i-abc123"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_elb.meneither", `{"id":"lb-abc123"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
m := testModule(t, "refresh-targeted")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
refreshedResources := make([]string, 0, 2)
p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
refreshedResources = append(refreshedResources, req.PriorState.GetAttr("id").AsString())
return providers.ReadResourceResponse{
NewState: req.PriorState,
}
}
_, diags := ctx.Refresh(m, state, &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "me",
),
},
})
if diags.HasErrors() {
t.Fatalf("refresh errors: %s", diags.Err())
}
expected := []string{"vpc-abc123", "i-bcd345"}
sort.Strings(expected)
sort.Strings(refreshedResources)
if !reflect.DeepEqual(refreshedResources, expected) {
t.Fatalf("expected: %#v, got: %#v", expected, refreshedResources)
}
@ -386,6 +475,97 @@ func TestContext2Refresh_targetedCount(t *testing.T) {
}
}
func TestContext2Refresh_excludedCount(t *testing.T) {
p := testProvider("aws")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
Provider: &configschema.Block{},
ResourceTypes: map[string]*configschema.Block{
"aws_elb": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"instances": {
Type: cty.Set(cty.String),
Optional: true,
},
},
},
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"vpc_id": {
Type: cty.String,
Optional: true,
},
},
},
"aws_vpc": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
},
},
},
})
// This test uses the same setup as TestContext2Refresh_targetedCount, but here the resources that should be
// refreshed are aws_vpc.metoo and aws_instance.notme. These are resources that are not aws_instance.me or
// dependent on it
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
testSetResourceInstanceCurrent(root, "aws_vpc.metoo", `{"id":"vpc-abc123"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_instance.notme", `{"id":"i-bcd345"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_instance.me[0]", `{"id":"i-abc123"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_instance.me[1]", `{"id":"i-cde567"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_instance.me[2]", `{"id":"i-cde789"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_elb.meneither", `{"id":"lb-abc123"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
m := testModule(t, "refresh-targeted-count")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
refreshedResources := make([]string, 0, 2)
p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
refreshedResources = append(refreshedResources, req.PriorState.GetAttr("id").AsString())
return providers.ReadResourceResponse{
NewState: req.PriorState,
}
}
_, diags := ctx.Refresh(m, state, &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "me",
),
},
})
if diags.HasErrors() {
t.Fatalf("refresh errors: %s", diags.Err())
}
// Target didn't specify index, so we should exclude all instances of aws_instance.me
expected := []string{
"vpc-abc123",
"i-bcd345",
}
sort.Strings(expected)
sort.Strings(refreshedResources)
if !reflect.DeepEqual(refreshedResources, expected) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", refreshedResources, expected)
}
}
func TestContext2Refresh_targetedCountIndex(t *testing.T) {
p := testProvider("aws")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
@ -463,6 +643,95 @@ func TestContext2Refresh_targetedCountIndex(t *testing.T) {
}
expected := []string{"vpc-abc123", "i-abc123"}
sort.Strings(expected)
sort.Strings(refreshedResources)
if !reflect.DeepEqual(refreshedResources, expected) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", refreshedResources, expected)
}
}
func TestContext2Refresh_excludedCountIndex(t *testing.T) {
p := testProvider("aws")
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
Provider: &configschema.Block{},
ResourceTypes: map[string]*configschema.Block{
"aws_elb": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"instances": {
Type: cty.Set(cty.String),
Optional: true,
},
},
},
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"vpc_id": {
Type: cty.String,
Optional: true,
},
},
},
"aws_vpc": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
},
},
},
})
// This test uses the same setup as TestContext2Refresh_targetedCountIndex, but here the resources that should be
// refreshed are aws_vpc.metoo, aws_instance.notme, aws_instance.me[1] and aws_instance.me[2]. These are resources
// that are not aws_instance.me[0] or dependent on it
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
testSetResourceInstanceCurrent(root, "aws_vpc.metoo", `{"id":"vpc-abc123"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_instance.notme", `{"id":"i-bcd345"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_instance.me[0]", `{"id":"i-abc123"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_instance.me[1]", `{"id":"i-cde567"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_instance.me[2]", `{"id":"i-cde789"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
testSetResourceInstanceCurrent(root, "aws_elb.meneither", `{"id":"lb-abc123"}`, `provider["registry.opentofu.org/hashicorp/aws"]`)
m := testModule(t, "refresh-targeted-count")
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
},
})
refreshedResources := make([]string, 0, 2)
p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
refreshedResources = append(refreshedResources, req.PriorState.GetAttr("id").AsString())
return providers.ReadResourceResponse{
NewState: req.PriorState,
}
}
_, diags := ctx.Refresh(m, state, &PlanOpts{
Mode: plans.NormalMode,
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "me", addrs.IntKey(0),
),
},
})
if diags.HasErrors() {
t.Fatalf("refresh errors: %s", diags.Err())
}
expected := []string{"vpc-abc123", "i-bcd345", "i-cde567", "i-cde789"}
sort.Strings(expected)
sort.Strings(refreshedResources)
if !reflect.DeepEqual(refreshedResources, expected) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", refreshedResources, expected)
}

View File

@ -46,6 +46,12 @@ type ApplyGraphBuilder struct {
// outputs should go into the diff so that this is unnecessary.
Targets []addrs.Targetable
// Excludes are resources to exclude. This is only required to make sure
// unnecessary outputs aren't included in the apply graph. The plan
// builder successfully handles targeting resources. In the future,
// outputs should go into the diff so that this is unnecessary.
Excludes []addrs.Targetable
// ForceReplace are the resource instance addresses that the user
// requested to force replacement for when creating the plan, if any.
// The apply step refers to these as part of verifying that the planned
@ -192,7 +198,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
&pruneUnusedNodesTransformer{},
// Target
&TargetsTransformer{Targets: b.Targets},
&TargetingTransformer{Targets: b.Targets, Excludes: b.Excludes},
// Close opened plugin connections
&CloseProviderTransformer{},

View File

@ -456,7 +456,42 @@ func TestApplyGraphBuilder_targetModule(t *testing.T) {
t.Fatalf("err: %s", err)
}
testGraphNotContains(t, g, "module.child1.output.instance_id")
testGraphNotContains(t, g, "test_object.foo")
}
func TestApplyGraphBuilder_excludeModule(t *testing.T) {
changes := &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: mustResourceInstanceAddr("test_object.foo"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Update,
},
},
{
Addr: mustResourceInstanceAddr("module.child2.test_object.foo"),
ChangeSrc: plans.ChangeSrc{
Action: plans.Update,
},
},
},
}
b := &ApplyGraphBuilder{
Config: testModule(t, "graph-builder-apply-target-module"),
Changes: changes,
Plugins: simpleMockPluginLibrary(),
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Child("child2", addrs.NoKey),
},
}
g, err := b.Build(addrs.RootModuleInstance)
if err != nil {
t.Fatalf("err: %s", err)
}
testGraphNotContains(t, g, "mod.child2.test_object.foo")
}
// Ensure that an update resulting from the removal of a resource happens after

View File

@ -46,6 +46,9 @@ type PlanGraphBuilder struct {
// Targets are resources to target
Targets []addrs.Targetable
// Excludes are resources to exclude
Excludes []addrs.Targetable
// ForceReplace are resource instances where if we would normally have
// generated a NoOp or Update action then we'll force generating a replace
// action instead. Create and Delete actions are not affected.
@ -226,7 +229,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
&attachDataResourceDependsOnTransformer{},
// DestroyEdgeTransformer is only required during a plan so that the
// TargetsTransformer can determine which nodes to keep in the graph.
// TargetingTransformer can determine which nodes to keep in the graph.
&DestroyEdgeTransformer{
Operation: b.Operation,
},
@ -236,7 +239,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
},
// Target
&TargetsTransformer{Targets: b.Targets},
&TargetingTransformer{Targets: b.Targets, Excludes: b.Excludes},
// Detect when create_before_destroy must be forced on for a particular
// node due to dependency edges, to avoid graph cycles during apply.

View File

@ -195,6 +195,27 @@ func TestPlanGraphBuilder_targetModule(t *testing.T) {
testGraphNotContains(t, g, "module.child1.test_object.foo")
}
func TestPlanGraphBuilder_excludeModule(t *testing.T) {
b := &PlanGraphBuilder{
Config: testModule(t, "graph-builder-plan-target-module-provider"),
Plugins: simpleMockPluginLibrary(),
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Child("child1", addrs.NoKey),
},
Operation: walkPlan,
}
g, err := b.Build(addrs.RootModuleInstance)
if err != nil {
t.Fatalf("err: %s", err)
}
t.Logf("Graph: %s", g.String())
testGraphNotContains(t, g, `module.child1.provider["registry.opentofu.org/hashicorp/test"]`)
testGraphNotContains(t, g, "module.child1.test_object.foo")
}
func TestPlanGraphBuilder_forEach(t *testing.T) {
awsProvider := mockProviderWithResourceTypeSchema("aws_instance", simpleTestSchema())

View File

@ -69,6 +69,9 @@ type NodeAbstractResource struct {
// Set from GraphNodeTargetable
Targets []addrs.Targetable
// Set from GraphNodeTargetable
Excludes []addrs.Targetable
// Set from AttachDataResourceDependsOn
dependsOn []addrs.ConfigResource
forceDependsOn bool
@ -394,6 +397,11 @@ func (n *NodeAbstractResource) SetTargets(targets []addrs.Targetable) {
n.Targets = targets
}
// GraphNodeTargetable
func (n *NodeAbstractResource) SetExcludes(excludes []addrs.Targetable) {
n.Excludes = excludes
}
// graphNodeAttachDataResourceDependsOn
func (n *NodeAbstractResource) AttachDataResourceDependsOn(deps []addrs.ConfigResource, force bool) {
n.dependsOn = deps

View File

@ -413,7 +413,7 @@ func (n *nodeExpandPlannableResource) resourceInstanceSubgraph(ctx EvalContext,
&AttachStateTransformer{State: state},
// Targeting
&TargetsTransformer{Targets: n.Targets},
&TargetingTransformer{Targets: n.Targets, Excludes: n.Excludes},
// Connect references so ordering is correct
&ReferenceTransformer{},

View File

@ -1,6 +1,10 @@
# This resource was previously "created" and the fixture represents
# it being destroyed subsequently
# These resources were previously "created" and the fixture represents
# them being destroyed subsequently
/*resource "aws_instance" "orphan" {*/
/*foo = "bar"*/
/*}*/
#resource "aws_instance" "orphan" {
# foo = "bar"
#}
#resource "aws_instance" "nottargeted" {
# foo = "bar"
#}

View File

@ -4,5 +4,5 @@ module "mod" {
resource "aws_instance" "c" {
name = "${module.mod.output}"
foo = "${module.mod.output}"
}

View File

@ -144,7 +144,7 @@ func TestCloseProviderTransformer_withTargets(t *testing.T) {
&MissingProviderTransformer{},
&ProviderTransformer{},
&CloseProviderTransformer{},
&TargetsTransformer{
&TargetingTransformer{
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "something", "else",
@ -166,6 +166,36 @@ func TestCloseProviderTransformer_withTargets(t *testing.T) {
}
}
func TestCloseProviderTransformer_withExcludes(t *testing.T) {
mod := testModule(t, "transform-provider-basic")
g := testProviderTransformerGraph(t, mod)
transforms := []GraphTransformer{
&MissingProviderTransformer{},
&ProviderTransformer{},
&CloseProviderTransformer{},
&TargetingTransformer{
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "web",
),
},
},
}
for _, tr := range transforms {
if err := tr.Transform(g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(``)
if actual != expected {
t.Fatalf("expected:%s\n\ngot:\n\n%s", expected, actual)
}
}
func TestMissingProviderTransformer(t *testing.T) {
mod := testModule(t, "transform-provider-missing")

View File

@ -13,44 +13,52 @@ import (
)
// GraphNodeTargetable is an interface for graph nodes to implement when they
// need to be told about incoming targets. This is useful for nodes that need
// to respect targets as they dynamically expand. Note that the list of targets
// provided will contain every target provided, and each implementing graph
// node must filter this list to targets considered relevant.
// need to be told about incoming targets or excluded targets. This is useful for
// nodes that need to respect targets and excludes as they dynamically expand.
// Note that the lists of targets and excludes provided will contain every target
// or every exclude provided, and each implementing graph node must filter this
// list to targets considered relevant.
type GraphNodeTargetable interface {
SetTargets([]addrs.Targetable)
SetExcludes([]addrs.Targetable)
}
// TargetsTransformer is a GraphTransformer that, when the user specifies a
// list of resources to target, limits the graph to only those resources and
// their dependencies.
type TargetsTransformer struct {
// TargetingTransformer is a GraphTransformer that, when the user specifies a
// list of resources to target, or a list of resources to exclude, limits the
// graph to only those resources and their dependencies (or in the case of
// excludes - limits the graph to all resources that are not excluded or not
// dependent on excluded resources).
type TargetingTransformer struct {
// List of targeted resource names specified by the user
Targets []addrs.Targetable
// List of excluded resource names specified by the user
Excludes []addrs.Targetable
}
func (t *TargetsTransformer) Transform(g *Graph) error {
func (t *TargetingTransformer) Transform(g *Graph) error {
var targetedNodes dag.Set
if len(t.Targets) > 0 {
targetedNodes, err := t.selectTargetedNodes(g, t.Targets)
if err != nil {
return err
}
targetedNodes = t.selectTargetedNodes(g, t.Targets)
} else if len(t.Excludes) > 0 {
targetedNodes = t.removeExcludedNodes(g, t.Excludes)
} else {
return nil
}
for _, v := range g.Vertices() {
if !targetedNodes.Include(v) {
log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v))
g.Remove(v)
}
for _, v := range g.Vertices() {
if !targetedNodes.Include(v) {
log.Printf("[DEBUG] Removing %q, filtered by targeting.", dag.VertexName(v))
g.Remove(v)
}
}
return nil
}
// Returns a set of targeted nodes. A targeted node is either addressed
// directly, address indirectly via its container, or it's a dependency of a
// targeted node.
func (t *TargetsTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targetable) (dag.Set, error) {
// selectTargetedNodes goes over a list of resource and modules targeted with a -target flag, and returns a set of
// targeted nodes. A targeted node is either addressed directly, address indirectly via its container, or it's a
// dependency of a targeted node.
func (t *TargetingTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targetable) dag.Set {
targetedNodes := make(dag.Set)
vertices := g.Vertices()
@ -73,10 +81,118 @@ func (t *TargetsTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targeta
}
}
targetedOutputNodes := t.getTargetedOutputNodes(targetedNodes, g)
for _, outputNode := range targetedOutputNodes {
targetedNodes.Add(outputNode)
}
return targetedNodes
}
func (t *TargetingTransformer) getTargetableNodeResourceAddr(v dag.Vertex) addrs.Targetable {
switch r := v.(type) {
case GraphNodeResourceInstance:
return r.ResourceInstanceAddr()
case GraphNodeConfigResource:
return r.ResourceAddr()
default:
// Only resource and resource instance nodes can be targeted.
return nil
}
}
// removeExcludedNodes goes over a list of excluded resources and modules, and returns a set of targeted nodes to be
// used for resource targeting. An excluded resource is either addressed directly, addressed indirectly via its
// container, or it's dependent on an excluded node. The rest are the targeted nodes used for resource targeting
func (t *TargetingTransformer) removeExcludedNodes(g *Graph, excludes []addrs.Targetable) dag.Set {
targetedNodes := make(dag.Set)
excludedNodes := make(dag.Set)
targetableNodes := make(dag.Set)
vertices := g.Vertices()
// Step 1: Find all excluded targetable nodes, and their descendants
for _, v := range vertices {
vertexAddr := t.getTargetableNodeResourceAddr(v)
if vertexAddr == nil {
continue
}
targetableNodes.Add(v)
nodeExcluded := t.nodeIsExcluded(vertexAddr, excludes)
if nodeExcluded {
excludedNodes.Add(v)
}
if nodeExcluded || t.nodeDescendantsExcluded(vertexAddr, excludes) {
deps, _ := g.Descendents(v)
for _, d := range deps {
// In general, we'd like to exclude any descendant targetable node of the current node.
// We exclude any resource dependent on this resource (which is more general than resources dependent
// on the resource instance, but is in-line with how -target works).
//
// The exception to this is when excluding a specific instance of a resource that has multiple instances.
// During apply, the specific instance tofu.NodeApplyableResourceInstance would be dependent on the
// resource tofu.nodeExpandApplyableResource.
// Since we do not want to exclude all resource instances (other than the ones that we've explicitly
// excluded), we should only exclude dependents whose target is not contained in the current node.
depVertexAddr := t.getTargetableNodeResourceAddr(d)
if depVertexAddr != nil && !vertexAddr.TargetContains(depVertexAddr) {
excludedNodes.Add(d)
}
}
}
}
// Step 2: Of the targetable nodes that were not excluded, build the graph similarly to -target
for _, v := range targetableNodes {
if !excludedNodes.Include(v) {
targetedNodes.Add(v)
// We inform nodes that ask about the list of excludes - helps for nodes
// that need to dynamically expand. Note that this only occurs for nodes
// that are targetable and we didn't exclude
if tn, ok := v.(GraphNodeTargetable); ok {
tn.SetExcludes(excludes)
}
deps, _ := g.Ancestors(v)
for _, d := range deps {
targetedNodes.Add(d)
}
}
}
// Step 3: Add outputs
targetedOutputNodes := t.getTargetedOutputNodes(targetedNodes, g)
for _, outputNode := range targetedOutputNodes {
targetedNodes.Add(outputNode)
}
return targetedNodes
}
func (t *TargetingTransformer) getTargetedOutputNodes(targetedNodes dag.Set, graph *Graph) dag.Set {
// It is expected that outputs which are only derived from targeted
// resources are also updated. While we don't include any other possible
// side effects from the targeted nodes, these are added because outputs
// cannot be targeted on their own.
//
// Note: This behaviour has some quirks, as there are specific cases where
// you would think an output should not be updated, but it is
// For example, when there's a module call with an input that is dependent
// on a root resource, and only the root resource is targeted, any output
// that depends on a module output might be updated, if said module output
// does not depend on any resource of the module itself.
// Right now, we will not change this behaviour, as this has been the
// behaviour for quite a while. A possible fix could be a more detailed
// analysis of the outputs, and making sure that module outputs are only
// referenced if any of the targeted nodes is in said module
targetedOutputNodes := make(dag.Set)
vertices := graph.Vertices()
// Start by finding the root module output nodes themselves
for _, v := range vertices {
// outputs are all temporary value types
@ -93,7 +209,7 @@ func (t *TargetsTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targeta
// If this output is descended only from targeted resources, then we
// will keep it
deps, _ := g.Ancestors(v)
deps, _ := graph.Ancestors(v)
found := 0
for _, d := range deps {
switch d.(type) {
@ -115,17 +231,65 @@ func (t *TargetsTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targeta
if found > 0 {
// we found an output we can keep; add it, and all it's dependencies
targetedNodes.Add(v)
targetedOutputNodes.Add(v)
for _, d := range deps {
targetedNodes.Add(d)
targetedOutputNodes.Add(d)
}
}
}
return targetedNodes, nil
return targetedOutputNodes
}
func (t *TargetsTransformer) nodeIsTarget(v dag.Vertex, targets []addrs.Targetable) bool {
func (t *TargetingTransformer) nodeIsExcluded(vertexAddr addrs.Targetable, excludes []addrs.Targetable) bool {
for _, excludeAddr := range excludes {
if excludeAddr.TargetContains(vertexAddr) {
return true
}
}
return false
}
func (t *TargetingTransformer) nodeDescendantsExcluded(vertexAddr addrs.Targetable, excludes []addrs.Targetable) bool {
for _, excludeAddr := range excludes {
// The behaviour here is a bit different from targets.
// Before expansion - We'd like to only exclude resources that were excluded by module or resource.
// If the excluded target is an AbsResourceInstance, then we'd want to skip exclude until we expand the resource
// After expansion - We'd like to exclude any vertex that contains the exclude address
// Since before expansion the vertexAddr is without an index, then if the excludeAddr is an instance, it will
// only contain vertexAddr if its key is NoKey
// So - a simple TargetContains here should be enough, both before and after expansion
if _, ok := vertexAddr.(addrs.ConfigResource); ok {
// Before expansion happens, we only have nodes that know their
// ConfigResource address. We need to take the more specific
// target addresses and generalize them in order to compare with a
// ConfigResource.
//
// If the excluded target, in is generalized form, contains the vertex address, then we know that we could remove the descendants
// even if we don't remove the node itself from the graph. However, this could cause cases where too many resources are excluded.
// For example, with -exclude=null_resource.a[1], and a null_resource.b[*] for which each instance depends on a single null_resource.a instance,
// all null_resource.b instances will be excluded. This is not accurate, but is in line with -target today, which over-targets dependencies
switch target := excludeAddr.(type) {
case addrs.AbsResourceInstance:
excludeAddr = target.ContainingResource().Config()
case addrs.AbsResource:
excludeAddr = target.Config()
case addrs.ModuleInstance:
excludeAddr = target.Module()
}
}
if excludeAddr.TargetContains(vertexAddr) {
return true
}
}
return false
}
func (t *TargetingTransformer) nodeIsTarget(v dag.Vertex, targets []addrs.Targetable) bool {
var vertexAddr addrs.Targetable
switch r := v.(type) {
case GraphNodeResourceInstance:

View File

@ -38,7 +38,7 @@ func TestTargetsTransformer(t *testing.T) {
}
{
transform := &TargetsTransformer{
transform := &TargetingTransformer{
Targets: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "me",
@ -63,6 +63,64 @@ aws_vpc.me
}
}
func TestTargetsTransformerExclude(t *testing.T) {
mod := testModule(t, "transform-targets-basic")
g := Graph{Path: addrs.RootModuleInstance}
{
tf := &ConfigTransformer{Config: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &AttachResourceConfigTransformer{Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &ReferenceTransformer{}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &TargetingTransformer{
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "me",
),
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_vpc", "notme",
),
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_subnet", "notme",
),
addrs.RootModuleInstance.Resource(
addrs.ManagedResourceMode, "aws_instance", "notme",
),
},
}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(`
aws_subnet.me
aws_vpc.me
aws_vpc.me
`)
if actual != expected {
t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual)
}
}
func TestTargetsTransformer_downstream(t *testing.T) {
mod := testModule(t, "transform-targets-downstream")
@ -103,7 +161,7 @@ func TestTargetsTransformer_downstream(t *testing.T) {
}
{
transform := &TargetsTransformer{
transform := &TargetingTransformer{
Targets: []addrs.Targetable{
addrs.RootModuleInstance.
Child("child", addrs.NoKey).
@ -135,7 +193,77 @@ output.grandchild_id (expand)
}
}
// This tests the TargetsTransformer targeting a whole module,
func TestTargetsTransformer_downstreamExclude(t *testing.T) {
mod := testModule(t, "transform-targets-downstream")
g := Graph{Path: addrs.RootModuleInstance}
{
transform := &ConfigTransformer{Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("%T failed: %s", transform, err)
}
}
{
transform := &AttachResourceConfigTransformer{Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("%T failed: %s", transform, err)
}
}
{
transform := &AttachResourceConfigTransformer{Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("%T failed: %s", transform, err)
}
}
{
transform := &OutputTransformer{Config: mod}
if err := transform.Transform(&g); err != nil {
t.Fatalf("%T failed: %s", transform, err)
}
}
{
transform := &ReferenceTransformer{}
if err := transform.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
transform := &TargetingTransformer{
Excludes: []addrs.Targetable{
addrs.RootModuleInstance.Resource(addrs.ManagedResourceMode, "aws_instance", "foo"),
addrs.RootModuleInstance.
Child("child", addrs.NoKey).
Resource(addrs.ManagedResourceMode, "aws_instance", "foo"),
},
}
if err := transform.Transform(&g); err != nil {
t.Fatalf("%T failed: %s", transform, err)
}
}
actual := strings.TrimSpace(g.String())
// Even though we only asked to exclude all resources in root and child, only including the grandchild resource
// all of the outputs that descend from it are also targeted.
expected := strings.TrimSpace(`
module.child.module.grandchild.aws_instance.foo
module.child.module.grandchild.output.id (expand)
module.child.module.grandchild.aws_instance.foo
module.child.output.grandchild_id (expand)
module.child.module.grandchild.output.id (expand)
output.grandchild_id (expand)
module.child.output.grandchild_id (expand)
`)
if actual != expected {
t.Fatalf("bad:\n\nexpected:\n%s\n\ngot:\n%s\n", expected, actual)
}
}
// This tests the TargetingTransformer targeting a whole module,
// rather than a resource within a module instance.
func TestTargetsTransformer_wholeModule(t *testing.T) {
mod := testModule(t, "transform-targets-downstream")
@ -177,7 +305,7 @@ func TestTargetsTransformer_wholeModule(t *testing.T) {
}
{
transform := &TargetsTransformer{
transform := &TargetingTransformer{
Targets: []addrs.Targetable{
addrs.RootModule.
Child("child").

View File

@ -123,6 +123,14 @@ In addition to alternate [planning modes](#planning-modes), there are several op
Use `-target=ADDRESS` in exceptional circumstances only, such as recovering from mistakes or working around OpenTofu limitations. Refer to [Resource Targeting](#resource-targeting) for more details.
:::
- `-exclude=ADDRESS` - Instructs OpenTofu to focus its planning efforts only
on resource instances which do not match the given excluded address, and that
do not depend on any such resources or modules that were excluded.
:::note
Use `-exclude=ADDRESS` in exceptional circumstances only, such as recovering from mistakes or working around OpenTofu limitations. Refer to [Resource Targeting](#resource-targeting) for more details.
:::
- `-var 'NAME=VALUE'` - Sets a value for a single
[input variable](../../language/values/variables.mdx) declared in the
root module of the configuration. Use this option multiple times to set
@ -217,8 +225,18 @@ input variables, see
### Resource Targeting
You can use the `-target` option to focus OpenTofu's attention on only a
subset of resources.
You can use the `-target` or the `-exclude` option to trigger resource targeting,
focusing OpenTofu's attention on only a subset of resources.
Using the `-target` option will focus OpenTofu's attention only on resources and
module that are directly targeted, or are dependencies of the target.
Using the `-exclude` option will focus OpenTofu's attention only on resources and
modules that are not directly excluded, and are not dependent on an excluded resource
or module.
You can use multiple `-target` flags in order to target multiple resources and modules,
and you can use multiple `-exclude` flags in order to exclude multiple resource and
modules. You cannot use both `-target` and `-exclude` flags together.
You can use [resource address syntax](../../cli/state/resource-addressing.mdx)
to specify the constraint. OpenTofu interprets the resource address as follows:
@ -238,18 +256,14 @@ to specify the constraint. OpenTofu interprets the resource address as follows:
select all instances of all resources that belong to that module instance
and all of its child module instances.
Once OpenTofu has selected one or more resource instances that you've directly
targeted, it will also then extend the selection to include all other objects
that those selections depend on either directly or indirectly.
This targeting capability is provided for exceptional circumstances, such
as recovering from mistakes or working around OpenTofu limitations. It
is _not recommended_ to use `-target` for routine operations, since this can
lead to undetected configuration drift and confusion about how the true state
of resources relates to configuration.
is _not recommended_ to use `-target` or `-exclude` for routine operations, since
this can lead to undetected configuration drift and confusion about how the true
state of resources relates to configuration.
Instead of using `-target` as a means to operate on isolated portions of very
large configurations, prefer instead to break large configurations into
Instead of using `-target` or `-exclude` as a means to operate on isolated portions
of very large configurations, prefer instead to break large configurations into
several smaller configurations that can each be independently applied.
[Data sources](../../language/data-sources/index.mdx) can be used to access
information about resources created in other configurations, allowing

View File

@ -98,7 +98,7 @@ This object has two attributes:
The keys of the map (or all the values in the case of a set of strings) must
be _known values_, or you will get an error message that `for_each` has dependencies
that cannot be determined before apply, and a `-target` may be needed.
that cannot be determined before apply, and a `-target`/`-exclude` may be needed.
`for_each` keys cannot be the result (or rely on the result of) of impure functions,
including `uuid`, `bcrypt`, or `timestamp`, as their evaluation is deferred during the

View File

@ -90,7 +90,7 @@ round trip time for each resource is hundreds of milliseconds. On top of this,
cloud providers almost always have API rate limiting so OpenTofu can only
request a certain number of resources in a period of time. Larger users
of OpenTofu make heavy use of the `-refresh=false` flag as well as the
`-target` flag in order to work around this. In these scenarios, the cached
`-target`/`-exclude` flags in order to work around this. In these scenarios, the cached
state is treated as the record of truth.
## Syncing