Skip to content

Commit

Permalink
Multicontainer change
Browse files Browse the repository at this point in the history
  • Loading branch information
Lukasz Stempniewicz committed Apr 28, 2018
1 parent 87156a3 commit 7f0c112
Show file tree
Hide file tree
Showing 5 changed files with 536 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
'LoginWithMicrosoftAccount': BuiltInAuthenticationProvider.microsoft_account,
'LoginWithTwitter': BuiltInAuthenticationProvider.twitter}

MULTI_CONTAINER_TYPES = ['COMPOSE', 'KUBE']

# pylint: disable=too-many-statements


Expand Down Expand Up @@ -73,6 +75,8 @@ def load_arguments(self, _):
with self.argument_context('webapp create') as c:
c.argument('name', options_list=['--name', '-n'], help='name of the new webapp')
c.argument('startup_file', help="Linux only. The web's startup file")
c.argument('multicontainer_config_type', options_list=['--multicontainer-config-type'], help="Linux only.", arg_type=get_enum_type(MULTI_CONTAINER_TYPES))
c.argument('multicontainer_config_file', options_list=['--multicontainer-config-file'], help="Linux only. Config file for linux multicontainers. (local or remote)")
c.argument('runtime', options_list=['--runtime', '-r'], help="canonicalized web runtime in the format of Framework|Version, e.g. \"PHP|5.6\". Use 'az webapp list-runtimes' for available list") # TODO ADD completer
c.argument('plan', options_list=['--plan', '-p'], configured_default='appserviceplan',
completer=get_resource_name_completion_list('Microsoft.Web/serverFarms'),
Expand Down Expand Up @@ -181,7 +185,6 @@ def load_arguments(self, _):

with self.argument_context('webapp log tail') as c:
c.argument('provider', help="By default all live traces configured by 'az webapp log config' will be shown, but you can scope to certain providers/folders, e.g. 'application', 'http', etc. For details, check out https://github.com/projectkudu/kudu/wiki/Diagnostic-Log-Stream")
c.argument('number_of_lines', type=int, help="Number of lines to be displayed (max of 512 kB), currently only works for linux")
with self.argument_context('webapp log download') as c:
c.argument('log_file', default='webapp_logs.zip', type=file_type, completer=FilesCompleter(), help='the downloaded zipped log file path')

Expand All @@ -200,6 +203,9 @@ def load_arguments(self, _):
c.argument('docker_registry_server_user', options_list=['--docker-registry-server-user', '-u'], help='the container registry server username')
c.argument('docker_registry_server_password', options_list=['--docker-registry-server-password', '-p'], help='the container registry server password')
c.argument('websites_enable_app_service_storage', options_list=['--enable-app-service-storage', '-t'], help='enables platform storage (custom container only)', arg_type=get_three_state_flag(return_label=True))
c.argument('multicontainer_config_type', options_list=['--multicontainer-config-type'], arg_type=get_enum_type(MULTI_CONTAINER_TYPES))
c.argument('multicontainer_config_file', options_list=['--multicontainer-config-file'], help="Config file for linux multicontainers")
c.argument('show_multicontainer_config', action='store_true', help='shows decoded config if a multicontainer config is set')

with self.argument_context('webapp config set') as c:
c.argument('remote_debugging_enabled', help='enable or disable remote debugging', arg_type=get_three_state_flag(return_label=True))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from azure.cli.core.commands import LongRunningOperation

from .vsts_cd_provider import VstsContinuousDeliveryProvider
from ._params import AUTH_TYPES
from ._params import AUTH_TYPES, MULTI_CONTAINER_TYPES
from ._client_factory import web_client_factory, ex_handler_factory
from ._appservice_utils import _generic_site_operation

Expand All @@ -47,7 +47,7 @@

def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_file=None,
deployment_container_image_name=None, deployment_source_url=None, deployment_source_branch='master',
deployment_local_git=None):
deployment_local_git=None, multicontainer_config_type=None, multicontainer_config_file=None):
if deployment_source_url and deployment_local_git:
raise CLIError('usage error: --deployment-source-url <url> | --deployment-local-git')
client = web_client_factory(cmd.cli_ctx)
Expand All @@ -66,8 +66,10 @@ def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_fi
helper = _StackRuntimeHelper(client, linux=is_linux)

