Move server => cli

Signed-off-by: Guillaume Tardif <guillaume.tardif@gmail.com>
This commit is contained in:
Guillaume Tardif 2021-01-15 15:32:21 +01:00
parent 65f53dff43
commit cd10d8eaa4
23 changed files with 8 additions and 16 deletions

View file

@ -0,0 +1,57 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// A gRPC server stream will only let you get its context but
// there is no way to set a new (augmented context) to the next
// handler (like we do for a unary request). We need to wrap the grpc.ServerSteam
// to be able to set a new context that will be sent to the next stream interceptor.
type contextServerStream struct {
ss grpc.ServerStream
ctx context.Context
}
func (css *contextServerStream) SetHeader(md metadata.MD) error {
return css.ss.SetHeader(md)
}
func (css *contextServerStream) SendHeader(md metadata.MD) error {
return css.ss.SendHeader(md)
}
func (css *contextServerStream) SetTrailer(md metadata.MD) {
css.ss.SetTrailer(md)
}
func (css *contextServerStream) Context() context.Context {
return css.ctx
}
func (css *contextServerStream) SendMsg(m interface{}) error {
return css.ss.SendMsg(m)
}
func (css *contextServerStream) RecvMsg(m interface{}) error {
return css.ss.RecvMsg(m)
}

124
cli/server/interceptor.go Normal file
View file

@ -0,0 +1,124 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"context"
"errors"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/cli/server/proxy"
"github.com/docker/compose-cli/config"
apicontext "github.com/docker/compose-cli/context"
"github.com/docker/compose-cli/context/store"
)
// key is the key where the current docker context is stored in the metadata
// of a gRPC request
const key = "context_key"
// unaryServerInterceptor configures the context and sends it to the next handler
func unaryServerInterceptor(clictx context.Context) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
currentContext, err := getIncomingContext(ctx)
if err != nil {
currentContext, err = getConfigContext(clictx)
if err != nil {
return nil, err
}
}
configuredCtx, err := configureContext(clictx, currentContext, info.FullMethod)
if err != nil {
return nil, err
}
return handler(configuredCtx, req)
}
}
// streamServerInterceptor configures the context and sends it to the next handler
func streamServerInterceptor(clictx context.Context) grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
currentContext, err := getIncomingContext(ss.Context())
if err != nil {
currentContext, err = getConfigContext(clictx)
if err != nil {
return err
}
}
ctx, err := configureContext(clictx, currentContext, info.FullMethod)
if err != nil {
return err
}
return handler(srv, &contextServerStream{
ss: ss,
ctx: ctx,
})
}
}
// Returns the current context from the configuration file
func getConfigContext(ctx context.Context) (string, error) {
configDir := config.Dir(ctx)
configFile, err := config.LoadFile(configDir)
if err != nil {
return "", err
}
return configFile.CurrentContext, nil
}
// Returns the context set by the caller if any, error otherwise
func getIncomingContext(ctx context.Context) (string, error) {
if md, ok := metadata.FromIncomingContext(ctx); ok {
if key, ok := md[key]; ok {
return key[0], nil
}
}
return "", errors.New("not found")
}
// configureContext populates the request context with objects the client
// needs: the context store and the api client
func configureContext(ctx context.Context, currentContext string, method string) (context.Context, error) {
configDir := config.Dir(ctx)
ctx = apicontext.WithCurrentContext(ctx, currentContext)
// The contexts service doesn't need the client
if !strings.Contains(method, "/com.docker.api.protos.context.v1.Contexts") {
c, err := client.New(ctx)
if err != nil {
return nil, err
}
ctx = proxy.WithClient(ctx, c)
}
s, err := store.New(configDir)
if err != nil {
return nil, err
}
ctx = store.WithContextStore(ctx, s)
return ctx, nil
}

View file

@ -0,0 +1,125 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"context"
"io/ioutil"
"os"
"path"
"testing"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
"github.com/docker/compose-cli/config"
apicontext "github.com/docker/compose-cli/context"
)
func testContext(t *testing.T) context.Context {
dir, err := ioutil.TempDir("", "example")
assert.NilError(t, err)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
ctx := context.Background()
ctx = config.WithDir(ctx, dir)
err = ioutil.WriteFile(path.Join(dir, "config.json"), []byte(`{"currentContext": "default"}`), 0644)
assert.NilError(t, err)
return ctx
}
func TestUnaryGetCurrentContext(t *testing.T) {
ctx := testContext(t)
interceptor := unaryServerInterceptor(ctx)
currentContext := callUnary(context.Background(), t, interceptor)
assert.Equal(t, currentContext, "default")
}
func TestUnaryContextFromMetadata(t *testing.T) {
ctx := testContext(t)
contextName := "test"
interceptor := unaryServerInterceptor(ctx)
reqCtx := context.Background()
reqCtx = metadata.NewIncomingContext(reqCtx, metadata.MD{
(key): []string{contextName},
})
currentContext := callUnary(reqCtx, t, interceptor)
assert.Equal(t, contextName, currentContext)
}
func TestStreamGetCurrentContext(t *testing.T) {
ctx := testContext(t)
interceptor := streamServerInterceptor(ctx)
currentContext := callStream(context.Background(), t, interceptor)
assert.Equal(t, currentContext, "default")
}
func TestStreamContextFromMetadata(t *testing.T) {
ctx := testContext(t)
contextName := "test"
interceptor := streamServerInterceptor(ctx)
reqCtx := context.Background()
reqCtx = metadata.NewIncomingContext(reqCtx, metadata.MD{
(key): []string{contextName},
})
currentContext := callStream(reqCtx, t, interceptor)
assert.Equal(t, currentContext, contextName)
}
func callStream(ctx context.Context, t *testing.T, interceptor grpc.StreamServerInterceptor) string {
currentContext := ""
err := interceptor(nil, &contextServerStream{
ctx: ctx,
}, &grpc.StreamServerInfo{
FullMethod: "/com.docker.api.protos.context.v1.Contexts/test",
}, func(srv interface{}, stream grpc.ServerStream) error {
currentContext = apicontext.CurrentContext(stream.Context())
return nil
})
assert.NilError(t, err)
return currentContext
}
func callUnary(ctx context.Context, t *testing.T, interceptor grpc.UnaryServerInterceptor) string {
currentContext := ""
resp, err := interceptor(ctx, nil, &grpc.UnaryServerInfo{
FullMethod: "/com.docker.api.protos.context.v1.Contexts/test",
}, func(ctx context.Context, req interface{}) (interface{}, error) {
currentContext = apicontext.CurrentContext(ctx)
return nil, nil
})
assert.NilError(t, err)
assert.Assert(t, cmp.Nil(resp))
return currentContext
}

