-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
45 changed files
with
10,561 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
` |
Oops, something went wrong.