Skip to content

Commit 8bb6ebc

Browse files
committed
Add Alibaba Cloud backend OSS with lock
1 parent 1e09bec commit 8bb6ebc

46 files changed

Lines changed: 10475 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/init/init.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
backendGCS "github.com/hashicorp/terraform/backend/remote-state/gcs"
2121
backendInmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
2222
backendManta "github.com/hashicorp/terraform/backend/remote-state/manta"
23+
backendOSS "github.com/hashicorp/terraform/backend/remote-state/oss"
2324
backendS3 "github.com/hashicorp/terraform/backend/remote-state/s3"
2425
backendSwift "github.com/hashicorp/terraform/backend/remote-state/swift"
2526
)
@@ -68,6 +69,7 @@ func Init(services *disco.Disco) {
6869
// Deprecated backends.
6970
"azure": deprecateBackend(backendAzure.New(),
7071
`Warning: "azure" name is deprecated, please use "azurerm"`),
72+
"oss": func() backend.Backend { return backendOSS.New() },
7173
}
7274

7375
// Add the legacy remote backends that haven't yet been converted to
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package oss
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/aliyun/aliyun-oss-go-sdk/oss"
7+
"github.com/denverdino/aliyungo/common"
8+
"github.com/denverdino/aliyungo/location"
9+
"github.com/hashicorp/terraform/backend"
10+
"github.com/hashicorp/terraform/helper/schema"
11+
"github.com/hashicorp/terraform/terraform"
12+
"os"
13+
"strings"
14+
15+
"log"
16+
"time"
17+
)
18+
19+
// New creates a new backend for OSS remote state.
20+
func New() backend.Backend {
21+
s := &schema.Backend{
22+
Schema: map[string]*schema.Schema{
23+
"access_key": &schema.Schema{
24+
Type: schema.TypeString,
25+
Optional: true,
26+
Description: "Alibaba Cloud Access Key ID",
27+
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ACCESS_KEY", os.Getenv("ALICLOUD_ACCESS_KEY_ID")),
28+
},
29+
30+
"secret_key": &schema.Schema{
31+
Type: schema.TypeString,
32+
Optional: true,
33+
Description: "Alibaba Cloud Access Secret Key",
34+
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SECRET_KEY", os.Getenv("ALICLOUD_ACCESS_KEY_SECRET")),
35+
},
36+
37+
"security_token": &schema.Schema{
38+
Type: schema.TypeString,
39+
Optional: true,
40+
Description: "Alibaba Cloud Security Token",
41+
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SECURITY_TOKEN", os.Getenv("SECURITY_TOKEN")),
42+
},
43+
44+
"region": &schema.Schema{
45+
Type: schema.TypeString,
46+
Optional: true,
47+
Description: "The region of the OSS bucket.",
48+
DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_REGION", os.Getenv("ALICLOUD_DEFAULT_REGION")),
49+
},
50+
51+
"bucket": &schema.Schema{
52+
Type: schema.TypeString,
53+
Required: true,
54+
Description: "The name of the OSS bucket",
55+
},
56+
57+
"path": &schema.Schema{
58+
Type: schema.TypeString,
59+
Required: true,
60+
Description: "The path relative to your object storage directory where the state file will be stored.",
61+
},
62+
63+
"name": &schema.Schema{
64+
Type: schema.TypeString,
65+
Optional: true,
66+
Description: "The name of the state file inside the bucket",
67+
ValidateFunc: func(v interface{}, s string) ([]string, []error) {
68+
if strings.HasPrefix(v.(string), "/") || strings.HasSuffix(v.(string), "/") {
69+
return nil, []error{fmt.Errorf("name can not start and end with '/'")}
70+
}
71+
return nil, nil
72+
},
73+
Default: "terraform.tfstate",
74+
},
75+
76+
"lock": &schema.Schema{
77+
Type: schema.TypeBool,
78+
Optional: true,
79+
Description: "Whether to lock state access. Defaults to true",
80+
Default: true,
81+
},
82+
83+
"encrypt": &schema.Schema{
84+
Type: schema.TypeBool,
85+
Optional: true,
86+
Description: "Whether to enable server side encryption of the state file",
87+
Default: false,
88+
},
89+
90+
"acl": &schema.Schema{
91+
Type: schema.TypeString,
92+
Optional: true,
93+
Description: "Object ACL to be applied to the state file",
94+
Default: "",
95+
ValidateFunc: func(v interface{}, k string) ([]string, []error) {
96+
if value := v.(string); value != "" {
97+
acls := oss.ACLType(value)
98+
if acls != oss.ACLPrivate && acls != oss.ACLPublicRead && acls != oss.ACLPublicReadWrite {
99+
return nil, []error{fmt.Errorf(
100+
"%q must be a valid ACL value , expected %s, %s or %s, got %q",
101+
k, oss.ACLPrivate, oss.ACLPublicRead, oss.ACLPublicReadWrite, acls)}
102+
}
103+
}
104+
return nil, nil
105+
},
106+
},
107+
},
108+
}
109+
110+
result := &Backend{Backend: s}
111+
result.Backend.ConfigureFunc = result.configure
112+
return result
113+
}
114+
115+
type Backend struct {
116+
*schema.Backend
117+
118+
// The fields below are set from configure
119+
ossClient *oss.Client
120+
121+
bucketName string
122+
statePath string
123+
stateName string
124+
serverSideEncryption bool
125+
acl string
126+
security_token string
127+
endpoint string
128+
lock bool
129+
}
130+
131+
func (b *Backend) configure(ctx context.Context) error {
132+
if b.ossClient != nil {
133+
return nil
134+
}
135+
136+
// Grab the resource data
137+
d := schema.FromContextBackendConfig(ctx)
138+
139+
b.bucketName = d.Get("bucket").(string)
140+
dir := strings.Trim(d.Get("path").(string), "/")
141+
if strings.HasPrefix(dir, "./") {
142+
dir = strings.TrimPrefix(dir, "./")
143+
144+
}
145+
146+
b.statePath = dir
147+
b.stateName = d.Get("name").(string)
148+
b.serverSideEncryption = d.Get("encrypt").(bool)
149+
b.acl = d.Get("acl").(string)
150+
b.lock = d.Get("lock").(bool)
151+
152+
access_key := d.Get("access_key").(string)
153+
secret_key := d.Get("secret_key").(string)
154+
security_token := d.Get("security_token").(string)
155+
endpoint := os.Getenv("OSS_ENDPOINT")
156+
if endpoint == "" {
157+
region := common.Region(d.Get("region").(string))
158+
if end, err := b.getOSSEndpointByRegion(access_key, secret_key, security_token, region); end != "" {
159+
endpoint = endpoint
160+
} else {
161+
log.Printf("[DEBUG] Describe OSS endpoint got an error: %#v", err)
162+
endpoint = fmt.Sprintf("oss-%s.aliyuncs.com", string(region))
163+
}
164+
}
165+
166+
log.Printf("[DEBUG] Instantiate OSS client using endpoint: %#v", endpoint)
167+
var options []oss.ClientOption
168+
if security_token != "" {
169+
options = append(options, oss.SecurityToken(security_token))
170+
}
171+
options = append(options, oss.UserAgent(fmt.Sprintf("HashiCorp-Terraform-v%s", strings.TrimSuffix(terraform.VersionString(), "-dev"))))
172+
173+
if client, err := oss.New(fmt.Sprintf("http://%s", endpoint), access_key, secret_key, options...); err != nil {
174+
return err
175+
} else {
176+
b.ossClient = client
177+
}
178+
179+
return nil
180+
}
181+
182+
func (b *Backend) getOSSEndpointByRegion(access_key, secret_key, security_token string, region common.Region) (string, error) {
183+
184+
endpointClient := location.NewClient(access_key, secret_key)
185+
endpointClient.SetSecurityToken(security_token)
186+
var endpointResp *location.DescribeEndpointsResponse
187+
invoker := NewInvoker()
188+
if err := invoker.Run(func() error {
189+
resp, err := endpointClient.DescribeEndpoints(&location.DescribeEndpointsArgs{
190+
Id: region,
191+
ServiceCode: "oss",
192+
Type: "openAPI",
193+
})
194+
endpointResp = resp
195+
return err
196+
}); err != nil {
197+
return "", fmt.Errorf("Describe endpoint using region: %#v got an error: %#v.", region, err)
198+
}
199+
endpointItem := endpointResp.Endpoints.Endpoint
200+
endpoint := ""
201+
if endpointItem != nil && len(endpointItem) > 0 {
202+
endpoint = endpointItem[0].Endpoint
203+
}
204+
205+
return endpoint, nil
206+
}
207+
208+
type Invoker struct {
209+
catchers []*Catcher
210+
}
211+
212+
type Catcher struct {
213+
Reason string
214+
RetryCount int
215+
RetryWaitSeconds int
216+
}
217+
218+
var ClientErrorCatcher = Catcher{"AliyunGoClientFailure", 10, 3}
219+
var ServiceBusyCatcher = Catcher{"ServiceUnavailable", 10, 3}
220+
221+
func NewInvoker() Invoker {
222+
i := Invoker{}
223+
i.AddCatcher(ClientErrorCatcher)
224+
i.AddCatcher(ServiceBusyCatcher)
225+
return i
226+
}
227+
228+
func (a *Invoker) AddCatcher(catcher Catcher) {
229+
a.catchers = append(a.catchers, &catcher)
230+
}
231+
232+
func (a *Invoker) Run(f func() error) error {
233+
err := f()
234+
235+
if err == nil {
236+
return nil
237+
}
238+
239+
for _, catcher := range a.catchers {
240+
if strings.Contains(err.Error(), catcher.Reason) {
241+
catcher.RetryCount--
242+
243+
if catcher.RetryCount <= 0 {
244+
return fmt.Errorf("Retry timeout and got an error: %#v.", err)
245+
} else {
246+
time.Sleep(time.Duration(catcher.RetryWaitSeconds) * time.Second)
247+
return a.Run(f)
248+
}
249+
}
250+
}
251+
return err
252+
}

0 commit comments

Comments
 (0)