diff --git a/azure/backend.go b/azure/backend.go index 40f112599..e86c989bd 100644 --- a/azure/backend.go +++ b/azure/backend.go @@ -155,17 +155,25 @@ func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerCo Published: p.HostPort, }) } + + projectVolumes, serviceConfigVolumes, err := convert.GetRunVolumes(r.Volumes) + if err != nil { + return err + } + project := compose.Project{ Name: r.ID, Config: types.Config{ Services: []types.ServiceConfig{ { - Name: singleContainerName, - Image: r.Image, - Ports: ports, - Labels: r.Labels, + Name: singleContainerName, + Image: r.Image, + Ports: ports, + Labels: r.Labels, + Volumes: serviceConfigVolumes, }, }, + Volumes: projectVolumes, }, } diff --git a/azure/convert/volume.go b/azure/convert/volume.go new file mode 100644 index 000000000..1a6598f61 --- /dev/null +++ b/azure/convert/volume.go @@ -0,0 +1,110 @@ +package convert + +import ( + "errors" + "fmt" + "net/url" + "path/filepath" + "strings" + + "github.com/compose-spec/compose-go/types" +) + +// GetRunVolumes return volume configurations for a project and a single service +// this is meant to be used as a compose project of a single service +func GetRunVolumes(volumes []string) (map[string]types.VolumeConfig, []types.ServiceVolumeConfig, error) { + var serviceConfigVolumes []types.ServiceVolumeConfig + projectVolumes := make(map[string]types.VolumeConfig, len(volumes)) + for i, v := range volumes { + var vi volumeInput + err := vi.parse(fmt.Sprintf("volume-%d", i), v) + if err != nil { + return nil, nil, err + } + projectVolumes[vi.name] = types.VolumeConfig{ + Name: vi.name, + Driver: azureFileDriverName, + DriverOpts: map[string]string{ + volumeDriveroptsAccountNameKey: vi.username, + volumeDriveroptsAccountKeyKey: vi.key, + volumeDriveroptsShareNameKey: vi.share, + }, + } + sv := types.ServiceVolumeConfig{ + Type: azureFileDriverName, + Source: vi.name, + Target: vi.target, + } + serviceConfigVolumes = append(serviceConfigVolumes, sv) + } + + return projectVolumes, serviceConfigVolumes, nil +} + +type volumeInput struct { + name string + username string + key string + share string + target string +} + +func scapeKeySlashes(rawURL string) (string, error) { + urlSplit := strings.Split(rawURL, "@") + if len(urlSplit) < 1 { + return "", errors.New("invalid url format " + rawURL) + } + userPasswd := strings.ReplaceAll(urlSplit[0], "/", "_") + scaped := userPasswd + rawURL[strings.Index(rawURL, "@"):] + + return scaped, nil +} + +func unscapeKey(passwd string) string { + return strings.ReplaceAll(passwd, "_", "/") +} + +// Removes the second ':' that separates the source from target +func volumeURL(pathURL string) (*url.URL, error) { + scapedURL, err := scapeKeySlashes(pathURL) + if err != nil { + return nil, err + } + pathURL = "//" + scapedURL + + count := strings.Count(pathURL, ":") + if count > 2 { + return nil, fmt.Errorf("unable to parse volume mount %q", pathURL) + } + if count == 2 { + tokens := strings.Split(pathURL, ":") + pathURL = fmt.Sprintf("%s:%s%s", tokens[0], tokens[1], tokens[2]) + } + return url.Parse(pathURL) +} + +func (v *volumeInput) parse(name string, s string) error { + volumeURL, err := volumeURL(s) + if err != nil { + return fmt.Errorf("volume specification %q could not be parsed %q", s, err) + } + v.username = volumeURL.User.Username() + if v.username == "" { + return fmt.Errorf("volume specification %q does not include a storage username", v) + } + passwd, ok := volumeURL.User.Password() + if !ok || passwd == "" { + return fmt.Errorf("volume specification %q does not include a storage key", v) + } + v.key = unscapeKey(passwd) + v.share = volumeURL.Host + if v.share == "" { + return fmt.Errorf("volume specification %q does not include a storage file share", v) + } + v.name = name + v.target = volumeURL.Path + if v.target == "" { + v.target = filepath.Join("/run/volumes/", v.share) + } + return nil +} diff --git a/cli/cmd/run/opts.go b/cli/cmd/run/opts.go new file mode 100644 index 000000000..b1dcb86e9 --- /dev/null +++ b/cli/cmd/run/opts.go @@ -0,0 +1,54 @@ +package run + +import ( + "fmt" + "strconv" + "strings" + + "github.com/docker/api/containers" +) + +type runOpts struct { + name string + publish []string + volumes []string +} + +func toPorts(ports []string) ([]containers.Port, error) { + var result []containers.Port + + for _, port := range ports { + parts := strings.Split(port, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("unable to parse ports %q", port) + } + source, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + destination, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + + result = append(result, containers.Port{ + HostPort: uint32(source), + ContainerPort: uint32(destination), + }) + } + return result, nil +} + +func (r *runOpts) toContainerConfig(image string) (containers.ContainerConfig, error) { + publish, err := toPorts(r.publish) + if err != nil { + return containers.ContainerConfig{}, err + } + + return containers.ContainerConfig{ + ID: r.name, + Image: image, + Ports: publish, + Volumes: r.volumes, + }, nil +} diff --git a/cli/cmd/run/run.go b/cli/cmd/run/run.go index 369b4ebd6..5b448f875 100644 --- a/cli/cmd/run/run.go +++ b/cli/cmd/run/run.go @@ -54,6 +54,7 @@ func Command() *cobra.Command { cmd.Flags().StringArrayVarP(&opts.Publish, "publish", "p", []string{}, "Publish a container's port(s). [HOST_PORT:]CONTAINER_PORT") cmd.Flags().StringVar(&opts.Name, "name", getRandomName(), "Assign a name to the container") cmd.Flags().StringArrayVarP(&opts.Labels, "label", "l", []string{}, "Set meta data on a container") + cmd.Flags().StringArrayVarP(&opts.Volumes, "volume", "v", []string{}, "Volume. Ex: user:key@my_share:/absolute/path/to/target") return cmd } @@ -64,18 +65,17 @@ func runRun(ctx context.Context, image string, opts run.Opts) error { return err } - project, err := opts.ToContainerConfig(image) + containerConfig, err := opts.ToContainerConfig(image) if err != nil { return err } - if err = c.ContainerService().Run(ctx, project); err != nil { + if err = c.ContainerService().Run(ctx, containerConfig); err != nil { return err } fmt.Println(opts.Name) return nil - } func getRandomName() string { diff --git a/cli/options/run/opts.go b/cli/options/run/opts.go index 73ce79830..168a4c63c 100644 --- a/cli/options/run/opts.go +++ b/cli/options/run/opts.go @@ -15,6 +15,7 @@ type Opts struct { Name string Publish []string Labels []string + Volumes []string } // ToContainerConfig convert run options to a container configuration diff --git a/containers/api.go b/containers/api.go index 741adee85..3f345ddd0 100644 --- a/containers/api.go +++ b/containers/api.go @@ -26,7 +26,7 @@ type Port struct { HostPort uint32 // ContainerPort is the port number inside the container ContainerPort uint32 - /// Protocol is the protocol of the port mapping + // Protocol is the protocol of the port mapping Protocol string // HostIP is the host ip to use HostIP string @@ -42,6 +42,8 @@ type ContainerConfig struct { Ports []Port // Labels set labels to the container Labels map[string]string + // Volumes to be mounted + Volumes []string } // LogsRequest contains configuration about a log request