feat: support more docker options

This commit is contained in:
fengcaiwen
2023-05-16 17:56:59 +08:00
parent f77a0170d7
commit f9c0a674be
13 changed files with 2533 additions and 480 deletions

View File

@@ -4,7 +4,8 @@ import (
"os" "os"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/opts" "github.com/docker/cli/cli/command"
dockercomp "github.com/docker/cli/cli/command/completion"
"github.com/spf13/cobra" "github.com/spf13/cobra"
cmdutil "k8s.io/kubectl/pkg/cmd/util" cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/completion"
@@ -18,21 +19,19 @@ import (
) )
func CmdDev(f cmdutil.Factory) *cobra.Command { func CmdDev(f cmdutil.Factory) *cobra.Command {
var devOptions = dev.Options{ var devOptions = &dev.Options{
Factory: f, Factory: f,
Entrypoint: "", NoProxy: false,
Publish: opts.NewListOpts(nil), ExtraCIDR: []string{},
Expose: opts.NewListOpts(nil), }
Env: opts.NewListOpts(nil), _, dockerCli, err := dev.GetClient()
Volumes: opts.NewListOpts(nil), if err != nil {
ExtraHosts: opts.NewListOpts(nil), panic(err)
NoProxy: false,
ExtraCIDR: []string{},
} }
var sshConf = &util.SshConfig{} var sshConf = &util.SshConfig{}
var transferImage bool var transferImage bool
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "dev", Use: "dev [OPTIONS] RESOURCE [COMMAND] [ARG...]",
Short: i18n.T("Startup your kubernetes workloads in local Docker container with same volume、env、and network"), Short: i18n.T("Startup your kubernetes workloads in local Docker container with same volume、env、and network"),
Long: templates.LongDesc(i18n.T(` Long: templates.LongDesc(i18n.T(`
Startup your kubernetes workloads in local Docker container with same volume、env、and network Startup your kubernetes workloads in local Docker container with same volume、env、and network
@@ -51,22 +50,22 @@ Startup your kubernetes workloads in local Docker container with same volume、e
kubevpn dev service/productpage kubevpn dev service/productpage
# Develop workloads with mesh, traffic with header a=1, will hit local PC, otherwise no effect # Develop workloads with mesh, traffic with header a=1, will hit local PC, otherwise no effect
kubevpn dev service/productpage --headers a=1 kubevpn dev --headers a=1 service/productpage
# Develop workloads without proxy traffic # Develop workloads without proxy traffic
kubevpn dev service/productpage --no-proxy kubevpn dev --no-proxy service/productpage
# Develop workloads which api-server behind of bastion host or ssh jump host # Develop workloads which api-server behind of bastion host or ssh jump host
kubevpn dev deployment/productpage --ssh-addr 192.168.1.100:22 --ssh-username root --ssh-keyfile /Users/naison/.ssh/ssh.pem kubevpn dev --ssh-addr 192.168.1.100:22 --ssh-username root --ssh-keyfile /Users/naison/.ssh/ssh.pem deployment/productpage
# it also support ProxyJump, like # it also support ProxyJump, like
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌────────────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌────────────┐
│ pc ├────►│ ssh1 ├────►│ ssh2 ├────►│ ssh3 ├─────►... ─────► │ api-server │ │ pc ├────►│ ssh1 ├────►│ ssh2 ├────►│ ssh3 ├─────►... ─────► │ api-server │
└──────┘ └──────┘ └──────┘ └──────┘ └────────────┘ └──────┘ └──────┘ └──────┘ └──────┘ └────────────┘
kubevpn dev deployment/productpage --ssh-alias <alias> kubevpn dev --ssh-alias <alias> deployment/productpage
`)), `)),
Args: cli.ExactArgs(1), Args: cli.RequiresMinArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
if !util.IsAdmin() { if !util.IsAdmin() {
util.RunWithElevated() util.RunWithElevated()
@@ -80,9 +79,14 @@ Startup your kubernetes workloads in local Docker container with same volume、e
} }
} }
return handler.SshJump(sshConf, cmd.Flags()) return handler.SshJump(sshConf, cmd.Flags())
return nil
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return dev.DoDev(devOptions, args, f) devOptions.Workload = args[0]
if len(args) > 1 {
devOptions.Copts.Args = args[1:]
}
return dev.DoDev(devOptions, cmd.Flags(), f)
}, },
} }
cmd.Flags().StringToStringVarP(&devOptions.Headers, "headers", "H", map[string]string{}, "Traffic with special headers with reverse it to local PC, you should startup your service after reverse workloads successfully, If not special, redirect all traffic to local PC, format is k=v, like: k1=v1,k2=v2") cmd.Flags().StringToStringVarP(&devOptions.Headers, "headers", "H", map[string]string{}, "Traffic with special headers with reverse it to local PC, you should startup your service after reverse workloads successfully, If not special, redirect all traffic to local PC, format is k=v, like: k1=v1,k2=v2")
@@ -96,24 +100,42 @@ Startup your kubernetes workloads in local Docker container with same volume、e
cmd.Flags().StringVar((*string)(&devOptions.ConnectMode), "connect-mode", string(dev.ConnectModeHost), "Connect to kubernetes network in container or in host, eg: ["+string(dev.ConnectModeContainer)+"|"+string(dev.ConnectModeHost)+"]") cmd.Flags().StringVar((*string)(&devOptions.ConnectMode), "connect-mode", string(dev.ConnectModeHost), "Connect to kubernetes network in container or in host, eg: ["+string(dev.ConnectModeContainer)+"|"+string(dev.ConnectModeHost)+"]")
cmd.Flags().BoolVar(&transferImage, "transfer-image", false, "transfer image to remote registry, it will transfer image "+config.OriginImage+" to flags `--image` special image, default: "+config.Image) cmd.Flags().BoolVar(&transferImage, "transfer-image", false, "transfer image to remote registry, it will transfer image "+config.OriginImage+" to flags `--image` special image, default: "+config.Image)
// docker options // diy docker options
cmd.Flags().Var(&devOptions.ExtraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")
// We allow for both "--net" and "--network", although the latter is the recommended way.
cmd.Flags().Var(&devOptions.NetMode, "net", "Connect a container to a network, eg: [default|bridge|host|none|container:$CONTAINER_ID]")
cmd.Flags().Var(&devOptions.NetMode, "network", "Connect a container to a network")
cmd.Flags().MarkHidden("net")
cmd.Flags().VarP(&devOptions.Volumes, "volume", "v", "Bind mount a volume")
cmd.Flags().Var(&devOptions.Mounts, "mount", "Attach a filesystem mount to the container")
cmd.Flags().Var(&devOptions.Expose, "expose", "Expose a port or a range of ports")
cmd.Flags().VarP(&devOptions.Publish, "publish", "p", "Publish a container's port(s) to the host")
cmd.Flags().BoolVarP(&devOptions.PublishAll, "publish-all", "P", false, "Publish all exposed ports to random ports")
cmd.Flags().VarP(&devOptions.Env, "env", "e", "Set environment variables")
cmd.Flags().StringVar(&devOptions.Entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image")
cmd.Flags().StringVar(&devOptions.DockerImage, "docker-image", "", "Overwrite the default K8s pod of the image") cmd.Flags().StringVar(&devOptions.DockerImage, "docker-image", "", "Overwrite the default K8s pod of the image")
//cmd.Flags().StringVar(&devOptions.Pull, "pull", container.PullImageMissing, `Pull image before creating ("`+container.PullImageAlways+`"|"`+container.PullImageMissing+`"|"`+container.PullImageNever+`")`) // origin docker options
cmd.Flags().StringVar(&devOptions.Platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") flags := cmd.Flags()
cmd.Flags().StringVar(&devOptions.VolumeDriver, "volume-driver", "", "Optional volume driver for the container") flags.SetInterspersed(false)
_ = cmd.Flags().SetAnnotation("platform", "version", []string{"1.32"})
// These are flags not stored in Config/HostConfig
flags.BoolVarP(&devOptions.Options.Detach, "detach", "d", false, "Run container in background and print container ID")
flags.StringVar(&devOptions.Options.Name, "name", "", "Assign a name to the container")
flags.StringVar(&devOptions.Options.Pull, "pull", dev.PullImageMissing, `Pull image before running ("`+dev.PullImageAlways+`"|"`+dev.PullImageMissing+`"|"`+dev.PullImageNever+`")`)
flags.BoolVarP(&devOptions.Options.Quiet, "quiet", "q", false, "Suppress the pull output")
// Add an explicit help that doesn't have a `-h` to prevent the conflict
// with hostname
flags.Bool("help", false, "Print usage")
command.AddPlatformFlag(flags, &devOptions.Options.Platform)
command.AddTrustVerificationFlags(flags, &devOptions.Options.Untrusted, dockerCli.ContentTrustEnabled())
devOptions.Copts = dev.AddFlags(flags)
_ = cmd.RegisterFlagCompletionFunc(
"env",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return os.Environ(), cobra.ShellCompDirectiveNoFileComp
},
)
_ = cmd.RegisterFlagCompletionFunc(
"env-file",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
},
)
_ = cmd.RegisterFlagCompletionFunc(
"network",
dockercomp.NetworkNames(nil),
)
addSshFlags(cmd, sshConf) addSshFlags(cmd, sshConf)
return cmd return cmd

191
pkg/dev/LICENSE Normal file
View File

@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2013-2017 Docker, Inc.
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
https://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.

View File

@@ -32,19 +32,23 @@ import (
) )
type RunConfig struct { type RunConfig struct {
containerName string
k8sContainerName string
config *container.Config config *container.Config
hostConfig *container.HostConfig hostConfig *container.HostConfig
networkingConfig *network.NetworkingConfig networkingConfig *network.NetworkingConfig
platform *v12.Platform platform *v12.Platform
containerName string
k8sContainerName string Options RunOptions
Copts *ContainerOptions
} }
func ConvertKubeResourceToContainer(namespace string, temp v1.PodTemplateSpec, envMap map[string][]string, mountVolume map[string][]mount.Mount, dnsConfig *miekgdns.ClientConfig) (runConfigList Run) { func ConvertKubeResourceToContainer(namespace string, temp v1.PodTemplateSpec, envMap map[string][]string, mountVolume map[string][]mount.Mount, dnsConfig *miekgdns.ClientConfig) (runConfigList ConfigList) {
spec := temp.Spec spec := temp.Spec
for _, c := range spec.Containers { for _, c := range spec.Containers {
var r RunConfig var r RunConfig
config := &container.Config{ tmpConfig := &container.Config{
Hostname: func() string { Hostname: func() string {
var hostname = spec.Hostname var hostname = spec.Hostname
if hostname == "" { if hostname == "" {
@@ -84,7 +88,7 @@ func ConvertKubeResourceToContainer(namespace string, temp v1.PodTemplateSpec, e
Shell: nil, Shell: nil,
} }
if temp.DeletionGracePeriodSeconds != nil { if temp.DeletionGracePeriodSeconds != nil {
config.StopTimeout = (*int)(unsafe.Pointer(temp.DeletionGracePeriodSeconds)) tmpConfig.StopTimeout = (*int)(unsafe.Pointer(temp.DeletionGracePeriodSeconds))
} }
hostConfig := &container.HostConfig{ hostConfig := &container.HostConfig{
Binds: []string{}, Binds: []string{},
@@ -138,7 +142,7 @@ func ConvertKubeResourceToContainer(namespace string, temp v1.PodTemplateSpec, e
portset[port1] = struct{}{} portset[port1] = struct{}{}
} }
hostConfig.PortBindings = portmap hostConfig.PortBindings = portmap
config.ExposedPorts = portset tmpConfig.ExposedPorts = portset
if c.SecurityContext != nil && c.SecurityContext.Capabilities != nil { if c.SecurityContext != nil && c.SecurityContext.Capabilities != nil {
hostConfig.CapAdd = append(hostConfig.CapAdd, *(*strslice.StrSlice)(unsafe.Pointer(&c.SecurityContext.Capabilities.Add))...) hostConfig.CapAdd = append(hostConfig.CapAdd, *(*strslice.StrSlice)(unsafe.Pointer(&c.SecurityContext.Capabilities.Add))...)
hostConfig.CapDrop = *(*strslice.StrSlice)(unsafe.Pointer(&c.SecurityContext.Capabilities.Drop)) hostConfig.CapDrop = *(*strslice.StrSlice)(unsafe.Pointer(&c.SecurityContext.Capabilities.Drop))
@@ -150,10 +154,10 @@ func ConvertKubeResourceToContainer(namespace string, temp v1.PodTemplateSpec, e
} }
r.containerName = fmt.Sprintf("%s_%s_%s_%s", c.Name, namespace, "kubevpn", suffix) r.containerName = fmt.Sprintf("%s_%s_%s_%s", c.Name, namespace, "kubevpn", suffix)
r.k8sContainerName = c.Name r.k8sContainerName = c.Name
r.config = config r.config = tmpConfig
r.hostConfig = hostConfig r.hostConfig = hostConfig
r.networkingConfig = &network.NetworkingConfig{EndpointsConfig: make(map[string]*network.EndpointSettings)} r.networkingConfig = &network.NetworkingConfig{EndpointsConfig: make(map[string]*network.EndpointSettings)}
r.platform = /*&v12.Platform{Architecture: "amd64", OS: "linux"}*/ nil r.platform = nil
runConfigList = append(runConfigList, &r) runConfigList = append(runConfigList, &r)
} }
@@ -179,11 +183,11 @@ func GetDNS(ctx context.Context, f util.Factory, ns, pod string) (*miekgdns.Clie
return nil, err return nil, err
} }
fromPod, err := dns.GetDNSServiceIPFromPod(clientSet, client, config, pod, ns) clientConfig, err := dns.GetDNSServiceIPFromPod(clientSet, client, config, pod, ns)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return fromPod, nil return clientConfig, nil
} }
// GetVolume key format: [container name]-[volume mount name] // GetVolume key format: [container name]-[volume mount name]

271
pkg/dev/dockercreate.go Normal file
View File

@@ -0,0 +1,271 @@
package dev
import (
"context"
"fmt"
"io"
"os"
"regexp"
"github.com/containerd/containerd/platforms"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/versions"
apiclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// Pull constants
const (
PullImageAlways = "always"
PullImageMissing = "missing" // Default (matches previous behavior)
PullImageNever = "never"
)
type createOptions struct {
Name string
Platform string
Untrusted bool
Pull string // always, missing, never
Quiet bool
}
func pullImage(ctx context.Context, dockerCli command.Cli, image string, platform string, out io.Writer) error {
ref, err := reference.ParseNormalizedNamed(image)
if err != nil {
return err
}
// Resolve the Repository name from fqn to RepositoryInfo
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return err
}
authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
if err != nil {
return err
}
options := types.ImageCreateOptions{
RegistryAuth: encodedAuth,
Platform: platform,
}
responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options)
if err != nil {
return err
}
defer responseBody.Close()
return jsonmessage.DisplayJSONMessagesStream(
responseBody,
out,
dockerCli.Out().FD(),
dockerCli.Out().IsTerminal(),
nil)
}
type cidFile struct {
path string
file *os.File
written bool
}
func (cid *cidFile) Close() error {
if cid.file == nil {
return nil
}
cid.file.Close()
if cid.written {
return nil
}
if err := os.Remove(cid.path); err != nil {
return errors.Wrapf(err, "failed to remove the CID file '%s'", cid.path)
}
return nil
}
func (cid *cidFile) Write(id string) error {
if cid.file == nil {
return nil
}
if _, err := cid.file.Write([]byte(id)); err != nil {
return errors.Wrap(err, "failed to write the container ID to the file")
}
cid.written = true
return nil
}
func newCIDFile(path string) (*cidFile, error) {
if path == "" {
return &cidFile{}, nil
}
if _, err := os.Stat(path); err == nil {
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", path)
}
f, err := os.Create(path)
if err != nil {
return nil, errors.Wrap(err, "failed to create the container ID file")
}
return &cidFile{path: path, file: f}, nil
}
//nolint:gocyclo
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, opts *createOptions) (*container.CreateResponse, error) {
config := containerConfig.Config
hostConfig := containerConfig.HostConfig
networkingConfig := containerConfig.NetworkingConfig
stderr := dockerCli.Err()
warnOnOomKillDisable(*hostConfig, stderr)
warnOnLocalhostDNS(*hostConfig, stderr)
var (
trustedRef reference.Canonical
namedRef reference.Named
)
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
if err != nil {
return nil, err
}
defer containerIDFile.Close()
ref, err := reference.ParseAnyReference(config.Image)
if err != nil {
return nil, err
}
if named, ok := ref.(reference.Named); ok {
namedRef = reference.TagNameOnly(named)
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !opts.Untrusted {
var err error
trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef, nil)
if err != nil {
return nil, err
}
config.Image = reference.FamiliarString(trustedRef)
}
}
pullAndTagImage := func() error {
pullOut := stderr
if opts.Quiet {
pullOut = io.Discard
}
if err := pullImage(ctx, dockerCli, config.Image, opts.Platform, pullOut); err != nil {
return err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
return image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef)
}
return nil
}
var platform *specs.Platform
// Engine API version 1.41 first introduced the option to specify platform on
// create. It will produce an error if you try to set a platform on older API
// versions, so check the API version here to maintain backwards
// compatibility for CLI users.
if opts.Platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
p, err := platforms.Parse(opts.Platform)
if err != nil {
return nil, errors.Wrap(err, "error parsing specified platform")
}
platform = &p
}
if opts.Pull == PullImageAlways {
if err := pullAndTagImage(); err != nil {
return nil, err
}
}
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.Name)
if err != nil {
// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
if apiclient.IsErrNotFound(err) && namedRef != nil && opts.Pull == PullImageMissing {
if !opts.Quiet {
// we don't want to write to stdout anything apart from container.ID
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
}
if err := pullAndTagImage(); err != nil {
return nil, err
}
var retryErr error
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.Name)
if retryErr != nil {
return nil, retryErr
}
} else {
return nil, err
}
}
for _, warning := range response.Warnings {
fmt.Fprintf(stderr, "WARNING: %s\n", warning)
}
err = containerIDFile.Write(response.ID)
return &response, err
}
func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 {
fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
}
}
// check the DNS settings passed via --dns against localhost regexp to warn if
// they are trying to set a DNS to a localhost address
func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
for _, dnsIP := range hostConfig.DNS {
if isLocalhost(dnsIP) {
fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
return
}
}
}
// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
const ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
var localhostIPRegexp = regexp.MustCompile(ipLocalhost)
// IsLocalhost returns true if ip matches the localhost IP regular expression.
// Used for determining if nameserver settings are being passed which are
// localhost addresses
func isLocalhost(ip string) bool {
return localhostIPRegexp.MatchString(ip)
}
func validatePullOpt(val string) error {
switch val {
case PullImageAlways, PullImageMissing, PullImageNever, "":
// valid option, but nothing to do yet
return nil
default:
return fmt.Errorf(
"invalid pull option: '%s': must be one of %q, %q or %q",
val,
PullImageAlways,
PullImageMissing,
PullImageNever,
)
}
}

