Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 80 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,76 +1,78 @@
# multikf
Multi-Kind leverages [Vagrant](https://github.com/hashicorp/vagrant) and [Kind](https://github.com/kubernetes-sigs/kind) (Kubernetes In Docker) to create multiple local kubernetes and kubeflow clusters inside the same host machine, see the following png for simple layout
![flow](./images/intro.png)
# multikf
Multi-Kind leverages [Vagrant](https://github.com/hashicorp/vagrant) and [Kind](https://github.com/kubernetes-sigs/kind) (Kubernetes In Docker) to create multiple local kubernetes and kubeflow clusters inside the same host machine, see the following png for simple layout
![flow](./images/intro.png)

#### Why we need this?
#### Why we need this?

As a machine gets more powerful, it is such a waste to have it running just one Kubernetes, especially for the applications which require only a local Kubernetes for practice. One example is our [Kubeflow workshop](https://github.com/footprintai/kubeflow-workshop).
To fully utilize hardware resources, we leverage vagrant to construct a fully isolated environment and install required packages on it (e.g. Kubernetes and Kubeflow and more ...), map ports for kubeApi and ssh, and also export its kubeconfg to host. Therefore, users on the host machine can easily talk to the guest Kube-API via kubectl.
As a machine gets more powerful, it is such a waste to have it running just one Kubernetes, especially for the applications which require only a local Kubernetes for practice. One example is our [Kubeflow workshop](https://github.com/footprintai/kubeflow-workshop). To fully utilize hardware resources, we leverage vagrant to construct a fully isolated environment and install required packages on it (e.g. Kubernetes and Kubeflow and more ...), map ports for kubeApi and ssh, and also export its kubeconfg to host. Therefore, users on the host machine can easily talk to the guest Kube-API via kubectl.

#### When we need this?
#### When we need this?

We expected the user are under:
We expected the user are under:
- Windows environemnt with docker desktop installed
- Linux environment

- Windows environemnt with docker desktop installed
- Linux environment
and this tool provides abstractions for them to operate clusters.

and this tool provides abstractions for them to operate clusters.
#### Why Vagrant is required?

#### Why Vagrant is required?
Idealy, we could just use Kind which running as a container to provide resource isolation. However, Kind was unable to isolate resources from its underlying kubelet(see [issue](https://github.com/kubernetes-sigs/kind/issues/877)) due to kubelet's implementation. Thus, Vagrant is served as a resource isolation and provide clean guest enviornment.

Idealy, we could just use Kind which running as a container to provide resource isolation. However, Kind was unable to isolate resources from its underlying kubelet(see [issue](https://github.com/kubernetes-sigs/kind/issues/877)) due to kubelet's implementation. Thus, Vagrant is served as a resource isolation and provide clean guest enviornment.

NOTE: Vagrant is not battle-tested, so use it with your cautions.

#### How to use?
NOTE: Vagrant is not battle-tested, so use it with your cautions.

#### How to use?
```
Usage:
multikf add <machine-name> [flags]

Flags:
--cpus int number of cpus allocated to the guest machine (default 1)
--export_ports string export ports to host, delimited by comma(example: 8443:443 stands for mapping host port 8443 to container port 443)
--f force to create instance regardless the machine status
-h, --help help for add
--memoryg int number of memory in gigabytes allocated to the guest machine (default 1)
--use_gpus int use gpu resources (default: 0), possible value (0 or 1)
--with_ip string with a specific ip address for kubeapi (default: 0.0.0.0) (default "0.0.0.0")
--with_kubeflow install kubeflow modules (default: true) (default true)
--with_password string with a specific password for default user (default: 12341234) (default "12341234")
--cpus int number of cpus allocated to the guest machine (default 1)
--export_ports string export ports to host, delimited by comma(example: 8443:443 stands for mapping host port 8443 to container port 443)
--f force to create instance regardless the machine status
-h, --help help for add
--memoryg int number of memory in gigabytes allocated to the guest machine (default 1)
--use_gpus int use gpu resources (default: 0), possible value (0 or 1)
--with_ip string with a specific ip address for kubeapi (default: 0.0.0.0) (default "0.0.0.0")
--with_kubeflow install kubeflow modules (default: true) (default true)
--with_password string with a specific password for default user (default: 12341234) (default "12341234")
--with_registry_mirrors string configure registry mirrors, format: source|mirror:username:password,source2|mirror2
```

##### Add a vagrant machine named test000 with 1 cpu and 1G memory.

##### Add a vagrant machine named test000 with 1 cpu and 1G memory.
```
./multikf add test000 --cpus 1 --memoryg 1 --provisioner=vagrant
```

##### Add a docker machine named test001 with 1 cpu, 1G memory, and all gpus.

##### Add a docker machine named test001 with 1 cpu, 1G memory, and all gpus.
```
./multikf add test000 --cpus=1 --memoryg=1 --use_gpus=1 --provisioner=docker
```


##### Add a docker machine named test002 with 1 cpu, 1G memory, and password for helloworld.

##### Add a docker machine named test002 with 1 cpu, 1G memory, and password for helloworld.
```
./multikf add test000 --cpus=1 --memoryg=16 --with_password=helloworld --provisioner=docker
```

##### Export a vargant machine's kubeconfig
##### Add a docker machine with registry mirrors to a private registry
```
./multikf export test000 --kubeconfig_path /tmp/test000.kubeconfig

run kubectl from host

kubectl get pods --all-namespaces --kubeconfig=/tmp/test000.kubeconfig
./multikf add test003 --cpus=1 --memoryg=1 --with_registry_mirrors="docker.io|https://reg.footprint-ai.com/kubeflow-mirror"
```

##### Add a docker machine with multiple registry mirrors and authentication
```
./multikf add test004 --cpus=1 --memoryg=1 --with_registry_mirrors="docker.io|https://reg.footprint-ai.com/kubeflow-mirror:username:password,k8s.gcr.io|https://reg.footprint-ai.com/k8s-mirror"
```

##### list machines
##### Export a vargant machine's kubeconfig
```
./multikf export test000 --kubeconfig_path /tmp/test000.kubeconfig

run kubectl from host

kubectl get pods --all-namespaces --kubeconfig=/tmp/test000.kubeconfig
```

##### list machines
```
./multikf list

Expand All @@ -81,40 +83,64 @@ run kubectl from host
+---------+------------------+---------+------+---------------+
```

##### delete a machine

##### delete a machine
```
./multikf delete test000
```

#### connect a machine
```
./multikf connect kubeflow test000
```

#### connect a machine
#### Registry Mirrors

You can configure registry mirrors to pull container images from your private registry instead of public registries like Docker Hub. This is useful for:

- Improving pull speeds by using a local mirror
- Working in air-gapped environments
- Rate limit mitigation
- Using custom private registries

##### Examples of Registry Mirror Configurations:

###### Basic mirroring (mirror docker.io to a private registry)
```
./multikf add my-cluster --with_registry_mirrors="docker.io|https://reg.footprint-ai.com/kubeflow-mirror"
```

###### Mirror with authentication
```
./multikf add my-cluster --with_registry_mirrors="docker.io|https://reg.footprint-ai.com/kubeflow-mirror:username:password"
```
./multikf connect kubeflow test000

###### Multiple registry mirrors with different projects
```
./multikf add my-cluster --with_registry_mirrors="docker.io|https://reg.footprint-ai.com/kubeflow-mirror,k8s.gcr.io|https://reg.footprint-ai.com/k8s-mirror"
```

#### Roadmap
The format is `source|mirror:username:password` where:
- `source`: The source registry (e.g., docker.io, k8s.gcr.io)
- `mirror`: Your private registry URL (including any project path)
- `username:password`: Optional authentication credentials

Fields listed here is on our roadmap.
#### Roadmap

Fields listed here is on our roadmap.

| Fields | machine(Docker) | machine(Vagrant) |
|------|------|------|
| Cpu Isolation | O | O |
| Memory Isolation | O | O |
| GPU Isolation | O | X |
| Expose KubeApi IP | O | O |

#### Gpu Passthough


#### Gpu Passthough

For passing gpu to docker container, one approach is to use `--gpus=all` when you launched docker container like.
For passing gpu to docker container, one approach is to use `--gpus=all` when you launched docker container like.

```
docker run -it --gpus=all ubuntu:21.10 /bin/bash

```
where it relies on the host's cuda driver.
However, Kind are NOT supported this approach, see [issue](https://github.com/kubernetes-sigs/kind/pull/1886)
However, we use our [home-crafted kind](https://github.com/footprintai/kind/tree/gpu) for this purpose.

where it relies on the host's cuda driver. However, Kind are NOT supported this approach, see [issue](https://github.com/kubernetes-sigs/kind/pull/1886) However, we use our [home-crafted kind](https://github.com/footprintai/kind/tree/gpu) for this purpose.
25 changes: 14 additions & 11 deletions cmd/multikf/cmd_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func NewAddCommand(logger log.Logger, ioStreams genericclioptions.IOStreams) *co
useLocalPath string // with localpath
withK8sVersion string
withK8sSHA256 string
withRegistryMirrors string // with registry mirrors
)

ensureNoGPUForVagrant := func(vag machine.MachineCURDFactory, useGPUs int) error {
Expand All @@ -55,17 +56,18 @@ func NewAddCommand(logger log.Logger, ioStreams genericclioptions.IOStreams) *co
}

m, err := vag.NewMachine(machineName, machineConfig{
logger: logger,
Cpus: cpus,
MemoryInG: memoryInG,
UseGPUs: useGPUs,
KubeAPIIP: withIP,
ExportPorts: exportPorts,
ForceOverwrite: forceOverwrite,
IsAuditEnabled: withAudit,
Workers: withWorkers,
NodeLabels: withLabels,
LocalPath: useLocalPath,
logger: logger,
Cpus: cpus,
MemoryInG: memoryInG,
UseGPUs: useGPUs,
KubeAPIIP: withIP,
ExportPorts: exportPorts,
ForceOverwrite: forceOverwrite,
IsAuditEnabled: withAudit,
Workers: withWorkers,
NodeLabels: withLabels,
LocalPath: useLocalPath,
RegistryMirrors: withRegistryMirrors,
NodeVersion: k8s.NewKindK8sVersion(
withK8sVersion,
withK8sSHA256,
Expand Down Expand Up @@ -111,6 +113,7 @@ func NewAddCommand(logger log.Logger, ioStreams genericclioptions.IOStreams) *co
cmd.Flags().StringVar(&useLocalPath, "use_localpath", "", "mount local path to kind cluster")
cmd.Flags().StringVar(&withK8sVersion, "with_k8s_version", k8s.DefaultVersion().Version(), fmt.Sprintf("support verisions:%s", strings.Join(k8s.ListVersionString(), ",")))
cmd.Flags().StringVar(&withK8sSHA256, "with_k8s_sha256", k8s.DefaultVersion().Sha256(), fmt.Sprintf("k8s version and its sha256 mapping list:%s", strings.Join(k8s.ListVersionSha256String(), ",")))
cmd.Flags().StringVar(&withRegistryMirrors, "with_registry_mirrors", "", "configure registry mirrors, format: source|mirror:username:password,source2|mirror2 (example: docker.io|https://reg.footprint-ai.com/kubeflow-mirror:username:password)")

return cmd
}
62 changes: 60 additions & 2 deletions cmd/multikf/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/footprintai/multikf/pkg/k8s"
"github.com/footprintai/multikf/pkg/machine"
"github.com/footprintai/multikf/pkg/machine/plugins"
"github.com/footprintai/multikf/pkg/mirror"
"sigs.k8s.io/kind/pkg/log"
)

Expand Down Expand Up @@ -78,12 +79,12 @@ type machineConfig struct {
NodeLabels string `json:"node_labels"`
LocalPath string `json:"local_path"`
NodeVersion k8s.KindK8sVersion `json:"node_version"`
RegistryMirrors string `json:"registry_mirrors"` // New field for registry mirrors
}

func (m machineConfig) Info() string {
bb, _ := json.Marshal(m)
return string(bb)

}

func (m machineConfig) GetNodeVersion() k8s.KindK8sVersion {
Expand Down Expand Up @@ -172,6 +173,64 @@ func (m machineConfig) GetLocalPath() string {
return m.LocalPath
}

// GetRegistry implements the mirror.Getter interface
// Format: source|mirror1,mirror2:username:password,source2|mirror3,mirror4
// Example: docker.io|https://reg.footprint-ai.com/kubeflow-mirror:username:password,k8s.gcr.io|https://reg.footprint-ai.com/k8s-mirror
func (m machineConfig) GetRegistry() []mirror.Registry {
if len(m.RegistryMirrors) == 0 {
m.logger.V(1).Infof("getregistry: no registry mirrors\n")
return nil
}

registryEntries := strings.Split(m.RegistryMirrors, ",")
var registries []mirror.Registry

for _, entry := range registryEntries {
parts := strings.Split(entry, "|")
if len(parts) != 2 {
m.logger.Errorf("getregistry: parse failed, expect: source|mirrors but got:%s\n", entry)
continue
}

source := parts[0]
mirrorParts := strings.Split(parts[1], ":")

// Check if authentication is provided
var auth *mirror.Auth
var mirrors []string

if len(mirrorParts) >= 3 {
// Format with auth: mirror:username:password
mirrors = []string{mirrorParts[0]}
auth = &mirror.Auth{
Username: mirrorParts[1],
Password: mirrorParts[2],
}
} else if len(mirrorParts) == 1 {
// Format without auth: just mirror
mirrors = []string{mirrorParts[0]}
auth = nil
} else {
m.logger.Errorf("getregistry: parse failed, invalid mirror format: %s\n", parts[1])
continue
}

registries = append(registries, mirror.Registry{
Source: source,
Mirrors: mirrors,
Auth: auth,
})
}

m.logger.V(1).Infof("getregistry: registry mirrors:%+v\n", registries)
return registries
}

func AuditFileAbsolutePath() string {
// Return the path directly if already set somewhere else in your code
return "/path/to/audit/policy.yaml"
}

type kubeflowPlugin struct {
withKubeflowDefaultPassword string
kubeflowVersion plugins.TypePluginVersion
Expand All @@ -183,7 +242,6 @@ func (k kubeflowPlugin) PluginType() plugins.TypePlugin {

func (k kubeflowPlugin) PluginVersion() plugins.TypePluginVersion {
return k.kubeflowVersion

}

func (k kubeflowPlugin) GetDefaultPassword() string {
Expand Down
1 change: 1 addition & 0 deletions pkg/machine/docker/hostmachine.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func (h *HostMachine) prepareFiles() error {
h.options.GetNodeLabels(),
h.options.GetLocalPath(),
h.options.GetNodeVersion(),
h.options.GetRegistry(),
)

vfolder := NewHostFolder(h.hostMachineDir)
Expand Down
20 changes: 19 additions & 1 deletion pkg/machine/docker/template/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,31 @@ package template
import (
"github.com/footprintai/multikf/pkg/k8s"
"github.com/footprintai/multikf/pkg/machine"
"github.com/footprintai/multikf/pkg/mirror"
pkgtemplateconfig "github.com/footprintai/multikf/pkg/template/config"
)

type DockerHostmachineTemplateConfig struct {
*pkgtemplateconfig.DefaultTemplateConfig
}

func NewDockerHostmachineTemplateConfig(name string, cpus int, memory int, sshport int, kubeApiPort int, kubeApiIP string, gpus int, exportPorts []machine.ExportPortPair, auditEnabled bool, auditFileAbsolutePath string, workerCount int, nodeLabels []machine.NodeLabel, localPath string, nodeVersion k8s.KindK8sVersion) *DockerHostmachineTemplateConfig {
func NewDockerHostmachineTemplateConfig(
name string,
cpus int,
memory int,
sshport int,
kubeApiPort int,
kubeApiIP string,
gpus int,
exportPorts []machine.ExportPortPair,
auditEnabled bool,
auditFileAbsolutePath string,
workerCount int,
nodeLabels []machine.NodeLabel,
localPath string,
nodeVersion k8s.KindK8sVersion,
registryMirrors []mirror.Registry,
) *DockerHostmachineTemplateConfig {
return &DockerHostmachineTemplateConfig{
DefaultTemplateConfig: pkgtemplateconfig.NewDefaultTemplateConfig(
name,
Expand All @@ -27,6 +44,7 @@ func NewDockerHostmachineTemplateConfig(name string, cpus int, memory int, sshpo
nodeLabels,
localPath,
nodeVersion,
registryMirrors,
),
}
}
Expand Down
Loading