78
cli/server/metrics.go Normal file
View file

@ -0,0 +1,78 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"context"
"google.golang.org/grpc"
"github.com/docker/compose-cli/cli/metrics"
"github.com/docker/compose-cli/cli/server/proxy"
)
var (
methodMapping = map[string]string{
"/com.docker.api.protos.containers.v1.Containers/List": "ps",
"/com.docker.api.protos.containers.v1.Containers/Start": "start",
"/com.docker.api.protos.containers.v1.Containers/Stop": "stop",
"/com.docker.api.protos.containers.v1.Containers/Run": "run",
"/com.docker.api.protos.containers.v1.Containers/Exec": "exec",
"/com.docker.api.protos.containers.v1.Containers/Delete": "rm",
"/com.docker.api.protos.containers.v1.Containers/Kill": "kill",
"/com.docker.api.protos.containers.v1.Containers/Inspect": "inspect",
"/com.docker.api.protos.containers.v1.Containers/Logs": "logs",
"/com.docker.api.protos.streams.v1.Streaming/NewStream": "streaming",
"/com.docker.api.protos.context.v1.Contexts/List": "context ls",
"/com.docker.api.protos.context.v1.Contexts/SetCurrent": "context use",
"/com.docker.api.protos.volumes.v1.Volumes/VolumesList": "volume ls",
"/com.docker.api.protos.volumes.v1.Volumes/VolumesDelete": "volume rm",
"/com.docker.api.protos.volumes.v1.Volumes/VolumesCreate": "volume create",
"/com.docker.api.protos.volumes.v1.Volumes/VolumesInspect": "volume inspect",
"/com.docker.api.protos.compose.v1.Compose/Up": "compose up",
"/com.docker.api.protos.compose.v1.Compose/Down": "compose down",
"/com.docker.api.protos.compose.v1.Compose/Stacks": "compose ls",
"/com.docker.api.protos.compose.v1.Compose/Services": "compose ps",
}
)
func metricsServerInterceptor(client metrics.Client) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
backendClient := proxy.Client(ctx)
contextType := ""
if backendClient != nil {
contextType = backendClient.ContextType()
}
data, err := handler(ctx, req)
status := metrics.SuccessStatus
if err != nil {
status = metrics.FailureStatus
}
command := methodMapping[info.FullMethod]
if command != "" {
client.Send(metrics.Command{
Command: command,
Context: contextType,
Source: metrics.APISource,
Status: status,
})
}
return data, err
}
}

131
cli/server/metrics_test.go Normal file
View file