1076
pkg/dev/dockeropts.go Normal file

File diff suppressed because it is too large Load Diff

108
pkg/dev/dockerrun.go Normal file
View File

@@ -0,0 +1,108 @@
package dev
import (
"context"
"fmt"
"io"
"strings"
"syscall"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
)
type RunOptions struct {
createOptions
Detach bool
sigProxy bool
detachKeys string
}
func attachContainer(ctx context.Context, dockerCli command.Cli, errCh *chan error, config *container.Config, containerID string) (func(), error) {
options := types.ContainerAttachOptions{
Stream: true,
Stdin: config.AttachStdin,
Stdout: config.AttachStdout,
Stderr: config.AttachStderr,
DetachKeys: dockerCli.ConfigFile().DetachKeys,
}
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options)
if errAttach != nil {
return nil, errAttach
}
var (
out, cerr io.Writer
in io.ReadCloser
)
if config.AttachStdin {
in = dockerCli.In()
}
if config.AttachStdout {
out = dockerCli.Out()
}
if config.AttachStderr {
if config.Tty {
cerr = dockerCli.Out()
} else {
cerr = dockerCli.Err()
}
}
ch := make(chan error, 1)
*errCh = ch
if in != nil && out != nil && cerr != nil {
}
go func() {
ch <- func() error {
streamer := hijackedIOStreamer{
streams: dockerCli,
inputStream: in,
outputStream: out,
errorStream: cerr,
resp: resp,
tty: config.Tty,
detachKeys: options.DetachKeys,
}
if errHijack := streamer.stream(ctx); errHijack != nil {
return errHijack
}
return errAttach
}()
}()
return resp.Close, nil
}
// reportError is a utility method that prints a user-friendly message
// containing the error that occurred during parsing and a suggestion to get help
func reportError(stderr io.Writer, name string, str string, withHelp bool) {
str = strings.TrimSuffix(str, ".") + "."
if withHelp {
str += "\nSee 'docker " + name + " --help'."
}
_, _ = fmt.Fprintln(stderr, "docker:", str)
}
// if container start fails with 'not found'/'no such' error, return 127
// if container start fails with 'permission denied' error, return 126
// return 125 for generic docker daemon failures
func runStartContainerErr(err error) error {
trimmedErr := strings.TrimPrefix(err.Error(), "Error response from daemon: ")
statusError := cli.StatusError{StatusCode: 125}
if strings.Contains(trimmedErr, "executable file not found") ||
strings.Contains(trimmedErr, "no such file or directory") ||
strings.Contains(trimmedErr, "system cannot find the file specified") {
statusError = cli.StatusError{StatusCode: 127}
} else if strings.Contains(trimmedErr, syscall.EACCES.Error()) {
statusError = cli.StatusError{StatusCode: 126}
}
return statusError
}

