Skip to content

Commit

Permalink
Add Alibaba Cloud backend OSS with lock
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaozhu36 committed Aug 9, 2018
1 parent 1e09bec commit 8bb6ebc
Show file tree
Hide file tree
Showing 46 changed files with 10,475 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 @@ -20,6 +20,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 @@ -68,6 +69,7 @@ func Init(services *disco.Disco) {
// Deprecated backends.
"azure": deprecateBackend(backendAzure.New(),
`Warning: "azure" name is deprecated, please use "azurerm"`),
"oss": func() backend.Backend { return backendOSS.New() },
}

// Add the legacy remote backends that haven't yet been converted to
Expand Down
252 changes: 252 additions & 0 deletions backend/remote-state/oss/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
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"
"time"
)

// New creates a new backend for OSS remote state.
func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"access_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Alibaba Cloud Access Key ID",
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ACCESS_KEY", os.Getenv("ALICLOUD_ACCESS_KEY_ID")),
},

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

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

"region": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The region of the OSS bucket.",
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_REGION", os.Getenv("ALICLOUD_DEFAULT_REGION")),
},

"bucket": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The name of the OSS bucket",
},

"path": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "The path relative to your object storage directory where the state file will be stored.",
},

"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "The name of the state file inside the bucket",
ValidateFunc: func(v interface{}, s string) ([]string, []error) {
if strings.HasPrefix(v.(string), "/") || strings.HasSuffix(v.(string), "/") {
return nil, []error{fmt.Errorf("name can not start and end with '/'")}
}
return nil, nil
},
Default: "terraform.tfstate",
},

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

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

"acl": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Object ACL to be applied to the state file",
Default: "",
ValidateFunc: func(v interface{}, k string) ([]string, []error) {
if value := v.(string); value != "" {
acls := oss.ACLType(value)
if acls != oss.ACLPrivate && acls != oss.ACLPublicRead && acls != oss.ACLPublicReadWrite {
return nil, []error{fmt.Errorf(
"%q must be a valid ACL value , expected %s, %s or %s, got %q",
k, oss.ACLPrivate, oss.ACLPublicRead, oss.ACLPublicReadWrite, acls)}
}
}
return nil, nil
},
},
},
}

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
statePath string
stateName string
serverSideEncryption bool
acl string
security_token string
endpoint string
lock bool
}

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

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

b.bucketName = d.Get("bucket").(string)
dir := strings.Trim(d.Get("path").(string), "/")
if strings.HasPrefix(dir, "./") {
dir = strings.TrimPrefix(dir, "./")

}

b.statePath = dir
b.stateName = d.Get("name").(string)
b.serverSideEncryption = d.Get("encrypt").(bool)
b.acl = d.Get("acl").(string)
b.lock = d.Get("lock").(bool)

access_key := d.Get("access_key").(string)
secret_key := d.Get("secret_key").(string)
security_token := d.Get("security_token").(string)
endpoint := os.Getenv("OSS_ENDPOINT")
if endpoint == "" {
region := common.Region(d.Get("region").(string))
if end, err := b.getOSSEndpointByRegion(access_key, secret_key, security_token, region); end != "" {
endpoint = endpoint
} else {
log.Printf("[DEBUG] Describe OSS endpoint got an error: %#v", err)
endpoint = fmt.Sprintf("oss-%s.aliyuncs.com", string(region))
}
}

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", strings.TrimSuffix(terraform.VersionString(), "-dev"))))

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, security_token string, region common.Region) (string, error) {

endpointClient := location.NewClient(access_key, secret_key)
endpointClient.SetSecurityToken(security_token)
var endpointResp *location.DescribeEndpointsResponse
invoker := NewInvoker()
if err := invoker.Run(func() error {
resp, err := endpointClient.DescribeEndpoints(&location.DescribeEndpointsArgs{
Id: region,
ServiceCode: "oss",
Type: "openAPI",
})
endpointResp = resp
return err
}); err != nil {
return "", fmt.Errorf("Describe endpoint using region: %#v got an error: %#v.", region, err)
}
endpointItem := endpointResp.Endpoints.Endpoint
endpoint := ""
if endpointItem != nil && len(endpointItem) > 0 {
endpoint = endpointItem[0].Endpoint
}

return endpoint, nil
}

type Invoker struct {
catchers []*Catcher
}

type Catcher struct {
Reason string
RetryCount int
RetryWaitSeconds int
}

var ClientErrorCatcher = Catcher{"AliyunGoClientFailure", 10, 3}
var ServiceBusyCatcher = Catcher{"ServiceUnavailable", 10, 3}

func NewInvoker() Invoker {
i := Invoker{}
i.AddCatcher(ClientErrorCatcher)
i.AddCatcher(ServiceBusyCatcher)
return i
}

func (a *Invoker) AddCatcher(catcher Catcher) {
a.catchers = append(a.catchers, &catcher)
}

func (a *Invoker) Run(f func() error) error {
err := f()

if err == nil {
return nil
}

for _, catcher := range a.catchers {
if strings.Contains(err.Error(), catcher.Reason) {
catcher.RetryCount--

if catcher.RetryCount <= 0 {
return fmt.Errorf("Retry timeout and got an error: %#v.", err)
} else {
time.Sleep(time.Duration(catcher.RetryWaitSeconds) * time.Second)
return a.Run(f)
}
}
}
return err
}
Loading

0 comments on commit 8bb6ebc

Please sign in to comment.