@ -0,0 +1,131 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"context"
"strings"
"testing"
"github.com/docker/compose-cli/api/resources"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"gotest.tools/v3/assert"
"github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/api/secrets"
"github.com/docker/compose-cli/api/volumes"
"github.com/docker/compose-cli/cli/metrics"
"github.com/docker/compose-cli/cli/server/proxy"
"github.com/docker/compose-cli/errdefs"
composev1 "github.com/docker/compose-cli/protos/compose/v1"
containersv1 "github.com/docker/compose-cli/protos/containers/v1"
contextsv1 "github.com/docker/compose-cli/protos/contexts/v1"
streamsv1 "github.com/docker/compose-cli/protos/streams/v1"
volumesv1 "github.com/docker/compose-cli/protos/volumes/v1"
)
func TestAllMethodsHaveCorrespondingCliCommand(t *testing.T) {
s := setupServer()
i := s.GetServiceInfo()
for k, v := range i {
if k == "grpc.health.v1.Health" {
continue
}
var errs []string
for _, m := range v.Methods {
name := "/" + k + "/" + m.Name
if _, keyExists := methodMapping[name]; !keyExists {
errs = append(errs, name+" not mapped to a corresponding cli command")
}
}
assert.Equal(t, "", strings.Join(errs, "\n"))
}
}
func TestTrackSuccess(t *testing.T) {
var mockMetrics = &mockMetricsClient{}
mockMetrics.On("Send", metrics.Command{Command: "ps", Context: "aci", Status: "success", Source: "api"}).Return()
newClient := client.NewClient("aci", noopService{})
interceptor := metricsServerInterceptor(mockMetrics)
ctx := proxy.WithClient(incomingContext("acicontext"), &newClient)
_, err := interceptor(ctx, nil, containerMethodRoute("List"), mockHandler(nil))
assert.NilError(t, err)
}
func TestTrackSFailures(t *testing.T) {
var mockMetrics = &mockMetricsClient{}
newClient := client.NewClient("moby", noopService{})
interceptor := metricsServerInterceptor(mockMetrics)
ctx := proxy.WithClient(incomingContext("default"), &newClient)
_, err := interceptor(ctx, nil, containerMethodRoute("Create"), mockHandler(errdefs.ErrLoginRequired))
assert.Assert(t, err == errdefs.ErrLoginRequired)
}
func containerMethodRoute(action string) *grpc.UnaryServerInfo {
var info = &grpc.UnaryServerInfo{
FullMethod: "/com.docker.api.protos.containers.v1.Containers/" + action,
}
return info
}
func mockHandler(err error) func(ctx context.Context, req interface{}) (interface{}, error) {
return func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, err
}
}
func incomingContext(status string) context.Context {
ctx := metadata.NewIncomingContext(context.TODO(), metadata.MD{
(key): []string{status},
})
return ctx
}
func setupServer() *grpc.Server {
ctx := context.TODO()
s := New(ctx)
p := proxy.New(ctx)
composev1.RegisterComposeServer(s, p)
containersv1.RegisterContainersServer(s, p)
streamsv1.RegisterStreamingServer(s, p)
volumesv1.RegisterVolumesServer(s, p)
contextsv1.RegisterContextsServer(s, p.ContextsProxy())
return s
}
type noopService struct{}
func (noopService) ContainerService() containers.Service { return nil }
func (noopService) ComposeService() compose.Service { return nil }
func (noopService) SecretsService() secrets.Service { return nil }
func (noopService) VolumeService() volumes.Service { return nil }
func (noopService) ResourceService() resources.Service { return nil }
type mockMetricsClient struct {
mock.Mock
}
func (s *mockMetricsClient) Send(command metrics.Command) {
s.Called(command)
}

102
cli/server/proxy/compose.go Normal file
View file

@ -0,0 +1,102 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proxy
import (
"context"
"github.com/docker/compose-cli/api/compose"
"github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/types"
composev1 "github.com/docker/compose-cli/protos/compose/v1"
)
func (p *proxy) Up(ctx context.Context, request *composev1.ComposeUpRequest) (*composev1.ComposeUpResponse, error) {
project, err := getComposeProject(request.Files, request.WorkDir, request.ProjectName)
if err != nil {
return nil, err
}
err = Client(ctx).ComposeService().Up(ctx, project, compose.UpOptions{Detach: true})
return &composev1.ComposeUpResponse{ProjectName: project.Name}, err
}
func (p *proxy) Down(ctx context.Context, request *composev1.ComposeDownRequest) (*composev1.ComposeDownResponse, error) {
projectName := request.GetProjectName()
if projectName == "" {
project, err := getComposeProject(request.Files, request.WorkDir, request.ProjectName)
if err != nil {
return nil, err
}
projectName = project.Name
}
err := Client(ctx).ComposeService().Down(ctx, projectName, compose.DownOptions{})
return &composev1.ComposeDownResponse{ProjectName: projectName}, err
}
func (p *proxy) Services(ctx context.Context, request *composev1.ComposeServicesRequest) (*composev1.ComposeServicesResponse, error) {
projectName := request.GetProjectName()
if projectName == "" {
project, err := getComposeProject(request.Files, request.WorkDir, request.ProjectName)
if err != nil {
return nil, err
}
projectName = project.Name
}
response := []*composev1.Service{}
_, err := Client(ctx).ComposeService().Ps(ctx, projectName)
if err != nil {
return nil, err
}
/* FIXME need to create `docker service ls` command to re-introduce this feature
for _, service := range services {
response = append(response, &composev1.Service{
Id: service.ID,
ProjectName: service.ProjectName,
Replicas: uint32(service.Replicas),
Desired: uint32(service.Desired),
Ports: service.Ports,
})
}*/
return &composev1.ComposeServicesResponse{Services: response}, nil
}
func (p *proxy) Stacks(ctx context.Context, request *composev1.ComposeStacksRequest) (*composev1.ComposeStacksResponse, error) {
stacks, err := Client(ctx).ComposeService().List(ctx, request.ProjectName)
if err != nil {
return nil, err
}
response := []*composev1.Stack{}
for _, stack := range stacks {
response = append(response, &composev1.Stack{
Id: stack.ID,
Name: stack.Name,
Status: stack.Status,
Reason: stack.Reason,
})
}
return &composev1.ComposeStacksResponse{Stacks: response}, nil
}
func getComposeProject(files []string, workingDir string, projectName string) (*types.Project, error) {
options, err := cli.NewProjectOptions(files, cli.WithWorkingDirectory(workingDir), cli.WithName(projectName))
if err != nil {
return nil, err
}
return cli.ProjectFromOptions(options)
}

View file

