Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.

Commit c1875a2

Browse files
authored
Merge pull request #72 from ulyssessouza/add-aci-volumes
Add volumes to run command
2 parents 7603a3b + d2fece3 commit c1875a2

File tree

12 files changed

+473
-23
lines changed

12 files changed

+473
-23
lines changed

azure/aci.go

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

12-
"github.com/docker/api/azure/login"
13-
1412
"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
1513
"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/subscription/mgmt/subscription"
1614
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
@@ -21,6 +19,9 @@ import (
2119
"github.com/gobwas/ws/wsutil"
2220
"github.com/pkg/errors"
2321

22+
"github.com/docker/api/azure/login"
23+
"github.com/docker/api/errdefs"
24+
2425
"github.com/docker/api/context/store"
2526
)
2627

@@ -257,11 +258,14 @@ func getContainerClient(subscriptionID string) (containerinstance.ContainerClien
257258
return containerClient, nil
258259
}
259260

260-
func getSubscriptionsClient() subscription.SubscriptionsClient {
261+
func getSubscriptionsClient() (subscription.SubscriptionsClient, error) {
261262
subc := subscription.NewSubscriptionsClient()
262-
authorizer, _ := login.NewAuthorizerFromLogin()
263+
authorizer, err := login.NewAuthorizerFromLogin()
264+
if err != nil {
265+
return subscription.SubscriptionsClient{}, errors.Wrap(errdefs.ErrLoginFailed, err.Error())
266+
}
263267
subc.Authorizer = authorizer
264-
return subc
268+
return subc, nil
265269
}
266270

267271
// GetGroupsClient ...
@@ -274,7 +278,10 @@ func GetGroupsClient(subscriptionID string) resources.GroupsClient {
274278

275279
// GetSubscriptionID ...
276280
func GetSubscriptionID(ctx context.Context) (string, error) {
277-
c := getSubscriptionsClient()
281+
c, err := getSubscriptionsClient()
282+
if err != nil {
283+
return "", err
284+
}
278285
res, err := c.List(ctx)
279286
if err != nil {
280287
return "", err

azure/backend.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,17 +155,25 @@ func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerCo
155155
Published: p.HostPort,
156156
})
157157
}
158+
159+
projectVolumes, serviceConfigVolumes, err := convert.GetRunVolumes(r.Volumes)
160+
if err != nil {
161+
return err
162+
}
163+
158164
project := compose.Project{
159165
Name: r.ID,
160166
Config: types.Config{
161167
Services: []types.ServiceConfig{
162168
{
163-
Name: singleContainerName,
164-
Image: r.Image,
165-
Ports: ports,
166-
Labels: r.Labels,
169+
Name: singleContainerName,
170+
Image: r.Image,
171+
Ports: ports,
172+
Labels: r.Labels,
173+
Volumes: serviceConfigVolumes,
167174
},
168175
},
176+
Volumes: projectVolumes,
169177
},
170178
}
171179

azure/convert/volume.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package convert
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/pkg/errors"
10+
11+
"github.com/compose-spec/compose-go/types"
12+
13+
"github.com/docker/api/errdefs"
14+
)
15+
16+
// GetRunVolumes return volume configurations for a project and a single service
17+
// this is meant to be used as a compose project of a single service
18+
func GetRunVolumes(volumes []string) (map[string]types.VolumeConfig, []types.ServiceVolumeConfig, error) {
19+
var serviceConfigVolumes []types.ServiceVolumeConfig
20+
projectVolumes := make(map[string]types.VolumeConfig, len(volumes))
21+
for i, v := range volumes {
22+
var vi volumeInput
23+
err := vi.parse(fmt.Sprintf("volume-%d", i), v)
24+
if err != nil {
25+
return nil, nil, err
26+
}
27+
projectVolumes[vi.name] = types.VolumeConfig{
28+
Name: vi.name,
29+
Driver: azureFileDriverName,
30+
DriverOpts: map[string]string{
31+
volumeDriveroptsAccountNameKey: vi.username,
32+
volumeDriveroptsAccountKeyKey: vi.key,
33+
volumeDriveroptsShareNameKey: vi.share,
34+
},
35+
}
36+
sv := types.ServiceVolumeConfig{
37+
Type: azureFileDriverName,
38+
Source: vi.name,
39+
Target: vi.target,
40+
}
41+
serviceConfigVolumes = append(serviceConfigVolumes, sv)
42+
}
43+
44+
return projectVolumes, serviceConfigVolumes, nil
45+
}
46+
47+
type volumeInput struct {
48+
name string
49+
username string
50+
key string
51+
share string
52+
target string
53+
}
54+
55+
func escapeKeySlashes(rawURL string) (string, error) {
56+
urlSplit := strings.Split(rawURL, "@")
57+
if len(urlSplit) < 1 {
58+
return "", errors.Wrap(errdefs.ErrParsingFailed, "invalid url format "+rawURL)
59+
}
60+
userPasswd := strings.ReplaceAll(urlSplit[0], "/", "_")
61+
62+
atIndex := strings.Index(rawURL, "@")
63+
if atIndex < 0 {
64+
return "", errors.Wrap(errdefs.ErrParsingFailed, "no share specified in "+rawURL)
65+
}
66+
67+
scaped := userPasswd + rawURL[atIndex:]
68+
69+
return scaped, nil
70+
}
71+
72+
func unescapeKey(key string) string {
73+
return strings.ReplaceAll(key, "_", "/")
74+
}
75+
76+
// Removes the second ':' that separates the source from target
77+
func volumeURL(pathURL string) (*url.URL, error) {
78+
scapedURL, err := escapeKeySlashes(pathURL)
79+
if err != nil {
80+
return nil, err
81+
}
82+
pathURL = "//" + scapedURL
83+
84+
count := strings.Count(pathURL, ":")
85+
if count > 2 {
86+
return nil, errors.Wrap(errdefs.ErrParsingFailed, fmt.Sprintf("unable to parse volume mount %q", pathURL))
87+
}
88+
if count == 2 {
89+
tokens := strings.Split(pathURL, ":")
90+
pathURL = fmt.Sprintf("%s:%s%s", tokens[0], tokens[1], tokens[2])
91+
}
92+
return url.Parse(pathURL)
93+
}
94+
95+
func (v *volumeInput) parse(name string, s string) error {
96+
volumeURL, err := volumeURL(s)
97+
if err != nil {
98+
return errors.Wrap(errdefs.ErrParsingFailed, fmt.Sprintf("volume specification %q could not be parsed %q", s, err))
99+
}
100+
v.username = volumeURL.User.Username()
101+
if v.username == "" {
102+
return errors.Wrap(errdefs.ErrParsingFailed, fmt.Sprintf("volume specification %q does not include a storage username", v))
103+
}
104+
key, ok := volumeURL.User.Password()
105+
if !ok || key == "" {
106+
return errors.Wrap(errdefs.ErrParsingFailed, fmt.Sprintf("volume specification %q does not include a storage key", v))
107+
}
108+
v.key = unescapeKey(key)
109+
v.share = volumeURL.Host
110+
if v.share == "" {
111+
return errors.Wrap(errdefs.ErrParsingFailed, fmt.Sprintf("volume specification %q does not include a storage file share", v))
112+
}
113+
v.name = name
114+
v.target = volumeURL.Path
115+
if v.target == "" {
116+
v.target = filepath.Join("/run/volumes/", v.share)
117+
}
118+
return nil
119+
}

