Skip to content

Commit

Permalink
add new backend OSS
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaozhu36 committed Dec 15, 2017
1 parent 03ddb91 commit 75d5061
Show file tree
Hide file tree
Showing 45 changed files with 10,561 additions and 0 deletions.
2 changes: 2 additions & 0 deletions backend/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
backendGCS "github.com/hashicorp/terraform/backend/remote-state/gcs"
backendinmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
backendManta "github.com/hashicorp/terraform/backend/remote-state/manta"
backendOSS "github.com/hashicorp/terraform/backend/remote-state/oss"
backendS3 "github.com/hashicorp/terraform/backend/remote-state/s3"
backendSwift "github.com/hashicorp/terraform/backend/remote-state/swift"
)
Expand Down Expand Up @@ -51,6 +52,7 @@ func init() {
"etcdv3": func() backend.Backend { return backendetcdv3.New() },
"gcs": func() backend.Backend { return backendGCS.New() },
"manta": func() backend.Backend { return backendManta.New() },
"oss": func() backend.Backend { return backendOSS.New() },
}

// Add the legacy remote backends that haven't yet been convertd to
Expand Down
178 changes: 178 additions & 0 deletions backend/remote-state/oss/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package oss

import (
"context"
"fmt"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/denverdino/aliyungo/common"
"github.com/denverdino/aliyungo/location"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"os"
"strings"

"log"
)

// New creates a new backend for OSS remote state.
func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"bucket": {
Type: schema.TypeString,
Required: true,
Description: "The name of the OSS bucket",
},

"key": {
Type: schema.TypeString,
Required: true,
Description: "The path to the state file inside the bucket",
ValidateFunc: func(v interface{}, s string) ([]string, []error) {
// oss will strip leading slashes from an object, so while this will
// technically be accepted by oss, it will break our workspace hierarchy.
if strings.HasPrefix(v.(string), "/") {
return nil, []error{fmt.Errorf("key must not start with '/'")}
}
return nil, nil
},
},
"access_key": {
Type: schema.TypeString,
Optional: true,
Description: "Alibaba Cloud Access Key ID",
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ACCESS_KEY", ""),
},

"secret_key": {
Type: schema.TypeString,
Optional: true,
Description: "Alibaba Cloud Access Secret Key",
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SECRET_KEY", ""),
},

"security_token": {
Type: schema.TypeString,
Optional: true,
Description: "Alibaba Cloud Security Token",
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SECURITY_TOKEN", os.Getenv("SECURITY_TOKEN")),
},

"region": {
Type: schema.TypeString,
Required: true,
Description: "The region of the OSS bucket. It will be ignored when 'endpoint' is specified.",
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_REGION", "cn-beijing"),
},

"endpoint": {
Type: schema.TypeString,
Optional: true,
Description: "A custom endpoint for the OSS API",
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_OSS_ENDPOINT", ""),
},

"encrypt": {
Type: schema.TypeBool,
Optional: true,
Description: "Whether to enable server side encryption of the state file",
Default: false,
},

"acl": {
Type: schema.TypeString,
Optional: true,
Description: "Object ACL to be applied to the state file",
Default: "",
},

"workspace_key_prefix": {
Type: schema.TypeString,
Optional: true,
Description: "The prefix applied to the non-default state path inside the bucket",
Default: "workspaces",
},
},
}

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

type Backend struct {
*schema.Backend

// The fields below are set from configure
ossClient *oss.Client

bucketName string
keyName string
serverSideEncryption bool
acl string
security_token string
endpoint string
workspaceKeyPrefix string
}

