hashicorp/terraform · error · LockError

state not locked

Error message

state not locked

What it means

A NestingSet block may not contain WriteOnly attributes. cty hoists value marks (including WriteOnly marks) up to the outer set value, so per-element WriteOnly cannot be tracked — only the whole set could be WriteOnly, which is not the intent. The validator calls blockS.Block.ContainsWriteOnly() and rejects.

Solutions

  1. Move the WriteOnly attribute to the outer (non-set) block so the mark applies there.
  2. Change the block from NestingSet to NestingList if per-element WriteOnly is required.
  3. Drop WriteOnly from the set's child attributes.

Example fix

// before
"secrets": {
  Nesting: configschema.NestingSet,
  Block: configschema.Block{Attributes: map[string]*configschema.Attribute{
    "token": { Type: cty.String, Optional: true, WriteOnly: true },
  }},
},
// after
"secrets": {
  Nesting: configschema.NestingList,
  Block: configschema.Block{Attributes: map[string]*configschema.Attribute{
    "token": { Type: cty.String, Optional: true, WriteOnly: true },
  }},
},
Defensive patterns

Strategy: validation

Validate before calling

// Reject NestingSet blocks that contain any WriteOnly attribute.
func setHasWriteOnly(nb *configschema.NestedBlock) bool {
    if nb.Nesting != configschema.NestingSet { return false }
    return nb.Block.ContainsWriteOnly()
}

Type guard

func setFreeOfWriteOnly(nb *configschema.NestedBlock) bool {
    return nb.Nesting != configschema.NestingSet || !nb.Block.ContainsWriteOnly()
}

Prevention

When it happens

Trigger: NestingSet block with any attribute having WriteOnly: true. Guard at internal_validate.go:100 is `blockS.Block.ContainsWriteOnly()`.

Common situations: Adding a secret WriteOnly attribute inside a set of credential blocks; reusing a NestingList-with-WriteOnly schema under a NestingSet.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/ade159f5c778d3b4. Report an issue: GitHub.

Appendix: source

Thrown at internal/backend/remote-state/inmem/backend.go:205

		// make a copy of the lock info to avoid any testing shenanigans
		*lockErr.Info = *lockInfo
		return "", lockErr
	}

	info.Created = time.Now().UTC()
	l.m[name] = info

	return info.ID, nil
}

func (l *lockMap) unlock(name, id string) error {
	l.Lock()
	defer l.Unlock()

	lockInfo := l.m[name]

	if lockInfo == nil {
		return errors.New("state not locked")
	}

	lockErr := &statemgr.LockError{
		Info: &statemgr.LockInfo{},
	}

	if id != lockInfo.ID {
		lockErr.Err = errors.New("invalid lock id")
		*lockErr.Info = *lockInfo
		return lockErr
	}

	delete(l.m, name)
	return nil
}

View on GitHub (pinned to d32a084675)