Skip to content

Commit

Permalink
feat: add state lock command
Browse files Browse the repository at this point in the history
  • Loading branch information
ventsislav-georgiev committed Nov 17, 2024
1 parent e044e56 commit c2f1336
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 209 deletions.
210 changes: 1 addition & 209 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
- releng/**
- tsccr-auto-pinning/**
- dependabot/**
- tf-state-lock
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'

Expand Down Expand Up @@ -109,215 +110,6 @@ jobs:
strategy:
matrix:
include:
- {goos: "freebsd", goarch: "386", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "freebsd", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "freebsd", goarch: "arm", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "linux", goarch: "386", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "linux", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "linux", goarch: "arm", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "linux", goarch: "arm64", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "openbsd", goarch: "386", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "openbsd", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "solaris", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "windows", goarch: "386", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "windows", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "darwin", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"}
- {goos: "darwin", goarch: "arm64", runson: "ubuntu-latest", cgo-enabled: "0"}
fail-fast: false

package-docker:
name: Build Docker image for linux_${{ matrix.arch }}
runs-on: ubuntu-latest
needs:
- get-product-version
- build
strategy:
matrix:
arch: ["amd64", "386", "arm", "arm64"]
fail-fast: false
env:
repo: "terraform"
version: ${{needs.get-product-version.outputs.product-version}}
steps:
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Build Docker images
uses: hashicorp/actions-docker-build@11d43ef520c65f58683d048ce9b47d6617893c9a # v2
with:
pkg_name: "terraform_${{env.version}}"
version: ${{env.version}}
bin_name: terraform
target: default
arch: ${{matrix.arch}}
dockerfile: .github/workflows/build-Dockerfile
smoke_test: .github/scripts/verify_docker v${{ env.version }}
tags: |
docker.io/hashicorp/${{env.repo}}:${{env.version}}
public.ecr.aws/hashicorp/${{env.repo}}:${{env.version}}
e2etest-build:
name: Build e2etest for ${{ matrix.goos }}_${{ matrix.goarch }}
runs-on: ubuntu-latest
outputs:
e2e-cache-key: ${{ steps.set-cache-values.outputs.e2e-cache-key }}
e2e-cache-path: ${{ steps.set-cache-values.outputs.e2e-cache-path }}
needs:
- get-product-version
- get-go-version
strategy:
matrix:
include:
- {goos: "darwin", goarch: "amd64"}
- {goos: "darwin", goarch: "arm64"}
- {goos: "windows", goarch: "amd64"}
- {goos: "windows", goarch: "386"}
- {goos: "linux", goarch: "386"}
- {goos: "linux", goarch: "amd64"}
- {goos: "linux", goarch: "arm"}
- {goos: "linux", goarch: "arm64"}
fail-fast: false

env:
build_script: ./internal/command/e2etest/make-archive.sh

steps:
- name: Set Cache Values
id: set-cache-values
run: |
cache_key=e2e-cache-${{ github.sha }}
cache_path=internal/command/e2etest/build
echo "e2e-cache-key=${cache_key}" | tee -a "${GITHUB_OUTPUT}"
echo "e2e-cache-path=${cache_path}" | tee -a "${GITHUB_OUTPUT}"
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4

- name: Install Go toolchain
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ needs.get-go-version.outputs.go-version }}

- name: Build test harness package
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GO_LDFLAGS: ${{ needs.get-product-version.outputs.go-ldflags }}
run: |
# NOTE: This script reacts to the GOOS, GOARCH, and GO_LDFLAGS
# environment variables defined above. The e2e test harness
# needs to know the version we're building for so it can verify
# that "terraform version" is returning that version number.
bash ./internal/command/e2etest/make-archive.sh
- name: Save test harness to cache
uses: actions/cache/save@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ${{ steps.set-cache-values.outputs.e2e-cache-path }}
key: ${{ steps.set-cache-values.outputs.e2e-cache-key }}_${{ matrix.goos }}_${{ matrix.goarch }}

e2e-test:
name: Run e2e test for ${{ matrix.goos }}_${{ matrix.goarch }}
runs-on: ${{ matrix.runson }}
needs:
- get-product-version
- build
- e2etest-build
strategy:
matrix:
include:
- { runson: ubuntu-latest, goos: linux, goarch: "amd64" }
- { runson: ubuntu-latest, goos: linux, goarch: "386" }
- { runson: ubuntu-latest, goos: linux, goarch: "arm" }
- { runson: ubuntu-latest, goos: linux, goarch: "arm64" }
- { runson: macos-latest, goos: darwin, goarch: "amd64" }
- { runson: windows-latest, goos: windows, goarch: "amd64" }
- { runson: windows-latest, goos: windows, goarch: "386" }
fail-fast: false

env:
os: ${{ matrix.goos }}
arch: ${{ matrix.goarch }}
version: ${{needs.get-product-version.outputs.product-version}}

steps:
# NOTE: This intentionally _does not_ check out the source code
# for the commit/tag we're building, because by now we should
# have everything we need in the combination of CLI release package
# and e2etest package for this platform. (This helps ensure that we're
# really testing the release package and not inadvertently testing a
# fresh build from source.)
- name: Checkout repo
if: ${{ (matrix.goos == 'linux') || (matrix.goos == 'darwin') }}
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: "Restore cache"
uses: actions/cache/restore@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
id: e2etestpkg
with:
path: ${{ needs.e2etest-build.outputs.e2e-cache-path }}
key: ${{ needs.e2etest-build.outputs.e2e-cache-key }}_${{ matrix.goos }}_${{ matrix.goarch }}
fail-on-cache-miss: true
enableCrossOsArchive: true
- name: "Download Terraform CLI package"
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
id: clipkg
with:
name: terraform_${{env.version}}_${{ env.os }}_${{ env.arch }}.zip
path: .
- name: Extract packages
if: ${{ matrix.goos == 'windows' }}
run: |
unzip "${{ needs.e2etest-build.outputs.e2e-cache-path }}/terraform-e2etest_${{ env.os }}_${{ env.arch }}.zip"
unzip "./terraform_${{env.version}}_${{ env.os }}_${{ env.arch }}.zip"
- name: Set up QEMU
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
if: ${{ contains(matrix.goarch, 'arm') }}
with:
platforms: all
- name: Run E2E Tests (Darwin & Linux)
id: get-product-version
shell: bash
if: ${{ (matrix.goos == 'linux') || (matrix.goos == 'darwin') }}
env:
e2e_cache_path: ${{ needs.e2etest-build.outputs.e2e-cache-path }}
run: .github/scripts/e2e_test_linux_darwin.sh
- name: Run E2E Tests (Windows)
if: ${{ matrix.goos == 'windows' }}
env:
TF_ACC: 1
shell: cmd
run: e2etest.exe -test.v


e2e-test-exec:
name: Run terraform-exec test for linux amd64
runs-on: ubuntu-latest
needs:
- get-product-version
- get-go-version
- build

env:
os: ${{ matrix.goos }}
arch: ${{ matrix.goarch }}
version: ${{needs.get-product-version.outputs.product-version}}

steps:
- name: Install Go toolchain
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ needs.get-go-version.outputs.go-version }}
- name: Download Terraform CLI package
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
id: clipkg
with:
name: terraform_${{ env.version }}_linux_amd64.zip
path: .
- name: Checkout terraform-exec repo
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
repository: hashicorp/terraform-exec
path: terraform-exec
- name: Run terraform-exec end-to-end tests
run: |
FULL_RELEASE_VERSION="${{ env.version }}"
unzip terraform_${FULL_RELEASE_VERSION}_linux_amd64.zip
export TFEXEC_E2ETEST_TERRAFORM_PATH="$(pwd)/terraform"
cd terraform-exec
go test -race -timeout=30m -v ./tfexec/internal/e2etest
6 changes: 6 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,12 @@ func initCommands(
}, nil
},

"state lock": func() (cli.Command, error) {
return &command.StateLockCommand{
Meta: meta,
}, nil
},

"state rm": func() (cli.Command, error) {
return &command.StateRmCommand{
StateMeta: command.StateMeta{
Expand Down
10 changes: 10 additions & 0 deletions internal/command/clistate/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ type Locker interface {

// Timeout returns the configured timeout duration
Timeout() time.Duration

LockID() string
}

type locker struct {
Expand Down Expand Up @@ -167,6 +169,10 @@ func (l *locker) Timeout() time.Duration {
return l.timeout
}

func (l *locker) LockID() string {
return l.lockID
}

type noopLocker struct{}

// NewNoopLocker returns a valid Locker that does nothing.
Expand All @@ -191,3 +197,7 @@ func (l noopLocker) Unlock() tfdiags.Diagnostics {
func (l noopLocker) Timeout() time.Duration {
return 0
}

func (l noopLocker) LockID() string {
return ""
}
120 changes: 120 additions & 0 deletions internal/command/state_lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package command

import (
"fmt"
"strings"

"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/states/statemgr"

"github.com/hashicorp/cli"
"github.com/hashicorp/terraform/internal/tfdiags"
)

// StateLockCommand is a cli.Command implementation that manually locks
// the state.
type StateLockCommand struct {
Meta
StateMeta
}

func (c *StateLockCommand) Run(args []string) int {
args = c.Meta.process(args)
var statePath string
cmdFlags := c.Meta.defaultFlagSet("state lock")
cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
cmdFlags.StringVar(&statePath, "state", "", "path")
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
return cli.RunResultHelp
}
args = cmdFlags.Args()

if statePath != "" {
c.Meta.statePath = statePath
}

// assume everything is initialized. The user can manually init if this is
// required.
configPath, err := ModulePath(args)
if err != nil {
c.Ui.Error(err.Error())
return 1
}

var diags tfdiags.Diagnostics

backendConfig, backendDiags := c.loadBackendConfig(configPath)
diags = diags.Append(backendDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
Config: backendConfig,
})
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

env, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
stateMgr, err := b.StateMgr(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}

_, isLocal := stateMgr.(*statemgr.Filesystem)
if isLocal {
c.Ui.Error("Local state locking is redundant.")
return 0
}

stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
if diags := stateLocker.Lock(stateMgr, "state-lock"); diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

c.Ui.Output(c.Colorize().Color(strings.TrimSpace(fmt.Sprintf(outputStateLockSuccess, stateLocker.LockID()))))
return 0
}

func (c *StateLockCommand) Help() string {
helpText := `
Usage: terraform [global options] state lock
Manually lock the state for the defined configuration.
This will not modify your infrastructure. This command adds a lock on the
state for the current workspace. The behavior of this lock is dependent
on the backend being used. Local state files cannot be locked.
`
return strings.TrimSpace(helpText)
}

func (c *StateLockCommand) Synopsis() string {
return "Acquire a lock on the current workspace"
}

const outputStateLockSuccess = `
LOCKID: %s
[reset][bold][green]Terraform state has been successfully been locked![reset][green]
The state has been locked, and Terraform commands should now be blocked from
obtaining a new lock on the remote state.
`

0 comments on commit c2f1336

Please sign in to comment.