Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backend/tikv: Add TiKV as remote state backend #23481

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
116 changes: 116 additions & 0 deletions backend/remote-state/tikv/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package tikv

import (
"context"

"github.com/tikv/client-go/config"
"github.com/tikv/client-go/rawkv"
"github.com/tikv/client-go/txnkv"

"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
)

func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"pd_address": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{
Type: schema.TypeString,
},
MinItems: 1,
Required: true,
Description: "address of the tikv pd cluster.",
},

"prefix": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Path to store state in TiKV",
},

"lock": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: "Lock state access",
Default: true,
},

"ca_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "A path to a PEM-encoded certificate authority used to verify the remote agent's certificate.",
},

"cert_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "A path to a PEM-encoded certificate provided to the remote agent; requires use of key_file.",
},

"key_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "A path to a PEM-encoded private key, required if cert_file is specified.",
},
},
}

result := &Backend{Backend: s}
result.Backend.ConfigureFunc = result.configure
return result
}

type Backend struct {
*schema.Backend

// The fields below are set from configure.
rawKvClient *rawkv.Client
txnKvClient *txnkv.Client
data *schema.ResourceData
lock bool
}

func (b *Backend) configure(ctx context.Context) error {
var err error

// Grab the resource data.
b.data = schema.FromContextBackendConfig(ctx)

// Store the lock information.
b.lock = b.data.Get("lock").(bool)
cfg := config.Default()
if v, ok := b.data.GetOk("ca_file"); ok && v.(string) != "" {
cfg.RPC.Security.SSLCA = v.(string)
}
if v, ok := b.data.GetOk("cert_file"); ok && v.(string) != "" {
cfg.RPC.Security.SSLCert = v.(string)
}
if v, ok := b.data.GetOk("key_file"); ok && v.(string) != "" {
cfg.RPC.Security.SSLKey = v.(string)
}

// Initialize tikv client
pdAddresses := retrieveAddresses(b.data.Get("pd_address"))
b.rawKvClient, err = rawkv.NewClient(ctx, pdAddresses, cfg)
if err != nil {
return err
}

b.txnKvClient, err = txnkv.NewClient(ctx, pdAddresses, cfg)
if err != nil {
return err
}

return err
}

func retrieveAddresses(v interface{}) []string {
var addresses []string
list := v.([]interface{})
for _, addr := range list {
addresses = append(addresses, addr.(string))
}
return addresses
}
189 changes: 189 additions & 0 deletions backend/remote-state/tikv/backend_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package tikv

import (
"context"
"fmt"
"github.com/tikv/client-go/key"
"github.com/tikv/client-go/txnkv"
"strings"

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statemgr"
)

const (
keyEnvPrefix = "-env:"
)

func (b *Backend) Workspaces() ([]string, error) {
// List our raw path
prefix := b.data.Get("prefix").(string) + keyEnvPrefix
keys, err := getKeys(b.txnKvClient, prefix)
if err != nil {
return nil, err
}

envs := map[string]struct{}{}
for _, k := range keys {
if strings.HasPrefix(k, prefix) {
k = strings.TrimPrefix(k, prefix)

if idx := strings.IndexRune(k, '/'); idx >= 0 {
continue
}

envs[k] = struct{}{}
}
}

result := make([]string, 1, len(envs)+1)
result[0] = backend.DefaultStateName
for k := range envs {
result = append(result, k)
}

return result, nil
}

func (b *Backend) DeleteWorkspace(name string) error {
if name == backend.DefaultStateName || name == "" {
return fmt.Errorf("can't delete default state")
}

// Determine the path of the data
path := b.path(name)

// Delete it. We just delete it without any locking since
// the DeleteState API is documented as such.
tx, err := b.txnKvClient.Begin(context.TODO())
if err != nil {
return err
}
err = tx.Delete([]byte(path))
if e := tx.Commit(context.TODO()); e != nil {
err = multierror.Append(err, e)
}
return err
}

func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
// Determine the path of the data
path := b.path(name)

// Build the state client
var stateMgr = &remote.State{
Client: &RemoteClient{
rawKvClient: b.rawKvClient,
txnKvClient: b.txnKvClient,
Key: path,
DoLock: b.lock,
},
}

if !b.lock {
stateMgr.DisableLocks()
}

// the default state always exists
if name == backend.DefaultStateName {
return stateMgr, nil
}

// Grab a lock, we use this to write an empty state if one doesn't
// exist already. We have to write an empty state as a sentinel value
// so States() knows it exists.
lockInfo := state.NewLockInfo()
lockInfo.Operation = "init"
lockId, err := stateMgr.Lock(lockInfo)
if err != nil {
return nil, fmt.Errorf("failed to lock state in Consul: %s", err)
}

// Local helper function so we can call it multiple places
lockUnlock := func(parent error) error {
if err := stateMgr.Unlock(lockId); err != nil {
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
}

return parent
}

// 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(); 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 getKeys(txnKvClient *txnkv.Client, prefix string) ([]string, error) {
ctx := context.TODO()
txn, err := txnKvClient.Begin(ctx)
if err != nil {
return nil, err
}

it, err := txn.Iter(ctx, key.Key(prefix), nil)
if err != nil {
return nil, err
}

var keys []string
prefixKey := key.Key(prefix)

for it.Valid() {
if !it.Key().HasPrefix(prefixKey) {
break
}

keys = append(keys, string(it.Key()))

err = it.Next(ctx)
if err != nil {
return nil, err
}
}

return keys, nil
}

func (b *Backend) path(name string) string {
path := b.data.Get("prefix").(string)
if name != backend.DefaultStateName {
path += fmt.Sprintf("%s%s", keyEnvPrefix, name)
}

return path
}

const errStateUnlock = `
Error unlocking TiKV state. Lock ID: %s

Error: %s

You may have to force-unlock this state in order to use it again.
The TiKV backend acquires a lock during initialization to ensure
the minimum required key/values are prepared.
`
Loading