207
pkg/dev/hijack.go Normal file
View File

@@ -0,0 +1,207 @@
package dev
import (
"context"
"fmt"
"io"
"runtime"
"sync"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/stdcopy"
"github.com/moby/term"
"github.com/sirupsen/logrus"
)
// The default escape key sequence: ctrl-p, ctrl-q
// TODO: This could be moved to `pkg/term`.
var defaultEscapeKeys = []byte{16, 17}
// A hijackedIOStreamer handles copying input to and output from streams to the
// connection.
type hijackedIOStreamer struct {
streams command.Streams
inputStream io.ReadCloser
outputStream io.Writer
errorStream io.Writer
resp types.HijackedResponse
tty bool
detachKeys string
}
// stream handles setting up the IO and then begins streaming stdin/stdout
// to/from the hijacked connection, blocking until it is either done reading
// output, the user inputs the detach key sequence when in TTY mode, or when
// the given context is cancelled.
func (h *hijackedIOStreamer) stream(ctx context.Context) error {
restoreInput, err := h.setupInput()
if err != nil {
return fmt.Errorf("unable to setup input stream: %s", err)
}
defer restoreInput()
outputDone := h.beginOutputStream(restoreInput)
inputDone, detached := h.beginInputStream(restoreInput)
select {
case err := <-outputDone:
return err
case <-inputDone:
// Input stream has closed.
if h.outputStream != nil || h.errorStream != nil {
// Wait for output to complete streaming.
select {
case err := <-outputDone:
return err
case <-ctx.Done():
return ctx.Err()
}
}
return nil
case err := <-detached:
// Got a detach key sequence.
return err
case <-ctx.Done():
return ctx.Err()
}
}
func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
if h.inputStream == nil || !h.tty {
// No need to setup input TTY.
// The restore func is a nop.
return func() {}, nil
}
if err := setRawTerminal(h.streams); err != nil {
return nil, fmt.Errorf("unable to set IO streams as raw terminal: %s", err)
}
// Use sync.Once so we may call restore multiple times but ensure we
// only restore the terminal once.
var restoreOnce sync.Once
restore = func() {
restoreOnce.Do(func() {
restoreTerminal(h.streams, h.inputStream)
})
}
// Wrap the input to detect detach escape sequence.
// Use default escape keys if an invalid sequence is given.
escapeKeys := defaultEscapeKeys
if h.detachKeys != "" {
customEscapeKeys, err := term.ToBytes(h.detachKeys)
if err != nil {
logrus.Warnf("invalid detach escape keys, using default: %s", err)
} else {
escapeKeys = customEscapeKeys
}
}
h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close)
return restore, nil
}
func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error {
if h.outputStream == nil && h.errorStream == nil {
// There is no need to copy output.
return nil
}
outputDone := make(chan error)
go func() {
var err error
// When TTY is ON, use regular copy
if h.outputStream != nil && h.tty {
_, err = io.Copy(h.outputStream, h.resp.Reader)
// We should restore the terminal as soon as possible
// once the connection ends so any following print
// messages will be in normal type.
restoreInput()
} else {
_, err = stdcopy.StdCopy(h.outputStream, h.errorStream, h.resp.Reader)
}
logrus.Debug("[hijack] End of stdout")
if err != nil {
logrus.Debugf("Error receiveStdout: %s", err)
}
outputDone <- err
}()
return outputDone
}
func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan struct{}, detachedC <-chan error) {
inputDone := make(chan struct{})
detached := make(chan error)
go func() {
if h.inputStream != nil {
_, err := io.Copy(h.resp.Conn, h.inputStream)
// We should restore the terminal as soon as possible
// once the connection ends so any following print
// messages will be in normal type.
restoreInput()
logrus.Debug("[hijack] End of stdin")
if _, ok := err.(term.EscapeError); ok {
detached <- err
return
}
if err != nil {
// This error will also occur on the receive
// side (from stdout) where it will be
// propagated back to the caller.
logrus.Debugf("Error sendStdin: %s", err)
}
}
if err := h.resp.CloseWrite(); err != nil {
logrus.Debugf("Couldn't send EOF: %s", err)
}
close(inputDone)
}()
return inputDone, detached
}
func setRawTerminal(streams command.Streams) error {
if err := streams.In().SetRawTerminal(); err != nil {
return err
}
return streams.Out().SetRawTerminal()
}
func restoreTerminal(streams command.Streams, in io.Closer) error {
streams.In().RestoreTerminal()
streams.Out().RestoreTerminal()
// WARNING: DO NOT REMOVE THE OS CHECKS !!!
// For some reason this Close call blocks on darwin..
// As the client exits right after, simply discard the close
// until we find a better solution.
//
// This can also cause the client on Windows to get stuck in Win32 CloseHandle()
// in some cases. See https://github.com/docker/docker/issues/28267#issuecomment-288237442
// Tracked internally at Microsoft by VSO #11352156. In the
// Windows case, you hit this if you are using the native/v2 console,
// not the "legacy" console, and you start the client in a new window. eg
// `start docker run --rm -it microsoft/nanoserver cmd /s /c echo foobar`
// will hang. Remove start, and it won't repro.
if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
return in.Close()
}
return nil
}

