Skip to content

Commit

Permalink
Bundle kubectl with Windows installer and set kubectl as brew require…
Browse files Browse the repository at this point in the history
…ment for MacOS (#1904)
  • Loading branch information
joedborg authored Jan 18, 2021
1 parent f320fb8 commit ed1ba27
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 27 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/build-installer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ jobs:
file-url: https://github.com/canonical/multipass/releases/download/v1.5.0/multipass-1.5.0+win-win64.exe
file-name: multipass.exe
location: ${{ github.workspace }}/installer/windows
- name: Download kubectl
uses: carlosperate/[email protected]
with:
file-url: https://storage.googleapis.com/kubernetes-release/release/v1.20.0/bin/windows/amd64/kubectl.exe
file-name: kubectl.exe
location: ${{ github.workspace }}/installer/windows
- name: Create installer
run: makensis.exe ${{ github.workspace }}/installer/windows/microk8s.nsi
- name: Upload installer
Expand Down
26 changes: 23 additions & 3 deletions installer/cli/microk8s.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
import click

from cli.echo import Echo
from common.auxiliary import Windows, MacOS
from common import definitions
from common.auxiliary import Windows, MacOS, Linux
from common.errors import BaseError
from common.file_utils import get_kubeconfig_path, clear_kubeconfig
from vm_providers.factory import get_provider_for
from vm_providers.errors import ProviderNotFound, ProviderInstanceNotFoundError
from common import definitions

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -50,6 +51,8 @@ def cli(ctx, help):
run(ctx.args)
stop()
exit(0)
elif ctx.args[0] == "kubectl":
exit(kubectl(ctx.args[1:]))
elif ctx.args[0] == "dashboard-proxy":
dashboard_proxy()
exit(0)
Expand Down Expand Up @@ -142,6 +145,11 @@ def install(args) -> None:
if not aux.is_enough_space():
echo.warning("VM disk size requested exceeds free space on host.")

else:
aux = Linux(args)
if not aux.is_enough_space():
echo.warning("VM disk size requested exceeds free space on host.")

vm_provider_name: str = "multipass"
vm_provider_class = get_provider_for(vm_provider_name)
try:
Expand All @@ -162,7 +170,9 @@ def install(args) -> None:
raise provider_error

instance = vm_provider_class(echoer=echo)
instance.launch_instance(vars(args))
spec = vars(args)
spec.update({"kubeconfig": get_kubeconfig_path()})
instance.launch_instance(spec)
echo.info("MicroK8s is up and running. See the available commands with `microk8s --help`.")


Expand All @@ -188,9 +198,19 @@ def uninstall() -> None:

instance = vm_provider_class(echoer=echo)
instance.destroy()
clear_kubeconfig()
echo.info("Thank you for using MicroK8s!")


def kubectl(args) -> int:
if platform == "win32":
return Windows(args).kubectl()
if platform == "darwin":
return MacOS(args).kubectl()
else:
return Linux(args).kubectl()


def dashboard_proxy() -> None:
vm_provider_name = "multipass"
vm_provider_class = get_provider_for(vm_provider_name)
Expand Down
68 changes: 65 additions & 3 deletions installer/common/auxiliary.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import ctypes
import logging
import os
import subprocess

from abc import ABC
from os.path import realpath
from shutil import disk_usage

from .file_utils import get_kubeconfig_path, get_kubectl_directory

logger = logging.getLogger(__name__)


class Auxiliary(object):
class Auxiliary(ABC):
"""
Base OS auxiliary class.
"""
Expand All @@ -19,7 +23,11 @@ def __init__(self, args) -> None:
:return: None
"""
self._args = args
self.minimum_disk = self._args.disk * 1024 * 1024 * 1024

if getattr(self._args, "disk", None):
self.minimum_disk = self._args.disk * 1024 * 1024 * 1024
else:
self.minimum_disk = 0

@staticmethod
def _free_space() -> int:
Expand All @@ -38,6 +46,47 @@ def is_enough_space(self) -> bool:
"""
return self._free_space() > self.minimum_disk

def get_kubectl_directory(self) -> str:
"""
Get the correct directory to install kubectl into,
we can then call this when running `microk8s kubectl`
without interfering with any systemwide install.
:return: String
"""
return get_kubectl_directory()

def get_kubeconfig_path(self) -> str:
"""
Get the correct path to write the kubeconfig
file to. This is then read by the installed
kubectl and won't interfere with one in the user's
home.
:return: String
"""
return get_kubeconfig_path()

def kubectl(self) -> int:
"""
Run kubectl on the host, with the generated kubeconf.
:return: None
"""
kctl_dir = self.get_kubectl_directory()
try:
exit_code = subprocess.check_call(
[
os.path.join(kctl_dir, "kubectl"),
"--kubeconfig={}".format(self.get_kubeconfig_path()),
]
+ self._args,
)
except subprocess.CalledProcessError as e:
return e.returncode
else:
return exit_code


class Windows(Auxiliary):
"""
Expand Down Expand Up @@ -104,7 +153,20 @@ def enable_hyperv() -> None:
raise


class MacOS(Auxiliary):
class Linux(Auxiliary):
"""
MacOS auxiliary methods.
"""

def __init__(self, args) -> None:
"""
:param args: ArgumentParser
:return: None
"""
super(Linux, self).__init__(args)


class MacOS(Linux):
"""
MacOS auxiliary methods.
"""
Expand Down
35 changes: 35 additions & 0 deletions installer/common/file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import hashlib
import logging
import os
import shutil
import sys

if sys.version_info < (3, 6):
Expand Down Expand Up @@ -54,3 +55,37 @@ def is_dumb_terminal():
is_stdout_tty = os.isatty(1)
is_term_dumb = os.environ.get("TERM", "") == "dumb"
return not is_stdout_tty or is_term_dumb


def get_kubectl_directory() -> str:
"""
Get the correct directory to install kubectl into,
we can then call this when running `microk8s kubectl`
without interfering with any systemwide install.
:return: String
"""
if sys.platform == "win32":
if getattr(sys, "frozen", None):
d = os.path.dirname(sys.executable)
else:
d = os.path.dirname(os.path.abspath(__file__))

return os.path.join(d, "kubectl")
else:
full_path = shutil.which("kubectl")
return os.path.dirname(full_path)


def get_kubeconfig_path():
"""Return a MicroK8s specific kubeconfig path."""
if sys.platform == "win32":
return os.path.join(os.environ.get('LocalAppData'), "MicroK8s", "config")
else:
return os.path.join(os.path.expanduser('~'), ".microk8s", "config")


def clear_kubeconfig():
"""Clean kubeconfig file."""
if os.path.isdir(get_kubeconfig_path()):
shutil.rmtree(os.path.dirname(get_kubeconfig_path()))
30 changes: 15 additions & 15 deletions installer/vm_providers/_base_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import abc
import logging
import os
import pathlib
import shlex
import sys
Expand Down Expand Up @@ -47,18 +48,6 @@ def __init__(

self._cached_home_directory: Optional[pathlib.Path] = None

def __enter__(self):
try:
self.create()
except errors.ProviderBaseError:
# Destroy is idempotent
self.destroy()
raise
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.destroy()

@classmethod
def ensure_provider(cls) -> None:
"""Necessary steps to ensure the provider is correctly setup."""
Expand Down Expand Up @@ -127,13 +116,14 @@ def shell(self) -> None:

def launch_instance(self, specs: Dict) -> None:
try:
# An ProviderStartError exception here means we need to create
# An ProviderStartError exception here means we need to create.
self._start()
except errors.ProviderInstanceNotFoundError:
self._launch(specs)
self._check_connectivity()
# We need to setup MicroK8s and scan for cli commands
# We need to setup MicroK8s and scan for cli commands.
self._setup_microk8s(specs)
self._copy_kubeconfig_to_kubectl(specs)

def _check_connectivity(self) -> None:
"""Check that the VM can access the internet."""
Expand All @@ -154,6 +144,16 @@ def _check_connectivity(self) -> None:
else:
raise

def _copy_kubeconfig_to_kubectl(self, specs: Dict):
kubeconfig_path = specs.get("kubeconfig")
kubeconfig = self.run(command=["microk8s", "config"], hide_output=True)

if not os.path.isdir(os.path.dirname(kubeconfig_path)):
os.mkdir(os.path.dirname(kubeconfig_path))

with open(kubeconfig_path, "wb") as f:
f.write(kubeconfig)

def _setup_microk8s(self, specs: Dict) -> None:
self.run("snap install microk8s --classic --channel {}".format(specs['channel']).split())
if sys.platform == "win32":
Expand Down Expand Up @@ -184,7 +184,7 @@ def _get_home_directory(self) -> pathlib.Path:
return self._cached_home_directory

command = ["printenv", "HOME"]
run_output = self._run(command=command, hide_output=True)
run_output = self.run(command=command, hide_output=True)

# Shouldn't happen, but due to _run()'s return type as being Optional,
# we need to check for it anyways for mypy.
Expand Down
10 changes: 5 additions & 5 deletions installer/vm_providers/_multipass/_multipass.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,10 @@ def __init__(
self._multipass_cmd = MultipassCommand(platform=sys.platform)
self._instance_info: Optional[InstanceInfo] = None

def create(self) -> None:
def create(self, specs: Dict) -> None:
"""Create the multipass instance and setup the build environment."""
self.echoer.info("Launching a VM.")
self.launch_instance()
self.launch_instance(specs)
self._instance_info = self._get_instance_info()

def destroy(self) -> None:
Expand All @@ -151,16 +151,16 @@ def pull_file(self, name: str, destination: str, delete: bool = False) -> None:
# TODO add instance check.

# check if file exists in instance
self._run(command=["test", "-f", name])
self.run(command=["test", "-f", name])

# copy file from instance
source = "{}:{}".format(self.instance_name, name)
self._multipass_cmd.copy_files(source=source, destination=destination)
if delete:
self._run(command=["rm", name])
self.run(command=["rm", name])

def shell(self) -> None:
self._run(command=["/bin/bash"])
self.run(command=["/bin/bash"])

def _get_instance_info(self) -> InstanceInfo:
instance_info_raw = self._multipass_cmd.info(
Expand Down
21 changes: 20 additions & 1 deletion installer/windows/microk8s.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
!include "Sections.nsh"

!define PRODUCT_NAME "MicroK8s"
!define PRODUCT_VERSION "2.0.0"
!define PRODUCT_VERSION "2.1.0"
!define PRODUCT_PUBLISHER "Canonical"
!define MUI_ICON ".\microk8s.ico"
!define MUI_HEADERIMAGE
Expand Down Expand Up @@ -71,6 +71,17 @@ Section "Multipass (Required)" multipass_id
endMultipass:
SectionEnd

Section "Kubectl (Required)" kubectl_id
SectionIn RO
beginKubectl:
SetOutPath $INSTDIR
File "kubectl.exe"
CopyFiles "$INSTDIR\kubectl.exe" "$INSTDIR\kubectl\kubectl.exe"
Delete "$INSTDIR\kubectl.exe"
Goto endKubectl
endKubectl:
SectionEnd

Section -Install
SectionIn RO
SetOutPath $INSTDIR
Expand All @@ -92,9 +103,14 @@ Section "Add 'microk8s' to PATH" add_to_path_id
EnVar::AddValue "path" "$INSTDIR"
SectionEnd

Section /o "Add 'kubectl' to PATH" add_kubectl_to_path_id
EnVar::AddValue "path" "$INSTDIR\kubectl"
SectionEnd

!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${multipass_id} "REQUIRED: If already installed, will be unticked and skipped.$\n$\nSee https://multipass.run for more."
!insertmacro MUI_DESCRIPTION_TEXT ${add_to_path_id} "Add the 'microk8s' executable to PATH.$\n$\nThis will allow you to run the command 'microk8s' in cmd and PowerShell in any directory."
!insertmacro MUI_DESCRIPTION_TEXT ${add_kubectl_to_path_id} "Add the 'kubectl' executable to PATH.$\n$\nThis will set the bundled 'kubectl' as system default."
!insertmacro MUI_FUNCTION_DESCRIPTION_END

Function .onInit
Expand Down Expand Up @@ -174,10 +190,13 @@ Section "Uninstall"
ExecWait "$INSTDIR\microk8s.exe uninstall"
Delete $INSTDIR\uninstall.exe
Delete $INSTDIR\microk8s.exe
Delete $INSTDIR\kubectl\kubectl.exe
RMDir $INSTDIR\kubectl
RMDir $INSTDIR

DeleteRegKey HKLM \
"Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"

EnVar::DeleteValue "path" "$INSTDIR"
EnVar::DeleteValue "path" "$INSTDIR\kubectl"
SectionEnd

0 comments on commit ed1ba27

Please sign in to comment.