@ -0,0 +1,200 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proxy
import (
"context"
"errors"
"github.com/compose-spec/compose-go/types"
"github.com/containerd/containerd/platforms"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/cli/server/proxy/streams"
"github.com/docker/compose-cli/formatter"
containersv1 "github.com/docker/compose-cli/protos/containers/v1"
)
func portsToGrpc(ports []containers.Port) []*containersv1.Port {
var result []*containersv1.Port
for _, port := range ports {
result = append(result, &containersv1.Port{
ContainerPort: port.ContainerPort,
HostPort: port.HostPort,
HostIp: port.HostIP,
Protocol: port.Protocol,
})
}
return result
}
func (p *proxy) List(ctx context.Context, request *containersv1.ListRequest) (*containersv1.ListResponse, error) {
containerList, err := Client(ctx).ContainerService().List(ctx, request.GetAll())
if err != nil {
return &containersv1.ListResponse{}, err
}
response := &containersv1.ListResponse{
Containers: []*containersv1.Container{},
}
for _, container := range containerList {
response.Containers = append(response.Containers, toGrpcContainer(container))
}
return response, nil
}
func (p *proxy) Start(ctx context.Context, request *containersv1.StartRequest) (*containersv1.StartResponse, error) {
return &containersv1.StartResponse{}, Client(ctx).ContainerService().Start(ctx, request.Id)
}
func (p *proxy) Stop(ctx context.Context, request *containersv1.StopRequest) (*containersv1.StopResponse, error) {
timeoutValue := request.GetTimeout()
return &containersv1.StopResponse{}, Client(ctx).ContainerService().Stop(ctx, request.Id, &timeoutValue)
}
func (p *proxy) Kill(ctx context.Context, request *containersv1.KillRequest) (*containersv1.KillResponse, error) {
signal := request.GetSignal()
return &containersv1.KillResponse{}, Client(ctx).ContainerService().Kill(ctx, request.Id, signal)
}
func (p *proxy) Run(ctx context.Context, request *containersv1.RunRequest) (*containersv1.RunResponse, error) {
containerConfig, err := grpcContainerToContainerConfig(request)
if err != nil {
return nil, err
}
return &containersv1.RunResponse{}, Client(ctx).ContainerService().Run(ctx, containerConfig)
}
func (p *proxy) Inspect(ctx context.Context, request *containersv1.InspectRequest) (*containersv1.InspectResponse, error) {
c, err := Client(ctx).ContainerService().Inspect(ctx, request.Id)
if err != nil {
return nil, err
}
response := &containersv1.InspectResponse{
Container: toGrpcContainer(c),
}
return response, err
}
func (p *proxy) Delete(ctx context.Context, request *containersv1.DeleteRequest) (*containersv1.DeleteResponse, error) {
return &containersv1.DeleteResponse{}, Client(ctx).ContainerService().Delete(ctx, request.Id, containers.DeleteRequest{
Force: request.Force,
})
}
func (p *proxy) Exec(ctx context.Context, request *containersv1.ExecRequest) (*containersv1.ExecResponse, error) {
p.mu.Lock()
stream, ok := p.streams[request.StreamId]
p.mu.Unlock()
if !ok {
return &containersv1.ExecResponse{}, errors.New("unknown stream id")
}
io := &streams.IO{
Stream: stream,
}
return &containersv1.ExecResponse{}, Client(ctx).ContainerService().Exec(ctx, request.GetId(), containers.ExecRequest{
Stdin: io,
Stdout: io,
Command: request.GetCommand(),
Tty: request.GetTty(),
})
}
func (p *proxy) Logs(request *containersv1.LogsRequest, stream containersv1.Containers_LogsServer) error {
return Client(stream.Context()).ContainerService().Logs(stream.Context(), request.GetContainerId(), containers.LogsRequest{
Follow: request.Follow,
Writer: &streams.Log{
Stream: stream,
},
})
}
func toGrpcContainer(c containers.Container) *containersv1.Container {
return &containersv1.Container{
Id: c.ID,
Image: c.Image,
Status: c.Status,
Command: c.Command,
CpuTime: c.CPUTime,
MemoryUsage: c.MemoryUsage,
Platform: c.Platform,
PidsCurrent: c.PidsCurrent,
PidsLimit: c.PidsLimit,
Labels: c.Config.Labels,
Ports: portsToGrpc(c.Ports),
HostConfig: &containersv1.HostConfig{
MemoryReservation: c.HostConfig.MemoryReservation,
MemoryLimit: c.HostConfig.MemoryLimit,
CpuReservation: uint64(c.HostConfig.CPUReservation),
CpuLimit: uint64(c.HostConfig.CPULimit),
RestartPolicy: c.HostConfig.RestartPolicy,
AutoRemove: c.HostConfig.AutoRemove,
},
Healthcheck: &containersv1.Healthcheck{
Disable: c.Healthcheck.Disable,
Test: c.Healthcheck.Test,
Interval: int64(c.Healthcheck.Interval),
},
}
}
func grpcContainerToContainerConfig(request *containersv1.RunRequest) (containers.ContainerConfig, error) {
var ports []containers.Port
for _, p := range request.GetPorts() {
ports = append(ports, containers.Port{
ContainerPort: p.ContainerPort,
HostIP: p.HostIp,
HostPort: p.HostPort,
Protocol: p.Protocol,
})
}
var platform *specs.Platform
if request.Platform != "" {
p, err := platforms.Parse(request.Platform)
if err != nil {
return containers.ContainerConfig{}, err
}
platform = &p
}
return containers.ContainerConfig{
ID: request.GetId(),
Image: request.GetImage(),
Command: request.GetCommand(),
Ports: ports,
Labels: request.GetLabels(),
Volumes: request.GetVolumes(),
MemLimit: formatter.MemBytes(request.GetMemoryLimit()),
CPULimit: float64(request.GetCpuLimit()),
RestartPolicyCondition: request.RestartPolicyCondition,
Environment: request.Environment,
AutoRemove: request.AutoRemove,
Healthcheck: containers.Healthcheck{
Disable: request.GetHealthcheck().GetDisable(),
Test: request.GetHealthcheck().GetTest(),
Interval: types.Duration(request.GetHealthcheck().GetInterval()),
},
Platform: platform,
}, nil
}

