Files
fx-serverless/container_runtimes/docker/sdk/docker.go
2019-11-11 15:35:52 +08:00

230 lines
5.6 KiB
Go

package docker
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/apex/log"
dockerTypes "github.com/docker/docker/api/types"
dockerTypesContainer "github.com/docker/docker/api/types/container"
dockerFilters "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/google/uuid"
containerruntimes "github.com/metrue/fx/container_runtimes"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils"
)
// Docker docker as image builder
type Docker struct {
*client.Client
}
// CreateClient create a docker instance
func CreateClient(ctx context.Context) (*Docker, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
cli.NegotiateAPIVersion(ctx)
return &Docker{cli}, nil
}
// BuildImage a directory to be a image
func (d *Docker) BuildImage(ctx context.Context, workdir string, name string) error {
tarDir, err := ioutil.TempDir("/tmp", "fx-tar")
if err != nil {
return err
}
defer os.RemoveAll(tarDir)
imageID := uuid.New().String()
tarFilePath := filepath.Join(tarDir, fmt.Sprintf("%s.tar", imageID))
if err := utils.TarDir(workdir, tarFilePath); err != nil {
return err
}
dockerBuildContext, err := os.Open(tarFilePath)
if err != nil {
return err
}
defer dockerBuildContext.Close()
options := dockerTypes.ImageBuildOptions{
Dockerfile: "Dockerfile",
Tags: []string{imageID, name},
Labels: map[string]string{
"belong-to": "fx",
},
}
resp, err := d.ImageBuild(ctx, dockerBuildContext, options)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
log.Info(string(body))
return nil
}
// PushImage push image to hub.docker.com
func (d *Docker) PushImage(ctx context.Context, name string) (string, error) {
username := os.Getenv("DOCKER_USERNAME")
password := os.Getenv("DOCKER_PASSWORD")
if username == "" || password == "" {
return "", fmt.Errorf("DOCKER_USERNAME and DOCKER_PASSWORD required for push image to registry")
}
// TODO support private registry, like Azure Container registry
authConfig := dockerTypes.AuthConfig{
Username: username,
Password: password,
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return "", err
}
nameWithTag := username + "/" + name
if err := d.ImageTag(ctx, name, nameWithTag); err != nil {
return "", err
}
options := dockerTypes.ImagePushOptions{
RegistryAuth: base64.URLEncoding.EncodeToString(encodedJSON),
}
resp, err := d.ImagePush(ctx, nameWithTag, options)
if err != nil {
return "", err
}
defer resp.Close()
if os.Getenv("DEBUG") != "" {
body, err := ioutil.ReadAll(resp)
if err != nil {
return "", err
}
log.Info(string(body))
}
return nameWithTag, nil
}
// InspectImage inspect a image
func (d *Docker) InspectImage(ctx context.Context, name string, img interface{}) error {
_, body, err := d.ImageInspectWithRaw(ctx, name)
if err != nil {
return err
}
rdr := bytes.NewReader(body)
return json.NewDecoder(rdr).Decode(&img)
}
// TagImage tag image
func (d *Docker) TagImage(ctx context.Context, name string, tag string) error {
return d.ImageTag(ctx, name, tag)
}
// StartContainer create and start a container from given image
func (d *Docker) StartContainer(ctx context.Context, name string, image string, ports []types.PortBinding) error {
portSet := nat.PortSet{}
portMap := nat.PortMap{}
for _, binding := range ports {
bindings := []nat.PortBinding{
nat.PortBinding{
HostIP: types.DefaultHost,
HostPort: fmt.Sprintf("%d", binding.ServiceBindingPort),
},
}
port := nat.Port(fmt.Sprintf("%d/tcp", binding.ContainerExposePort))
portSet[port] = struct{}{}
portMap[port] = bindings
}
config := &dockerTypesContainer.Config{
Image: image,
ExposedPorts: portSet,
}
hostConfig := &dockerTypesContainer.HostConfig{
AutoRemove: true,
PortBindings: portMap,
}
resp, err := d.ContainerCreate(ctx, config, hostConfig, nil, name)
if os.Getenv("DEBUG") != "" {
body, err := json.Marshal(resp)
if err != nil {
return err
}
log.Info(string(body))
}
if err != nil {
return err
}
if err := d.ContainerStart(ctx, resp.ID, dockerTypes.ContainerStartOptions{}); err != nil {
return err
}
return nil
}
// StopContainer stop and remove container
func (d *Docker) StopContainer(ctx context.Context, name string) error {
return d.ContainerStop(ctx, name, nil)
}
// InspectContainer inspect a container
func (d *Docker) InspectContainer(ctx context.Context, name string, container interface{}) error {
return nil
}
// ListContainer list containers
func (d *Docker) ListContainer(ctx context.Context, name string) ([]types.Service, error) {
args := dockerFilters.NewArgs(
dockerFilters.Arg("label", "belong-to=fx"),
)
containers, err := d.ContainerList(ctx, dockerTypes.ContainerListOptions{
Filters: args,
})
if err != nil {
return []types.Service{}, err
}
svs := make(map[string]types.Service)
for _, container := range containers {
// container name have extra forward slash
// https://github.com/moby/moby/issues/6705
if strings.HasPrefix(container.Names[0], fmt.Sprintf("/%s", name)) {
svs[container.Image] = types.Service{
Name: container.Names[0],
Image: container.Image,
ID: container.ID,
Host: container.Ports[0].IP,
Port: int(container.Ports[0].PublicPort),
State: container.State,
}
}
}
services := []types.Service{}
for _, s := range svs {
services = append(services, s)
}
return services, nil
}
var (
_ containerruntimes.ContainerRuntime = &Docker{}
)