340 lines
11 KiB
Python
340 lines
11 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2019 Google LLC. 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.
|
|
"""Library for generating the files for local development environment."""
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
|
|
import subprocess
|
|
import sys
|
|
|
|
from googlecloudsdk.command_lib.code import run_subprocess
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core.console import console_io
|
|
from googlecloudsdk.core.util import platforms
|
|
from googlecloudsdk.core.util import times
|
|
import six
|
|
|
|
DEFAULT_CLUSTER_NAME = 'gcloud-local-dev'
|
|
|
|
|
|
class _KubeCluster(object):
|
|
"""A kubernetes cluster.
|
|
|
|
Attributes:
|
|
context_name: Kubernetes context name.
|
|
env_vars: Docker env vars.
|
|
shared_docker: Whether the kubernetes cluster shares a docker instance with
|
|
the developer's machine.
|
|
"""
|
|
|
|
def __init__(self, context_name, shared_docker):
|
|
"""Initializes KubeCluster with cluster name.
|
|
|
|
Args:
|
|
context_name: Kubernetes context.
|
|
shared_docker: Whether the kubernetes cluster shares a docker instance
|
|
with the developer's machine.
|
|
"""
|
|
self.context_name = context_name
|
|
self.shared_docker = shared_docker
|
|
|
|
@property
|
|
def env_vars(self):
|
|
return {}
|
|
|
|
|
|
def GetMinikubeVersion():
|
|
"""Returns the current version of minikube."""
|
|
return six.ensure_text(subprocess.check_output([_FindMinikube(), 'version']))
|
|
|
|
|
|
class MinikubeCluster(_KubeCluster):
|
|
"""A cluster on minikube.
|
|
|
|
Attributes:
|
|
context_name: Kubernetes context name.
|
|
env_vars: Docker environment variables.
|
|
shared_docker: Whether the kubernetes cluster shares a docker instance with
|
|
the developer's machine.
|
|
"""
|
|
|
|
@property
|
|
def env_vars(self):
|
|
return _GetMinikubeDockerEnvs(self.context_name)
|
|
|
|
|
|
class Minikube(object):
|
|
"""Starts and stops a minikube cluster."""
|
|
|
|
def __init__(self,
|
|
cluster_name,
|
|
stop_cluster=True,
|
|
vm_driver=None,
|
|
debug=False):
|
|
self._cluster_name = cluster_name
|
|
self._stop_cluster = stop_cluster
|
|
self._vm_driver = vm_driver
|
|
self._debug = debug
|
|
|
|
def __enter__(self):
|
|
_StartMinikubeCluster(self._cluster_name, self._vm_driver, self._debug)
|
|
return MinikubeCluster(self._cluster_name, self._vm_driver == 'docker')
|
|
|
|
def __exit__(self, exc_type, exc_value, tb):
|
|
if self._stop_cluster:
|
|
_StopMinikube(self._cluster_name, self._debug)
|
|
|
|
|
|
def _FindMinikube():
|
|
return (properties.VALUES.code.minikube_path_override.Get() or
|
|
run_subprocess.GetGcloudPreferredExecutable('minikube'))
|
|
|
|
|
|
class MinikubeStartError(exceptions.Error):
|
|
"""Error if minikube fails to start."""
|
|
|
|
|
|
_MINIKUBE_STEP = 'io.k8s.sigs.minikube.step'
|
|
|
|
_MINIKUBE_DOWNLOAD_PROGRESS = 'io.k8s.sigs.minikube.download.progress'
|
|
|
|
_MINIKUBE_ERROR = 'io.k8s.sigs.minikube.error'
|
|
|
|
_MINIKUBE_NOT_ENOUGH_CPU_FRAGMENT = 'The minimum allowed is 2 CPUs.'
|
|
|
|
# pylint: disable=line-too-long
|
|
# See https://github.com/kubernetes/minikube/blob/master/pkg/minikube/reason/exitcodes.go
|
|
# pylint: enable=line-too-long
|
|
_MINIKUBE_ERROR_MESSAGES = {
|
|
'29': 'Not enough CPUs. Cloud Run Emulator requires 2 CPUs.',
|
|
'69': 'Cannot reach docker daemon.',
|
|
}
|
|
|
|
_MINIKUBE_PASSTHROUGH_ADVICE_IDS = frozenset(['HOST_HOME_PERMISSION'])
|
|
|
|
if platforms.OperatingSystem.Current() != platforms.OperatingSystem.LINUX:
|
|
_MINIKUBE_ERROR_MESSAGES['29'] += ' Increase Docker VM CPUs to 2.'
|
|
|
|
|
|
def _StartMinikubeCluster(cluster_name, vm_driver, debug=False):
|
|
"""Starts a minikube cluster."""
|
|
# pylint: disable=broad-except
|
|
try:
|
|
if not _IsMinikubeClusterUp(cluster_name):
|
|
cmd = [
|
|
_FindMinikube(),
|
|
'start',
|
|
'-p',
|
|
cluster_name,
|
|
'--keep-context',
|
|
'--interactive=false',
|
|
'--delete-on-failure',
|
|
'--install-addons=false',
|
|
'--output=json',
|
|
]
|
|
if vm_driver:
|
|
cmd.append('--vm-driver=' + vm_driver)
|
|
if vm_driver == 'docker':
|
|
cmd.append('--container-runtime=docker')
|
|
if debug:
|
|
cmd.extend(['--alsologtostderr', '-v8'])
|
|
|
|
start_msg = "Starting development environment '%s' ..." % cluster_name
|
|
|
|
event_timeout = times.ParseDuration(
|
|
properties.VALUES.code.minikube_event_timeout.Get(
|
|
required=True)).total_seconds
|
|
|
|
with console_io.ProgressBar(start_msg) as progress_bar:
|
|
for json_obj in run_subprocess.StreamOutputJson(
|
|
cmd, event_timeout_sec=event_timeout, show_stderr=debug):
|
|
if debug:
|
|
print('minikube', json_obj)
|
|
|
|
_HandleMinikubeStatusEvent(progress_bar, json_obj)
|
|
except Exception as e:
|
|
six.reraise(MinikubeStartError, e, sys.exc_info()[2])
|
|
|
|
|
|
def _HandleMinikubeStatusEvent(progress_bar, json_obj):
|
|
"""Handle a minikube json event."""
|
|
if json_obj['type'] == _MINIKUBE_STEP:
|
|
data = json_obj['data']
|
|
|
|
# https://github.com/kubernetes/minikube/issues/9754
|
|
# currentstep and totalsteps could be:
|
|
# missing -> invalid
|
|
# '' -> invalid
|
|
# '0' -> ok
|
|
# 0 -> ok
|
|
# pylint:disable=g-explicit-bool-comparison
|
|
if data.get('currentstep', '') != '' and data.get('totalsteps', '') != '':
|
|
current_step = int(data['currentstep'])
|
|
total_steps = int(data['totalsteps'])
|
|
completion_fraction = current_step / float(total_steps)
|
|
progress_bar.SetProgress(completion_fraction)
|
|
elif json_obj['type'] == _MINIKUBE_DOWNLOAD_PROGRESS:
|
|
data = json_obj['data']
|
|
|
|
# https://github.com/kubernetes/minikube/issues/9754
|
|
# currentstep and totalsteps could be:
|
|
# missing -> invalid
|
|
# '' -> invalid
|
|
# '0' -> ok
|
|
# 0 -> ok
|
|
# pylint:disable=g-explicit-bool-comparison
|
|
if (data.get('currentstep', '') != '' and
|
|
data.get('totalsteps', '') != '' and 'progress' in data):
|
|
current_step = int(data['currentstep'])
|
|
total_steps = int(data['totalsteps'])
|
|
download_progress = float(data['progress'])
|
|
|
|
completion_fraction = (current_step + download_progress) / total_steps
|
|
progress_bar.SetProgress(completion_fraction)
|
|
elif (json_obj['type'] == _MINIKUBE_ERROR and 'exitcode' in json_obj['data']):
|
|
data = json_obj['data']
|
|
if ('id' in data and 'advice' in data and
|
|
data['id'] in _MINIKUBE_PASSTHROUGH_ADVICE_IDS):
|
|
raise MinikubeStartError(data['advice'])
|
|
else:
|
|
exit_code = data['exitcode']
|
|
msg = _MINIKUBE_ERROR_MESSAGES.get(exit_code,
|
|
'Unable to start Cloud Run Emulator.')
|
|
raise MinikubeStartError(msg)
|
|
|
|
|
|
def _GetMinikubeDockerEnvs(cluster_name):
|
|
"""Get the docker environment settings for a given cluster."""
|
|
cmd = [_FindMinikube(), 'docker-env', '-p', cluster_name, '--shell=none']
|
|
lines = run_subprocess.GetOutputLines(cmd, timeout_sec=20)
|
|
return dict(
|
|
line.split('=', 1) for line in lines if line and not line.startswith('#'))
|
|
|
|
|
|
def _IsMinikubeClusterUp(cluster_name):
|
|
"""Checks if a minikube cluster is running."""
|
|
cmd = [_FindMinikube(), 'status', '-p', cluster_name, '-o', 'json']
|
|
try:
|
|
status = run_subprocess.GetOutputJson(
|
|
cmd, timeout_sec=20, show_stderr=False)
|
|
return 'Host' in status and status['Host'].strip() == 'Running'
|
|
except (ValueError, subprocess.CalledProcessError):
|
|
return False
|
|
|
|
|
|
def _StopMinikube(cluster_name, debug=False):
|
|
"""Stop a minikube cluster."""
|
|
cmd = [_FindMinikube(), 'stop', '-p', cluster_name]
|
|
print("Stopping development environment '%s' ..." % cluster_name)
|
|
run_subprocess.Run(cmd, timeout_sec=150, show_output=debug)
|
|
print('Development environment stopped.')
|
|
|
|
|
|
def DeleteMinikube(cluster_name):
|
|
"""Delete a minikube cluster."""
|
|
cmd = [_FindMinikube(), 'delete', '-p', cluster_name]
|
|
print("Deleting development environment '%s' ..." % cluster_name)
|
|
run_subprocess.Run(cmd, timeout_sec=150, show_output=False)
|
|
print('Development environment stopped.')
|
|
|
|
|
|
class ExternalCluster(_KubeCluster):
|
|
"""A external kubernetes cluster.
|
|
|
|
Attributes:
|
|
context_name: Kubernetes context name.
|
|
env_vars: Docker environment variables.
|
|
shared_docker: Whether the kubernetes cluster shares a docker instance with
|
|
the developer's machine.
|
|
"""
|
|
|
|
def __init__(self, cluster_name):
|
|
"""Initializes ExternalCluster with profile name.
|
|
|
|
Args:
|
|
cluster_name: Name of the cluster.
|
|
"""
|
|
super(ExternalCluster, self).__init__(cluster_name, False)
|
|
|
|
|
|
class ExternalClusterContext(object):
|
|
"""Do nothing context manager for external clusters."""
|
|
|
|
def __init__(self, kube_context):
|
|
self._kube_context = kube_context
|
|
|
|
def __enter__(self):
|
|
return ExternalCluster(self._kube_context)
|
|
|
|
def __exit__(self, exc_type, exc_value, tb):
|
|
pass
|
|
|
|
|
|
def _FindKubectl():
|
|
return run_subprocess.GetGcloudPreferredExecutable('kubectl')
|
|
|
|
|
|
def _NamespaceExists(namespace, context_name=None):
|
|
cmd = [_FindKubectl()]
|
|
if context_name:
|
|
cmd += ['--context', context_name]
|
|
cmd += ['get', 'namespaces', '-o', 'name']
|
|
namespaces = run_subprocess.GetOutputLines(
|
|
cmd, timeout_sec=20, show_stderr=False)
|
|
return 'namespace/' + namespace in namespaces
|
|
|
|
|
|
def _CreateNamespace(namespace, context_name=None):
|
|
cmd = [_FindKubectl()]
|
|
if context_name:
|
|
cmd += ['--context', context_name]
|
|
cmd += ['create', 'namespace', namespace]
|
|
run_subprocess.Run(cmd, timeout_sec=20, show_output=False)
|
|
|
|
|
|
def _DeleteNamespace(namespace, context_name=None):
|
|
cmd = [_FindKubectl()]
|
|
if context_name:
|
|
cmd += ['--context', context_name]
|
|
cmd += ['delete', 'namespace', namespace]
|
|
run_subprocess.Run(cmd, timeout_sec=20, show_output=False)
|
|
|
|
|
|
class KubeNamespace(object):
|
|
"""Context to create and tear down kubernetes namespace."""
|
|
|
|
def __init__(self, namespace, context_name=None):
|
|
"""Initialize KubeNamespace.
|
|
|
|
Args:
|
|
namespace: (str) Namespace name.
|
|
context_name: (str) Kubernetes context name.
|
|
"""
|
|
self._namespace = namespace
|
|
self._context_name = context_name
|
|
self._delete_namespace = False
|
|
|
|
def __enter__(self):
|
|
if not _NamespaceExists(self._namespace, self._context_name):
|
|
_CreateNamespace(self._namespace, self._context_name)
|
|
self._delete_namespace = True
|
|
|
|
def __exit__(self, exc_type, exc_value, tb):
|
|
if self._delete_namespace:
|
|
_DeleteNamespace(self._namespace, self._context_name)
|