View file

@ -0,0 +1,71 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proxy
import (
"testing"
"gotest.tools/v3/assert"
"github.com/docker/compose-cli/api/containers"
"github.com/docker/compose-cli/formatter"
containersv1 "github.com/docker/compose-cli/protos/containers/v1"
)
func TestGrpcContainerToContainerConfig(t *testing.T) {
r := &containersv1.RunRequest{
Id: "myId",
Image: "myImage",
Ports: []*containersv1.Port{
{
HostPort: 8080,
ContainerPort: 80,
Protocol: "tcp",
HostIp: "42.42.42.42",
},
},
Labels: map[string]string{
"mykey": "mylabel",
},
Volumes: []string{
"myvolume",
},
MemoryLimit: 41,
CpuLimit: 42,
Environment: []string{"PROTOVAR=VALUE"},
}
cc, err := grpcContainerToContainerConfig(r)
assert.NilError(t, err)
assert.Equal(t, cc.ID, "myId")
assert.Equal(t, cc.Image, "myImage")
assert.Equal(t, cc.MemLimit, formatter.MemBytes(41))
assert.Equal(t, cc.CPULimit, float64(42))
assert.DeepEqual(t, cc.Volumes, []string{"myvolume"})
assert.DeepEqual(t, cc.Ports, []containers.Port{
{
HostPort: uint32(8080),
ContainerPort: 80,
Protocol: "tcp",
HostIP: "42.42.42.42",
},
})
assert.DeepEqual(t, cc.Labels, map[string]string{
"mykey": "mylabel",
})
assert.DeepEqual(t, cc.Environment, []string{"PROTOVAR=VALUE"})
}

View file

@ -0,0 +1,111 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proxy
import (
"context"
"github.com/docker/compose-cli/config"
"github.com/docker/compose-cli/context/store"
contextsv1 "github.com/docker/compose-cli/protos/contexts/v1"
)
type contextsProxy struct {
configDir string
}
func (cp *contextsProxy) SetCurrent(ctx context.Context, request *contextsv1.SetCurrentRequest) (*contextsv1.SetCurrentResponse, error) {
if err := config.WriteCurrentContext(cp.configDir, request.GetName()); err != nil {
return &contextsv1.SetCurrentResponse{}, err
}
return &contextsv1.SetCurrentResponse{}, nil
}
func (cp *contextsProxy) List(ctx context.Context, request *contextsv1.ListRequest) (*contextsv1.ListResponse, error) {
s := store.ContextStore(ctx)
configFile, err := config.LoadFile(cp.configDir)
if err != nil {
return nil, err
}
contexts, err := s.List()
if err != nil {
return &contextsv1.ListResponse{}, err
}
return convertContexts(contexts, configFile.CurrentContext), nil
}
func convertContexts(contexts []*store.DockerContext, currentContext string) *contextsv1.ListResponse {
result := &contextsv1.ListResponse{}
for _, c := range contexts {
endpointName := c.Type()
if c.Type() == store.DefaultContextType {
endpointName = "docker"
}
var endpoint interface{} = c.Endpoints[endpointName]
context := contextsv1.Context{
Name: c.Name,
ContextType: c.Type(),
Description: c.Metadata.Description,
Current: c.Name == currentContext,
}
switch c.Type() {
case store.DefaultContextType:
context.Endpoint = getDockerEndpoint(endpoint)
case store.AciContextType:
context.Endpoint = getAciEndpoint(endpoint)
case store.EcsContextType:
context.Endpoint = getEcsEndpoint(endpoint)
}
result.Contexts = append(result.Contexts, &context)
}
return result
}
func getDockerEndpoint(endpoint interface{}) *contextsv1.Context_DockerEndpoint {
typedEndpoint := endpoint.(*store.Endpoint)
return &contextsv1.Context_DockerEndpoint{
DockerEndpoint: &contextsv1.DockerEndpoint{
Host: typedEndpoint.Host,
},
}
}
func getAciEndpoint(endpoint interface{}) *contextsv1.Context_AciEndpoint {
typedEndpoint := endpoint.(*store.AciContext)
return &contextsv1.Context_AciEndpoint{
AciEndpoint: &contextsv1.AciEndpoint{
ResourceGroup: typedEndpoint.ResourceGroup,
Region: typedEndpoint.Location,
SubscriptionId: typedEndpoint.SubscriptionID,
},
}
}
func getEcsEndpoint(endpoint interface{}) *contextsv1.Context_EcsEndpoint {
typedEndpoint := endpoint.(*store.EcsContext)
return &contextsv1.Context_EcsEndpoint{
EcsEndpoint: &contextsv1.EcsEndpoint{
FromEnvironment: typedEndpoint.CredentialsFromEnv,
Profile: typedEndpoint.Profile,
},
}
}