View File

@@ -29,10 +29,12 @@ import (
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
"github.com/google/uuid" "github.com/google/uuid"
specs "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/image-spec/specs-go/v1"
pkgerr "github.com/pkg/errors" pkgerr "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/pflag"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
@@ -67,22 +69,12 @@ type Options struct {
ConnectMode ConnectMode ConnectMode ConnectMode
// docker options // docker options
Platform string DockerImage string
//Pull string // always, missing, never Options RunOptions
PublishAll bool Copts *ContainerOptions
Entrypoint string
DockerImage string
Publish opts.ListOpts
Expose opts.ListOpts
ExtraHosts opts.ListOpts
NetMode opts.NetworkOpt
Env opts.ListOpts
Mounts opts.MountOpt
Volumes opts.ListOpts
VolumeDriver string
} }
func (d Options) Main(ctx context.Context) error { func (d *Options) Main(ctx context.Context, cli *client.Client, dockerCli *command.DockerCli, tempContainerConfig *containerConfig) error {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
object, err := util.GetUnstructuredObject(d.Factory, d.Namespace, d.Workload) object, err := util.GetUnstructuredObject(d.Factory, d.Namespace, d.Workload)
if err != nil { if err != nil {
@@ -133,31 +125,25 @@ func (d Options) Main(ctx context.Context) error {
} }
mesh.RemoveContainers(templateSpec) mesh.RemoveContainers(templateSpec)
list := ConvertKubeResourceToContainer(d.Namespace, *templateSpec, env, volume, dns) runConfigList := ConvertKubeResourceToContainer(d.Namespace, *templateSpec, env, volume, dns)
err = fillOptions(list, d) err = mergeDockerOptions(runConfigList, d, tempContainerConfig)
if err != nil { if err != nil {
return fmt.Errorf("can not fill docker options, err: %v", err) return fmt.Errorf("can not fill docker options, err: %v", err)
} }
var dockerCli *command.DockerCli
var cli *client.Client
cli, dockerCli, err = GetClient()
if err != nil {
return err
}
// check resource // check resource
var outOfMemory bool var outOfMemory bool
outOfMemory, _ = checkOutOfMemory(templateSpec, cli) outOfMemory, _ = checkOutOfMemory(templateSpec, cli)
if outOfMemory { if outOfMemory {
return fmt.Errorf("your pod resource request is bigger than docker-desktop resource, please adjust your docker-desktop resource") return fmt.Errorf("your pod resource request is bigger than docker-desktop resource, please adjust your docker-desktop resource")
} }
mode := container.NetworkMode(d.NetMode.NetworkMode()) mode := container.NetworkMode(d.Copts.netMode.NetworkMode())
if len(d.NetMode.Value()) != 0 { if len(d.Copts.netMode.Value()) != 0 {
for _, runConfig := range list[:] { for _, runConfig := range runConfigList[:] {
// remove expose port // remove expose port
runConfig.config.ExposedPorts = nil runConfig.config.ExposedPorts = nil
runConfig.hostConfig.NetworkMode = mode runConfig.hostConfig.NetworkMode = mode
if mode.IsContainer() { if mode.IsContainer() {
runConfig.hostConfig.PidMode = containertypes.PidMode(d.NetMode.NetworkMode()) runConfig.hostConfig.PidMode = containertypes.PidMode(d.Copts.netMode.NetworkMode())
} }
runConfig.hostConfig.PortBindings = nil runConfig.hostConfig.PortBindings = nil
@@ -175,15 +161,32 @@ func (d Options) Main(ctx context.Context) error {
return err return err
} }
list[0].networkingConfig.EndpointsConfig[list[0].containerName] = &network.EndpointSettings{ runConfigList[len(runConfigList)-1].networkingConfig.EndpointsConfig[runConfigList[len(runConfigList)-1].containerName] = &network.EndpointSettings{
NetworkID: networkID, NetworkID: networkID,
} }
// skip first var portmap = nat.PortMap{}
for _, runConfig := range list[1:] { var portset = nat.PortSet{}
for _, runConfig := range runConfigList {
for k, v := range runConfig.hostConfig.PortBindings {
if oldValue, ok := portmap[k]; ok {
portmap[k] = append(oldValue, v...)
} else {
portmap[k] = v
}
}
for k, v := range runConfig.config.ExposedPorts {
portset[k] = v
}
}
runConfigList[len(runConfigList)-1].hostConfig.PortBindings = portmap
runConfigList[len(runConfigList)-1].config.ExposedPorts = portset
// skip last, use last container network
for _, runConfig := range runConfigList[:len(runConfigList)-1] {
// remove expose port // remove expose port
runConfig.config.ExposedPorts = nil runConfig.config.ExposedPorts = nil
runConfig.hostConfig.NetworkMode = containertypes.NetworkMode("container:" + list[0].containerName) runConfig.hostConfig.NetworkMode = containertypes.NetworkMode("container:" + runConfigList[len(runConfigList)-1].containerName)
runConfig.hostConfig.PidMode = containertypes.PidMode("container:" + list[0].containerName) runConfig.hostConfig.PidMode = containertypes.PidMode("container:" + runConfigList[len(runConfigList)-1].containerName)
runConfig.hostConfig.PortBindings = nil runConfig.hostConfig.PortBindings = nil
// remove dns // remove dns
@@ -196,25 +199,20 @@ func (d Options) Main(ctx context.Context) error {
} }
handler.RollbackFuncList = append(handler.RollbackFuncList, func() { handler.RollbackFuncList = append(handler.RollbackFuncList, func() {
_ = list.Remove(ctx) _ = runConfigList.Remove(ctx, cli)
}) })
err = list.Run(ctx, volume) err = runConfigList.Run(ctx, volume, cli, dockerCli)
if err != nil { if err != nil {
return err return err
} }
return terminal(list[0].containerName, dockerCli) return terminal(runConfigList[0].containerName, dockerCli)
} }
type Run []*RunConfig type ConfigList []*RunConfig
func (r Run) Remove(ctx context.Context) error { func (l ConfigList) Remove(ctx context.Context, cli *client.Client) error {
cli, _, err := GetClient() for _, runConfig := range l {
if err != nil { err := cli.NetworkDisconnect(ctx, runConfig.containerName, runConfig.containerName, true)
return err
}
for _, runConfig := range r {
err = cli.NetworkDisconnect(ctx, runConfig.containerName, runConfig.containerName, true)
if err != nil { if err != nil {
log.Debug(err) log.Debug(err)
} }
@@ -223,8 +221,7 @@ func (r Run) Remove(ctx context.Context) error {
log.Debug(err) log.Debug(err)
} }
} }
var i types.NetworkResource i, err := cli.NetworkInspect(ctx, config.ConfigMapPodTrafficManager, types.NetworkInspectOptions{})
i, err = cli.NetworkInspect(ctx, config.ConfigMapPodTrafficManager, types.NetworkInspectOptions{})
if err != nil { if err != nil {
return err return err
} }
@@ -254,32 +251,35 @@ func GetClient() (*client.Client, *command.DockerCli, error) {
return cli, dockerCli, nil return cli, dockerCli, nil
} }
func (r Run) Run(ctx context.Context, volume map[string][]mount.Mount) error { func (l ConfigList) Run(ctx context.Context, volume map[string][]mount.Mount, cli *client.Client, dockerCli *command.DockerCli) error {
cli, c, err := GetClient() for index := len(l) - 1; index >= 0; index-- {
if err != nil { runConfig := l[index]
return err if index == 0 {
} _, err := runFirst(ctx, runConfig, cli, dockerCli)
for _, runConfig := range r {
var id string
id, err = run(ctx, runConfig, cli, c)
if err != nil {
// try another way to startup container
log.Infof("occur err: %v, try another way to startup container...", err)
runConfig.hostConfig.Mounts = nil
id, err = run(ctx, runConfig, cli, c)
if err != nil { if err != nil {
return err return err
} }
err = r.copyToContainer(ctx, volume[runConfig.k8sContainerName], cli, id) } else {
id, err := run(ctx, runConfig, cli, dockerCli)
if err != nil { if err != nil {
return err // try another way to startup container
log.Infof("occur err: %v, try another way to startup container...", err)
runConfig.hostConfig.Mounts = nil
id, err = run(ctx, runConfig, cli, dockerCli)
if err != nil {
return err
}
err = l.copyToContainer(ctx, volume[runConfig.k8sContainerName], cli, id)
if err != nil {
return err
}
} }
} }
} }
return nil return nil
} }
func (r Run) copyToContainer(ctx context.Context, volume []mount.Mount, cli *client.Client, id string) error { func (l ConfigList) copyToContainer(ctx context.Context, volume []mount.Mount, cli *client.Client, id string) error {
// copy volume into container // copy volume into container
for _, v := range volume { for _, v := range volume {
target, err := createFolder(ctx, cli, id, v.Source, v.Target) target, err := createFolder(ctx, cli, id, v.Source, v.Target)
@@ -367,22 +367,22 @@ func checkOutOfMemory(spec *v1.PodTemplateSpec, cli *client.Client) (outOfMemory
return return
} }
func DoDev(devOptions Options, args []string, f cmdutil.Factory) error { func DoDev(devOptions *Options, flags *pflag.FlagSet, f cmdutil.Factory) error {
connect := handler.ConnectOptions{ connect := handler.ConnectOptions{
Headers: devOptions.Headers, Headers: devOptions.Headers,
Workloads: args, Workloads: []string{devOptions.Workload},
ExtraCIDR: devOptions.ExtraCIDR, ExtraCIDR: devOptions.ExtraCIDR,
ExtraDomain: devOptions.ExtraDomain, ExtraDomain: devOptions.ExtraDomain,
} }
cli, dockerCli, err := GetClient()
if err != nil {
return err
}
mode := container.NetworkMode(devOptions.NetMode.NetworkMode()) mode := container.NetworkMode(devOptions.Copts.netMode.NetworkMode())
if mode.IsContainer() { if mode.IsContainer() {
client, _, err := GetClient()
if err != nil {
return err
}
var inspect types.ContainerJSON var inspect types.ContainerJSON
inspect, err = client.ContainerInspect(context.Background(), mode.ConnectedContainer()) inspect, err = cli.ContainerInspect(context.Background(), mode.ConnectedContainer())
if err != nil { if err != nil {
return err return err
} }
@@ -397,8 +397,7 @@ func DoDev(devOptions Options, args []string, f cmdutil.Factory) error {
if err := connect.InitClient(f); err != nil { if err := connect.InitClient(f); err != nil {
return err return err
} }
err := connect.PreCheckResource() if err = connect.PreCheckResource(); err != nil {
if err != nil {
return err return err
} }
@@ -410,8 +409,8 @@ func DoDev(devOptions Options, args []string, f cmdutil.Factory) error {
} }
var platform *specs.Platform var platform *specs.Platform
if devOptions.Platform != "" { if devOptions.Options.Platform != "" {
p, err := platforms.Parse(devOptions.Platform) p, err := platforms.Parse(devOptions.Options.Platform)
if err != nil { if err != nil {
return pkgerr.Wrap(err, "error parsing specified platform") return pkgerr.Wrap(err, "error parsing specified platform")
} }
@@ -441,117 +440,15 @@ func DoDev(devOptions Options, args []string, f cmdutil.Factory) error {
return err return err
} }
case ConnectModeContainer: case ConnectModeContainer:
var dockerCli *command.DockerCli var connectContainer *RunConfig
var cli *client.Client connectContainer, err = createConnectContainer(*devOptions, connect, path, err, cli, platform)
cli, dockerCli, err = GetClient()
if err != nil { if err != nil {
return err return err
} }
var entrypoint []string
if devOptions.NoProxy {
entrypoint = []string{"kubevpn", "connect", "-n", connect.Namespace, "--kubeconfig", "/root/.kube/config", "--image", config.Image}
for _, v := range connect.ExtraCIDR {
entrypoint = append(entrypoint, "--extra-cidr", v)
}
for _, v := range connect.ExtraDomain {
entrypoint = append(entrypoint, "--extra-domain", v)
}
} else {
entrypoint = []string{"kubevpn", "proxy", connect.Workloads[0], "-n", connect.Namespace, "--kubeconfig", "/root/.kube/config", "--image", config.Image}
for k, v := range connect.Headers {
entrypoint = append(entrypoint, "--headers", fmt.Sprintf("%s=%s", k, v))
}
for _, v := range connect.ExtraCIDR {
entrypoint = append(entrypoint, "--extra-cidr", v)
}
for _, v := range connect.ExtraDomain {
entrypoint = append(entrypoint, "--extra-domain", v)
}
}
runConfig := &container.Config{
User: "root",
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
ExposedPorts: nil,
StdinOnce: false,
Env: []string{fmt.Sprintf("%s=1", config.EnvStartSudoKubeVPNByKubeVPN)},
Cmd: []string{},
Healthcheck: nil,
ArgsEscaped: false,
Image: config.Image,
Volumes: nil,
Entrypoint: entrypoint,
NetworkDisabled: false,
MacAddress: "",
OnBuild: nil,
StopSignal: "",
StopTimeout: nil,
Shell: nil,
}
hostConfig := &container.HostConfig{
Binds: []string{fmt.Sprintf("%s:%s", path, "/root/.kube/config")},
LogConfig: container.LogConfig{},
PortBindings: nil,
RestartPolicy: container.RestartPolicy{},
AutoRemove: true,
VolumeDriver: "",
VolumesFrom: nil,
ConsoleSize: [2]uint{},
CapAdd: strslice.StrSlice{"SYS_PTRACE", "SYS_ADMIN"}, // for dlv
CgroupnsMode: "",
ExtraHosts: nil,
GroupAdd: nil,
IpcMode: "",
Cgroup: "",
Links: nil,
OomScoreAdj: 0,
PidMode: "",
Privileged: true,
PublishAllPorts: false,
ReadonlyRootfs: false,
SecurityOpt: []string{"apparmor=unconfined", "seccomp=unconfined"},
StorageOpt: nil,
Tmpfs: nil,
UTSMode: "",
UsernsMode: "",
ShmSize: 0,
Sysctls: nil,
Runtime: "",
Isolation: "",
Resources: container.Resources{},
MaskedPaths: nil,
ReadonlyPaths: nil,
Init: nil,
}
var suffix string
if newUUID, err := uuid.NewUUID(); err == nil {
suffix = strings.ReplaceAll(newUUID.String(), "-", "")[:5]
}
var kubevpnNetwork string
kubevpnNetwork, err = createKubevpnNetwork(context.Background(), cli)
if err != nil {
return err
}
name := fmt.Sprintf("%s_%s_%s", "kubevpn", "local", suffix)
c := &RunConfig{
config: runConfig,
hostConfig: hostConfig,
networkingConfig: &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{name: {
NetworkID: kubevpnNetwork,
}},
},
platform: platform,
containerName: name,
k8sContainerName: name,
}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
var id string var id string
if id, err = run(ctx, c, cli, dockerCli); err != nil { if id, err = run(ctx, connectContainer, cli, dockerCli); err != nil {
return err return err
} }
h := interrupt.New(func(signal os.Signal) { h := interrupt.New(func(signal os.Signal) {
@@ -570,21 +467,150 @@ func DoDev(devOptions Options, args []string, f cmdutil.Factory) error {
} }
return err return err
} }
if err = devOptions.NetMode.Set("container:" + id); err != nil { if err = devOptions.Copts.netMode.Set("container:" + id); err != nil {
return err return err
} }
default: default:
return fmt.Errorf("unsupport connect mode: %s", devOptions.ConnectMode) return fmt.Errorf("unsupport connect mode: %s", devOptions.ConnectMode)
} }
var tempContainerConfig *containerConfig
{
if err := validatePullOpt(devOptions.Options.Pull); err != nil {
return err
}
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(devOptions.Copts.env.GetAll()))
newEnv := []string{}
for k, v := range proxyConfig {
if v == nil {
newEnv = append(newEnv, k)
} else {
newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, *v))
}
}
devOptions.Copts.env = *opts.NewListOptsRef(&newEnv, nil)
tempContainerConfig, err = parse(flags, devOptions.Copts, dockerCli.ServerInfo().OSType)
// just in case the parse does not exit
if err != nil {
return err
}
if err = validateAPIVersion(tempContainerConfig, dockerCli.Client().ClientVersion()); err != nil {
return err
}
}
devOptions.Namespace = connect.Namespace devOptions.Namespace = connect.Namespace
err = devOptions.Main(context.Background()) err = devOptions.Main(context.Background(), cli, dockerCli, tempContainerConfig)
if err != nil { if err != nil {
log.Errorln(err) log.Errorln(err)
} }
return err return err
} }
func createConnectContainer(devOptions Options, connect handler.ConnectOptions, path string, err error, cli *client.Client, platform *specs.Platform) (*RunConfig, error) {
var entrypoint []string
if devOptions.NoProxy {
entrypoint = []string{"kubevpn", "connect", "-n", connect.Namespace, "--kubeconfig", "/root/.kube/config", "--image", config.Image}
for _, v := range connect.ExtraCIDR {
entrypoint = append(entrypoint, "--extra-cidr", v)
}
for _, v := range connect.ExtraDomain {
entrypoint = append(entrypoint, "--extra-domain", v)
}
} else {
entrypoint = []string{"kubevpn", "proxy", connect.Workloads[0], "-n", connect.Namespace, "--kubeconfig", "/root/.kube/config", "--image", config.Image}
for k, v := range connect.Headers {
entrypoint = append(entrypoint, "--headers", fmt.Sprintf("%s=%s", k, v))
}
for _, v := range connect.ExtraCIDR {
entrypoint = append(entrypoint, "--extra-cidr", v)
}
for _, v := range connect.ExtraDomain {
entrypoint = append(entrypoint, "--extra-domain", v)
}
}
runConfig := &container.Config{
User: "root",
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
ExposedPorts: nil,
StdinOnce: false,
Env: []string{fmt.Sprintf("%s=1", config.EnvStartSudoKubeVPNByKubeVPN)},
Cmd: []string{},
Healthcheck: nil,
ArgsEscaped: false,
Image: config.Image,
Volumes: nil,
Entrypoint: entrypoint,
NetworkDisabled: false,
MacAddress: "",
OnBuild: nil,
StopSignal: "",
StopTimeout: nil,
Shell: nil,
}
hostConfig := &container.HostConfig{
Binds: []string{fmt.Sprintf("%s:%s", path, "/root/.kube/config")},
LogConfig: container.LogConfig{},
PortBindings: nil,
RestartPolicy: container.RestartPolicy{},
AutoRemove: true,
VolumeDriver: "",
VolumesFrom: nil,
ConsoleSize: [2]uint{},
CapAdd: strslice.StrSlice{"SYS_PTRACE", "SYS_ADMIN"}, // for dlv
CgroupnsMode: "",
ExtraHosts: nil,
GroupAdd: nil,
IpcMode: "",
Cgroup: "",
Links: nil,
OomScoreAdj: 0,
PidMode: "",
Privileged: true,
PublishAllPorts: false,
ReadonlyRootfs: false,
SecurityOpt: []string{"apparmor=unconfined", "seccomp=unconfined"},
StorageOpt: nil,
Tmpfs: nil,
UTSMode: "",
UsernsMode: "",
ShmSize: 0,
Sysctls: nil,
Runtime: "",
Isolation: "",
Resources: container.Resources{},
MaskedPaths: nil,
ReadonlyPaths: nil,
Init: nil,
}
var suffix string
if newUUID, err := uuid.NewUUID(); err == nil {
suffix = strings.ReplaceAll(newUUID.String(), "-", "")[:5]
}
var kubevpnNetwork string
kubevpnNetwork, err = createKubevpnNetwork(context.Background(), cli)
if err != nil {
return nil, err
}
name := fmt.Sprintf("%s_%s_%s", "kubevpn", "local", suffix)
c := &RunConfig{
config: runConfig,
hostConfig: hostConfig,
networkingConfig: &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{name: {
NetworkID: kubevpnNetwork,
}},
},
platform: platform,
containerName: name,
k8sContainerName: name,
}
return c, nil
}
func runLogsWaitRunning(ctx context.Context, dockerCli command.Cli, container string) error { func runLogsWaitRunning(ctx context.Context, dockerCli command.Cli, container string) error {
c, err := dockerCli.Client().ContainerInspect(ctx, container) c, err := dockerCli.Client().ContainerInspect(ctx, container)
if err != nil { if err != nil {
@@ -685,37 +711,6 @@ func runKill(dockerCli command.Cli, containers ...string) error {
} }
return nil return nil
} }
func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error {
if len(containers) == 0 {
return nil
}
const defaultParallel int = 50
sem := make(chan struct{}, defaultParallel)
errChan := make(chan error)
// make sure result is printed in correct order
output := map[string]chan error{}
for _, c := range containers {
output[c] = make(chan error, 1)
}
go func() {
for _, c := range containers {
err := <-output[c]
errChan <- err
}
}()
go func() {
for _, c := range containers {
sem <- struct{}{} // Wait for active queue sem to drain.
go func(container string) {
output[container] <- op(ctx, container)
<-sem
}(c)
}
}()
return errChan
}
func createKubevpnNetwork(ctx context.Context, cli *client.Client) (string, error) { func createKubevpnNetwork(ctx context.Context, cli *client.Client) (string, error) {
by := map[string]string{"owner": config.ConfigMapPodTrafficManager} by := map[string]string{"owner": config.ConfigMapPodTrafficManager}

View File

@@ -1,26 +1,13 @@
package dev package dev
import ( import (
"fmt" "github.com/containerd/containerd/platforms"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/docker/errdefs"
"github.com/docker/go-connections/nat"
v12 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/wencaiwulue/kubevpn/pkg/util"
) )
func fillOptions(r Run, copts Options) error { func mergeDockerOptions(r ConfigList, copts *Options, tempContainerConfig *containerConfig) error {
if copts.ContainerName != "" { if copts.ContainerName != "" {
var index = -1 var index = -1
for i, config := range r { for i, config := range r {
@@ -35,226 +22,49 @@ func fillOptions(r Run, copts Options) error {
} }
config := r[0] config := r[0]
config.hostConfig.PublishAllPorts = copts.PublishAll config.Options = copts.Options
config.Copts = copts.Copts
if copts.DockerImage != "" { if copts.DockerImage != "" {
config.config.Image = copts.DockerImage config.config.Image = copts.DockerImage
} }
if copts.Options.Name != "" {
if copts.Entrypoint != "" { config.containerName = copts.Options.Name
if strings.Count(copts.Entrypoint, " ") != 0 { } else {
split := strings.Split(copts.Entrypoint, " ") config.Options.Name = config.containerName
config.config.Entrypoint = split
} else {
config.config.Entrypoint = strslice.StrSlice{copts.Entrypoint}
}
config.config.Cmd = []string{}
} }
if copts.Platform != "" { if copts.Options.Platform != "" {
split := strings.Split(copts.Platform, "/") p, err := platforms.Parse(copts.Options.Platform)
if len(split) != 2 {
return errors.Errorf("invalid port format for --platform: %s", copts.Platform)
}
config.platform = &v12.Platform{
OS: split[0],
Architecture: split[1],
}
}
// collect all the environment variables for the container
envVariables, err := opts.ReadKVEnvStrings([]string{}, copts.Env.GetAll())
if err != nil {
return err
}
config.config.Env = append(config.config.Env, envVariables...)
publishOpts := copts.Publish.GetAll()
var (
ports map[nat.Port]struct{}
portBindings map[nat.Port][]nat.PortBinding
convertedOpts []string
)
convertedOpts, err = convertToStandardNotation(publishOpts)
if err != nil {
return err
}
ports, portBindings, err = nat.ParsePortSpecs(convertedOpts)
if err != nil {
return err
}
// Merge in exposed ports to the map of published ports
for _, e := range copts.Expose.GetAll() {
if strings.Contains(e, ":") {
return errors.Errorf("invalid port format for --expose: %s", e)
}
// support two formats for expose, original format <portnum>/[<proto>]
// or <startport-endport>/[<proto>]
proto, port := nat.SplitProtoPort(e)
// parse the start and end port and create a sequence of ports to expose
// if expose a port, the start and end port are the same
start, end, err := nat.ParsePortRange(port)
if err != nil { if err != nil {
return errors.Errorf("invalid range format for --expose: %s, error: %s", e, err) return errors.Wrap(err, "error parsing specified platform")
} }
for i := start; i <= end; i++ { config.platform = &p
p, err := nat.NewPort(proto, strconv.FormatUint(i, 10))
if err != nil {
return err
}
if _, exists := ports[p]; !exists {
ports[p] = struct{}{}
}
}
}
for port, bindings := range portBindings {
config.hostConfig.PortBindings[port] = bindings
}
for port, s := range ports {
config.config.ExposedPorts[port] = s
} }
mounts := copts.Mounts.Value() config.hostConfig = tempContainerConfig.HostConfig
if len(mounts) > 0 && copts.VolumeDriver != "" { config.networkingConfig.EndpointsConfig = util.Merge[string, *network.EndpointSettings](tempContainerConfig.NetworkingConfig.EndpointsConfig, config.networkingConfig.EndpointsConfig)
logrus.Warn("`--volume-driver` is ignored for volumes specified via `--mount`. Use `--mount type=volume,volume-driver=...` instead.")
c := tempContainerConfig.Config
var entrypoint = config.config.Entrypoint
var args = config.config.Cmd
// if special --entrypoint, then use it
if len(c.Entrypoint) != 0 {
entrypoint = c.Entrypoint
args = c.Cmd
} }
var binds []string if len(c.Cmd) != 0 {
volumes := copts.Volumes.GetMap() args = c.Cmd
// add any bind targets to the list of container volumes
for bind := range copts.Volumes.GetMap() {
parsed, _ := loader.ParseVolume(bind)
if parsed.Source != "" {
toBind := bind
if parsed.Type == string(mounttypes.TypeBind) {
if arr := strings.SplitN(bind, ":", 2); len(arr) == 2 {
hostPart := arr[0]
if strings.HasPrefix(hostPart, "."+string(filepath.Separator)) || hostPart == "." {
if absHostPart, err := filepath.Abs(hostPart); err == nil {
hostPart = absHostPart
}
}
toBind = hostPart + ":" + arr[1]
}
}
// after creating the bind mount we want to delete it from the copts.volumes values because
// we do not want bind mounts being committed to image configs
binds = append(binds, toBind)
// We should delete from the map (`volumes`) here, as deleting from copts.volumes will not work if
// there are duplicates entries.
delete(volumes, bind)
}
} }
c.Entrypoint = entrypoint
config.hostConfig.Binds = binds c.Cmd = args
networkOpts, err := parseNetworkOpts(copts) c.Env = append(config.config.Env, c.Env...)
if err != nil { c.Image = config.config.Image
return err if c.User == "" {
c.User = config.config.User
} }
config.networkingConfig = &network.NetworkingConfig{EndpointsConfig: networkOpts} c.Volumes = util.Merge[string, struct{}](c.Volumes, config.config.Volumes)
config.config = c
return nil return nil
} }
func convertToStandardNotation(ports []string) ([]string, error) {
optsList := []string{}
for _, publish := range ports {
if strings.Contains(publish, "=") {
params := map[string]string{"protocol": "tcp"}
for _, param := range strings.Split(publish, ",") {
opt := strings.Split(param, "=")
if len(opt) < 2 {
return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
}
params[opt[0]] = opt[1]
}
optsList = append(optsList, fmt.Sprintf("%s:%s/%s", params["published"], params["target"], params["protocol"]))
} else {
optsList = append(optsList, publish)
}
}
return optsList, nil
}
// parseNetworkOpts converts --network advanced options to endpoint-specs, and combines
// them with the old --network-alias and --links. If returns an error if conflicting options
// are found.
//
// this function may return _multiple_ endpoints, which is not currently supported
// by the daemon, but may be in future; it's up to the daemon to produce an error
// in case that is not supported.
func parseNetworkOpts(copts Options) (map[string]*network.EndpointSettings, error) {
var (
endpoints = make(map[string]*network.EndpointSettings, len(copts.NetMode.Value()))
hasUserDefined, hasNonUserDefined bool
)
for i, n := range copts.NetMode.Value() {
n := n
if container.NetworkMode(n.Target).IsUserDefined() {
hasUserDefined = true
} else {
hasNonUserDefined = true
}
ep, err := parseNetworkAttachmentOpt(n)
if err != nil {
return nil, err
}
if _, ok := endpoints[n.Target]; ok {
return nil, errdefs.InvalidParameter(errors.Errorf("network %q is specified multiple times", n.Target))
}
// For backward compatibility: if no custom options are provided for the network,
// and only a single network is specified, omit the endpoint-configuration
// on the client (the daemon will still create it when creating the container)
if i == 0 && len(copts.NetMode.Value()) == 1 {
if ep == nil || reflect.DeepEqual(*ep, network.EndpointSettings{}) {
continue
}
}
endpoints[n.Target] = ep
}
if hasUserDefined && hasNonUserDefined {
return nil, errdefs.InvalidParameter(errors.New("conflicting options: cannot attach both user-defined and non-user-defined network-modes"))
}
return endpoints, nil
}
func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.EndpointSettings, error) {
if strings.TrimSpace(ep.Target) == "" {
return nil, errors.New("no name set for network")
}
if !container.NetworkMode(ep.Target).IsUserDefined() {
if len(ep.Aliases) > 0 {
return nil, errors.New("network-scoped aliases are only supported for user-defined networks")
}
if len(ep.Links) > 0 {
return nil, errors.New("links are only supported for user-defined networks")
}
}
epConfig := &network.EndpointSettings{
NetworkID: ep.Target,
}
epConfig.Aliases = append(epConfig.Aliases, ep.Aliases...)
if len(ep.DriverOpts) > 0 {
epConfig.DriverOpts = make(map[string]string)
epConfig.DriverOpts = ep.DriverOpts
}
if len(ep.Links) > 0 {
epConfig.Links = ep.Links
}
if ep.IPv4Address != "" || ep.IPv6Address != "" || len(ep.LinkLocalIPs) > 0 {
epConfig.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: ep.IPv4Address,
IPv6Address: ep.IPv6Address,
LinkLocalIPs: ep.LinkLocalIPs,
}
}
return epConfig, nil
}

View File

@@ -2,11 +2,13 @@ package dev
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -20,10 +22,13 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
typescommand "github.com/docker/docker/api/types/container" typescommand "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
apiclient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
"github.com/moby/term"
dockerterm "github.com/moby/term" dockerterm "github.com/moby/term"
v12 "github.com/opencontainers/image-spec/specs-go/v1" v12 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
@@ -90,6 +95,11 @@ func run(ctx context.Context, runConfig *RunConfig, cli *client.Client, c *comma
if err != nil { if err != nil {
return return
} }
if inspect.State != nil && (inspect.State.Status == "exited" || inspect.State.Status == "dead" || inspect.State.Dead) {
once.Do(func() { close(chanStop) })
err = errors.New(fmt.Sprintf("container status: %s", inspect.State.Status))
return
}
if inspect.State != nil && inspect.State.Running { if inspect.State != nil && inspect.State.Running {
once.Do(func() { close(chanStop) }) once.Do(func() { close(chanStop) })
return return
@@ -125,6 +135,175 @@ func run(ctx context.Context, runConfig *RunConfig, cli *client.Client, c *comma
return return
} }
func runFirst(ctx context.Context, runConfig *RunConfig, cli *apiclient.Client, dockerCli *command.DockerCli) (id string, err error) {
rand.New(rand.NewSource(time.Now().UnixNano()))
defer func() {
if err != nil {
_ = cli.ContainerRemove(ctx, id, types.ContainerRemoveOptions{Force: true})
}
}()
stdout, stderr := dockerCli.Out(), dockerCli.Err()
client := dockerCli.Client()
runConfig.config.ArgsEscaped = false
if err := dockerCli.In().CheckTty(runConfig.config.AttachStdin, runConfig.config.Tty); err != nil {
return id, err
}
if !runConfig.Options.Detach {
if err := dockerCli.In().CheckTty(runConfig.config.AttachStdin, runConfig.config.Tty); err != nil {
return id, err
}
} else {
if runConfig.Copts.attach.Len() != 0 {
return id, errors.New("Conflicting options: -a and -d")
}
runConfig.config.AttachStdin = false
runConfig.config.AttachStdout = false
runConfig.config.AttachStderr = false
runConfig.config.StdinOnce = false
}
ctx, cancelFun := context.WithCancel(context.Background())
defer cancelFun()
createResponse, err := createContainer(ctx, dockerCli, &containerConfig{
Config: runConfig.config,
HostConfig: runConfig.hostConfig,
NetworkingConfig: runConfig.networkingConfig,
}, &runConfig.Options.createOptions)
if err != nil {
return "", err
}
log.Infof("Created container: %s", runConfig.containerName)
var (
waitDisplayID chan struct{}
errCh chan error
)
if !runConfig.config.AttachStdout && !runConfig.config.AttachStderr {
// Make this asynchronous to allow the client to write to stdin before having to read the ID
waitDisplayID = make(chan struct{})
go func() {
defer close(waitDisplayID)
fmt.Fprintln(stdout, createResponse.ID)
}()
}
attach := runConfig.config.AttachStdin || runConfig.config.AttachStdout || runConfig.config.AttachStderr
if attach {
close, err := attachContainer(ctx, dockerCli, &errCh, runConfig.config, createResponse.ID)
if err != nil {
return id, err
}
defer close()
}
statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, runConfig.Copts.autoRemove)
// start the container
if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil {
// If we have hijackedIOStreamer, we should notify
// hijackedIOStreamer we are going to exit and wait
// to avoid the terminal are not restored.
if attach {
cancelFun()
<-errCh
}
reportError(stderr, "run", err.Error(), false)
if runConfig.Copts.autoRemove {
// wait container to be removed
<-statusChan
}
return id, runStartContainerErr(err)
}
if (runConfig.config.AttachStdin || runConfig.config.AttachStdout || runConfig.config.AttachStderr) && runConfig.config.Tty && dockerCli.Out().IsTerminal() {
if err := container.MonitorTtySize(ctx, dockerCli, createResponse.ID, false); err != nil {
fmt.Fprintln(stderr, "Error monitoring TTY size:", err)
}
}
if errCh != nil {
if err := <-errCh; err != nil {
if _, ok := err.(term.EscapeError); ok {
// The user entered the detach escape sequence.
return id, nil
}
logrus.Debugf("Error hijack: %s", err)
return id, err
}
}
// Detached mode: wait for the id to be displayed and return.
if !runConfig.config.AttachStdout && !runConfig.config.AttachStderr {
// Detached mode
<-waitDisplayID
return id, nil
}
status := <-statusChan
if status != 0 {
return id, errors.New(strconv.Itoa(status))
}
log.Infof("Wait container %s to be running...", runConfig.containerName)
chanStop := make(chan struct{})
var inspect types.ContainerJSON
var once = &sync.Once{}
wait.Until(func() {
inspect, err = cli.ContainerInspect(ctx, createResponse.ID)
if err != nil && errdefs.IsNotFound(err) {
once.Do(func() { close(chanStop) })
return
}
if err != nil {
return
}
if inspect.State != nil && (inspect.State.Status == "exited" || inspect.State.Status == "dead" || inspect.State.Dead) {
once.Do(func() { close(chanStop) })
err = errors.New(fmt.Sprintf("container status: %s", inspect.State.Status))
return
}
if inspect.State != nil && inspect.State.Running {
once.Do(func() { close(chanStop) })
return
}
}, time.Second, chanStop)
if err != nil {
err = fmt.Errorf("failed to wait container to be ready: %v", err)
return
}
// print port mapping to host
var empty = true
var str string
if inspect.NetworkSettings != nil && inspect.NetworkSettings.Ports != nil {
var list []string
for port, bindings := range inspect.NetworkSettings.Ports {
var p []string
for _, binding := range bindings {
if binding.HostPort != "" {
p = append(p, binding.HostPort)
empty = false
}
}
list = append(list, fmt.Sprintf("%s:%s", port, strings.Join(p, ",")))
}
str = fmt.Sprintf("Container %s is running on port %s now", runConfig.containerName, strings.Join(list, " "))
}
if !empty {
log.Infoln(str)
} else {
log.Infof("Container %s is running now", runConfig.containerName)
}
return
}
func PullImage(ctx context.Context, platform *v12.Platform, cli *client.Client, c *command.DockerCli, img string) error { func PullImage(ctx context.Context, platform *v12.Platform, cli *client.Client, c *command.DockerCli, img string) error {
var readCloser io.ReadCloser var readCloser io.ReadCloser
var plat string var plat string

162
pkg/dev/utils.go Normal file
View File

@@ -0,0 +1,162 @@
package dev
import (
"context"
"strconv"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/versions"
"github.com/sirupsen/logrus"
)
func waitExitOrRemoved(ctx context.Context, dockerCli command.Cli, containerID string, waitRemove bool) <-chan int {
if len(containerID) == 0 {
// containerID can never be empty
panic("Internal Error: waitExitOrRemoved needs a containerID as parameter")
}
// Older versions used the Events API, and even older versions did not
// support server-side removal. This legacyWaitExitOrRemoved method
// preserves that old behavior and any issues it may have.
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") {
return legacyWaitExitOrRemoved(ctx, dockerCli, containerID, waitRemove)
}
condition := container.WaitConditionNextExit
if waitRemove {
condition = container.WaitConditionRemoved
}
resultC, errC := dockerCli.Client().ContainerWait(ctx, containerID, condition)
statusC := make(chan int)
go func() {
select {
case result := <-resultC:
if result.Error != nil {
logrus.Errorf("Error waiting for container: %v", result.Error.Message)
statusC <- 125
} else {
statusC <- int(result.StatusCode)
}
case err := <-errC:
logrus.Errorf("error waiting for container: %v", err)
statusC <- 125
}
}()
return statusC
}
func legacyWaitExitOrRemoved(ctx context.Context, dockerCli command.Cli, containerID string, waitRemove bool) <-chan int {
var removeErr error
statusChan := make(chan int)
exitCode := 125
// Get events via Events API
f := filters.NewArgs()
f.Add("type", "container")
f.Add("container", containerID)
options := types.EventsOptions{
Filters: f,
}
eventCtx, cancel := context.WithCancel(ctx)
eventq, errq := dockerCli.Client().Events(eventCtx, options)
eventProcessor := func(e events.Message) bool {
stopProcessing := false
switch e.Status {
case "die":
if v, ok := e.Actor.Attributes["exitCode"]; ok {
code, cerr := strconv.Atoi(v)
if cerr != nil {
logrus.Errorf("failed to convert exitcode '%q' to int: %v", v, cerr)
} else {
exitCode = code
}
}
if !waitRemove {
stopProcessing = true
} else {
// If we are talking to an older daemon, `AutoRemove` is not supported.
// We need to fall back to the old behavior, which is client-side removal
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.25") {
go func() {
removeErr = dockerCli.Client().ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{RemoveVolumes: true})
if removeErr != nil {
logrus.Errorf("error removing container: %v", removeErr)
cancel() // cancel the event Q
}
}()
}
}
case "detach":
exitCode = 0
stopProcessing = true
case "destroy":
stopProcessing = true
}
return stopProcessing
}
go func() {
defer func() {
statusChan <- exitCode // must always send an exit code or the caller will block
cancel()
}()
for {
select {
case <-eventCtx.Done():
if removeErr != nil {
return
}
case evt := <-eventq:
if eventProcessor(evt) {
return
}
case err := <-errq:
logrus.Errorf("error getting events from daemon: %v", err)
return
}
}
}()
return statusChan
}
func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error {
if len(containers) == 0 {
return nil
}
const defaultParallel int = 50
sem := make(chan struct{}, defaultParallel)
errChan := make(chan error)
// make sure result is printed in correct order
output := map[string]chan error{}
for _, c := range containers {
output[c] = make(chan error, 1)
}
go func() {
for _, c := range containers {
err := <-output[c]
errChan <- err
}
}()
go func() {
for _, c := range containers {
sem <- struct{}{} // Wait for active queue sem to drain.
go func(container string) {
output[container] <- op(ctx, container)
<-sem
}(c)
}
}()
return errChan
}

16
pkg/test/2pod.yaml Normal file
View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Pod
metadata:
name: test
labels:
app: test
spec:
terminationGracePeriodSeconds: 0
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
- name: tomcat
image: tomcat
imagePullPolicy: IfNotPresent
restartPolicy: Always

View File

@@ -749,3 +749,15 @@ func MoveToTemp() {
log.Debugln(err) log.Debugln(err)
} }
} }
func Merge[K comparable, V any](fromMap, ToMap map[K]V) map[K]V {
for keyToMap, valueToMap := range ToMap {
fromMap[keyToMap] = valueToMap
}
if fromMap == nil {
// merge(nil, map[string]interface{...}) -> map[string]interface{...}
return ToMap
}
return fromMap
}