azure/convert/volume_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
Copyright (c) 2020 Docker Inc.
3+
4+
Permission is hereby granted, free of charge, to any person
5+
obtaining a copy of this software and associated documentation
6+
files (the "Software"), to deal in the Software without
7+
restriction, including without limitation the rights to use, copy,
8+
modify, merge, publish, distribute, sublicense, and/or sell copies
9+
of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be
13+
included in all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
EXPRESS OR IMPLIED,
17+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20+
HOLDERS BE LIABLE FOR ANY CLAIM,
21+
DAMAGES OR OTHER LIABILITY,
22+
WHETHER IN AN ACTION OF CONTRACT,
23+
TORT OR OTHERWISE,
24+
ARISING FROM, OUT OF OR IN CONNECTION WITH
25+
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26+
*/
27+
28+
package convert
29+
30+
import (
31+
"testing"
32+
33+
"github.com/compose-spec/compose-go/types"
34+
"gotest.tools/assert"
35+
36+
"github.com/docker/api/errdefs"
37+
)
38+
39+
const (
40+
storageAccountNameKey = "storage_account_name"
41+
storageAccountKeyKey = "storage_account_key"
42+
shareNameKey = "share_name"
43+
)
44+
45+
func TestGetRunVolumes(t *testing.T) {
46+
volumeStrings := []string{
47+
"myuser1:mykey1@myshare1/my/path/to/target1",
48+
"myuser2:mykey2@myshare2/my/path/to/target2",
49+
"myuser3:mykey3@mydefaultsharename", // Use default placement at '/run/volumes/<share_name>'
50+
}
51+
var goldenVolumeConfigs = map[string]types.VolumeConfig{
52+
"volume-0": {
53+
Name: "volume-0",
54+
Driver: "azure_file",
55+
DriverOpts: map[string]string{
56+
storageAccountNameKey: "myuser1",
57+
storageAccountKeyKey: "mykey1",
58+
shareNameKey: "myshare1",
59+
},
60+
},
61+
"volume-1": {
62+
Name: "volume-1",
63+
Driver: "azure_file",
64+
DriverOpts: map[string]string{
65+
storageAccountNameKey: "myuser2",
66+
storageAccountKeyKey: "mykey2",
67+
shareNameKey: "myshare2",
68+
},
69+
},
70+
"volume-2": {
71+
Name: "volume-2",
72+
Driver: "azure_file",
73+
DriverOpts: map[string]string{
74+
storageAccountNameKey: "myuser3",
75+
storageAccountKeyKey: "mykey3",
76+
shareNameKey: "mydefaultsharename",
77+
},
78+
},
79+
}
80+
goldenServiceVolumeConfigs := []types.ServiceVolumeConfig{
81+
{
82+
Type: "azure_file",
83+
Source: "volume-0",
84+
Target: "/my/path/to/target1",
85+
},
86+
{
87+
Type: "azure_file",
88+
Source: "volume-1",
89+
Target: "/my/path/to/target2",
90+
},
91+
{
92+
Type: "azure_file",
93+
Source: "volume-2",
94+
Target: "/run/volumes/mydefaultsharename",
95+
},
96+
}
97+
98+
volumeConfigs, serviceVolumeConfigs, err := GetRunVolumes(volumeStrings)
99+
assert.NilError(t, err)
100+
for k, v := range volumeConfigs {
101+
assert.DeepEqual(t, goldenVolumeConfigs[k], v)
102+
}
103+
for i, v := range serviceVolumeConfigs {
104+
assert.DeepEqual(t, goldenServiceVolumeConfigs[i], v)
105+
}
106+
}
107+
108+
func TestGetRunVolumesMissingFileShare(t *testing.T) {
109+
_, _, err := GetRunVolumes([]string{"myuser:mykey@"})
110+
assert.Equal(t, true, errdefs.IsErrParsingFailed(err))
111+
assert.ErrorContains(t, err, "does not include a storage file share")
112+
}
113+
114+
func TestGetRunVolumesMissingUser(t *testing.T) {
115+
_, _, err := GetRunVolumes([]string{":mykey@myshare"})
116+
assert.Equal(t, true, errdefs.IsErrParsingFailed(err))
117+
assert.ErrorContains(t, err, "does not include a storage username")
118+
}
119+
120+
func TestGetRunVolumesMissingKey(t *testing.T) {
121+
_, _, err := GetRunVolumes([]string{"userwithnokey:@myshare"})
122+
assert.Equal(t, true, errdefs.IsErrParsingFailed(err))
123+
assert.ErrorContains(t, err, "does not include a storage key")
124+
125+
_, _, err = GetRunVolumes([]string{"userwithnokeytoo@myshare"})
126+
assert.Equal(t, true, errdefs.IsErrParsingFailed(err))
127+
assert.ErrorContains(t, err, "does not include a storage key")
128+
}
129+
130+
func TestGetRunVolumesNoShare(t *testing.T) {
131+
_, _, err := GetRunVolumes([]string{"noshare"})
132+
assert.Equal(t, true, errdefs.IsErrParsingFailed(err))
133+
assert.ErrorContains(t, err, "no share specified")
134+
}