View file

@ -0,0 +1,111 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proxy
import (
"testing"
"gotest.tools/v3/assert"
"github.com/docker/compose-cli/context/store"
contextsv1 "github.com/docker/compose-cli/protos/contexts/v1"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestConvertContext(t *testing.T) {
contexts := []*store.DockerContext{
{
Name: store.DefaultContextName,
Metadata: store.ContextMetadata{
Description: "description 1",
Type: store.DefaultContextType,
},
Endpoints: map[string]interface{}{
"docker": &store.Endpoint{
Host: "unix://var/run/docker.sock",
},
},
},
{
Name: "acicontext",
Metadata: store.ContextMetadata{
Description: "group1@eastus",
Type: store.AciContextType,
},
Endpoints: map[string]interface{}{
"aci": &store.AciContext{
Location: "eastus",
ResourceGroup: "group1",
SubscriptionID: "Subscription id",
},
},
},
{
Name: "ecscontext",
Metadata: store.ContextMetadata{
Description: "ecs description",
Type: store.EcsContextType,
},
Endpoints: map[string]interface{}{
"ecs": &store.EcsContext{
CredentialsFromEnv: false,
Profile: "awsprofile",
},
},
},
}
converted := convertContexts(contexts, "acicontext")
expected := []*contextsv1.Context{
{
Name: store.DefaultContextName,
Current: false,
ContextType: store.DefaultContextType,
Description: "description 1",
Endpoint: &contextsv1.Context_DockerEndpoint{
DockerEndpoint: &contextsv1.DockerEndpoint{
Host: "unix://var/run/docker.sock",
},
},
},
{
Name: "acicontext",
Current: true,
ContextType: store.AciContextType,
Description: "group1@eastus",
Endpoint: &contextsv1.Context_AciEndpoint{
AciEndpoint: &contextsv1.AciEndpoint{
Region: "eastus",
ResourceGroup: "group1",
SubscriptionId: "Subscription id",
},
},
},
{
Name: "ecscontext",
Current: false,
ContextType: store.EcsContextType,
Description: "ecs description",
Endpoint: &contextsv1.Context_EcsEndpoint{
EcsEndpoint: &contextsv1.EcsEndpoint{
FromEnvironment: false,
Profile: "awsprofile",
},
},
},
}
assert.DeepEqual(t, converted.Contexts, expected, cmpopts.IgnoreUnexported(contextsv1.Context{}, contextsv1.DockerEndpoint{}, contextsv1.AciEndpoint{}, contextsv1.EcsEndpoint{}))
}

77
cli/server/proxy/proxy.go Normal file
View file

@ -0,0 +1,77 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proxy
import (
"context"
"sync"
"github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/cli/server/proxy/streams"
"github.com/docker/compose-cli/config"
composev1 "github.com/docker/compose-cli/protos/compose/v1"
containersv1 "github.com/docker/compose-cli/protos/containers/v1"
contextsv1 "github.com/docker/compose-cli/protos/contexts/v1"
streamsv1 "github.com/docker/compose-cli/protos/streams/v1"
volumesv1 "github.com/docker/compose-cli/protos/volumes/v1"
)
type clientKey struct{}
// WithClient adds the client to the context
func WithClient(ctx context.Context, c *client.Client) context.Context {
return context.WithValue(ctx, clientKey{}, c)
}
// Client returns the client from the context
func Client(ctx context.Context) *client.Client {
c, _ := ctx.Value(clientKey{}).(*client.Client)
return c
}
// Proxy implements the gRPC server and forwards the actions
// to the right backend
type Proxy interface {
composev1.ComposeServer
containersv1.ContainersServer
streamsv1.StreamingServer
volumesv1.VolumesServer
ContextsProxy() contextsv1.ContextsServer
}
type proxy struct {
configDir string
mu sync.Mutex
streams map[string]*streams.Stream
contextsProxy *contextsProxy
}
// New creates a new proxy server
func New(ctx context.Context) Proxy {
configDir := config.Dir(ctx)
return &proxy{
configDir: configDir,
streams: map[string]*streams.Stream{},
contextsProxy: &contextsProxy{
configDir: configDir,
},
}
}
func (p *proxy) ContextsProxy() contextsv1.ContextsServer {
return p.contextsProxy
}

View file

@ -0,0 +1,67 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proxy
import (
"github.com/hashicorp/go-uuid"
"github.com/sirupsen/logrus"
"google.golang.org/grpc/metadata"
"github.com/docker/compose-cli/cli/server/proxy/streams"
streamsv1 "github.com/docker/compose-cli/protos/streams/v1"
)
func (p *proxy) NewStream(stream streamsv1.Streaming_NewStreamServer) error {
var (
ctx = stream.Context()
)
id, err := uuid.GenerateUUID()
if err != nil {
return err
}
md := metadata.New(map[string]string{
"id": id,
})
// return the id of the stream to the client
if err := stream.SendHeader(md); err != nil {
return err
}
errc := make(chan error)
p.mu.Lock()
p.streams[id] = &streams.Stream{
Streaming_NewStreamServer: stream,
ErrChan: errc,
}
p.mu.Unlock()
defer func() {
p.mu.Lock()
delete(p.streams, id)
p.mu.Unlock()
}()
select {
case err := <-errc:
return err
case <-ctx.Done():
logrus.Debug("client context canceled")
return ctx.Err()
}
}

View file

@ -0,0 +1,61 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package streams
import (
"github.com/golang/protobuf/ptypes"
streamsv1 "github.com/docker/compose-cli/protos/streams/v1"
)
// IO implements an io.ReadWriter that forwards everything to the stream
type IO struct {
Stream *Stream
}
func (io *IO) Read(p []byte) (int, error) {
a, err := io.Stream.Recv()
if err != nil {
return 0, err
}
var m streamsv1.BytesMessage
err = ptypes.UnmarshalAny(a, &m)
if err != nil {
return 0, err
}
return copy(p, m.Value), nil
}
func (io *IO) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
message := streamsv1.BytesMessage{
Type: streamsv1.IOStream_STDOUT,
Value: p,
}
m, err := ptypes.MarshalAny(&message)
if err != nil {
return 0, err
}
return len(message.Value), io.Stream.SendMsg(m)
}

