Remove prefix parameter and add prefix command
Move the "sudo" prefix to a central location, instead of having it all over the place. Assume only needed on Linux.pull/7631/head
parent
f78e00e99b
commit
78a22f5056
|
@ -90,17 +90,17 @@ func init() {
|
|||
|
||||
func deleteContainersAndVolumes() {
|
||||
delLabel := fmt.Sprintf("%s=%s", oci.CreatedByLabelKey, "true")
|
||||
errs := oci.DeleteContainersByLabel(oci.Env, oci.Docker, delLabel)
|
||||
errs := oci.DeleteContainersByLabel(oci.Docker, delLabel)
|
||||
if len(errs) > 0 { // it will error if there is no container to delete
|
||||
glog.Infof("error delete containers by label %q (might be okay): %+v", delLabel, errs)
|
||||
}
|
||||
|
||||
errs = oci.DeleteAllVolumesByLabel(oci.Env, oci.Docker, delLabel)
|
||||
errs = oci.DeleteAllVolumesByLabel(oci.Docker, delLabel)
|
||||
if len(errs) > 0 { // it will not error if there is nothing to delete
|
||||
glog.Warningf("error delete volumes by label %q (might be okay): %+v", delLabel, errs)
|
||||
}
|
||||
|
||||
errs = oci.PruneAllVolumesByLabel(oci.Env, oci.Docker, delLabel)
|
||||
errs = oci.PruneAllVolumesByLabel(oci.Docker, delLabel)
|
||||
if len(errs) > 0 { // it will not error if there is nothing to delete
|
||||
glog.Warningf("error pruning volumes by label %q (might be okay): %+v", delLabel, errs)
|
||||
}
|
||||
|
@ -193,14 +193,12 @@ func DeleteProfiles(profiles []*config.Profile) []error {
|
|||
|
||||
func deletePossibleKicLeftOver(name string) {
|
||||
delLabel := fmt.Sprintf("%s=%s", oci.ProfileLabelKey, name)
|
||||
prefixes := []string{oci.Env, oci.Sudo}
|
||||
for i, bin := range []string{oci.Docker, oci.Podman} {
|
||||
prefix := prefixes[i]
|
||||
cs, err := oci.ListContainersByLabel(prefix, bin, delLabel)
|
||||
for _, bin := range []string{oci.Docker, oci.Podman} {
|
||||
cs, err := oci.ListContainersByLabel(bin, delLabel)
|
||||
if err == nil && len(cs) > 0 {
|
||||
for _, c := range cs {
|
||||
out.T(out.DeletingHost, `Deleting container "{{.name}}" ...`, out.V{"name": name})
|
||||
err := oci.DeleteContainer(prefix, bin, c)
|
||||
err := oci.DeleteContainer(bin, c)
|
||||
if err != nil { // it will error if there is no container to delete
|
||||
glog.Errorf("error deleting container %q. you might want to delete that manually :\n%v", name, err)
|
||||
}
|
||||
|
@ -208,12 +206,12 @@ func deletePossibleKicLeftOver(name string) {
|
|||
}
|
||||
}
|
||||
|
||||
errs := oci.DeleteAllVolumesByLabel(prefix, bin, delLabel)
|
||||
errs := oci.DeleteAllVolumesByLabel(bin, delLabel)
|
||||
if errs != nil { // it will not error if there is nothing to delete
|
||||
glog.Warningf("error deleting volumes (might be okay).\nTo see the list of volumes run: 'docker volume ls'\n:%v", errs)
|
||||
}
|
||||
|
||||
errs = oci.PruneAllVolumesByLabel(prefix, bin, delLabel)
|
||||
errs = oci.PruneAllVolumesByLabel(bin, delLabel)
|
||||
if len(errs) > 0 { // it will not error if there is nothing to delete
|
||||
glog.Warningf("error pruning volume (might be okay):\n%v", errs)
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@ func generateTarball(kubernetesVersion, containerRuntime, tarballFilename string
|
|||
driver := kic.NewDriver(kic.Config{
|
||||
KubernetesVersion: kubernetesVersion,
|
||||
ContainerRuntime: containerRuntime,
|
||||
OCIPrefix: oci.Env,
|
||||
OCIBinary: oci.Docker,
|
||||
MachineName: profile,
|
||||
ImageDigest: kic.BaseImage,
|
||||
|
@ -70,7 +69,7 @@ func generateTarball(kubernetesVersion, containerRuntime, tarballFilename string
|
|||
imgs = append(imgs, kic.OverlayImage)
|
||||
}
|
||||
|
||||
runner := command.NewKICRunner(profile, driver.OCIPrefix, driver.OCIBinary)
|
||||
runner := command.NewKICRunner(profile, driver.OCIBinary)
|
||||
|
||||
// will need to do this to enable the container run-time service
|
||||
sv, err := util.ParseKubernetesVersion(kubernetesVersion)
|
||||
|
|
|
@ -48,7 +48,6 @@ type Driver struct {
|
|||
URL string
|
||||
exec command.Runner
|
||||
NodeConfig Config
|
||||
OCIPrefix string // env, sudo
|
||||
OCIBinary string // docker,podman
|
||||
}
|
||||
|
||||
|
@ -59,9 +58,8 @@ func NewDriver(c Config) *Driver {
|
|||
MachineName: c.MachineName,
|
||||
StorePath: c.StorePath,
|
||||
},
|
||||
exec: command.NewKICRunner(c.MachineName, c.OCIPrefix, c.OCIBinary),
|
||||
exec: command.NewKICRunner(c.MachineName, c.OCIBinary),
|
||||
NodeConfig: c,
|
||||
OCIPrefix: c.OCIPrefix,
|
||||
OCIBinary: c.OCIBinary,
|
||||
}
|
||||
return d
|
||||
|
@ -78,7 +76,6 @@ func (d *Driver) Create() error {
|
|||
Memory: strconv.Itoa(d.NodeConfig.Memory) + "mb",
|
||||
Envs: d.NodeConfig.Envs,
|
||||
ExtraArgs: []string{"--expose", fmt.Sprintf("%d", d.NodeConfig.APIServerPort)},
|
||||
OCIPrefix: d.NodeConfig.OCIPrefix,
|
||||
OCIBinary: d.NodeConfig.OCIBinary,
|
||||
APIServerPort: d.NodeConfig.APIServerPort,
|
||||
}
|
||||
|
@ -102,15 +99,15 @@ func (d *Driver) Create() error {
|
|||
},
|
||||
)
|
||||
|
||||
exists, err := oci.ContainerExists(d.OCIPrefix, d.OCIBinary, params.Name, true)
|
||||
exists, err := oci.ContainerExists(d.OCIBinary, params.Name, true)
|
||||
if err != nil {
|
||||
glog.Warningf("failed to check if container already exists: %v", err)
|
||||
}
|
||||
if exists {
|
||||
// if container was created by minikube it is safe to delete and recreate it.
|
||||
if oci.IsCreatedByMinikube(d.OCIPrefix, d.OCIBinary, params.Name) {
|
||||
if oci.IsCreatedByMinikube(d.OCIBinary, params.Name) {
|
||||
glog.Info("Found already existing abandoned minikube container, will try to delete.")
|
||||
if err := oci.DeleteContainer(d.OCIPrefix, d.OCIBinary, params.Name); err != nil {
|
||||
if err := oci.DeleteContainer(d.OCIBinary, params.Name); err != nil {
|
||||
glog.Errorf("Failed to delete a conflicting minikube container %s. You might need to restart your %s daemon and delete it manually and try again: %v", params.Name, params.OCIBinary, err)
|
||||
}
|
||||
} else {
|
||||
|
@ -162,7 +159,7 @@ func (d *Driver) prepareSSH() error {
|
|||
return errors.Wrap(err, "generate ssh key")
|
||||
}
|
||||
|
||||
cmder := command.NewKICRunner(d.NodeConfig.MachineName, d.NodeConfig.OCIPrefix, d.NodeConfig.OCIBinary)
|
||||
cmder := command.NewKICRunner(d.NodeConfig.MachineName, d.NodeConfig.OCIBinary)
|
||||
f, err := assets.NewFileAsset(d.GetSSHKeyPath()+".pub", "/home/docker/.ssh/", "authorized_keys", "0644")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "create pubkey assetfile ")
|
||||
|
@ -237,23 +234,23 @@ func (d *Driver) GetURL() (string, error) {
|
|||
|
||||
// GetState returns the state that the host is in (running, stopped, etc)
|
||||
func (d *Driver) GetState() (state.State, error) {
|
||||
return oci.ContainerStatus(d.OCIPrefix, d.OCIBinary, d.MachineName, true)
|
||||
return oci.ContainerStatus(d.OCIBinary, d.MachineName, true)
|
||||
}
|
||||
|
||||
// Kill stops a host forcefully, including any containers that we are managing.
|
||||
func (d *Driver) Kill() error {
|
||||
// on init this doesn't get filled when called from cmd
|
||||
d.exec = command.NewKICRunner(d.MachineName, d.OCIPrefix, d.OCIBinary)
|
||||
d.exec = command.NewKICRunner(d.MachineName, d.OCIBinary)
|
||||
if err := sysinit.New(d.exec).ForceStop("kubelet"); err != nil {
|
||||
glog.Warningf("couldn't force stop kubelet. will continue with kill anyways: %v", err)
|
||||
}
|
||||
|
||||
if err := oci.ShutDown(d.OCIPrefix, d.OCIBinary, d.MachineName); err != nil {
|
||||
if err := oci.ShutDown(d.OCIBinary, d.MachineName); err != nil {
|
||||
glog.Warningf("couldn't shutdown the container, will continue with kill anyways: %v", err)
|
||||
}
|
||||
|
||||
cr := command.NewExecRunner() // using exec runner for interacting with dameon.
|
||||
if _, err := cr.RunCmd(exec.Command(d.NodeConfig.OCIPrefix, d.NodeConfig.OCIBinary, "kill", d.MachineName)); err != nil {
|
||||
if _, err := cr.RunCmd(oci.PrefixCmd(exec.Command(d.NodeConfig.OCIBinary, "kill", d.MachineName))); err != nil {
|
||||
return errors.Wrapf(err, "killing %q", d.MachineName)
|
||||
}
|
||||
return nil
|
||||
|
@ -261,11 +258,11 @@ func (d *Driver) Kill() error {
|
|||
|
||||
// Remove will delete the Kic Node Container
|
||||
func (d *Driver) Remove() error {
|
||||
if _, err := oci.ContainerID(d.OCIPrefix, d.OCIBinary, d.MachineName); err != nil {
|
||||
if _, err := oci.ContainerID(d.OCIBinary, d.MachineName); err != nil {
|
||||
glog.Infof("could not find the container %s to remove it. will try anyways", d.MachineName)
|
||||
}
|
||||
|
||||
if err := oci.DeleteContainer(d.NodeConfig.OCIPrefix, d.NodeConfig.OCIBinary, d.MachineName); err != nil {
|
||||
if err := oci.DeleteContainer(d.NodeConfig.OCIBinary, d.MachineName); err != nil {
|
||||
if strings.Contains(err.Error(), "is already in progress") {
|
||||
return errors.Wrap(err, "stuck delete")
|
||||
}
|
||||
|
@ -276,7 +273,7 @@ func (d *Driver) Remove() error {
|
|||
}
|
||||
|
||||
// check there be no container left after delete
|
||||
if id, err := oci.ContainerID(d.OCIPrefix, d.OCIBinary, d.MachineName); err == nil && id != "" {
|
||||
if id, err := oci.ContainerID(d.OCIBinary, d.MachineName); err == nil && id != "" {
|
||||
return fmt.Errorf("expected no container ID be found for %q after delete. but got %q", d.MachineName, id)
|
||||
}
|
||||
return nil
|
||||
|
@ -304,11 +301,11 @@ func (d *Driver) Restart() error {
|
|||
// Start an already created kic container
|
||||
func (d *Driver) Start() error {
|
||||
cr := command.NewExecRunner() // using exec runner for interacting with docker/podman daemon
|
||||
if _, err := cr.RunCmd(exec.Command(d.NodeConfig.OCIPrefix, d.NodeConfig.OCIBinary, "start", d.MachineName)); err != nil {
|
||||
if _, err := cr.RunCmd(oci.PrefixCmd(exec.Command(d.NodeConfig.OCIBinary, "start", d.MachineName))); err != nil {
|
||||
return errors.Wrap(err, "start")
|
||||
}
|
||||
checkRunning := func() error {
|
||||
s, err := oci.ContainerStatus(d.NodeConfig.OCIPrefix, d.NodeConfig.OCIBinary, d.MachineName)
|
||||
s, err := oci.ContainerStatus(d.NodeConfig.OCIBinary, d.MachineName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -328,7 +325,7 @@ func (d *Driver) Start() error {
|
|||
// Stop a host gracefully, including any containers that we are managing.
|
||||
func (d *Driver) Stop() error {
|
||||
// on init this doesn't get filled when called from cmd
|
||||
d.exec = command.NewKICRunner(d.MachineName, d.OCIPrefix, d.OCIBinary)
|
||||
d.exec = command.NewKICRunner(d.MachineName, d.OCIBinary)
|
||||
// docker does not send right SIG for systemd to know to stop the systemd.
|
||||
// to avoid bind address be taken on an upgrade. more info https://github.com/kubernetes/minikube/issues/7171
|
||||
if err := sysinit.New(d.exec).Stop("kubelet"); err != nil {
|
||||
|
@ -363,7 +360,7 @@ func (d *Driver) Stop() error {
|
|||
glog.Warningf("couldn't stop kube-apiserver proc: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(d.NodeConfig.OCIPrefix, d.NodeConfig.OCIBinary, "stop", d.MachineName)
|
||||
cmd := exec.Command(d.NodeConfig.OCIBinary, "stop", d.MachineName)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.Wrapf(err, "stopping %s", d.MachineName)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -63,8 +64,24 @@ func (rr RunResult) Output() string {
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
// PrefixCmd adds any needed prefix (such as sudo) to the command
|
||||
func PrefixCmd(cmd *exec.Cmd) *exec.Cmd {
|
||||
if cmd.Args[0] == Podman && runtime.GOOS == "linux" { // want sudo when not running podman-remote
|
||||
cmdWithSudo := exec.Command("sudo", cmd.Args...)
|
||||
cmdWithSudo.Env = cmd.Env
|
||||
cmdWithSudo.Dir = cmd.Dir
|
||||
cmdWithSudo.Stdin = cmd.Stdin
|
||||
cmdWithSudo.Stdout = cmd.Stdout
|
||||
cmdWithSudo.Stderr = cmd.Stderr
|
||||
cmd = cmdWithSudo
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runCmd runs a command exec.Command against docker daemon or podman
|
||||
func runCmd(cmd *exec.Cmd, warnSlow ...bool) (*RunResult, error) {
|
||||
cmd = PrefixCmd(cmd)
|
||||
|
||||
warn := false
|
||||
if len(warnSlow) > 0 {
|
||||
warn = warnSlow[0]
|
||||
|
|
|
@ -231,7 +231,7 @@ func dockerSystemInfo() (dockerSysInfo, error) {
|
|||
// podmanSysInfo returns podman system info --format '{{json .}}'
|
||||
func podmanSystemInfo() (podmanSysInfo, error) {
|
||||
var ps podmanSysInfo
|
||||
rr, err := runCmd(exec.Command(Sudo, Podman, "system", "info", "--format", "json"))
|
||||
rr, err := runCmd(exec.Command(Podman, "system", "info", "--format", "json"))
|
||||
if err != nil {
|
||||
return ps, errors.Wrap(err, "get podman system info")
|
||||
}
|
||||
|
|
|
@ -56,13 +56,13 @@ func digDNS(ociBin, containerName, dns string) (net.IP, error) {
|
|||
// dockerGatewayIP gets the default gateway ip for the docker bridge on the user's host machine
|
||||
// gets the ip from user's host docker
|
||||
func dockerGatewayIP() (net.IP, error) {
|
||||
rr, err := runCmd(exec.Command(Env, Docker, "network", "ls", "--filter", "name=bridge", "--format", "{{.ID}}"))
|
||||
rr, err := runCmd(exec.Command(Docker, "network", "ls", "--filter", "name=bridge", "--format", "{{.ID}}"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "get network bridge")
|
||||
}
|
||||
|
||||
bridgeID := strings.TrimSpace(rr.Stdout.String())
|
||||
rr, err = runCmd(exec.Command(Env, Docker, "inspect",
|
||||
rr, err = runCmd(exec.Command(Docker, "inspect",
|
||||
"--format", "{{(index .IPAM.Config 0).Gateway}}", bridgeID))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "inspect IP bridge network %q.", bridgeID)
|
||||
|
@ -84,12 +84,12 @@ func ForwardedPort(ociBin string, ociID string, contPort int) (int, error) {
|
|||
|
||||
if ociBin == Podman {
|
||||
//podman inspect -f "{{range .NetworkSettings.Ports}}{{if eq .ContainerPort "80"}}{{.HostPort}}{{end}}{{end}}"
|
||||
rr, err = runCmd(exec.Command(Sudo, ociBin, "inspect", "-f", fmt.Sprintf("{{range .NetworkSettings.Ports}}{{if eq .ContainerPort %s}}{{.HostPort}}{{end}}{{end}}", fmt.Sprint(contPort)), ociID))
|
||||
rr, err = runCmd(exec.Command(ociBin, "inspect", "-f", fmt.Sprintf("{{range .NetworkSettings.Ports}}{{if eq .ContainerPort %s}}{{.HostPort}}{{end}}{{end}}", fmt.Sprint(contPort)), ociID))
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "get port %d for %q", contPort, ociID)
|
||||
}
|
||||
} else {
|
||||
rr, err = runCmd(exec.Command(Env, ociBin, "inspect", "-f", fmt.Sprintf("'{{(index (index .NetworkSettings.Ports \"%d/tcp\") 0).HostPort}}'", contPort), ociID))
|
||||
rr, err = runCmd(exec.Command(ociBin, "inspect", "-f", fmt.Sprintf("'{{(index (index .NetworkSettings.Ports \"%d/tcp\") 0).HostPort}}'", contPort), ociID))
|
||||
if err != nil {
|
||||
return 0, errors.Wrapf(err, "get port %d for %q", contPort, ociID)
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ func ContainerIPs(ociBin string, name string) (string, string, error) {
|
|||
|
||||
// podmanConttainerIP returns ipv4, ipv6 of container or error
|
||||
func podmanConttainerIP(name string) (string, string, error) {
|
||||
rr, err := runCmd(exec.Command(Sudo, Podman, "inspect",
|
||||
rr, err := runCmd(exec.Command(Podman, "inspect",
|
||||
"-f", "{{.NetworkSettings.IPAddress}}",
|
||||
name))
|
||||
if err != nil {
|
||||
|
@ -132,7 +132,7 @@ func podmanConttainerIP(name string) (string, string, error) {
|
|||
// dockerContainerIP returns ipv4, ipv6 of container or error
|
||||
func dockerContainerIP(name string) (string, string, error) {
|
||||
// retrieve the IP address of the node using docker inspect
|
||||
lines, err := inspect(Env, Docker, name, "{{range .NetworkSettings.Networks}}{{.IPAddress}},{{.GlobalIPv6Address}}{{end}}")
|
||||
lines, err := inspect(Docker, name, "{{range .NetworkSettings.Networks}}{{.IPAddress}},{{.GlobalIPv6Address}}{{end}}")
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "inspecting NetworkSettings.Networks")
|
||||
}
|
||||
|
|
|
@ -38,10 +38,10 @@ import (
|
|||
|
||||
// DeleteContainersByLabel deletes all containers that have a specific label
|
||||
// if there no containers found with the given label, it will return nil
|
||||
func DeleteContainersByLabel(prefix string, ociBin string, label string) []error {
|
||||
func DeleteContainersByLabel(ociBin string, label string) []error {
|
||||
var deleteErrs []error
|
||||
|
||||
cs, err := ListContainersByLabel(prefix, ociBin, label)
|
||||
cs, err := ListContainersByLabel(ociBin, label)
|
||||
if err != nil {
|
||||
return []error{fmt.Errorf("listing containers by label %q", label)}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func DeleteContainersByLabel(prefix string, ociBin string, label string) []error
|
|||
}
|
||||
|
||||
for _, c := range cs {
|
||||
_, err := ContainerStatus(prefix, ociBin, c)
|
||||
_, err := ContainerStatus(ociBin, c)
|
||||
// only try to delete if docker/podman inspect returns
|
||||
// if it doesn't it means docker daemon is stuck and needs restart
|
||||
if err != nil {
|
||||
|
@ -59,11 +59,11 @@ func DeleteContainersByLabel(prefix string, ociBin string, label string) []error
|
|||
glog.Errorf("%s daemon seems to be stuck. Please try restarting your %s. :%v", ociBin, ociBin, err)
|
||||
continue
|
||||
}
|
||||
if err := ShutDown(prefix, ociBin, c); err != nil {
|
||||
if err := ShutDown(ociBin, c); err != nil {
|
||||
glog.Infof("couldn't shut down %s (might be okay): %v ", c, err)
|
||||
}
|
||||
|
||||
if _, err := runCmd(exec.Command(prefix, ociBin, "rm", "-f", "-v", c)); err != nil {
|
||||
if _, err := runCmd(exec.Command(ociBin, "rm", "-f", "-v", c)); err != nil {
|
||||
deleteErrs = append(deleteErrs, errors.Wrapf(err, "delete container %s: output %s", c, err))
|
||||
}
|
||||
|
||||
|
@ -72,18 +72,18 @@ func DeleteContainersByLabel(prefix string, ociBin string, label string) []error
|
|||
}
|
||||
|
||||
// DeleteContainer deletes a container by ID or Name
|
||||
func DeleteContainer(prefix string, ociBin string, name string) error {
|
||||
func DeleteContainer(ociBin string, name string) error {
|
||||
|
||||
_, err := ContainerStatus(prefix, ociBin, name)
|
||||
_, err := ContainerStatus(ociBin, name)
|
||||
if err != nil {
|
||||
glog.Errorf("%s daemon seems to be stuck. Please try restarting your %s. Will try to delete anyways: %v", ociBin, ociBin, err)
|
||||
}
|
||||
// try to delete anyways
|
||||
if err := ShutDown(prefix, ociBin, name); err != nil {
|
||||
if err := ShutDown(ociBin, name); err != nil {
|
||||
glog.Infof("couldn't shut down %s (might be okay): %v ", name, err)
|
||||
}
|
||||
|
||||
if _, err := runCmd(exec.Command(prefix, ociBin, "rm", "-f", "-v", name)); err != nil {
|
||||
if _, err := runCmd(exec.Command(ociBin, "rm", "-f", "-v", name)); err != nil {
|
||||
return errors.Wrapf(err, "delete %s", name)
|
||||
}
|
||||
return nil
|
||||
|
@ -175,25 +175,25 @@ func CreateContainerNode(p CreateParams) error {
|
|||
// adds node specific args
|
||||
runArgs = append(runArgs, p.ExtraArgs...)
|
||||
|
||||
if enabled := isUsernsRemapEnabled(p.OCIPrefix, p.OCIBinary); enabled {
|
||||
if enabled := isUsernsRemapEnabled(p.OCIBinary); enabled {
|
||||
// We need this argument in order to make this command work
|
||||
// in systems that have userns-remap enabled on the docker daemon
|
||||
runArgs = append(runArgs, "--userns=host")
|
||||
}
|
||||
|
||||
if err := createContainer(p.OCIPrefix, p.OCIBinary, p.Image, withRunArgs(runArgs...), withMounts(p.Mounts), withPortMappings(p.PortMappings)); err != nil {
|
||||
if err := createContainer(p.OCIBinary, p.Image, withRunArgs(runArgs...), withMounts(p.Mounts), withPortMappings(p.PortMappings)); err != nil {
|
||||
return errors.Wrap(err, "create container")
|
||||
}
|
||||
|
||||
checkRunning := func() error {
|
||||
r, err := ContainerRunning(p.OCIPrefix, p.OCIBinary, p.Name)
|
||||
r, err := ContainerRunning(p.OCIBinary, p.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("temporary error checking running for %q : %v", p.Name, err)
|
||||
}
|
||||
if !r {
|
||||
return fmt.Errorf("temporary error created container %q is not running yet", p.Name)
|
||||
}
|
||||
s, err := ContainerStatus(p.OCIPrefix, p.OCIBinary, p.Name)
|
||||
s, err := ContainerStatus(p.OCIBinary, p.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("temporary error checking status for %q : %v", p.Name, err)
|
||||
}
|
||||
|
@ -213,7 +213,7 @@ func CreateContainerNode(p CreateParams) error {
|
|||
}
|
||||
|
||||
// CreateContainer creates a container with "docker/podman run"
|
||||
func createContainer(prefix string, ociBin string, image string, opts ...createOpt) error {
|
||||
func createContainer(ociBin string, image string, opts ...createOpt) error {
|
||||
o := &createOpts{}
|
||||
for _, opt := range opts {
|
||||
o = opt(o)
|
||||
|
@ -227,7 +227,7 @@ func createContainer(prefix string, ociBin string, image string, opts ...createO
|
|||
runArgs = append(runArgs, generatePortMappings(portMapping)...)
|
||||
}
|
||||
// construct the actual docker run argv
|
||||
args := []string{ociBin, "run"}
|
||||
args := []string{"run"}
|
||||
|
||||
// to run nested container from privileged container in podman https://bugzilla.redhat.com/show_bug.cgi?id=1687713
|
||||
if ociBin == Podman {
|
||||
|
@ -238,7 +238,7 @@ func createContainer(prefix string, ociBin string, image string, opts ...createO
|
|||
args = append(args, image)
|
||||
args = append(args, o.ContainerArgs...)
|
||||
|
||||
if _, err := runCmd(exec.Command(prefix, args...)); err != nil {
|
||||
if _, err := runCmd(exec.Command(ociBin, args...)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -246,8 +246,8 @@ func createContainer(prefix string, ociBin string, image string, opts ...createO
|
|||
}
|
||||
|
||||
// ContainerID returns id of a container name
|
||||
func ContainerID(prefix, ociBin string, nameOrID string) (string, error) {
|
||||
rr, err := runCmd(exec.Command(prefix, ociBin, "inspect", "-f", "{{.Id}}", nameOrID))
|
||||
func ContainerID(ociBin string, nameOrID string) (string, error) {
|
||||
rr, err := runCmd(exec.Command(ociBin, "inspect", "-f", "{{.Id}}", nameOrID))
|
||||
if err != nil { // don't return error if not found, only return empty string
|
||||
if strings.Contains(rr.Stdout.String(), "Error: No such object:") || strings.Contains(rr.Stdout.String(), "unable to find") {
|
||||
err = nil
|
||||
|
@ -258,7 +258,7 @@ func ContainerID(prefix, ociBin string, nameOrID string) (string, error) {
|
|||
}
|
||||
|
||||
// ContainerExists checks if container name exists (either running or exited)
|
||||
func ContainerExists(prefix string, ociBin string, name string, warnSlow ...bool) (bool, error) {
|
||||
func ContainerExists(ociBin string, name string, warnSlow ...bool) (bool, error) {
|
||||
rr, err := runCmd(exec.Command(ociBin, "ps", "-a", "--format", "{{.Names}}"), warnSlow...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -276,8 +276,8 @@ func ContainerExists(prefix string, ociBin string, name string, warnSlow ...bool
|
|||
|
||||
// IsCreatedByMinikube returns true if the container was created by minikube
|
||||
// with default assumption that it is not created by minikube when we don't know for sure
|
||||
func IsCreatedByMinikube(prefix, ociBin string, nameOrID string) bool {
|
||||
rr, err := runCmd(exec.Command(prefix, ociBin, "inspect", nameOrID, "--format", "{{.Config.Labels}}"))
|
||||
func IsCreatedByMinikube(ociBin string, nameOrID string) bool {
|
||||
rr, err := runCmd(exec.Command(ociBin, "inspect", nameOrID, "--format", "{{.Config.Labels}}"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
@ -289,13 +289,14 @@ func IsCreatedByMinikube(prefix, ociBin string, nameOrID string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func ListOwnedContainers(prefix string, ociBin string) ([]string, error) {
|
||||
return ListContainersByLabel(prefix, ociBin, ProfileLabelKey)
|
||||
// ListOwnedContainers lists all the containres that kic driver created on user's machine using a label
|
||||
func ListOwnedContainers(ociBin string) ([]string, error) {
|
||||
return ListContainersByLabel(ociBin, ProfileLabelKey)
|
||||
}
|
||||
|
||||
// inspect return low-level information on containers
|
||||
func inspect(prefix string, ociBin string, containerNameOrID, format string) ([]string, error) {
|
||||
cmd := exec.Command(prefix, ociBin, "inspect",
|
||||
func inspect(ociBin string, containerNameOrID, format string) ([]string, error) {
|
||||
cmd := exec.Command(ociBin, "inspect",
|
||||
"-f", format,
|
||||
containerNameOrID) // ... against the "node" container
|
||||
var buff bytes.Buffer
|
||||
|
@ -357,8 +358,8 @@ func generateMountBindings(mounts ...Mount) []string {
|
|||
}
|
||||
|
||||
// isUsernsRemapEnabled checks if userns-remap is enabled in docker
|
||||
func isUsernsRemapEnabled(prefix string, ociBin string) bool {
|
||||
cmd := exec.Command(prefix, ociBin, "info", "--format", "'{{json .SecurityOptions}}'")
|
||||
func isUsernsRemapEnabled(ociBin string) bool {
|
||||
cmd := exec.Command(ociBin, "info", "--format", "'{{json .SecurityOptions}}'")
|
||||
var buff bytes.Buffer
|
||||
cmd.Stdout = &buff
|
||||
cmd.Stderr = &buff
|
||||
|
@ -420,8 +421,8 @@ func withPortMappings(portMappings []PortMapping) createOpt {
|
|||
}
|
||||
|
||||
// ListContainersByLabel returns all the container names with a specified label
|
||||
func ListContainersByLabel(prefix string, ociBin string, label string, warnSlow ...bool) ([]string, error) {
|
||||
rr, err := runCmd(exec.Command(prefix, ociBin, "ps", "-a", "--filter", fmt.Sprintf("label=%s", label), "--format", "{{.Names}}"), warnSlow...)
|
||||
func ListContainersByLabel(ociBin string, label string, warnSlow ...bool) ([]string, error) {
|
||||
rr, err := runCmd(exec.Command(ociBin, "ps", "-a", "--filter", fmt.Sprintf("label=%s", label), "--format", "{{.Names}}"), warnSlow...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -456,8 +457,8 @@ func PointToHostDockerDaemon() error {
|
|||
}
|
||||
|
||||
// ContainerRunning returns running state of a container
|
||||
func ContainerRunning(prefix string, ociBin string, name string, warnSlow ...bool) (bool, error) {
|
||||
rr, err := runCmd(exec.Command(prefix, ociBin, "inspect", name, "--format={{.State.Running}}"), warnSlow...)
|
||||
func ContainerRunning(ociBin string, name string, warnSlow ...bool) (bool, error) {
|
||||
rr, err := runCmd(exec.Command(ociBin, "inspect", name, "--format={{.State.Running}}"), warnSlow...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -465,8 +466,8 @@ func ContainerRunning(prefix string, ociBin string, name string, warnSlow ...boo
|
|||
}
|
||||
|
||||
// ContainerStatus returns status of a container running,exited,...
|
||||
func ContainerStatus(prefix string, ociBin string, name string, warnSlow ...bool) (state.State, error) {
|
||||
cmd := exec.Command(prefix, ociBin, "inspect", name, "--format={{.State.Status}}")
|
||||
func ContainerStatus(ociBin string, name string, warnSlow ...bool) (state.State, error) {
|
||||
cmd := exec.Command(ociBin, "inspect", name, "--format={{.State.Status}}")
|
||||
rr, err := runCmd(cmd, warnSlow...)
|
||||
o := strings.TrimSpace(rr.Stdout.String())
|
||||
switch o {
|
||||
|
@ -490,15 +491,15 @@ func ContainerStatus(prefix string, ociBin string, name string, warnSlow ...bool
|
|||
// ShutDown will run command to shut down the container
|
||||
// to ensure the containers process and networking bindings are all closed
|
||||
// to avoid containers getting stuck before delete https://github.com/kubernetes/minikube/issues/7657
|
||||
func ShutDown(prefix string, ociBin string, name string) error {
|
||||
if _, err := runCmd(exec.Command(prefix, ociBin, "exec", "--privileged", "-t", name, "/bin/bash", "-c", "sudo init 0")); err != nil {
|
||||
func ShutDown(ociBin string, name string) error {
|
||||
if _, err := runCmd(exec.Command(ociBin, "exec", "--privileged", "-t", name, "/bin/bash", "-c", "sudo init 0")); err != nil {
|
||||
glog.Infof("error shutdown %s: %v", name, err)
|
||||
}
|
||||
// helps with allowing docker realize the container is exited and report its status correctly.
|
||||
time.Sleep(time.Second * 1)
|
||||
// wait till it is stoped
|
||||
stopped := func() error {
|
||||
st, err := ContainerStatus(prefix, ociBin, name)
|
||||
st, err := ContainerStatus(ociBin, name)
|
||||
if st == state.Stopped {
|
||||
glog.Infof("container %s status is %s", name, st)
|
||||
return nil
|
||||
|
|
|
@ -19,10 +19,6 @@ package oci
|
|||
const (
|
||||
// DefaultBindIPV4 is The default IP the container will listen on.
|
||||
DefaultBindIPV4 = "127.0.0.1"
|
||||
// Env is env
|
||||
Env = "env"
|
||||
// Sudo is sudo
|
||||
Sudo = "sudo"
|
||||
// Docker is docker
|
||||
Docker = "docker"
|
||||
// Podman is podman
|
||||
|
@ -51,7 +47,6 @@ type CreateParams struct {
|
|||
Memory string // memory (mbs) to assign to the container
|
||||
Envs map[string]string // environment variables to pass to the container
|
||||
ExtraArgs []string // a list of any extra option to pass to oci binary during creation time, for example --expose 8080...
|
||||
OCIPrefix string // env or sudo
|
||||
OCIBinary string // docker or podman
|
||||
}
|
||||
|
||||
|
|
|
@ -29,18 +29,18 @@ import (
|
|||
|
||||
// DeleteAllVolumesByLabel deletes all volumes that have a specific label
|
||||
// if there is no volume to delete it will return nil
|
||||
func DeleteAllVolumesByLabel(prefix string, ociBin string, label string, warnSlow ...bool) []error {
|
||||
func DeleteAllVolumesByLabel(ociBin string, label string, warnSlow ...bool) []error {
|
||||
var deleteErrs []error
|
||||
glog.Infof("trying to delete all %s volumes with label %s", ociBin, label)
|
||||
|
||||
vs, err := allVolumesByLabel(prefix, ociBin, label)
|
||||
vs, err := allVolumesByLabel(ociBin, label)
|
||||
|
||||
if err != nil {
|
||||
return []error{fmt.Errorf("listing volumes by label %q: %v", label, err)}
|
||||
}
|
||||
|
||||
for _, v := range vs {
|
||||
if _, err := runCmd(exec.Command(prefix, ociBin, "volume", "rm", "--force", v), warnSlow...); err != nil {
|
||||
if _, err := runCmd(exec.Command(ociBin, "volume", "rm", "--force", v), warnSlow...); err != nil {
|
||||
deleteErrs = append(deleteErrs, fmt.Errorf("deleting %q", v))
|
||||
}
|
||||
}
|
||||
|
@ -51,10 +51,10 @@ func DeleteAllVolumesByLabel(prefix string, ociBin string, label string, warnSlo
|
|||
// PruneAllVolumesByLabel deletes all volumes that have a specific label
|
||||
// if there is no volume to delete it will return nil
|
||||
// example: docker volume prune -f --filter label=name.minikube.sigs.k8s.io=minikube
|
||||
func PruneAllVolumesByLabel(prefix string, ociBin string, label string, warnSlow ...bool) []error {
|
||||
func PruneAllVolumesByLabel(ociBin string, label string, warnSlow ...bool) []error {
|
||||
var deleteErrs []error
|
||||
glog.Infof("trying to prune all %s volumes with label %s", ociBin, label)
|
||||
cmd := exec.Command(prefix, ociBin, "volume", "prune", "-f", "--filter", "label="+label)
|
||||
cmd := exec.Command(ociBin, "volume", "prune", "-f", "--filter", "label="+label)
|
||||
if _, err := runCmd(cmd, warnSlow...); err != nil {
|
||||
deleteErrs = append(deleteErrs, errors.Wrapf(err, "prune volume by label %s", label))
|
||||
}
|
||||
|
@ -64,8 +64,8 @@ func PruneAllVolumesByLabel(prefix string, ociBin string, label string, warnSlow
|
|||
|
||||
// allVolumesByLabel returns name of all docker volumes by a specific label
|
||||
// will not return error if there is no volume found.
|
||||
func allVolumesByLabel(prefix string, ociBin string, label string) ([]string, error) {
|
||||
rr, err := runCmd(exec.Command(prefix, ociBin, "volume", "ls", "--filter", "label="+label, "--format", "{{.Name}}"))
|
||||
func allVolumesByLabel(ociBin string, label string) ([]string, error) {
|
||||
rr, err := runCmd(exec.Command(ociBin, "volume", "ls", "--filter", "label="+label, "--format", "{{.Name}}"))
|
||||
s := bufio.NewScanner(bytes.NewReader(rr.Stdout.Bytes()))
|
||||
var vols []string
|
||||
for s.Scan() {
|
||||
|
|
|
@ -49,7 +49,6 @@ type Config struct {
|
|||
CPU int // Number of CPU cores assigned to the container
|
||||
Memory int // max memory in MB
|
||||
StorePath string // libmachine store path
|
||||
OCIPrefix string // prefix to use (env, sudo, ...)
|
||||
OCIBinary string // oci tool to use (docker, podman,...)
|
||||
ImageDigest string // image name with sha to use for the node
|
||||
Mounts []oci.Mount // mounts
|
||||
|
|
|
@ -38,22 +38,19 @@ import (
|
|||
// It implements the CommandRunner interface.
|
||||
type kicRunner struct {
|
||||
nameOrID string
|
||||
prefix string
|
||||
ociBin string
|
||||
}
|
||||
|
||||
// NewKICRunner returns a kicRunner implementor of runner which runs cmds inside a container
|
||||
func NewKICRunner(containerNameOrID string, prefix string, oci string) Runner {
|
||||
func NewKICRunner(containerNameOrID string, oci string) Runner {
|
||||
return &kicRunner{
|
||||
nameOrID: containerNameOrID,
|
||||
prefix: prefix, // env or sudo
|
||||
ociBin: oci, // docker or podman
|
||||
ociBin: oci, // docker or podman
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kicRunner) RunCmd(cmd *exec.Cmd) (*RunResult, error) {
|
||||
args := []string{
|
||||
k.ociBin,
|
||||
"exec",
|
||||
// run with privileges so we can remount etc..
|
||||
"--privileged",
|
||||
|
@ -84,7 +81,7 @@ func (k *kicRunner) RunCmd(cmd *exec.Cmd) (*RunResult, error) {
|
|||
args,
|
||||
cmd.Args...,
|
||||
)
|
||||
oc := exec.Command(k.prefix, args...)
|
||||
oc := exec.Command(k.ociBin, args...)
|
||||
oc.Stdin = cmd.Stdin
|
||||
oc.Stdout = cmd.Stdout
|
||||
oc.Stderr = cmd.Stderr
|
||||
|
@ -111,6 +108,9 @@ func (k *kicRunner) RunCmd(cmd *exec.Cmd) (*RunResult, error) {
|
|||
oc.Stdout = outb
|
||||
oc.Stderr = errb
|
||||
|
||||
oc = oci.PrefixCmd(oc)
|
||||
glog.Infof("Args: %v", oc.Args)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
err := oc.Run()
|
||||
|
@ -202,14 +202,14 @@ func (k *kicRunner) chmod(dst string, perm string) error {
|
|||
|
||||
// Podman cp command doesn't match docker and doesn't have -a
|
||||
func copyToPodman(src string, dest string) error {
|
||||
if out, err := exec.Command(oci.Sudo, oci.Podman, "cp", src, dest).CombinedOutput(); err != nil {
|
||||
if out, err := oci.PrefixCmd(exec.Command(oci.Podman, "cp", src, dest)).CombinedOutput(); err != nil {
|
||||
return errors.Wrapf(err, "podman copy %s into %s, output: %s", src, dest, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyToDocker(src string, dest string) error {
|
||||
if out, err := exec.Command(oci.Docker, "cp", "-a", src, dest).CombinedOutput(); err != nil {
|
||||
if out, err := oci.PrefixCmd(exec.Command(oci.Docker, "cp", "-a", src, dest)).CombinedOutput(); err != nil {
|
||||
return errors.Wrapf(err, "docker copy %s into %s, output: %s", src, dest, string(out))
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -203,7 +203,7 @@ func ListProfiles(miniHome ...string) (validPs []*Profile, inValidPs []*Profile,
|
|||
return nil, nil, err
|
||||
}
|
||||
// try to get profiles list based on all contrainers created by docker driver
|
||||
cs, err := oci.ListOwnedContainers(oci.Env, oci.Docker)
|
||||
cs, err := oci.ListOwnedContainers(oci.Docker)
|
||||
if err == nil {
|
||||
pDirs = append(pDirs, cs...)
|
||||
}
|
||||
|
|
|
@ -35,12 +35,12 @@ import (
|
|||
|
||||
// deleteOrphanedKIC attempts to delete an orphaned docker instance for machines without a config file
|
||||
// used as last effort clean up not returning errors, wont warn user.
|
||||
func deleteOrphanedKIC(prefix string, ociBin string, name string) {
|
||||
func deleteOrphanedKIC(ociBin string, name string) {
|
||||
if !(ociBin == oci.Podman || ociBin == oci.Docker) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err := oci.ContainerStatus(prefix, ociBin, name)
|
||||
_, err := oci.ContainerStatus(ociBin, name)
|
||||
if err != nil {
|
||||
glog.Infof("couldn't inspect container %q before deleting: %v", name, err)
|
||||
return
|
||||
|
@ -49,10 +49,10 @@ func deleteOrphanedKIC(prefix string, ociBin string, name string) {
|
|||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := oci.ShutDown(prefix, ociBin, name); err != nil {
|
||||
if err := oci.ShutDown(ociBin, name); err != nil {
|
||||
glog.Infof("couldn't shut down %s (might be okay): %v ", name, err)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, prefix, ociBin, "rm", "-f", "-v", name)
|
||||
cmd := exec.CommandContext(ctx, ociBin, "rm", "-f", "-v", name)
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
glog.Infof("Found stale kic container and successfully cleaned it up!")
|
||||
|
@ -63,8 +63,8 @@ func deleteOrphanedKIC(prefix string, ociBin string, name string) {
|
|||
func DeleteHost(api libmachine.API, machineName string) error {
|
||||
host, err := api.Load(machineName)
|
||||
if err != nil && host == nil {
|
||||
deleteOrphanedKIC(oci.Env, oci.Docker, machineName)
|
||||
deleteOrphanedKIC(oci.Sudo, oci.Podman, machineName)
|
||||
deleteOrphanedKIC(oci.Docker, machineName)
|
||||
deleteOrphanedKIC(oci.Podman, machineName)
|
||||
// Keep going even if minikube does not know about the host
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ func trySSHPowerOff(h *host.Host) error {
|
|||
out.T(out.Shutdown, `Powering off "{{.profile_name}}" via SSH ...`, out.V{"profile_name": h.Name})
|
||||
// differnet for kic because RunSSHCommand is not implemented by kic
|
||||
if driver.IsKIC(h.DriverName) {
|
||||
err := oci.ShutDown("sudo", h.DriverName, h.Name)
|
||||
err := oci.ShutDown(h.DriverName, h.Name)
|
||||
glog.Infof("shutdown container: err=%v", err)
|
||||
} else {
|
||||
out, err := h.RunSSHCommand("sudo poweroff")
|
||||
|
|
|
@ -48,7 +48,7 @@ func init() {
|
|||
if err := registry.Register(registry.DriverDef{
|
||||
Name: driver.Docker,
|
||||
Config: configure,
|
||||
Init: func() drivers.Driver { return kic.NewDriver(kic.Config{OCIPrefix: oci.Env, OCIBinary: oci.Docker}) },
|
||||
Init: func() drivers.Driver { return kic.NewDriver(kic.Config{OCIBinary: oci.Docker}) },
|
||||
Status: status,
|
||||
Priority: priority,
|
||||
}); err != nil {
|
||||
|
@ -63,7 +63,6 @@ func configure(cc config.ClusterConfig, n config.Node) (interface{}, error) {
|
|||
ImageDigest: viper.GetString("base-image"),
|
||||
CPU: cc.CPUs,
|
||||
Memory: cc.Memory,
|
||||
OCIPrefix: oci.Env,
|
||||
OCIBinary: oci.Docker,
|
||||
APIServerPort: cc.Nodes[0].Port,
|
||||
KubernetesVersion: cc.KubernetesConfig.KubernetesVersion,
|
||||
|
|
|
@ -51,7 +51,7 @@ func init() {
|
|||
if err := registry.Register(registry.DriverDef{
|
||||
Name: driver.Podman,
|
||||
Config: configure,
|
||||
Init: func() drivers.Driver { return kic.NewDriver(kic.Config{OCIPrefix: oci.Sudo, OCIBinary: oci.Podman}) },
|
||||
Init: func() drivers.Driver { return kic.NewDriver(kic.Config{OCIBinary: oci.Podman}) },
|
||||
Status: status,
|
||||
Priority: priority,
|
||||
}); err != nil {
|
||||
|
@ -67,7 +67,6 @@ func configure(cc config.ClusterConfig, n config.Node) (interface{}, error) {
|
|||
ImageDigest: strings.Split(baseImage, "@")[0], // for podman does not support docker images references with both a tag and digest.
|
||||
CPU: cc.CPUs,
|
||||
Memory: cc.Memory,
|
||||
OCIPrefix: oci.Sudo,
|
||||
OCIBinary: oci.Podman,
|
||||
APIServerPort: cc.Nodes[0].Port,
|
||||
KubernetesVersion: cc.KubernetesConfig.KubernetesVersion,
|
||||
|
@ -89,7 +88,7 @@ func status() registry.State {
|
|||
cmd := exec.CommandContext(ctx, oci.Podman, "version", "--format", "{{.Server.Version}}")
|
||||
// Run with sudo on linux (local), otherwise podman-remote (as podman)
|
||||
if runtime.GOOS == "linux" {
|
||||
cmd = exec.CommandContext(ctx, oci.Sudo, "-n", oci.Podman, "version", "--format", "{{.Version}}")
|
||||
cmd = exec.CommandContext(ctx, "sudo", "-n", oci.Podman, "version", "--format", "{{.Version}}")
|
||||
cmd.Env = append(os.Environ(), "LANG=C", "LC_ALL=C") // sudo is localized
|
||||
}
|
||||
o, err := cmd.Output()
|
||||
|
|
Loading…
Reference in New Issue