cli/cmd/run/run.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func Command() *cobra.Command {
5454
cmd.Flags().StringArrayVarP(&opts.Publish, "publish", "p", []string{}, "Publish a container's port(s). [HOST_PORT:]CONTAINER_PORT")
5555
cmd.Flags().StringVar(&opts.Name, "name", getRandomName(), "Assign a name to the container")
5656
cmd.Flags().StringArrayVarP(&opts.Labels, "label", "l", []string{}, "Set meta data on a container")
57+
cmd.Flags().StringArrayVarP(&opts.Volumes, "volume", "v", []string{}, "Volume. Ex: user:key@my_share:/absolute/path/to/target")
5758

5859
return cmd
5960
}
@@ -64,18 +65,17 @@ func runRun(ctx context.Context, image string, opts run.Opts) error {
6465
return err
6566
}
6667

67-
project, err := opts.ToContainerConfig(image)
68+
containerConfig, err := opts.ToContainerConfig(image)
6869
if err != nil {
6970
return err
7071
}
7172

72-
if err = c.ContainerService().Run(ctx, project); err != nil {
73+
if err = c.ContainerService().Run(ctx, containerConfig); err != nil {
7374
return err
7475
}
7576
fmt.Println(opts.Name)
7677

7778
return nil
78-
7979
}
8080

8181
func getRandomName() string {

cli/options/run/opts.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type Opts struct {
1515
Name string
1616
Publish []string
1717
Labels []string
18+
Volumes []string
1819
}
1920

2021
// ToContainerConfig convert run options to a container configuration
@@ -30,10 +31,11 @@ func (r *Opts) ToContainerConfig(image string) (containers.ContainerConfig, erro
3031
}
3132

3233
return containers.ContainerConfig{
33-
ID: r.Name,
34-
Image: image,
35-
Ports: publish,
36-
Labels: labels,
34+
ID: r.Name,
35+
Image: image,
36+
Ports: publish,
37+
Labels: labels,
38+
Volumes: r.Volumes,
3739
}, nil
3840
}
3941

0 commit comments

Comments
 (0)