View file

@ -0,0 +1,42 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package streams
import (
"io"
"google.golang.org/grpc"
containersv1 "github.com/docker/compose-cli/protos/containers/v1"
)
// Log implements an io.Writer that proxies logs over a gRPC stream
type Log struct {
Stream grpc.ServerStream
}
func newStreamWriter(stream grpc.ServerStream) io.Writer {
return &Log{
Stream: stream,
}
}
func (w *Log) Write(p []byte) (n int, err error) {
return len(p), w.Stream.SendMsg(&containersv1.LogsResponse{
Value: p,
})
}

View file

@ -0,0 +1,77 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package streams
import (
"context"
"testing"
"google.golang.org/grpc/metadata"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
v1 "github.com/docker/compose-cli/protos/containers/v1"
)
type logServer struct {
logs interface{}
}
func (ls *logServer) Send(response *v1.LogsResponse) error {
return nil
}
func (ls *logServer) SetHeader(metadata.MD) error {
return nil
}
func (ls *logServer) SendHeader(metadata.MD) error {
return nil
}
func (ls *logServer) SetTrailer(metadata.MD) {
}
func (ls *logServer) Context() context.Context {
return nil
}
func (ls *logServer) SendMsg(m interface{}) error {
ls.logs = m
return nil
}
func (ls *logServer) RecvMsg(m interface{}) error {
return nil
}
func TestLogStreamWriter(t *testing.T) {
ls := &logServer{}
sw := newStreamWriter(ls)
in := []byte{104, 101, 108, 108, 111}
expected := &v1.LogsResponse{
Value: in,
}
l, err := sw.Write(in)
assert.NilError(t, err)
assert.Assert(t, cmp.Len(in, l))
logs, ok := (ls.logs).(*v1.LogsResponse)
assert.Assert(t, ok)
assert.DeepEqual(t, logs.Value, expected.Value)
}

View file

@ -0,0 +1,47 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package streams
import (
"sync"
streamsv1 "github.com/docker/compose-cli/protos/streams/v1"
)
// Stream is a bidirectional stream for container IO
type Stream struct {
streamsv1.Streaming_NewStreamServer
errm sync.Mutex
ErrChan chan<- error
}
// CloseWithError sends the result of an action to the errChan or nil
// if no erros
func (s *Stream) CloseWithError(err error) error {
s.errm.Lock()
defer s.errm.Unlock()
if s.ErrChan != nil {
if err != nil {
s.ErrChan <- err
}
close(s.ErrChan)
s.ErrChan = nil
}
return nil
}

View file

@ -0,0 +1,141 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package streams
import (
"context"
"errors"
"testing"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/anypb"
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/any"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
streamsv1 "github.com/docker/compose-cli/protos/streams/v1"
)
type byteStream struct {
recvResult *any.Any
recvErr error
sendResult interface{}
}
func (bs *byteStream) SetHeader(metadata.MD) error {
return nil
}
func (bs *byteStream) SendHeader(metadata.MD) error {
return nil
}
func (bs *byteStream) SetTrailer(metadata.MD) {
}
func (bs *byteStream) Context() context.Context {
return nil
}
func (bs *byteStream) SendMsg(m interface{}) error {
bs.sendResult = m
return nil
}
func (bs *byteStream) Send(*any.Any) error {
return nil
}
func (bs *byteStream) Recv() (*any.Any, error) {
return bs.recvResult, bs.recvErr
}
func (bs *byteStream) RecvMsg(m interface{}) error {
return nil
}
func getReader(t *testing.T, in []byte, errResult error) IO {
message := streamsv1.BytesMessage{
Type: streamsv1.IOStream_STDOUT,
Value: in,
}
m, err := ptypes.MarshalAny(&message)
assert.NilError(t, err)
return IO{
Stream: &Stream{
Streaming_NewStreamServer: &byteStream{
recvResult: m,
recvErr: errResult,
},
},
}
}
func getAny(t *testing.T, in []byte) *any.Any {
value, err := ptypes.MarshalAny(&streamsv1.BytesMessage{
Type: streamsv1.IOStream_STDOUT,
Value: in,
})
assert.NilError(t, err)
return value
}
func TestStreamReader(t *testing.T) {
in := []byte{104, 101, 108, 108, 111}
r := getReader(t, in, nil)
buffer := make([]byte, 5)
n, err := r.Read(buffer)
assert.NilError(t, err)
assert.Equal(t, n, 5)
assert.DeepEqual(t, buffer, in)
}
func TestStreamReaderError(t *testing.T) {
errResult := errors.New("err")
r := getReader(t, nil, errResult)
var buffer []byte
n, err := r.Read(buffer)
assert.Equal(t, n, 0)
assert.Error(t, err, errResult.Error())
}
func TestStreamWriter(t *testing.T) {
in := []byte{104, 101, 108, 108, 111}
expected := getAny(t, in)
bs := byteStream{}
w := IO{
Stream: &Stream{
Streaming_NewStreamServer: &bs,
},
}
n, err := w.Write(in)
assert.NilError(t, err)
assert.Assert(t, cmp.Len(in, n))
sendResult, ok := (bs.sendResult).(*anypb.Any)
assert.Assert(t, ok)
assert.DeepEqual(t, sendResult.Value, expected.Value)
}

