diff --git a/pkg/addons/addons.go b/pkg/addons/addons.go index cc34641b6aaf..e4d462390554 100644 --- a/pkg/addons/addons.go +++ b/pkg/addons/addons.go @@ -450,6 +450,19 @@ func enableOrDisableAddonInternal(cc *config.ClusterConfig, addon *assets.Addon, } } + if addon.HelmChart != nil { + err := helmInstallBinary(addon, runner) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + cmd := helmUninstallOrInstall(ctx, addon.HelmChart, enable) + _, err = runner.RunCmd(cmd) + return err + } + // on the first attempt try without force, but on subsequent attempts use force force := false @@ -457,7 +470,9 @@ func enableOrDisableAddonInternal(cc *config.ClusterConfig, addon *assets.Addon, apply := func() error { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() - _, err := runner.RunCmd(kubectlCommand(ctx, cc, deployFiles, enable, force)) + cmd := kubectlCommand(ctx, cc, deployFiles, enable, force) + _, err := runner.RunCmd(cmd) + if err != nil { klog.Warningf("apply failed, will retry: %v", err) force = true diff --git a/pkg/addons/helm.go b/pkg/addons/helm.go new file mode 100644 index 000000000000..4da37440e68c --- /dev/null +++ b/pkg/addons/helm.go @@ -0,0 +1,93 @@ +/* +Copyright 2025 The Kubernetes Authors All rights reserved. + +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 + + http://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. +*/ + +package addons + +import ( + "context" + "fmt" + "os/exec" + "path" + + "github.com/pkg/errors" + "k8s.io/minikube/pkg/minikube/assets" + "k8s.io/minikube/pkg/minikube/command" + "k8s.io/minikube/pkg/minikube/vmpath" +) + +// runs a helm install within the minikube vm or container based on the contents of chart *assets.HelmChart +func installHelmChart(ctx context.Context, chart *assets.HelmChart) *exec.Cmd { + args := []string{ + fmt.Sprintf("KUBECONFIG=%s", path.Join(vmpath.GuestPersistentDir, "kubeconfig")), + "helm", "upgrade", "--install", chart.Name, chart.Repo, "--create-namespace", + } + if chart.Namespace != "" { + args = append(args, "--namespace", chart.Namespace) + } + + if chart.Values != nil { + for _, value := range chart.Values { + args = append(args, "--set", value) + } + } + + if chart.ValueFiles != nil { + for _, value := range chart.ValueFiles { + args = append(args, "--values", value) + } + } + + return exec.CommandContext(ctx, "sudo", args...) +} + +// runs a helm uninstall based on the contents of chart *assets.HelmChart +func uninstalllHelmChart(ctx context.Context, chart *assets.HelmChart) *exec.Cmd { + args := []string{ + fmt.Sprintf("KUBECONFIG=%s", path.Join(vmpath.GuestPersistentDir, "kubeconfig")), + "helm", "uninstall", chart.Name, + } + if chart.Namespace != "" { + args = append(args, "--namespace", chart.Namespace) + } + return exec.CommandContext(ctx, "sudo", args...) +} + +// based on enable will execute installHelmChart or uninstallHelmChart +func helmUninstallOrInstall(ctx context.Context, chart *assets.HelmChart, enable bool) *exec.Cmd { + if enable { + return installHelmChart(ctx, chart) + } + return uninstalllHelmChart(ctx, chart) +} + +func helmInstallBinary(addon *assets.Addon, runner command.Runner) error { + _, err := runner.RunCmd(exec.Command("test", "-f", "/usr/bin/helm")) + if err != nil { + _, err = runner.RunCmd(exec.Command("test", "-d", "/usr/local/bin")) + if err != nil { + _, err = runner.RunCmd(exec.Command("sudo", "mkdir", "-p", "/usr/local/bin")) + } + + installCmd := fmt.Sprint("curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 && chmod 700 get_helm.sh && ./get_helm.sh") + _, err = runner.RunCmd(exec.Command("sudo", "bash", "-c", installCmd)) + // we copy the binary from /usr/local/bin to /usr/bin because /usr/local/bin is not in PATH in both iso and kicbase + _, err = runner.RunCmd(exec.Command("sudo", "mv", "/usr/local/bin/helm", "/usr/bin/helm")) + if err != nil { + return errors.Wrap(err, "installing helm") + } + } + return err +} diff --git a/pkg/addons/helm_test.go b/pkg/addons/helm_test.go new file mode 100644 index 000000000000..9c0cb05054c5 --- /dev/null +++ b/pkg/addons/helm_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2025 The Kubernetes Authors All rights reserved. + +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 + + http://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. +*/ + +package addons + +import ( + "context" + "strings" + "testing" + + "k8s.io/minikube/pkg/minikube/assets" +) + +func TestHelmCommand(t *testing.T) { + tests := []struct { + description string + chart *assets.HelmChart + enable bool + expected string + mode string + }{ + { + description: "enable an addon", + chart: &assets.HelmChart{ + Name: "addon-name", + Repo: "addon-repo/addon-chart", + Namespace: "addon-namespace", + Values: []string{"key=value"}, + ValueFiles: []string{"/etc/kubernetes/addons/values.yaml"}, + }, + enable: true, + expected: "sudo KUBECONFIG=/var/lib/minikube/kubeconfig helm upgrade --install addon-name addon-repo/addon-chart --create-namespace --namespace addon-namespace --set key=value --values /etc/kubernetes/addons/values.yaml", + }, + { + description: "enable an addon without namespace", + chart: &assets.HelmChart{ + Name: "addon-name", + Repo: "addon-repo/addon-chart", + Values: []string{"key=value"}, + ValueFiles: []string{"/etc/kubernetes/addons/values.yaml"}, + }, + enable: true, + expected: "sudo KUBECONFIG=/var/lib/minikube/kubeconfig helm upgrade --install addon-name addon-repo/addon-chart --create-namespace --set key=value --values /etc/kubernetes/addons/values.yaml", + }, + { + description: "disable an addon", + chart: &assets.HelmChart{ + Name: "addon-name", + Namespace: "addon-namespace", + }, + enable: false, + expected: "sudo KUBECONFIG=/var/lib/minikube/kubeconfig helm uninstall addon-name --namespace addon-namespace", + mode: "cpu", + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + command := helmUninstallOrInstall(context.Background(), test.chart, test.enable) + actual := strings.Join(command.Args, " ") + if actual != test.expected { + t.Errorf("helm command mismatch:\nexpected: %s\nactual: %s", test.expected, actual) + } + }) + } +} diff --git a/pkg/minikube/assets/addons.go b/pkg/minikube/assets/addons.go index e6e430396625..0f6fcdf3aec4 100644 --- a/pkg/minikube/assets/addons.go +++ b/pkg/minikube/assets/addons.go @@ -34,6 +34,15 @@ import ( "k8s.io/minikube/pkg/version" ) +// HelmChart holds information about a helm chart. +type HelmChart struct { + Name string + Repo string + Namespace string + Values []string + ValueFiles []string +} + // Addon is a named list of assets, that can be enabled type Addon struct { Assets []*BinAsset @@ -46,6 +55,8 @@ type Addon struct { // Registries currently only shows the default registry of images Registries map[string]string + + HelmChart *HelmChart } // NetworkInfo contains control plane node IP address used for add on template @@ -55,7 +66,7 @@ type NetworkInfo struct { } // NewAddon creates a new Addon -func NewAddon(assets []*BinAsset, enabled bool, addonName, maintainer, verifiedMaintainer, docs string, images, registries map[string]string) *Addon { +func NewAddon(assets []*BinAsset, enabled bool, addonName, maintainer, verifiedMaintainer, docs string, images, registries map[string]string, helmChart *HelmChart) *Addon { return &Addon{ Assets: assets, enabled: enabled, @@ -65,6 +76,7 @@ func NewAddon(assets []*BinAsset, enabled bool, addonName, maintainer, verifiedM Docs: docs, Images: images, Registries: registries, + HelmChart: helmChart, } } @@ -139,7 +151,7 @@ var Addons = map[string]*Addon{ "AutoPauseHook": "k8s-minikube/auto-pause-hook:v0.0.5@sha256:d613ed2c891882b602b5aca668e92d4606a1b3832d96750ab25804de15929522", }, map[string]string{ "AutoPauseHook": "gcr.io", - }), + }, nil), "dashboard": NewAddon([]*BinAsset{ // We want to create the kubernetes-dashboard ns first so that every subsequent object can be created MustBinAsset(addons.DashboardAssets, "dashboard/dashboard-ns.yaml", vmpath.GuestAddonsDir, "dashboard-ns.yaml", "0640"), @@ -158,21 +170,21 @@ var Addons = map[string]*Addon{ }, map[string]string{ "Dashboard": "docker.io", "MetricsScraper": "docker.io", - }), + }, nil), "default-storageclass": NewAddon([]*BinAsset{ MustBinAsset(addons.DefaultStorageClassAssets, "storageclass/storageclass.yaml", vmpath.GuestAddonsDir, "storageclass.yaml", "0640"), - }, true, "default-storageclass", "Kubernetes", "", "https://minikube.sigs.k8s.io/docs/handbook/persistent_volumes/", nil, nil), + }, true, "default-storageclass", "Kubernetes", "", "https://minikube.sigs.k8s.io/docs/handbook/persistent_volumes/", nil, nil, nil), "pod-security-policy": NewAddon([]*BinAsset{ MustBinAsset(addons.PodSecurityPolicyAssets, "pod-security-policy/pod-security-policy.yaml", vmpath.GuestAddonsDir, "pod-security-policy.yaml", "0640"), - }, false, "pod-security-policy", "3rd party (unknown)", "", "", nil, nil), + }, false, "pod-security-policy", "3rd party (unknown)", "", "", nil, nil, nil), "storage-provisioner": NewAddon([]*BinAsset{ MustBinAsset(addons.StorageProvisionerAssets, "storage-provisioner/storage-provisioner.yaml.tmpl", @@ -183,7 +195,7 @@ var Addons = map[string]*Addon{ "StorageProvisioner": fmt.Sprintf("k8s-minikube/storage-provisioner:%s", version.GetStorageProvisionerVersion()), }, map[string]string{ "StorageProvisioner": "gcr.io", - }), + }, nil), "storage-provisioner-rancher": NewAddon([]*BinAsset{ MustBinAsset(addons.StorageProvisionerRancherAssets, "storage-provisioner-rancher/storage-provisioner-rancher.yaml.tmpl", @@ -196,7 +208,7 @@ var Addons = map[string]*Addon{ }, map[string]string{ "LocalPathProvisioner": "docker.io", "Helper": "docker.io", - }), + }, nil), "efk": NewAddon([]*BinAsset{ MustBinAsset(addons.EfkAssets, "efk/elasticsearch-rc.yaml.tmpl", @@ -238,7 +250,7 @@ var Addons = map[string]*Addon{ "FluentdElasticsearch": "registry.k8s.io", "Kibana": "docker.elastic.co", "Alpine": "docker.io", - }), + }, nil), "ingress": NewAddon([]*BinAsset{ MustBinAsset(addons.IngressAssets, "ingress/ingress-deploy.yaml.tmpl", @@ -256,7 +268,7 @@ var Addons = map[string]*Addon{ "IngressController": "registry.k8s.io", "KubeWebhookCertgenCreate": "registry.k8s.io", "KubeWebhookCertgenPatch": "registry.k8s.io", - }), + }, nil), "istio-provisioner": NewAddon([]*BinAsset{ MustBinAsset(addons.IstioProvisionerAssets, "istio-provisioner/istio-operator.yaml.tmpl", @@ -267,14 +279,14 @@ var Addons = map[string]*Addon{ "IstioOperator": "istio/operator:1.23.3@sha256:fcdc8f506f6b19b8265e6c8c28fa3d1c2f5b9069ad8ea6e1d8b353b233ec3079", }, map[string]string{ "IstioOperator": "docker.io", - }), + }, nil), "istio": NewAddon([]*BinAsset{ MustBinAsset(addons.IstioAssets, "istio/istio-default-profile.yaml", vmpath.GuestAddonsDir, "istio-default-profile.yaml", "0640"), - }, false, "istio", "3rd party (Istio)", "", "https://istio.io/latest/docs/setup/platform-setup/minikube/", nil, nil), + }, false, "istio", "3rd party (Istio)", "", "https://istio.io/latest/docs/setup/platform-setup/minikube/", nil, nil, nil), "inspektor-gadget": NewAddon([]*BinAsset{ MustBinAsset(addons.InspektorGadgetAssets, "inspektor-gadget/ig-deployment.yaml.tmpl", vmpath.GuestAddonsDir, "ig-deployment.yaml", "0640"), }, false, "inspektor-gadget", "3rd party (inspektor-gadget.io)", "https://github.com/orgs/inspektor-gadget/people", "https://minikube.sigs.k8s.io/docs/handbook/addons/inspektor-gadget/", @@ -282,7 +294,7 @@ var Addons = map[string]*Addon{ "InspektorGadget": "inspektor-gadget/inspektor-gadget:v0.46.0@sha256:a4b4360159036e9d1eea4e03a935d3b621bcd6487a70e770dd4642e84af5a6d1", }, map[string]string{ "InspektorGadget": "ghcr.io", - }), + }, nil), "kong": NewAddon([]*BinAsset{ MustBinAsset(addons.KongAssets, "kong/kong-ingress-controller.yaml.tmpl", @@ -295,7 +307,7 @@ var Addons = map[string]*Addon{ }, map[string]string{ "Kong": "docker.io", "KongIngress": "docker.io", - }), + }, nil), "kubevirt": NewAddon([]*BinAsset{ MustBinAsset(addons.KubevirtAssets, "kubevirt/pod.yaml.tmpl", @@ -306,7 +318,7 @@ var Addons = map[string]*Addon{ "Kubectl": "bitnami/kubectl:latest", }, map[string]string{ "Kubectl": "docker.io", - }), + }, nil), "metrics-server": NewAddon([]*BinAsset{ MustBinAsset(addons.MetricsServerAssets, "metrics-server/metrics-apiservice.yaml", @@ -332,7 +344,7 @@ var Addons = map[string]*Addon{ "MetricsServer": "metrics-server/metrics-server:v0.8.0@sha256:89258156d0e9af60403eafd44da9676fd66f600c7934d468ccc17e42b199aee2", }, map[string]string{ "MetricsServer": "registry.k8s.io", - }), + }, nil), "olm": NewAddon([]*BinAsset{ MustBinAsset(addons.OlmAssets, "olm/crds.yaml", @@ -351,7 +363,7 @@ var Addons = map[string]*Addon{ }, map[string]string{ "OLM": "quay.io", "UpstreamCommunityOperators": "quay.io", - }), + }, nil), "registry": NewAddon([]*BinAsset{ MustBinAsset(addons.RegistryAssets, "registry/registry-rc.yaml.tmpl", @@ -374,7 +386,7 @@ var Addons = map[string]*Addon{ }, map[string]string{ "KubeRegistryProxy": "gcr.io", "Registry": "docker.io", - }), + }, nil), "registry-creds": NewAddon([]*BinAsset{ MustBinAsset(addons.RegistryCredsAssets, "registry-creds/registry-creds-rc.yaml.tmpl", @@ -385,7 +397,7 @@ var Addons = map[string]*Addon{ "RegistryCreds": "upmcenterprises/registry-creds:1.10@sha256:93a633d4f2b76a1c66bf19c664dbddc56093a543de6d54320f19f585ccd7d605", }, map[string]string{ "RegistryCreds": "docker.io", - }), + }, nil), "registry-aliases": NewAddon([]*BinAsset{ MustBinAsset(addons.RegistryAliasesAssets, "registry-aliases/registry-aliases-sa.yaml", @@ -420,7 +432,7 @@ var Addons = map[string]*Addon{ "CoreDNSPatcher": "quay.io", "Pause": "gcr.io", "Alpine": "docker.io", - }), + }, nil), "freshpod": NewAddon([]*BinAsset{ MustBinAsset(addons.FreshpodAssets, "freshpod/freshpod-rc.yaml.tmpl", @@ -431,7 +443,7 @@ var Addons = map[string]*Addon{ "FreshPod": "google-samples/freshpod:v0.0.1@sha256:b9efde5b509da3fd2959519c4147b653d0c5cefe8a00314e2888e35ecbcb46f9", }, map[string]string{ "FreshPod": "gcr.io", - }), + }, nil), "nvidia-driver-installer": NewAddon([]*BinAsset{ MustBinAsset(addons.NvidiaDriverInstallerAssets, "gpu/nvidia-driver-installer.yaml.tmpl", @@ -444,7 +456,7 @@ var Addons = map[string]*Addon{ }, map[string]string{ "NvidiaDriverInstaller": "registry.k8s.io", "Pause": "registry.k8s.io", - }), + }, nil), "nvidia-gpu-device-plugin": NewAddon([]*BinAsset{ MustBinAsset(addons.NvidiaGpuDevicePluginAssets, "gpu/nvidia-gpu-device-plugin.yaml.tmpl", @@ -455,7 +467,7 @@ var Addons = map[string]*Addon{ "NvidiaDevicePlugin": "nvidia-gpu-device-plugin@sha256:4b036e8844920336fa48f36edeb7d4398f426d6a934ba022848deed2edbf09aa", }, map[string]string{ "NvidiaDevicePlugin": "registry.k8s.io", - }), + }, nil), "amd-gpu-device-plugin": NewAddon([]*BinAsset{ MustBinAsset(addons.AmdGpuDevicePluginAssets, "gpu/amd-gpu-device-plugin.yaml.tmpl", @@ -466,7 +478,7 @@ var Addons = map[string]*Addon{ "AmdDevicePlugin": "rocm/k8s-device-plugin:1.25.2.8@sha256:f3835498cf2274e0a07c32b38c166c05a876f8eb776d756cc06805e599a3ba5f", }, map[string]string{ "AmdDevicePlugin": "docker.io", - }), + }, nil), "logviewer": NewAddon([]*BinAsset{ MustBinAsset(addons.LogviewerAssets, "logviewer/logviewer-dp-and-svc.yaml.tmpl", @@ -482,7 +494,7 @@ var Addons = map[string]*Addon{ "LogViewer": "ivans3/minikube-log-viewer:latest@sha256:75854f45305cc47d17b04c6c588fa60777391761f951e3a34161ddf1f1b06405", }, map[string]string{ "LogViewer": "docker.io", - }), + }, nil), "gvisor": NewAddon([]*BinAsset{ MustBinAsset(addons.GvisorAssets, "gvisor/gvisor-pod.yaml.tmpl", @@ -498,7 +510,7 @@ var Addons = map[string]*Addon{ "GvisorAddon": "k8s-minikube/gvisor-addon:v0.0.2@sha256:511bf52bdec6be2b846f98e92addf70c544044f96e35085f85ae9bdcd4df2da2", }, map[string]string{ "GvisorAddon": "gcr.io", - }), + }, nil), "ingress-dns": NewAddon([]*BinAsset{ MustBinAsset(addons.IngressDNSAssets, "ingress-dns/ingress-dns-pod.yaml.tmpl", @@ -509,7 +521,7 @@ var Addons = map[string]*Addon{ "IngressDNS": "kicbase/minikube-ingress-dns:0.0.4@sha256:d7c3fd25a0ea8fa62d4096eda202b3fc69d994b01ed6ab431def629f16ba1a89", }, map[string]string{ "IngressDNS": "docker.io", - }), + }, nil), "metallb": NewAddon([]*BinAsset{ MustBinAsset(addons.MetallbAssets, "metallb/metallb.yaml.tmpl", @@ -527,7 +539,7 @@ var Addons = map[string]*Addon{ }, map[string]string{ "Speaker": "quay.io", "Controller": "quay.io", - }), + }, nil), "ambassador": NewAddon([]*BinAsset{ MustBinAsset(addons.AmbassadorAssets, "ambassador/ambassador-operator-crds.yaml", @@ -548,7 +560,7 @@ var Addons = map[string]*Addon{ "AmbassadorOperator": "datawire/ambassador-operator:v1.2.3@sha256:492f33e0828a371aa23331d75c11c251b21499e31287f026269e3f6ec6da34ed", }, map[string]string{ "AmbassadorOperator": "quay.io", - }), + }, nil), "gcp-auth": NewAddon([]*BinAsset{ MustBinAsset(addons.GcpAuthAssets, "gcp-auth/gcp-auth-ns.yaml", @@ -571,7 +583,7 @@ var Addons = map[string]*Addon{ }, map[string]string{ "GCPAuthWebhook": "gcr.io", "KubeWebhookCertgen": "registry.k8s.io", - }), + }, nil), "volcano": NewAddon([]*BinAsset{ MustBinAsset(addons.VolcanoAssets, "volcano/volcano-development.yaml.tmpl", @@ -586,7 +598,7 @@ var Addons = map[string]*Addon{ "vc_webhook_manager": "docker.io", "vc_controller_manager": "docker.io", "vc_scheduler": "docker.io", - }), + }, nil), "volumesnapshots": NewAddon([]*BinAsset{ // make sure the order of apply. `csi-hostpath-snapshotclass` must be the first position, because it depends on `snapshot.storage.k8s.io_volumesnapshotclasses` // if user disable volumesnapshots addon and delete `csi-hostpath-snapshotclass` after `snapshot.storage.k8s.io_volumesnapshotclasses`, kubernetes will return the error @@ -624,7 +636,7 @@ var Addons = map[string]*Addon{ "SnapshotController": "sig-storage/snapshot-controller:v6.1.0@sha256:823c75d0c45d1427f6d850070956d9ca657140a7bbf828381541d1d808475280", }, map[string]string{ "SnapshotController": "registry.k8s.io", - }), + }, nil), "csi-hostpath-driver": NewAddon([]*BinAsset{ MustBinAsset(addons.CsiHostpathDriverAssets, "csi-hostpath-driver/rbac/rbac-external-attacher.yaml", @@ -699,7 +711,7 @@ var Addons = map[string]*Addon{ "Resizer": "registry.k8s.io", "Snapshotter": "registry.k8s.io", "Provisioner": "registry.k8s.io", - }), + }, nil), "portainer": NewAddon([]*BinAsset{ MustBinAsset(addons.PortainerAssets, "portainer/portainer.yaml.tmpl", @@ -710,7 +722,7 @@ var Addons = map[string]*Addon{ "Portainer": "portainer/portainer-ce:2.15.1@sha256:5466af30b8eaf3f75edd3c74703d1c9973f0963acd6ef164913ea6f195d640c2", }, map[string]string{ "Portainer": "docker.io", - }), + }, nil), "inaccel": NewAddon([]*BinAsset{ MustBinAsset(addons.InAccelAssets, "inaccel/fpga-operator.yaml.tmpl", @@ -721,7 +733,7 @@ var Addons = map[string]*Addon{ "Helm3": "alpine/helm:3.9.0@sha256:9f4bf4d24241f983910550b1fe8688571cd684046500abe58cef14308f9cb19e", }, map[string]string{ "Helm3": "docker.io", - }), + }, nil), "headlamp": NewAddon([]*BinAsset{ MustBinAsset(addons.HeadlampAssets, "headlamp/headlamp-namespace.yaml", vmpath.GuestAddonsDir, "headlamp-namespace.yaml", "0640"), MustBinAsset(addons.HeadlampAssets, "headlamp/headlamp-service.yaml", vmpath.GuestAddonsDir, "headlamp-service.yaml", "0640"), @@ -734,17 +746,17 @@ var Addons = map[string]*Addon{ }, map[string]string{ "Headlamp": "ghcr.io", - }), + }, nil), "cloud-spanner": NewAddon([]*BinAsset{ MustBinAsset(addons.CloudSpanner, "cloud-spanner/deployment.yaml.tmpl", vmpath.GuestAddonsDir, "deployment.yaml", "0640"), }, false, "cloud-spanner", "Google", "", "https://minikube.sigs.k8s.io/docs/handbook/addons/cloud-spanner/", map[string]string{ "CloudSpanner": "cloud-spanner-emulator/emulator:1.5.43@sha256:a0e788c709a2746cba1b8d27e913b952809b92b23a6325e940e07c038d2d07d3", }, map[string]string{ "CloudSpanner": "gcr.io", - }), + }, nil), "kubeflow": NewAddon([]*BinAsset{ MustBinAsset(addons.Kubeflow, "kubeflow/kubeflow.yaml", vmpath.GuestAddonsDir, "kubeflow.yaml", "0640"), - }, false, "kubeflow", "3rd party", "", "", nil, nil, + }, false, "kubeflow", "3rd party", "", "", nil, nil, nil, ), "nvidia-device-plugin": NewAddon([]*BinAsset{ MustBinAsset(addons.NvidiaDevicePlugin, "nvidia-device-plugin/nvidia-device-plugin.yaml.tmpl", vmpath.GuestAddonsDir, "nvidia-device-plugin.yaml", "0640"), @@ -753,7 +765,7 @@ var Addons = map[string]*Addon{ "NvidiaDevicePlugin": "nvidia/k8s-device-plugin:v0.18.0@sha256:2d16df5f3f12081b4bd6b317cf697e5c7a195c53cec7e0bab756db02a06b985c", }, map[string]string{ "NvidiaDevicePlugin": "nvcr.io", - }), + }, nil), "yakd": NewAddon([]*BinAsset{ MustBinAsset(addons.YakdAssets, "yakd/yakd-ns.yaml", vmpath.GuestAddonsDir, "yakd-ns.yaml", "0640"), MustBinAsset(addons.YakdAssets, "yakd/yakd-sa.yaml", vmpath.GuestAddonsDir, "yakd-sa.yaml", "0640"), @@ -766,7 +778,7 @@ var Addons = map[string]*Addon{ }, map[string]string{ "Yakd": "docker.io", - }), + }, nil), "kubetail": NewAddon([]*BinAsset{ MustBinAsset(addons.KubetailAssets, "kubetail/kubetail-namespace.yaml", vmpath.GuestAddonsDir, "kubetail-namespace.yaml", "0640"), MustBinAsset(addons.KubetailAssets, "kubetail/kubetail-dashboard.yaml.tmpl", vmpath.GuestAddonsDir, "kubetail-dashboard.yaml", "0640"), @@ -781,7 +793,7 @@ var Addons = map[string]*Addon{ }, map[string]string{ "Kubetail": "docker.io", - }), + }, nil), } // parseMapString creates a map based on `str` which is encoded as =,=,... diff --git a/site/content/en/docs/contrib/addons.en.md b/site/content/en/docs/contrib/addons.en.md index ba1a67b1d858..551bd3b4b25e 100644 --- a/site/content/en/docs/contrib/addons.en.md +++ b/site/content/en/docs/contrib/addons.en.md @@ -93,6 +93,8 @@ The boolean value on the last line is whether the addon should be enabled by def To see other examples, see the [addons commit history](https://github.com/kubernetes/minikube/commits/master/deploy/addons) for other recent examples. +To add a helm based addon please check out [Helm Based Addons]({{% relref "/docs/contrib/helm-addons" %}}) + ## "addons open" support If your addon contains a NodePort Service, please add the `kubernetes.io/minikube-addons-endpoint: ` label, which is used by the `minikube addons open` command: diff --git a/site/content/en/docs/contrib/helm-addons.md b/site/content/en/docs/contrib/helm-addons.md new file mode 100644 index 000000000000..19fe56df7d16 --- /dev/null +++ b/site/content/en/docs/contrib/helm-addons.md @@ -0,0 +1,87 @@ +--- +title: "Helm Based Addons" +weight: 5 +description: > + How to develop minikube addons using Helm charts. +--- + +## Overview + +Minikube supports creating addons that are deployed via Helm charts. This allows for more complex applications to be managed as addons. This guide will walk you through creating a Helm-based addon. + +For a general overview of creating addons, please see the [Creating a new addon](https://minikube.sigs.k8s.io/docs/contrib/addons/) guide first. This guide focuses on the specifics of Helm-based addons. + +## Creating a Helm-based addon + +Creating a Helm-based addon is very similar to creating a standard addon, with a few key differences in how the addon is defined. + +### 1. Define the Addon in `pkg/minikube/assets/addons.go` + +The core of a Helm-based addon is the `HelmChart` struct within your `Addon` definition. You will define your addon in `pkg/minikube/assets/addons.go`. + +Here is an example of what a Helm-based addon definition looks like: + +```go +"my-helm-addon": NewAddon( + []*BinAsset{}, // Usually empty for pure Helm addons + false, + "my-helm-addon", + "Your Name", + "", + "path/to/your/addon/docs.md", + map[string]string{ + // Optional: Define images for caching if not in the chart + }, + map[string]string{ + // Optional: Define registries for images + }, + &HelmChart{ + Name: "my-helm-addon-release", + Repo: "oci://my-repo/my-chart", + Namespace: "my-addon-namespace", + Values: []string{ + "key1=value1", + "key2=value2", + }, + ValueFiles: []string{ + // Paths to values files inside the minikube VM + }, + }, +), +``` + +#### `HelmChart` struct fields: + +* `Name`: The release name for the Helm installation (`helm install `). +* `Repo`: The Helm chart repository URL (e.g., `stable/chart-name` or `oci://my-repo/my-chart`). +* `Namespace`: The Kubernetes namespace to install the chart into. The `--create-namespace` flag is always used. +* `Values`: A slice of strings for setting individual values via `--set` (e.g., `key=value`). +* `ValueFiles`: A slice of strings pointing to paths of YAML value files inside the minikube VM. These are passed to Helm with the `--values` flag. + +When the addon is enabled, minikube will automatically ensure the `helm` binary is installed within the cluster and then run `helm upgrade --install` with the parameters you have defined. When disabled, it will run `helm uninstall`. + +### 2. Add the Addon to `pkg/addons/config.go` + +To make your addon visible to `minikube addons list` and enable it to be managed, you need to add an entry for it in `pkg/addons/config.go`. + +```go +{ + name: "my-helm-addon", + set: SetBool, + callbacks: []setFn{EnableOrDisableAddon}, +}, +``` + +### 3. Testing your Helm Addon + +To test your new addon, rebuild minikube and enable it: + +```shell +make && ./out/minikube addons enable my-helm-addon +``` + +You can then verify that the Helm chart was deployed correctly using `kubectl`. + +```bash +kubectl -n my-addon-namespace get pods +``` \ No newline at end of file diff --git a/test/integration/iso_test.go b/test/integration/iso_test.go index 4ca6c69b8894..1fb1ee7c970e 100644 --- a/test/integration/iso_test.go +++ b/test/integration/iso_test.go @@ -118,7 +118,7 @@ func TestISOImage(t *testing.T) { t.Logf(" %s: %s", k, v) } }) - + t.Run("eBPFSupport", func(t *testing.T) { // Ensure that BTF type information is available (https://github.com/kubernetes/minikube/issues/21788) btfFile := "/sys/kernel/btf/vmlinux"