mirror of
https://github.com/ChristianLight/tutor.git
synced 2024-11-05 21:07:50 +00:00
Run pylint on code base
This commit is contained in:
parent
ce1bb05d8e
commit
207229e16e
11
tutor/cli.py
11
tutor/cli.py
@ -6,9 +6,9 @@ import click_repl
|
||||
|
||||
from .__about__ import __version__
|
||||
from .android import android
|
||||
from .config import config
|
||||
from .config import config_command
|
||||
from .dev import dev
|
||||
from .images import images
|
||||
from .images import images_command
|
||||
from .k8s import k8s
|
||||
from .local import local
|
||||
from .ui import ui
|
||||
@ -24,11 +24,13 @@ def main():
|
||||
sys.stderr.write(fmt.error("Error: {}\n".format(e.args[0])))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@click.group(context_settings={'help_option_names': ['-h', '--help', 'help']})
|
||||
@click.version_option(version=__version__)
|
||||
def cli():
|
||||
pass
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Print this help",
|
||||
name="help",
|
||||
@ -37,9 +39,10 @@ def print_help():
|
||||
with click.Context(cli) as context:
|
||||
click.echo(cli.get_help(context))
|
||||
|
||||
|
||||
click_repl.register_repl(cli, name="ui")
|
||||
cli.add_command(images)
|
||||
cli.add_command(config)
|
||||
cli.add_command(images_command, name="images")
|
||||
cli.add_command(config_command, name="config")
|
||||
cli.add_command(local)
|
||||
cli.add_command(dev)
|
||||
cli.add_command(android)
|
||||
|
@ -17,7 +17,7 @@ from .__about__ import __version__
|
||||
short_help="Configure Open edX",
|
||||
help="""Configure Open edX and store configuration values in $TUTOR_ROOT/config.yml"""
|
||||
)
|
||||
def config():
|
||||
def config_command():
|
||||
pass
|
||||
|
||||
|
||||
@ -33,8 +33,7 @@ def save_command(root, silent1, silent2, set_):
|
||||
|
||||
def save(root, silent=False, keyvalues=None):
|
||||
keyvalues = keyvalues or []
|
||||
config = {}
|
||||
load_current(config, root)
|
||||
config = load_current(root)
|
||||
for k, v in keyvalues:
|
||||
config[k] = v
|
||||
if not silent:
|
||||
@ -57,8 +56,7 @@ def printroot(root):
|
||||
@opts.root
|
||||
@click.argument("key")
|
||||
def printvalue(root, key):
|
||||
config = {}
|
||||
load_current(config, root)
|
||||
config = load_current(root)
|
||||
load_defaults(config)
|
||||
try:
|
||||
print(config[key])
|
||||
@ -71,8 +69,7 @@ def load(root):
|
||||
Load configuration, and generate it interactively if the file does not
|
||||
exist.
|
||||
"""
|
||||
config = {}
|
||||
load_current(config, root)
|
||||
config = load_current(root)
|
||||
|
||||
should_update_env = False
|
||||
if not os.path.exists(config_path(root)):
|
||||
@ -119,20 +116,22 @@ def pre_upgrade_announcement(root):
|
||||
)
|
||||
|
||||
|
||||
def load_current(config, root):
|
||||
def load_current(root):
|
||||
convert_json2yml(root)
|
||||
load_base(config, root)
|
||||
config = {}
|
||||
load_base(config)
|
||||
load_user(config, root)
|
||||
load_env(config, root)
|
||||
load_env(config)
|
||||
return config
|
||||
|
||||
|
||||
def load_base(config, root):
|
||||
def load_base(config):
|
||||
base = serialize.load(env.read("config-base.yml"))
|
||||
for k, v in base.items():
|
||||
config[k] = v
|
||||
|
||||
|
||||
def load_env(config, root):
|
||||
def load_env(config):
|
||||
base_config = serialize.load(env.read("config-base.yml"))
|
||||
default_config = serialize.load(env.read("config-defaults.yml"))
|
||||
keys = set(list(base_config.keys()) + list(default_config.keys()))
|
||||
@ -272,6 +271,6 @@ def config_path(root):
|
||||
return os.path.join(root, "config.yml")
|
||||
|
||||
|
||||
config.add_command(save_command, name="save")
|
||||
config.add_command(printroot)
|
||||
config.add_command(printvalue)
|
||||
config_command.add_command(save_command, name="save")
|
||||
config_command.add_command(printroot)
|
||||
config_command.add_command(printvalue)
|
||||
|
@ -13,6 +13,7 @@ from . import utils
|
||||
def dev():
|
||||
pass
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Run a command in one of the containers",
|
||||
context_settings={"ignore_unknown_options": True},
|
||||
@ -34,6 +35,7 @@ def run(root, edx_platform_path, edx_platform_settings, service, command, args):
|
||||
root, edx_platform_path, edx_platform_settings, port, *run_command
|
||||
)
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Run a development server",
|
||||
)
|
||||
@ -48,11 +50,13 @@ def runserver(root, edx_platform_path, edx_platform_settings, service):
|
||||
service, "./manage.py", service, "runserver", "0.0.0.0:{}".format(port),
|
||||
)
|
||||
|
||||
|
||||
@click.command(help="Stop a running development platform",)
|
||||
@opts.root
|
||||
def stop(root):
|
||||
docker_compose(root, "rm", "--stop", "--force")
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Watch for changes in your themes and recompile assets when needed"
|
||||
)
|
||||
@ -65,12 +69,14 @@ def watchthemes(root, edx_platform_path, edx_platform_settings):
|
||||
"--no-deps", "lms", "openedx-assets", "watch-themes", "--env", "dev"
|
||||
)
|
||||
|
||||
|
||||
def docker_compose_run_with_port(root, edx_platform_path, edx_platform_settings, port, *command):
|
||||
docker_compose_run(
|
||||
root, edx_platform_path, edx_platform_settings,
|
||||
"-p", "{port}:{port}".format(port=port), *command
|
||||
)
|
||||
|
||||
|
||||
def docker_compose_run(root, edx_platform_path, edx_platform_settings, *command):
|
||||
run_command = [
|
||||
"run", "--rm",
|
||||
@ -85,6 +91,7 @@ def docker_compose_run(root, edx_platform_path, edx_platform_settings, *command)
|
||||
run_command += command
|
||||
docker_compose(root, *run_command)
|
||||
|
||||
|
||||
def docker_compose(root, *command):
|
||||
return utils.docker_compose(
|
||||
"-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"),
|
||||
@ -92,9 +99,11 @@ def docker_compose(root, *command):
|
||||
*command
|
||||
)
|
||||
|
||||
|
||||
def service_port(service):
|
||||
return 8000 if service == "lms" else 8001
|
||||
|
||||
|
||||
dev.add_command(run)
|
||||
dev.add_command(runserver)
|
||||
dev.add_command(stop)
|
||||
|
@ -63,7 +63,6 @@ def render_dict(config):
|
||||
rendered[key] = value
|
||||
for k, v in rendered.items():
|
||||
config[k] = v
|
||||
pass
|
||||
|
||||
|
||||
def render_str(config, text):
|
||||
@ -146,9 +145,9 @@ def walk_templates(root, target):
|
||||
def is_part_of_env(path):
|
||||
basename = os.path.basename(path)
|
||||
return not (
|
||||
basename.startswith(".") or
|
||||
basename.endswith(".pyc") or
|
||||
basename == "__pycache__"
|
||||
basename.startswith(".")
|
||||
or basename.endswith(".pyc")
|
||||
or basename == "__pycache__"
|
||||
)
|
||||
|
||||
|
||||
|
@ -6,10 +6,12 @@ from . import fmt
|
||||
from . import opts
|
||||
from . import utils
|
||||
|
||||
|
||||
@click.group(short_help="Manage docker images")
|
||||
def images():
|
||||
def images_command():
|
||||
pass
|
||||
|
||||
|
||||
OPENEDX_IMAGES = ["openedx", "forum", "notes", "xqueue", "android"]
|
||||
VENDOR_IMAGES = ["elasticsearch", "memcached", "mongodb", "mysql", "nginx", "rabbitmq", "smtp"]
|
||||
argument_openedx_image = click.argument(
|
||||
@ -19,6 +21,7 @@ argument_image = click.argument(
|
||||
"image", type=click.Choice(["all"] + OPENEDX_IMAGES + VENDOR_IMAGES),
|
||||
)
|
||||
|
||||
|
||||
@click.command(
|
||||
short_help="Build docker images",
|
||||
help="Build the docker images necessary for an Open edX platform."
|
||||
@ -31,8 +34,7 @@ argument_image = click.argument(
|
||||
)
|
||||
def build(root, image, build_arg):
|
||||
config = tutor_config.load(root)
|
||||
for image in openedx_image_names(config, image):
|
||||
tag = get_tag(config, image)
|
||||
for tag in openedx_image_tags(config, image):
|
||||
click.echo(fmt.info("Building image {}".format(tag)))
|
||||
command = [
|
||||
"build", "-t", tag,
|
||||
@ -44,26 +46,28 @@ def build(root, image, build_arg):
|
||||
]
|
||||
utils.docker(*command)
|
||||
|
||||
|
||||
@click.command(short_help="Pull images from the Docker registry")
|
||||
@opts.root
|
||||
@argument_image
|
||||
def pull(root, image):
|
||||
config = tutor_config.load(root)
|
||||
for image in image_names(config, image):
|
||||
tag = get_tag(config, image)
|
||||
for img in image_names(config, image):
|
||||
tag = get_tag(config, img)
|
||||
click.echo(fmt.info("Pulling image {}".format(tag)))
|
||||
utils.execute("docker", "pull", tag)
|
||||
|
||||
|
||||
@click.command(short_help="Push images to the Docker registry")
|
||||
@opts.root
|
||||
@argument_openedx_image
|
||||
def push(root, image):
|
||||
config = tutor_config.load(root)
|
||||
for image in openedx_image_names(config, image):
|
||||
tag = get_tag(config, image)
|
||||
for tag in openedx_image_tags(config, image):
|
||||
click.echo(fmt.info("Pushing image {}".format(tag)))
|
||||
utils.execute("docker", "push", tag)
|
||||
|
||||
|
||||
def get_tag(config, name):
|
||||
image = config["DOCKER_IMAGE_" + name.upper()]
|
||||
return "{registry}{image}".format(
|
||||
@ -71,9 +75,16 @@ def get_tag(config, name):
|
||||
image=image,
|
||||
)
|
||||
|
||||
|
||||
def image_names(config, image):
|
||||
return openedx_image_names(config, image) + vendor_image_names(config, image)
|
||||
|
||||
|
||||
def openedx_image_tags(config, image):
|
||||
for img in openedx_image_names(config, image):
|
||||
yield get_tag(config, img)
|
||||
|
||||
|
||||
def openedx_image_names(config, image):
|
||||
if image == "all":
|
||||
images = OPENEDX_IMAGES[:]
|
||||
@ -84,6 +95,7 @@ def openedx_image_names(config, image):
|
||||
return images
|
||||
return [image]
|
||||
|
||||
|
||||
def vendor_image_names(config, image):
|
||||
if image == "all":
|
||||
images = VENDOR_IMAGES[:]
|
||||
@ -100,6 +112,7 @@ def vendor_image_names(config, image):
|
||||
return images
|
||||
return [image]
|
||||
|
||||
images.add_command(build)
|
||||
images.add_command(pull)
|
||||
images.add_command(push)
|
||||
|
||||
images_command.add_command(build)
|
||||
images_command.add_command(pull)
|
||||
images_command.add_command(push)
|
||||
|
23
tutor/k8s.py
23
tutor/k8s.py
@ -13,6 +13,7 @@ from . import utils
|
||||
def k8s():
|
||||
pass
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Configure and run Open edX from scratch"
|
||||
)
|
||||
@ -27,6 +28,7 @@ def quickstart(root):
|
||||
click.echo(fmt.title("Running migrations. NOTE: this might fail. If it does, please retry 'tutor k8s databases' later"))
|
||||
databases.callback(root)
|
||||
|
||||
|
||||
@click.command(help="Run all configured Open edX services")
|
||||
@opts.root
|
||||
def start(root):
|
||||
@ -45,10 +47,12 @@ def start(root):
|
||||
kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "services.yml"))
|
||||
kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "deployments.yml"))
|
||||
|
||||
|
||||
@click.command(help="Stop a running platform")
|
||||
def stop():
|
||||
kubectl("delete", "deployments,services,ingress,configmaps", "--all")
|
||||
|
||||
|
||||
@click.command(help="Completely delete an existing platform")
|
||||
@click.option("-y", "--yes", is_flag=True, help="Do not ask for confirmation")
|
||||
def delete(yes):
|
||||
@ -56,6 +60,7 @@ def delete(yes):
|
||||
click.confirm('Are you sure you want to delete the platform? All data will be removed.', abort=True)
|
||||
kubectl("delete", "namespace", K8s.NAMESPACE)
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Create databases and run database migrations",
|
||||
)
|
||||
@ -63,6 +68,7 @@ def delete(yes):
|
||||
def databases(root):
|
||||
scripts.migrate(root, run_bash)
|
||||
|
||||
|
||||
@click.command(help="Create an Open edX user and interactively set their password")
|
||||
@opts.root
|
||||
@click.option("--superuser", is_flag=True, help="Make superuser")
|
||||
@ -72,6 +78,7 @@ def databases(root):
|
||||
def createuser(root, superuser, staff, name, email):
|
||||
scripts.create_user(root, run_bash, superuser, staff, name, email)
|
||||
|
||||
|
||||
@click.command(help="Import the demo course")
|
||||
@opts.root
|
||||
def importdemocourse(root):
|
||||
@ -80,6 +87,7 @@ def importdemocourse(root):
|
||||
click.echo(fmt.info("Re-indexing courses"))
|
||||
indexcourses.callback(root)
|
||||
|
||||
|
||||
@click.command(help="Re-index courses for better searching")
|
||||
@opts.root
|
||||
def indexcourses(root):
|
||||
@ -87,24 +95,26 @@ def indexcourses(root):
|
||||
# I'm not quite sure the settings are correctly picked up. Which is weird because migrations work very well.
|
||||
scripts.index_courses(root, run_bash)
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Launch a shell in LMS or CMS",
|
||||
)
|
||||
@opts.root
|
||||
@click.argument("service", type=click.Choice(["lms", "cms"]))
|
||||
def shell(root, service):
|
||||
def shell(service):
|
||||
K8s().execute(service, "bash")
|
||||
|
||||
|
||||
@click.command(help="Create a Kubernetesadmin user")
|
||||
@opts.root
|
||||
def adminuser(root):
|
||||
utils.kubectl("create", "-f", tutor_env.pathjoin(root, "k8s", "adminuser.yml"))
|
||||
|
||||
|
||||
@click.command(help="Print the Kubernetes admin user token")
|
||||
@opts.root
|
||||
def admintoken(root):
|
||||
def admintoken():
|
||||
click.echo(K8s().admin_token())
|
||||
|
||||
|
||||
def kubectl(*command):
|
||||
"""
|
||||
Run kubectl commands in the right namespace. Also, errors are completely
|
||||
@ -116,6 +126,7 @@ def kubectl(*command):
|
||||
]
|
||||
kubectl_no_fail(*args)
|
||||
|
||||
|
||||
def kubectl_no_fail(*command):
|
||||
"""
|
||||
Run kubectl commands and ignore exceptions, to avoid stopping on
|
||||
@ -164,9 +175,11 @@ class K8s:
|
||||
podname = self.pod_name(app)
|
||||
kubectl_no_fail("exec", "--namespace", self.NAMESPACE, "-it", podname, "--", *command)
|
||||
|
||||
def run_bash(root, service, command):
|
||||
|
||||
def run_bash(root, service, command): # pylint: disable=unused-argument
|
||||
K8s().execute(service, "bash", "-e", "-c", command)
|
||||
|
||||
|
||||
k8s.add_command(quickstart)
|
||||
k8s.add_command(start)
|
||||
k8s.add_command(stop)
|
||||
|
@ -21,6 +21,7 @@ from . import utils
|
||||
def local():
|
||||
pass
|
||||
|
||||
|
||||
@click.command(help="Configure and run Open edX from scratch")
|
||||
@click.option("-p", "--pullimages", "pullimages_", is_flag=True, help="Update docker images")
|
||||
@opts.root
|
||||
@ -39,12 +40,14 @@ def quickstart(pullimages_, root):
|
||||
click.echo(fmt.title("Starting the platform in detached mode"))
|
||||
start.callback(root, True)
|
||||
|
||||
|
||||
@click.command(help="Update docker images")
|
||||
@opts.root
|
||||
def pullimages(root):
|
||||
config = tutor_config.load(root)
|
||||
docker_compose(root, config, "pull")
|
||||
|
||||
|
||||
@click.command(help="Run all configured Open edX services")
|
||||
@opts.root
|
||||
@click.option("-d", "--detach", is_flag=True, help="Start in daemon mode")
|
||||
@ -97,6 +100,7 @@ def restart(root, service):
|
||||
command += [service]
|
||||
docker_compose(root, config, *command)
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Run a command in one of the containers",
|
||||
context_settings={"ignore_unknown_options": True},
|
||||
@ -118,6 +122,7 @@ def run(root, service, command, args):
|
||||
config = tutor_config.load(root)
|
||||
docker_compose(root, config, *run_command)
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Create databases and run database migrations",
|
||||
)
|
||||
@ -126,6 +131,7 @@ def databases(root):
|
||||
init_mysql(root)
|
||||
scripts.migrate(root, run_bash)
|
||||
|
||||
|
||||
def init_mysql(root):
|
||||
config = tutor_config.load(root)
|
||||
if not config['ACTIVATE_MYSQL']:
|
||||
@ -139,20 +145,23 @@ def init_mysql(root):
|
||||
click.echo(fmt.info(" waiting for mysql initialization"))
|
||||
# TODO this is duplicate code with the docker_compose function. We
|
||||
# should rely on a dedicated function in utils module.
|
||||
logs = subprocess.check_output([
|
||||
mysql_logs = subprocess.check_output([
|
||||
"docker-compose", "-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"),
|
||||
"--project-name", config["LOCAL_PROJECT_NAME"], "logs", "mysql",
|
||||
])
|
||||
if b"MySQL init process done. Ready for start up." in logs:
|
||||
# pylint: disable=unsupported-membership-test
|
||||
if b"MySQL init process done. Ready for start up." in mysql_logs:
|
||||
click.echo(fmt.info("MySQL database initialized"))
|
||||
docker_compose(root, config, "stop", "mysql")
|
||||
return
|
||||
sleep(4)
|
||||
|
||||
|
||||
@click.group(help="Manage https certificates")
|
||||
def https():
|
||||
pass
|
||||
|
||||
|
||||
@click.command(help="Create https certificates", name="create")
|
||||
@opts.root
|
||||
def https_create(root):
|
||||
@ -188,6 +197,7 @@ See the official certbot documentation for your platform: https://certbot.eff.or
|
||||
"-e", "-c", script,
|
||||
)
|
||||
|
||||
|
||||
@click.command(help="Renew https certificates", name="renew")
|
||||
@opts.root
|
||||
def https_renew(root):
|
||||
@ -211,6 +221,7 @@ See the official certbot documentation for your platform: https://certbot.eff.or
|
||||
]
|
||||
utils.docker_run(*docker_run)
|
||||
|
||||
|
||||
@click.command(help="View output from containers")
|
||||
@opts.root
|
||||
@click.option("-f", "--follow", is_flag=True, help="Follow log output")
|
||||
@ -226,6 +237,7 @@ def logs(root, follow, tail, service):
|
||||
config = tutor_config.load(root)
|
||||
docker_compose(root, config, *command)
|
||||
|
||||
|
||||
@click.command(help="Create an Open edX user and interactively set their password")
|
||||
@opts.root
|
||||
@click.option("--superuser", is_flag=True, help="Make superuser")
|
||||
@ -237,6 +249,7 @@ def createuser(root, superuser, staff, name, email):
|
||||
check_service_is_activated(config, "lms")
|
||||
scripts.create_user(root, run_bash, superuser, staff, name, email)
|
||||
|
||||
|
||||
@click.command(help="Import the demo course")
|
||||
@opts.root
|
||||
def importdemocourse(root):
|
||||
@ -247,6 +260,7 @@ def importdemocourse(root):
|
||||
click.echo(fmt.info("Re-indexing courses"))
|
||||
indexcourses.callback(root)
|
||||
|
||||
|
||||
@click.command(help="Re-index courses for better searching")
|
||||
@opts.root
|
||||
def indexcourses(root):
|
||||
@ -254,6 +268,7 @@ def indexcourses(root):
|
||||
check_service_is_activated(config, "cms")
|
||||
scripts.index_courses(root, run_bash)
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Run Portainer (https://portainer.io), a UI for container supervision",
|
||||
short_help="Run Portainer, a UI for container supervision",
|
||||
@ -271,14 +286,17 @@ def portainer(root, port):
|
||||
click.echo(fmt.info("View the Portainer UI at http://localhost:{port}".format(port=port)))
|
||||
utils.docker_run(*docker_run)
|
||||
|
||||
|
||||
def check_service_is_activated(config, service):
|
||||
if not config["ACTIVATE_" + service.upper()]:
|
||||
raise exceptions.TutorError("This command may only be executed on the server where the {} is running".format(service))
|
||||
|
||||
|
||||
def run_bash(root, service, command):
|
||||
config = tutor_config.load(root)
|
||||
docker_compose(root, config, "run", "--rm", service, "bash", "-e", "-c", command)
|
||||
|
||||
|
||||
def docker_compose(root, config, *command):
|
||||
return utils.docker_compose(
|
||||
"-f", tutor_env.pathjoin(root, "local", "docker-compose.yml"),
|
||||
@ -286,6 +304,7 @@ def docker_compose(root, config, *command):
|
||||
*command
|
||||
)
|
||||
|
||||
|
||||
https.add_command(https_create)
|
||||
https.add_command(https_renew)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import click
|
||||
import click_repl
|
||||
|
||||
|
||||
@click.command(
|
||||
short_help="Interactive shell",
|
||||
help="Launch an interactive shell for launching Tutor commands"
|
||||
@ -14,5 +15,5 @@ Type <ctrl-d> to exit.""")
|
||||
try:
|
||||
click_repl.repl(click.get_current_context())
|
||||
return # this happens on a ctrl+d
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
@ -1,9 +1,10 @@
|
||||
import click
|
||||
import random
|
||||
import shutil
|
||||
import string
|
||||
import subprocess
|
||||
|
||||
import click
|
||||
|
||||
from . import exceptions
|
||||
from . import fmt
|
||||
|
||||
@ -11,6 +12,7 @@ from . import fmt
|
||||
def random_string(length):
|
||||
return "".join([random.choice(string.ascii_letters + string.digits) for _ in range(length)])
|
||||
|
||||
|
||||
def common_domain(d1, d2):
|
||||
"""
|
||||
Return the common domain between two domain names.
|
||||
@ -27,19 +29,23 @@ def common_domain(d1, d2):
|
||||
break
|
||||
return ".".join(common[::-1])
|
||||
|
||||
|
||||
def docker_run(*command):
|
||||
return docker("run", "--rm", "-it", *command)
|
||||
|
||||
|
||||
def docker(*command):
|
||||
if shutil.which("docker") is None:
|
||||
raise exceptions.TutorError("docker is not installed. Please follow instructions from https://docs.docker.com/install/")
|
||||
return execute("docker", *command)
|
||||
|
||||
|
||||
def docker_compose(*command):
|
||||
if shutil.which("docker-compose") is None:
|
||||
raise exceptions.TutorError("docker-compose is not installed. Please follow instructions from https://docs.docker.com/compose/install/")
|
||||
return execute("docker-compose", *command)
|
||||
|
||||
|
||||
def kubectl(*command):
|
||||
if shutil.which("kubectl") is None:
|
||||
raise exceptions.TutorError(
|
||||
@ -47,6 +53,7 @@ def kubectl(*command):
|
||||
)
|
||||
return execute("kubectl", *command)
|
||||
|
||||
|
||||
def execute(*command):
|
||||
click.echo(fmt.command(" ".join(command)))
|
||||
with subprocess.Popen(command) as p:
|
||||
|
Loading…
Reference in New Issue
Block a user