View file

@ -0,0 +1,87 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proxy
import (
"context"
"github.com/docker/compose-cli/aci"
"github.com/docker/compose-cli/api/volumes"
volumesv1 "github.com/docker/compose-cli/protos/volumes/v1"
)
// VolumesCreate creates a volume.
func (p *proxy) VolumesCreate(ctx context.Context, req *volumesv1.VolumesCreateRequest) (*volumesv1.VolumesCreateResponse, error) {
storageAccount := ""
aciReq := req.GetAciOption()
if aciReq != nil {
storageAccount = aciReq.StorageAccount
}
aciOpts := aci.VolumeCreateOptions{
Account: storageAccount,
}
v, err := Client(ctx).VolumeService().Create(ctx, req.Name, aciOpts)
if err != nil {
return &volumesv1.VolumesCreateResponse{}, err
}
return &volumesv1.VolumesCreateResponse{
Volume: toGrpcVolume(v),
}, nil
}
// VolumesList lists the volumes.
func (p *proxy) VolumesList(ctx context.Context, req *volumesv1.VolumesListRequest) (*volumesv1.VolumesListResponse, error) {
volumeList, err := Client(ctx).VolumeService().List(ctx)
if err != nil {
return &volumesv1.VolumesListResponse{}, err
}
return &volumesv1.VolumesListResponse{
Volumes: toGrpcVolumeList(volumeList),
}, nil
}
// VolumesDelete deletes a volume.
func (p *proxy) VolumesDelete(ctx context.Context, req *volumesv1.VolumesDeleteRequest) (*volumesv1.VolumesDeleteResponse, error) {
err := Client(ctx).VolumeService().Delete(ctx, req.Id, nil)
return &volumesv1.VolumesDeleteResponse{}, err
}
// VolumesInspect inspects a volume.
func (p *proxy) VolumesInspect(ctx context.Context, req *volumesv1.VolumesInspectRequest) (*volumesv1.VolumesInspectResponse, error) {
v, err := Client(ctx).VolumeService().Inspect(ctx, req.Id)
return &volumesv1.VolumesInspectResponse{
Volume: toGrpcVolume(v),
}, err
}
func toGrpcVolumeList(volumeList []volumes.Volume) []*volumesv1.Volume {
var ret []*volumesv1.Volume
for _, v := range volumeList {
ret = append(ret, toGrpcVolume(v))
}
return ret
}
func toGrpcVolume(v volumes.Volume) *volumesv1.Volume {
return &volumesv1.Volume{
Id: v.ID,
Description: v.Description,
}
}

52
cli/server/server.go Normal file
View file

@ -0,0 +1,52 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"context"
"net"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"github.com/docker/compose-cli/cli/metrics"
)
// New returns a new GRPC server.
func New(ctx context.Context) *grpc.Server {
s := grpc.NewServer(
grpc.ChainUnaryInterceptor(
unaryServerInterceptor(ctx),
metricsServerInterceptor(metrics.NewClient()),
),
grpc.StreamInterceptor(streamServerInterceptor(ctx)),
)
hs := health.NewServer()
grpc_health_v1.RegisterHealthServer(s, hs)
return s
}
// CreateListener creates a listener either on tcp://, or local listener,
// supporting unix:// for unix socket or npipe:// for named pipes on windows
func CreateListener(address string) (net.Listener, error) {
if strings.HasPrefix(address, "tcp://") {
return net.Listen("tcp", strings.TrimPrefix(address, "tcp://"))
}
return createLocalListener(address)
}

32
cli/server/socket_unix.go Normal file
View file

@ -0,0 +1,32 @@
// +build !windows
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"errors"
"net"
"strings"
)
func createLocalListener(address string) (net.Listener, error) {
if !strings.HasPrefix(address, "unix://") {
return nil, errors.New("Cannot parse address, must start with unix:// or tcp:// : " + address)
}
return net.Listen("unix", strings.TrimPrefix(address, "unix://"))
}

View file

@ -0,0 +1,38 @@
// +build windows
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package server
import (
"errors"
"net"
"strings"
"github.com/Microsoft/go-winio"
)
func createLocalListener(address string) (net.Listener, error) {
if !strings.HasPrefix(address, "npipe://") {
return nil, errors.New("Cannot parse address, must start with npipe:// or tcp:// : " + address)
}
return winio.ListenPipe(strings.TrimPrefix(address, "npipe://"), &winio.PipeConfig{
MessageMode: true, // Use message mode so that CloseWrite() is supported
InputBufferSize: 65536, // Use 64KB buffers to improve performance
OutputBufferSize: 65536,
})
}