if is_linux:
if runtime and deployment_container_image_name:
raise CLIError('usage error: --runtime | --deployment-container-image-name')
if not validate_linux_create_options(runtime, deployment_container_image_name,
multicontainer_config_type, multicontainer_config_file):
raise CLIError("usage error: --runtime | --deployment-container-image-name |"
" --multicontainer-config-type with --multicontainer-config-file")
if startup_file:
site_config.app_command_line = startup_file

Expand All @@ -77,16 +79,18 @@ def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_fi
if not match:
raise CLIError("Linux Runtime '{}' is not supported."
"Please invoke 'list-runtimes' to cross check".format(runtime))

elif deployment_container_image_name:
site_config.linux_fx_version = _format_linux_fx_version(deployment_container_image_name)
site_config.app_settings.append(NameValuePair("WEBSITES_ENABLE_APP_SERVICE_STORAGE", "false"))
else: # must specify runtime
raise CLIError('usage error: must specify --runtime | --deployment-container-image-name') # pylint: disable=line-too-long
elif multicontainer_config_type and multicontainer_config_file:
encoded_config_file = _get_linux_multicontainer_config_file(cmd, resource_group_name,
name, multicontainer_config_file)
site_config.linux_fx_version = _format_linux_fx_version(encoded_config_file, multicontainer_config_type)