func (b *Backend) configure(ctx context.Context) error {
if b.ossClient != nil {
return nil
}

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

b.bucketName = data.Get("bucket").(string)
b.keyName = data.Get("key").(string)
b.serverSideEncryption = data.Get("encrypt").(bool)
b.acl = data.Get("acl").(string)
b.workspaceKeyPrefix = data.Get("workspace_key_prefix").(string)
access_key := data.Get("access_key").(string)
secret_key := data.Get("secret_key").(string)
security_token := data.Get("security_token").(string)
endpoint := data.Get("endpoint").(string)
if endpoint == "" {
region := common.Region(data.Get("region").(string))
if end, err := b.getOSSEndpointByRegion(access_key, secret_key, region); err != nil {
return err
} else {
endpoint = end
}
}

log.Printf("[DEBUG] Instantiate OSS client using endpoint: %#v", endpoint)
var options []oss.ClientOption
if security_token != "" {
options = append(options, oss.SecurityToken(security_token))
}
options = append(options, oss.UserAgent(fmt.Sprintf("HashiCorp-Terraform-v%s", terraform.VersionString())))

if client, err := oss.New(fmt.Sprintf("http://%s", endpoint), access_key, secret_key, options...); err != nil {
return err
} else {
b.ossClient = client
}

return nil
}

func (b *Backend) getOSSEndpointByRegion(access_key, secret_key string, region common.Region) (string, error) {

endpoints, err := location.NewClient(access_key, secret_key).DescribeEndpoints(&location.DescribeEndpointsArgs{
Id: region,
ServiceCode: "oss",
Type: "openAPI",
})
if err != nil {
return "", fmt.Errorf("Describe endpoint using region: %#v got an error: %#v.", region, err)
}
endpointItem := endpoints.Endpoints.Endpoint
endpoint := ""
if endpointItem != nil && len(endpointItem) > 0 {
endpoint = endpointItem[0].Endpoint
}

return endpoint, nil
}
192 changes: 192 additions & 0 deletions backend/remote-state/oss/backend_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package oss

import (
"errors"
"fmt"
"sort"
"strings"

"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
"log"
"path"
)

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,
statePath: b.statePath(name),
lockPath: b.lockPath(name),
serverSideEncryption: b.serverSideEncryption,
acl: b.acl,
}

return client, nil
}

func (b *Backend) State(name string) (state.State, 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.States()
if err != nil {
return nil, err
}
log.Printf("Current state name: %s. All States:%#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 := state.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
// This is to ensure that no one beat us to writing a state between
// the `exists` check and taking the lock.
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(terraform.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 (b *Backend) States() ([]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.workspaceKeyPrefix))
resp, err := bucket.ListObjects(options...)

if err != nil {
return nil, err
}

workspaces := []string{backend.DefaultStateName}
for _, obj := range resp.Objects {
workspace := b.keyEnv(obj.Key)
if workspace != "" {
workspaces = append(workspaces, workspace)
}
}

sort.Strings(workspaces[1:])
return workspaces, nil
}

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

client, err := b.remoteClient(name)
if err != nil {
return err
}
log.Printf("Delete state %s ...", name)
return client.Delete()
}

// extract the object name from the OSS key
func (b *Backend) keyEnv(key string) string {
// we have 3 parts, the workspace key prefix, the workspace name, and the state key name
parts := strings.SplitN(key, "/", 3)
if len(parts) < 3 {
// no workspace prefix here
return ""
}

// shouldn't happen since we listed by prefix
if parts[0] != b.workspaceKeyPrefix {
return ""
}

// not our key, so don't include it in our listing
if parts[2] != b.keyName {
return ""
}

return parts[1]
}

func (b *Backend) statePath(name string) string {
if name == backend.DefaultStateName && b.keyName != "" {
return b.keyName
}
return path.Join(b.workspaceKeyPrefix, name, b.keyName)
}

func (b *Backend) lockPath(name string) string {
if name == backend.DefaultStateName && b.keyName != "" {
return b.keyName + lockFileSuffix
}
return path.Join(b.workspaceKeyPrefix, name, b.keyName+lockFileSuffix)
}

const stateUnlockError = `
Error unlocking Alicloud 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.
`
Loading

0 comments on commit 75d5061

Please sign in to comment.