github.com/git-ogawa/go-dbyml@v1.2.1/dbyml/build.go (about)

     1  package dbyml
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/container"
    13  	"github.com/docker/docker/api/types/network"
    14  	dockerStrSlice "github.com/docker/docker/api/types/strslice"
    15  	"github.com/docker/docker/client"
    16  	"github.com/docker/docker/pkg/jsonmessage"
    17  	"github.com/moby/moby/api/types/strslice"
    18  	"github.com/moby/moby/pkg/stdcopy"
    19  	"github.com/moby/term"
    20  	specs "github.com/opencontainers/image-spec/specs-go/v1"
    21  )
    22  
    23  // The container image used in buildkitd container
    24  const buildkitImageName = "moby/buildkit:v0.10.3"
    25  
    26  // BuildkitInfo defines setting on build with buildkit.
    27  type BuildkitInfo struct {
    28  	Enabled  bool                   `yaml:"enabled"`
    29  	Output   map[string]interface{} `yaml:"output"`
    30  	Cache    map[string]interface{} `yaml:"cache"`
    31  	Platform []string               `yaml:"platform"`
    32  	Remove   bool                   `yaml:"remove"`
    33  }
    34  
    35  // NewBuildkitInfo makes BuildkitInfo object with default values.
    36  func NewBuildkitInfo() *BuildkitInfo {
    37  	build := new(BuildkitInfo)
    38  	build.Enabled = false
    39  	build.Output = map[string]interface{}{}
    40  	build.Cache = map[string]interface{}{}
    41  	build.Remove = true
    42  	return build
    43  }
    44  
    45  // ParseOptions parses options related to buildkit and sets buildctl args.
    46  func (buildkit *BuildkitInfo) ParseOptions(imageInfo ImageInfo) []string {
    47  	var opts []string
    48  	var cmd string
    49  
    50  	// Output
    51  	cmd = fmt.Sprintf("type=%s,name=%s,push=true", buildkit.Output["type"], buildkit.Output["name"])
    52  	if buildkit.Output["insecure"] == true {
    53  		cmd = fmt.Sprintf("%s,registry.insecure=true", cmd)
    54  	}
    55  	opts = append(opts, "--output", cmd)
    56  
    57  	// Cache
    58  	if len(buildkit.Cache) != 0 {
    59  		exportCache := buildkit.Cache["export"].(map[string]string)["type"]
    60  		if exportCache == "inline" {
    61  			cmd = "type=inline"
    62  		} else if exportCache == "registry" {
    63  			cmd = fmt.Sprintf("type=registry,ref=%s", buildkit.Cache["export"].(map[string]string)["value"])
    64  		}
    65  		opts = append(opts, "--export-cache", cmd)
    66  
    67  		importCache := buildkit.Cache["import"].(map[string]string)["type"]
    68  		if importCache == "registry" {
    69  			cmd = fmt.Sprintf(
    70  				"type=registry,ref=%s",
    71  				buildkit.Cache["import"].(map[string]string)["value"],
    72  			)
    73  		}
    74  		opts = append(opts, "--import-cache", cmd)
    75  	}
    76  
    77  	// Platform
    78  	if len(buildkit.Platform) != 0 {
    79  		cmd = fmt.Sprintf("platform=%s", strings.Join(buildkit.Platform, ","))
    80  		opts = append(opts, "--opt", cmd)
    81  	}
    82  
    83  	// Other imageInfo options
    84  	if len(imageInfo.Labels) != 0 {
    85  		for k, v := range imageInfo.Labels {
    86  			cmd = fmt.Sprintf("label:%s=%s", k, v)
    87  			opts = append(opts, "--opt", cmd)
    88  		}
    89  	}
    90  
    91  	if len(imageInfo.BuildArgs) != 0 {
    92  		for k, v := range imageInfo.BuildArgs {
    93  			cmd = fmt.Sprintf("build-arg:%s=%s", k, *v)
    94  			opts = append(opts, "--opt", cmd)
    95  		}
    96  	}
    97  
    98  	return opts
    99  }
   100  
   101  // Builder describes a container information on buildkit
   102  type Builder struct {
   103  	Name           string                // The name of builder container
   104  	Image          BuildkitImage         // The image of builder container
   105  	ID             string                // The container ID
   106  	Config         *container.Config     // Container config
   107  	HostConfig     *container.HostConfig // Container host config
   108  	Context        string                // The build context in builder
   109  	DockerfilePath string                // The path to Dockerfile in builder
   110  	Cmd            []string              // The command executed in the builder
   111  	Client         *client.Client        // Docker client for connecting to builder
   112  }
   113  
   114  // NewBuilder creates a builder object with the default values.
   115  func NewBuilder() (builder *Builder) {
   116  	builder = new(Builder)
   117  	builder.Name = "dbyml-buildkit-builder"
   118  	builder.Image = BuildkitImage{buildkitImageName}
   119  	builder.Context = "/tmp"
   120  	builder.DockerfilePath = "/tmp"
   121  	builder.Cmd = []string{
   122  		"buildctl",
   123  		"build",
   124  		"--frontend",
   125  		"dockerfile.v0",
   126  		"--local",
   127  		fmt.Sprintf("context=%s", builder.Context),
   128  		"--local",
   129  		fmt.Sprintf("dockerfile=%s", builder.DockerfilePath),
   130  	}
   131  	builder.Config = &container.Config{
   132  		Image: builder.Image.Name,
   133  		Entrypoint: dockerStrSlice.StrSlice(
   134  			[]string{"buildkitd", "--config", "/etc/buildkitd.toml"},
   135  		),
   136  	}
   137  	builder.HostConfig = &container.HostConfig{
   138  		NetworkMode: "host",
   139  		Privileged:  true,
   140  	}
   141  	builder.Client, _ = client.NewClientWithOpts(client.FromEnv)
   142  	return builder
   143  }
   144  
   145  // AddCmd adds arguments passed to buildctl.
   146  func (builder *Builder) AddCmd(cmd ...string) {
   147  	builder.Cmd = append(builder.Cmd, cmd...)
   148  }
   149  
   150  // Setup creates a builder container and copy setting toml into the builder.
   151  func (builder *Builder) Setup(config *RegistryInfo) error {
   152  	err := builder.Create()
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	path, err := MakeBuildkitToml(config)
   158  	if err != nil {
   159  		return err
   160  	}
   161  
   162  	if err = builder.CopyFiles(path, "/etc"); err != nil {
   163  		return err
   164  	}
   165  
   166  	if err = os.Remove(path); err != nil {
   167  		return err
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  // Exists checks if a builder container exists.
   174  func (builder *Builder) Exists() bool {
   175  	json, err := builder.Inspect()
   176  	if err != nil {
   177  		panic(err)
   178  	}
   179  	if json.ContainerJSONBase != nil {
   180  		return true
   181  	}
   182  	return false
   183  }
   184  
   185  // SetContainerID sets container ID of a builder.
   186  func (builder *Builder) SetContainerID() error {
   187  	json, err := builder.Inspect()
   188  	if err != nil {
   189  		return err
   190  	}
   191  	builder.ID = json.ContainerJSONBase.ID
   192  	return nil
   193  }
   194  
   195  // Inspect gets a builder information.
   196  func (builder *Builder) Inspect() (types.ContainerJSON, error) {
   197  	var json types.ContainerJSON
   198  	ret, err := builder.Client.ContainerList(
   199  		context.Background(),
   200  		types.ContainerListOptions{All: true},
   201  	)
   202  	if err != nil {
   203  		return json, err
   204  	}
   205  
   206  	target := "/" + builder.Name
   207  	if len(ret) > 0 {
   208  		for _, container := range ret {
   209  			for _, name := range container.Names {
   210  				if name == target {
   211  					return builder.Client.ContainerInspect(context.Background(), container.ID)
   212  				}
   213  			}
   214  		}
   215  	}
   216  	return json, nil
   217  }
   218  
   219  // Create creates a builder container.
   220  func (builder *Builder) Create() error {
   221  	body, err := builder.Client.ContainerCreate(
   222  		context.Background(),
   223  		builder.Config,
   224  		builder.HostConfig,
   225  		&network.NetworkingConfig{},
   226  		&specs.Platform{},
   227  		builder.Name,
   228  	)
   229  	if err != nil {
   230  		return err
   231  	}
   232  	builder.ID = body.ID
   233  	return nil
   234  }
   235  
   236  // Start starts a builder container.
   237  func (builder *Builder) Start() error {
   238  	return builder.Client.ContainerStart(
   239  		context.Background(),
   240  		builder.ID,
   241  		types.ContainerStartOptions{},
   242  	)
   243  }
   244  
   245  // Stop stops a builder container.
   246  func (builder *Builder) Stop() error {
   247  	timeout := time.Second * 60
   248  	return builder.Client.ContainerStop(context.Background(), builder.ID, &timeout)
   249  }
   250  
   251  // Remove removes a builder container.
   252  func (builder *Builder) Remove() error {
   253  	return builder.Client.ContainerRemove(
   254  		context.Background(),
   255  		builder.ID,
   256  		types.ContainerRemoveOptions{RemoveVolumes: true, RemoveLinks: false, Force: true},
   257  	)
   258  }
   259  
   260  // CopyFiles copies directory in client to builder container.
   261  // If the directory contains some other directories, copy them recursively.
   262  func (builder *Builder) CopyFiles(path string, dst string) error {
   263  	opts := types.CopyToContainerOptions{
   264  		AllowOverwriteDirWithFile: true,
   265  		CopyUIDGID:                false,
   266  	}
   267  	return builder.Client.CopyToContainer(
   268  		context.Background(),
   269  		builder.ID,
   270  		dst,
   271  		GetBuildkitContext(path),
   272  		opts,
   273  	)
   274  }
   275  
   276  // Build builds a image in a builder.
   277  func (builder *Builder) Build(debug bool) error {
   278  	if debug {
   279  		fmt.Println("The following command will be run in buildkit container.")
   280  		re := regexp.MustCompile(`\s{1}-{2}`)
   281  		cmd := strings.Join(builder.Cmd, " ")
   282  		cmd = re.ReplaceAllString(cmd, "\n\t--")
   283  		fmt.Println(cmd)
   284  	}
   285  	return builder.Exec(builder.Cmd)
   286  }
   287  
   288  // Exec runs a command in buildkit container.
   289  func (builder *Builder) Exec(cmd []string) error {
   290  	execConfig := types.ExecConfig{
   291  		Privileged:   true,
   292  		AttachStdin:  true,
   293  		AttachStdout: true,
   294  		AttachStderr: true,
   295  		Tty:          true,
   296  		Detach:       false,
   297  		Cmd:          strslice.StrSlice(cmd),
   298  	}
   299  
   300  	// Create a new exec configuration to run an exec process.
   301  	res, err := builder.Client.ContainerExecCreate(context.Background(), builder.Name, execConfig)
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	// Run the exec process and attach it.
   307  	hijackRes, _ := builder.Client.ContainerExecAttach(
   308  		context.Background(),
   309  		res.ID,
   310  		types.ExecStartCheck{},
   311  	)
   312  	defer func() error {
   313  		return hijackRes.Conn.Close()
   314  	}()
   315  	_, err = stdcopy.StdCopy(os.Stdout, os.Stderr, hijackRes.Reader)
   316  	if err != nil {
   317  		return err
   318  	}
   319  	return nil
   320  }
   321  
   322  // BuildkitImage defines a docker image used in a builder container.
   323  type BuildkitImage struct {
   324  	Name string
   325  }
   326  
   327  // Exists checks if the image exists on host.
   328  func (buildkit *BuildkitImage) Exists() bool {
   329  	cli, _ := client.NewClientWithOpts(client.FromEnv)
   330  	imgs, err := cli.ImageList(context.Background(), types.ImageListOptions{})
   331  	if err != nil {
   332  		panic(err)
   333  	}
   334  	for _, img := range imgs {
   335  		if contains(img.RepoTags, buildkit.Name) {
   336  			return true
   337  		}
   338  	}
   339  	return false
   340  }
   341  
   342  // Pull pulls a buildkit image from official dockerhub.
   343  func (buildkit *BuildkitImage) Pull() error {
   344  	cli, _ := client.NewClientWithOpts(client.FromEnv)
   345  	ret, err := cli.ImagePull(context.Background(), buildkit.Name, types.ImagePullOptions{})
   346  	if err != nil {
   347  		return err
   348  	}
   349  
   350  	termFd, isTerm := term.GetFdInfo(os.Stderr)
   351  	return jsonmessage.DisplayJSONMessagesStream(ret, os.Stderr, termFd, isTerm, nil)
   352  }
   353  
   354  func contains(s []string, tag string) bool {
   355  	for _, v := range s {
   356  		if tag == v {
   357  			return true
   358  		}
   359  	}
   360  	return false
   361  }