Skip to content

Commit 110a740

Browse files
Domas Monkus0x2b3bfa0
andauthored
Support reusing existing storage containers across task providers (#687)
* Support for existing storage containers in aws and gcp. (#651) * Update config schema. * AWS support for existing S3 buckets. * Existing bucket support for gcp. * Add mocks and tests to existing bucket resource in aws. * Update docs with new pre-allocated container fields. * Support using pre-allocated blob containers in azure. (#660) * AWS support for existing S3 buckets. * Existing bucket support for gcp. * Fix subdirectory in rclone remote. * Blob container generates the rclone connection string. * Introduce a type for generating rclone connection strings. * Azure support for reusable blob containers. * Update docs. * Fix path prefix. * Initialize s3 existing bucket with RemoteStorage struct. * Update gcp and aws existing bucket data sources to align with the azure data source. Use common.RemoteStorage to initialize the data sources. Using rclone to verify storage during Read. Remove aws s3 client mocks and tests that rely on them. * Fix comment. * K8s support for specifying an existing persistent volume claim (#661) * K8s support for specifying an existing persistent volume claim. Co-authored-by: Helio Machado <0x2b3bfa0+git@googlemail.com> * Update use of Identifier struct. * Combine container and container_path config keys. * Update docs. * Use split function from rclone. Co-authored-by: Helio Machado <0x2b3bfa0+git@googlemail.com>
1 parent 127dcf5 commit 110a740

31 files changed

+923
-210
lines changed

docs/resources/task.md

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ resource "iterative_task" "example" {
6565
- `storage.workdir` - (Optional) Local working directory to upload and use as the `script` working directory.
6666
- `storage.output` - (Optional) Results directory (**relative to `workdir`**) to download (default: no download).
6767
- `storage.exclude` - (Optional) List of files and globs to exclude from transfering. Excluded files are neither uploaded to cloud storage nor downloaded from it. Exclusions are defined relative to `storage.workdir`.
68+
- `storage.container` - (Optional) Pre-allocated container to use for storage of task data, results and status.
69+
- `storage.container_opts` - (Optional) Block of cloud-specific container settings.
6870
- `environment` - (Optional) Map of environment variable names and values for the task script. Empty string values are replaced with local environment values. Empty values may also be combined with a [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>) name to import all matching variables.
6971
- `timeout` - (Optional) Maximum number of seconds to run before instances are force-terminated. The countdown is reset each time TPI auto-respawns a spot instance.
7072
- `tags` - (Optional) Map of tags for the created cloud resources.
@@ -281,25 +283,86 @@ spec:
281283
282284
## Permission Set
283285
286+
### Generic
287+
284288
A set of "permissions" assigned to the `task` instance, format depends on the cloud provider
285289

286-
#### Amazon Web Services
290+
### Cloud-specific
291+
292+
#### Kubernetes
293+
294+
The name of a service account in the current namespace.
295+
296+
### Amazon Web Services
287297

288298
An [instance profile `arn`](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html), e.g.:
289299
`permission_set = "arn:aws:iam:1234567890:instance-profile/rolename"`
290300

291-
#### Google Cloud Platform
301+
### Google Cloud Platform
292302

293303
A service account email and a [list of scopes](https://cloud.google.com/sdk/gcloud/reference/alpha/compute/instances/set-scopes#--scopes), e.g.:
294304
`permission_set = "sa-name@project_id.iam.gserviceaccount.com,scopes=storage-rw"`
295305

296-
#### Microsoft Azure
306+
### Microsoft Azure
297307
A comma-separated list of [user-assigned identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) ARM resource ids, e.g.:
298308
`permission_set = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}"`
299309

310+
## Pre-allocated blob container
311+
312+
### Generic
313+
314+
To use a pre-allocated container for storing task data, specify the `container` key in the `storage` section
315+
of the config:
316+
317+
```hcl
318+
resource "iterative_task" "example" {
319+
(...)
320+
storage {
321+
container = "container-name/path/path"
322+
}
323+
(...)
324+
}
325+
```
326+
327+
The container name may include a path component, in this case the specified subdirectory will be used
328+
to store task execution results. Otherwise, a subdirectory will be created with a name matchin the
329+
task's randomly generated id.
330+
331+
If the container name is suffixed with a forward slash, (`container-name/`), the root of the container
332+
will be used for storage.
333+
334+
### Cloud-specific
335+
336+
#### Amazon Web Services
337+
338+
The container name is the name of the S3 container. It should be in the same region as the task deployment.
339+
340+
#### Google Cloud Platform
341+
342+
The container name is the name of the google cloud storage container.
343+
300344
#### Kubernetes
301345

302-
The name of a service account in the current namespace.
346+
The container name is the name of a predefined persistent volume claim.
347+
348+
#### Microsoft Azure
349+
350+
To use a pre-allocated azure blob container, the storage account name and access key need to be specified in
351+
the `storage` section:
352+
353+
```
354+
resource "iterative_task" "example" {
355+
(...)
356+
storage {
357+
container = "container-name"
358+
container_opts = {
359+
account = "storage-account-name"
360+
key = "storage-account-key"
361+
}
362+
}
363+
(...)
364+
}
365+
```
303366

304367
## Known Issues
305368

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/docker/go-units v0.4.0
2424
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
2525
github.com/gobwas/glob v0.2.3
26+
github.com/golang/mock v1.6.0 // indirect
2627
github.com/google/go-github/v42 v42.0.0
2728
github.com/google/go-github/v45 v45.2.0
2829
github.com/google/uuid v1.3.0

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
443443
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
444444
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
445445
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
446+
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
446447
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
447448
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
448449
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

iterative/resource_task.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99
"strings"
1010
"time"
1111

12-
"github.com/sirupsen/logrus"
13-
1412
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1513
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
14+
"github.com/rclone/rclone/lib/bucket"
15+
"github.com/sirupsen/logrus"
1616

1717
"terraform-provider-iterative/iterative/utils"
1818
"terraform-provider-iterative/task"
@@ -139,6 +139,20 @@ func resourceTask() *schema.Resource {
139139
Optional: true,
140140
Default: "",
141141
},
142+
"container": {
143+
Type: schema.TypeString,
144+
ForceNew: true,
145+
Optional: true,
146+
Default: "",
147+
},
148+
"container_opts": {
149+
Type: schema.TypeMap,
150+
ForceNew: true,
151+
Optional: true,
152+
Elem: &schema.Schema{
153+
Type: schema.TypeString,
154+
},
155+
},
142156
"exclude": {
143157
Type: schema.TypeList,
144158
ForceNew: false,
@@ -349,19 +363,38 @@ func resourceTaskBuild(ctx context.Context, d *schema.ResourceData, m interface{
349363
var directory string
350364
var directoryOut string
351365
var excludeList []string
366+
var remoteStorage *common.RemoteStorage
352367
if d.Get("storage").(*schema.Set).Len() > 0 {
353368
storage := d.Get("storage").(*schema.Set).List()[0].(map[string]interface{})
354369
directory = storage["workdir"].(string)
355370
directoryOut = storage["output"].(string)
356-
directoryOut = filepath.Clean(directoryOut)
357371
if filepath.IsAbs(directoryOut) || strings.HasPrefix(directoryOut, "../") {
358372
return nil, errors.New("storage.output must be inside storage.workdir")
359373
}
360-
361374
excludes := storage["exclude"].([]interface{})
362375
for _, exclude := range excludes {
363376
excludeList = append(excludeList, exclude.(string))
364377
}
378+
379+
// Propagate configuration for pre-allocated storage container.
380+
containerRaw := storage["container"].(string)
381+
if containerRaw != "" {
382+
container, containerPath := bucket.Split(containerRaw)
383+
remoteStorage = &common.RemoteStorage{
384+
Container: container,
385+
Path: containerPath,
386+
Config: map[string]string{},
387+
}
388+
if storage["container_opts"] != nil {
389+
remoteConfig := storage["container_opts"].(map[string]interface{})
390+
var ok bool
391+
for key, value := range remoteConfig {
392+
if remoteStorage.Config[key], ok = value.(string); !ok {
393+
return nil, fmt.Errorf("invalid value for remote config key %q: %v", key, value)
394+
}
395+
}
396+
}
397+
}
365398
}
366399

367400
t := common.Task{
@@ -384,6 +417,7 @@ func resourceTaskBuild(ctx context.Context, d *schema.ResourceData, m interface{
384417
},
385418
// Egress is open on every port
386419
},
420+
RemoteStorage: remoteStorage,
387421
Spot: common.Spot(d.Get("spot").(float64)),
388422
Parallelism: uint16(d.Get("parallelism").(int)),
389423
PermissionSet: d.Get("permission_set").(string),

task/aws/client/client.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,18 @@ func New(ctx context.Context, cloud common.Cloud, tags map[string]string) (*Clie
3333
if err != nil {
3434
return nil, err
3535
}
36-
36+
credentials, err := config.Credentials.Retrieve(ctx)
37+
if err != nil {
38+
return nil, err
39+
}
3740
c := &Client{
38-
Cloud: cloud,
39-
Region: region,
40-
Tags: cloud.Tags,
41-
Config: config,
41+
Cloud: cloud,
42+
Region: region,
43+
Tags: cloud.Tags,
44+
Config: config,
45+
credentials: credentials,
4246
}
47+
4348
c.Services.EC2 = ec2.NewFromConfig(config)
4449
c.Services.S3 = s3.NewFromConfig(config)
4550
c.Services.STS = sts.NewFromConfig(config)
@@ -52,8 +57,9 @@ type Client struct {
5257
Region string
5358
Tags map[string]string
5459

55-
Config aws.Config
56-
Services struct {
60+
Config aws.Config
61+
credentials aws.Credentials
62+
Services struct {
5763
EC2 *ec2.Client
5864
S3 *s3.Client
5965
STS *sts.Client
@@ -93,3 +99,8 @@ func (c *Client) DecodeError(ctx context.Context, encoded error) error {
9399

94100
return fmt.Errorf("unable to decode: %s", encoded.Error())
95101
}
102+
103+
// Credentials returns the AWS credentials the client is currently using.
104+
func (c *Client) Credentials() aws.Credentials {
105+
return c.credentials
106+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package resources
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/aws/aws-sdk-go-v2/aws"
8+
9+
"terraform-provider-iterative/task/common"
10+
"terraform-provider-iterative/task/common/machine"
11+
)
12+
13+
// NewExistingS3Bucket returns a new data source refering to a pre-allocated
14+
// S3 bucket.
15+
func NewExistingS3Bucket(credentials aws.Credentials, storageParams common.RemoteStorage) *ExistingS3Bucket {
16+
return &ExistingS3Bucket{
17+
credentials: credentials,
18+
params: storageParams,
19+
}
20+
}
21+
22+
// ExistingS3Bucket identifies an existing S3 bucket.
23+
type ExistingS3Bucket struct {
24+
credentials aws.Credentials
25+
26+
params common.RemoteStorage
27+
}
28+
29+
// Read verifies the specified S3 bucket is accessible.
30+
func (b *ExistingS3Bucket) Read(ctx context.Context) error {
31+
err := machine.CheckStorage(ctx, b.connection())
32+
if err != nil {
33+
return fmt.Errorf("failed to verify existing s3 bucket: %w", err)
34+
}
35+
return nil
36+
}
37+
38+
func (b *ExistingS3Bucket) connection() machine.RcloneConnection {
39+
region := b.params.Config["region"]
40+
return machine.RcloneConnection{
41+
Backend: machine.RcloneBackendS3,
42+
Container: b.params.Container,
43+
Path: b.params.Path,
44+
Config: map[string]string{
45+
"provider": "AWS",
46+
"region": region,
47+
"access_key_id": b.credentials.AccessKeyID,
48+
"secret_access_key": b.credentials.SecretAccessKey,
49+
"session_token": b.credentials.SessionToken,
50+
},
51+
}
52+
}
53+
54+
// ConnectionString implements common.StorageCredentials.
55+
// The method returns the rclone connection string for the specific bucket.
56+
func (b *ExistingS3Bucket) ConnectionString(ctx context.Context) (string, error) {
57+
connection := b.connection()
58+
return connection.String(), nil
59+
}
60+
61+
// build-time check to ensure Bucket implements BucketCredentials.
62+
var _ common.StorageCredentials = (*ExistingS3Bucket)(nil)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package resources_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/aws/aws-sdk-go-v2/aws"
8+
"github.com/stretchr/testify/require"
9+
10+
"terraform-provider-iterative/task/aws/resources"
11+
"terraform-provider-iterative/task/common"
12+
)
13+
14+
func TestExistingBucketConnectionString(t *testing.T) {
15+
ctx := context.Background()
16+
creds := aws.Credentials{
17+
AccessKeyID: "access-key-id",
18+
SecretAccessKey: "secret-access-key",
19+
SessionToken: "session-token",
20+
}
21+
b := resources.NewExistingS3Bucket(creds, common.RemoteStorage{
22+
Container: "pre-created-bucket",
23+
Config: map[string]string{"region": "us-east-1"},
24+
Path: "subdirectory"})
25+
connStr, err := b.ConnectionString(ctx)
26+
require.NoError(t, err)
27+
require.Equal(t, ":s3,access_key_id='access-key-id',provider='AWS',region='us-east-1',secret_access_key='secret-access-key',session_token='session-token':pre-created-bucket/subdirectory", connStr)
28+
}

task/aws/resources/data_source_credentials.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ package resources
22

33
import (
44
"context"
5-
"fmt"
65

76
"terraform-provider-iterative/task/aws/client"
87
"terraform-provider-iterative/task/common"
98
)
109

11-
func NewCredentials(client *client.Client, identifier common.Identifier, bucket *Bucket) *Credentials {
10+
func NewCredentials(client *client.Client, identifier common.Identifier, bucket common.StorageCredentials) *Credentials {
1211
c := &Credentials{
1312
client: client,
1413
Identifier: identifier.Long(),
@@ -21,7 +20,7 @@ type Credentials struct {
2120
client *client.Client
2221
Identifier string
2322
Dependencies struct {
24-
Bucket *Bucket
23+
Bucket common.StorageCredentials
2524
}
2625
Resource map[string]string
2726
}
@@ -32,20 +31,16 @@ func (c *Credentials) Read(ctx context.Context) error {
3231
return err
3332
}
3433

35-
connectionString := fmt.Sprintf(
36-
":s3,provider=AWS,region=%s,access_key_id=%s,secret_access_key=%s,session_token=%s:%s",
37-
c.client.Region,
38-
credentials.AccessKeyID,
39-
credentials.SecretAccessKey,
40-
credentials.SessionToken,
41-
c.Dependencies.Bucket.Identifier,
42-
)
34+
bucketConnStr, err := c.Dependencies.Bucket.ConnectionString(ctx)
35+
if err != nil {
36+
return err
37+
}
4338

4439
c.Resource = map[string]string{
4540
"AWS_ACCESS_KEY_ID": credentials.AccessKeyID,
4641
"AWS_SECRET_ACCESS_KEY": credentials.SecretAccessKey,
4742
"AWS_SESSION_TOKEN": credentials.SessionToken,
48-
"RCLONE_REMOTE": connectionString,
43+
"RCLONE_REMOTE": bucketConnStr,
4944
"TPI_TASK_CLOUD_PROVIDER": string(c.client.Cloud.Provider),
5045
"TPI_TASK_CLOUD_REGION": c.client.Region,
5146
"TPI_TASK_IDENTIFIER": c.Identifier,

0 commit comments

Comments
 (0)