elif runtime: # windows webapp with runtime specified
if startup_file or deployment_container_image_name:
raise CLIError("usage error: --startup-file or --deployment-container-image-name is "
raise CLIError("usage error: --startup-file or --deployment-container-image-name or "
"--multicontainer-config-type with --multicontainer-config-file is "
"only appliable on linux webapp")
match = helper.resolve(runtime)
if not match:
Expand Down Expand Up @@ -117,6 +121,14 @@ def create_webapp(cmd, resource_group_name, name, plan, runtime=None, startup_fi
return webapp


def validate_linux_create_options(runtime=None, deployment_container_image_name=None,
multicontainer_config_type=None, multicontainer_config_file=None):
if bool(multicontainer_config_type) != bool(multicontainer_config_file):
return False
opts = [bool(runtime), bool(deployment_container_image_name), bool(multicontainer_config_type)]
return len([x for x in opts if x]) == 1 # you can only specify one out the combinations


def update_app_settings(cmd, resource_group_name, name, settings=None, slot=None, slot_settings=None):
if not settings and not slot_settings:
raise CLIError('Usage Error: --settings |--slot-settings')
Expand Down Expand Up @@ -378,12 +390,14 @@ def _fill_ftp_publishing_url(cmd, webapp, resource_group_name, name, slot=None):
return webapp


def _format_linux_fx_version(custom_image_name):
def _format_linux_fx_version(custom_image_name, custom_prefix=None):
fx_version = custom_image_name.strip()
fx_version_lower = fx_version.lower()
# handles case of only spaces
if fx_version:
if not fx_version_lower.startswith('docker|'):
if custom_prefix:
fx_version = '{}|{}'.format(custom_prefix, custom_image_name)
elif not fx_version_lower.startswith('docker|'):
fx_version = '{}|{}'.format('DOCKER', custom_image_name)
else:
fx_version = ' '
Expand All @@ -404,6 +418,42 @@ def _get_linux_fx_version(cmd, resource_group_name, name, slot=None):
return site_config.linux_fx_version


def url_validator(url):
try:
result = urlparse(url)
return all([result.scheme, result.netloc, result.path])
except:
return False


def _get_linux_multicontainer_config_file(cmd, resource_group_name, name, file_name=None, encoded=True, slot=None):
from base64 import b64encode, b64decode
config_file_bytes = None
if file_name:
if url_validator(file_name):
from urllib.request import urlopen
response = urlopen(file_name)
config_file_bytes = response.read()
else:
with open(file_name) as f:
file_contents = f.read()
config_file_bytes = file_contents.encode('utf-8')
else:
linux_fx_version = _get_linux_fx_version(cmd, resource_group_name, name, slot)
if not any([linux_fx_version.startswith(s) for s in MULTI_CONTAINER_TYPES]):
raise CLIError("Cannot decode config that is not one of the"
" following types: {}".format(', '.join(MULTI_CONTAINER_TYPES)))
try:
config_file_bytes = b64decode(linux_fx_version.split('|')[1].encode('utf-8'))
except:
raise CLIError('Could not decode config')

if encoded:
return b64encode(config_file_bytes).decode('utf-8')

return config_file_bytes.decode('utf-8')


# for any modifications to the non-optional parameters, adjust the reflection logic accordingly
# in the method
def update_site_configs(cmd, resource_group_name, name, slot=None,
Expand Down Expand Up @@ -528,7 +578,7 @@ def delete_connection_strings(cmd, resource_group_name, name, setting_names, slo
def update_container_settings(cmd, resource_group_name, name, docker_registry_server_url=None,
docker_custom_image_name=None, docker_registry_server_user=None,
websites_enable_app_service_storage=None, docker_registry_server_password=None,
slot=None):
multicontainer_config_type=None, multicontainer_config_file=None, slot=None):
settings = []
if docker_registry_server_url is not None:
settings.append('DOCKER_REGISTRY_SERVER_URL=' + docker_registry_server_url)
Expand Down Expand Up @@ -556,6 +606,14 @@ def update_container_settings(cmd, resource_group_name, name, docker_registry_se
update_app_settings(cmd, resource_group_name, name, settings, slot)
settings = get_app_settings(cmd, resource_group_name, name, slot)

if bool(multicontainer_config_file) != bool(multicontainer_config_type):
encoded_config_file = _get_linux_multicontainer_config_file(cmd, resource_group_name,
name, multicontainer_config_file)
linux_fx_version = _format_linux_fx_version(encoded_config_file, multicontainer_config_type)
update_site_configs(cmd, resource_group_name, name, linux_fx_version=linux_fx_version)
else:
raise CLIError('Must use --multicontainer-config-file with --multicontainer-config-type')

return _mask_creds_related_appsettings(_filter_for_container_settings(cmd, resource_group_name, name, settings))


Expand Down Expand Up @@ -585,19 +643,23 @@ def delete_container_settings(cmd, resource_group_name, name, slot=None):
delete_app_settings(cmd, resource_group_name, name, CONTAINER_APPSETTING_NAMES, slot)


def show_container_settings(cmd, resource_group_name, name, slot=None):
def show_container_settings(cmd, resource_group_name, name, show_multicontainer_config=None, slot=None):
settings = get_app_settings(cmd, resource_group_name, name, slot)
return _mask_creds_related_appsettings(_filter_for_container_settings(cmd, resource_group_name,
name, settings, slot))
return _mask_creds_related_appsettings(_filter_for_container_settings(cmd, resource_group_name, name, settings, show_multicontainer_config, slot))


def _filter_for_container_settings(cmd, resource_group_name, name, settings, slot=None):
def _filter_for_container_settings(cmd, resource_group_name, name, settings, show_multicontainer_config=None, slot=None):
result = [x for x in settings if x['name'] in CONTAINER_APPSETTING_NAMES]
fx_version = _get_linux_fx_version(cmd, resource_group_name, name, slot).strip()
if fx_version:
added_image_name = {'name': 'DOCKER_CUSTOM_IMAGE_NAME',
'value': fx_version}
result.append(added_image_name)
if show_multicontainer_config:
decoded_value = _get_linux_multicontainer_config_file(cmd, resource_group_name, name, False, slot)
decoded_image_name = {'name': 'DOCKER_CUSTOM_IMAGE_NAME_DECODED',
'value': decoded_value}
result.append(decoded_image_name)
return result


Expand Down Expand Up @@ -1258,27 +1320,20 @@ def clear_traffic_routing(cmd, resource_group_name, name):
set_traffic_routing(cmd, resource_group_name, name, [])


def get_streaming_log(cmd, resource_group_name, name, number_of_lines=None, provider=None,
slot=None):
def get_streaming_log(cmd, resource_group_name, name, provider=None, slot=None):
scm_url = _get_scm_url(cmd, resource_group_name, name, slot)
streaming_url = scm_url + '/logstream'
import time
if provider:
streaming_url += ('/' + provider.lstrip('/'))
client = web_client_factory(cmd.cli_ctx)
user, password = _get_site_credential(cmd.cli_ctx, resource_group_name, name)
if number_of_lines:
webapp = client.web_apps.get(resource_group_name, name)
if not webapp.reserved:
raise CLIError('--number-of-lines is only supported for App Service on Linux apps')
url = scm_url + '/api/logs/docker'
_get_log(url, user, password, number_of_lines=number_of_lines)
else:
t = threading.Thread(target=_get_log, args=(streaming_url, user, password))
t.daemon = True
t.start()
while True:
time.sleep(100) # so that ctrl+c can stop the command

user, password = _get_site_credential(cmd.cli_ctx, resource_group_name, name, slot)
t = threading.Thread(target=_get_log, args=(streaming_url, user, password))
t.daemon = True
t.start()

while True:
time.sleep(100) # so that ctrl+c can stop the command


def download_historical_logs(cmd, resource_group_name, name, log_file=None, slot=None):
Expand All @@ -1295,7 +1350,7 @@ def _get_site_credential(cli_ctx, resource_group_name, name, slot=None):
return (creds.publishing_user_name, creds.publishing_password)


def _get_log(url, user_name, password, log_file=None, number_of_lines=None):
def _get_log(url, user_name, password, log_file=None):
import sys
import certifi
import urllib3
Expand Down Expand Up @@ -1323,9 +1378,6 @@ def _get_log(url, user_name, password, log_file=None, number_of_lines=None):
if not data:
break
f.write(data)
elif number_of_lines: # print last 'n' number of lines
api_str = r.read().decode(encoding='utf-8')
_print_site_logs(api_str, number_of_lines, 512000, http, headers)
else: # streaming
std_encoding = sys.stdout.encoding
for chunk in r.stream():
Expand All @@ -1337,47 +1389,6 @@ def _get_log(url, user_name, password, log_file=None, number_of_lines=None):
r.release_conn()


def _print_site_logs(api_string, number_of_lines, byte_limit, http, headers):
import sys
import json
std_encoding = sys.stdout.encoding

data = json.loads(api_string)
data_length = len(data)

if data_length > 1:
logger.info('Will show last %i lines of logs per instance', number_of_lines)

for x in range(0, data_length):
href_str = data[x]['href']
size = data[x]['size']
logger.info('\nSite Instance #%i', x + 1)
logger.info('\nUsing endpoint: %s', href_str)
byte_limit_exceeded = (int(size) > byte_limit)
if byte_limit_exceeded:
headers.update({'Range': 'bytes=-512000'}) # only get the last 512kB

r = http.request(
'GET',
href_str,
headers=headers,
preload_content=False
)
lines = (r.data.decode(encoding='utf-8', errors='replace')
.encode(std_encoding, errors='replace')
.decode(std_encoding, errors='replace')
.replace('\\n', '\n'))
lines_split = lines.splitlines()
lines_split_length = len(lines_split)
if byte_limit_exceeded:
if lines_split_length < number_of_lines:
logger.warning("Hit internal limit of %s bytes."
"Please download to see full logs", byte_limit)
number_of_lines = min(number_of_lines, lines_split_length)
last_n_lines = ('\n'.join(lines_split[-(number_of_lines)::])).lstrip('\n')
print(last_n_lines)


def upload_ssl_cert(cmd, resource_group_name, name, certificate_password, certificate_file):
client = web_client_factory(cmd.cli_ctx)
webapp = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get')
Expand Down
Loading

0 comments on commit 7f0c112

Please sign in to comment.