| package oss |
| |
| import ( |
| "errors" |
| "fmt" |
| "log" |
| "path" |
| "sort" |
| "strings" |
| |
| "github.com/aliyun/aliyun-oss-go-sdk/oss" |
| "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore" |
| |
| "github.com/hashicorp/terraform/internal/backend" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/states/remote" |
| "github.com/hashicorp/terraform/internal/states/statemgr" |
| ) |
| |
| const ( |
| lockFileSuffix = ".tflock" |
| ) |
| |
| // get a remote client configured for this state |
| func (b *Backend) remoteClient(name string) (*RemoteClient, error) { |
| if name == "" { |
| return nil, errors.New("missing state name") |
| } |
| |
| client := &RemoteClient{ |
| ossClient: b.ossClient, |
| bucketName: b.bucketName, |
| stateFile: b.stateFile(name), |
| lockFile: b.lockFile(name), |
| serverSideEncryption: b.serverSideEncryption, |
| acl: b.acl, |
| otsTable: b.otsTable, |
| otsClient: b.otsClient, |
| } |
| if b.otsEndpoint != "" && b.otsTable != "" { |
| _, err := b.otsClient.DescribeTable(&tablestore.DescribeTableRequest{ |
| TableName: b.otsTable, |
| }) |
| if err != nil { |
| return client, fmt.Errorf("error describing table store %s: %#v", b.otsTable, err) |
| } |
| } |
| |
| return client, nil |
| } |
| |
| func (b *Backend) Workspaces() ([]string, error) { |
| bucket, err := b.ossClient.Bucket(b.bucketName) |
| if err != nil { |
| return []string{""}, fmt.Errorf("error getting bucket: %#v", err) |
| } |
| |
| var options []oss.Option |
| options = append(options, oss.Prefix(b.statePrefix+"/"), oss.MaxKeys(1000)) |
| resp, err := bucket.ListObjects(options...) |
| if err != nil { |
| return nil, err |
| } |
| |
| result := []string{backend.DefaultStateName} |
| prefix := b.statePrefix |
| lastObj := "" |
| for { |
| for _, obj := range resp.Objects { |
| // we have 3 parts, the state prefix, the workspace name, and the state file: <prefix>/<worksapce-name>/<key> |
| if path.Join(b.statePrefix, b.stateKey) == obj.Key { |
| // filter the default workspace |
| continue |
| } |
| lastObj = obj.Key |
| parts := strings.Split(strings.TrimPrefix(obj.Key, prefix+"/"), "/") |
| if len(parts) > 0 && parts[0] != "" { |
| result = append(result, parts[0]) |
| } |
| } |
| if resp.IsTruncated { |
| if len(options) == 3 { |
| options[2] = oss.Marker(lastObj) |
| } else { |
| options = append(options, oss.Marker(lastObj)) |
| } |
| resp, err = bucket.ListObjects(options...) |
| if err != nil { |
| return nil, err |
| } |
| } else { |
| break |
| } |
| } |
| sort.Strings(result[1:]) |
| return result, nil |
| } |
| |
| func (b *Backend) DeleteWorkspace(name string, _ bool) error { |
| if name == backend.DefaultStateName || name == "" { |
| return fmt.Errorf("can't delete default state") |
| } |
| |
| client, err := b.remoteClient(name) |
| if err != nil { |
| return err |
| } |
| return client.Delete() |
| } |
| |
| func (b *Backend) StateMgr(name string) (statemgr.Full, error) { |
| client, err := b.remoteClient(name) |
| if err != nil { |
| return nil, err |
| } |
| stateMgr := &remote.State{Client: client} |
| |
| // Check to see if this state already exists. |
| existing, err := b.Workspaces() |
| if err != nil { |
| return nil, err |
| } |
| |
| log.Printf("[DEBUG] Current workspace name: %s. All workspaces:%#v", name, existing) |
| |
| exists := false |
| for _, s := range existing { |
| if s == name { |
| exists = true |
| break |
| } |
| } |
| // We need to create the object so it's listed by States. |
| if !exists { |
| // take a lock on this state while we write it |
| lockInfo := statemgr.NewLockInfo() |
| lockInfo.Operation = "init" |
| lockId, err := client.Lock(lockInfo) |
| if err != nil { |
| return nil, fmt.Errorf("failed to lock OSS state: %s", err) |
| } |
| |
| // Local helper function so we can call it multiple places |
| lockUnlock := func(e error) error { |
| if err := stateMgr.Unlock(lockId); err != nil { |
| return fmt.Errorf(strings.TrimSpace(stateUnlockError), lockId, err) |
| } |
| return e |
| } |
| |
| // Grab the value |
| if err := stateMgr.RefreshState(); err != nil { |
| err = lockUnlock(err) |
| return nil, err |
| } |
| |
| // If we have no state, we have to create an empty state |
| if v := stateMgr.State(); v == nil { |
| if err := stateMgr.WriteState(states.NewState()); err != nil { |
| err = lockUnlock(err) |
| return nil, err |
| } |
| if err := stateMgr.PersistState(nil); err != nil { |
| err = lockUnlock(err) |
| return nil, err |
| } |
| } |
| |
| // Unlock, the state should now be initialized |
| if err := lockUnlock(nil); err != nil { |
| return nil, err |
| } |
| |
| } |
| return stateMgr, nil |
| } |
| |
| func (b *Backend) stateFile(name string) string { |
| if name == backend.DefaultStateName { |
| return path.Join(b.statePrefix, b.stateKey) |
| } |
| return path.Join(b.statePrefix, name, b.stateKey) |
| } |
| |
| func (b *Backend) lockFile(name string) string { |
| return b.stateFile(name) + lockFileSuffix |
| } |
| |
| const stateUnlockError = ` |
| Error unlocking Alibaba Cloud OSS state file: |
| |
| Lock ID: %s |
| Error message: %#v |
| |
| You may have to force-unlock this state in order to use it again. |
| The Alibaba Cloud backend acquires a lock during initialization to ensure the initial state file is created. |
| ` |