Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ACR] Add new command az acr helm install-cli #12336

Merged
merged 10 commits into from
Feb 27, 2020
18 changes: 18 additions & 0 deletions src/azure-cli/azure/cli/command_modules/acr/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,24 @@
az acr helm show -n MyRegistry mychart --version 0.3.2
"""

helps['acr helm install-cli'] = """
type: command
short-summary: Download and install Helm command-line tool.
examples:
- name: Install the default version of Helm CLI to the default location
text: >
az acr helm install-cli
- name: Install a specified version of Helm CLI to the default location
text: >
az acr helm install-cli --client-version x.x.x
- name: Install the default version of Helm CLI to a specified location
text: >
az acr helm install-cli --install-location /folder/filename
- name: Install a specified version of Helm CLI to a specified location
text: >
az acr helm install-cli --client-version x.x.x --install-location /folder/filename
"""

helps['acr import'] = """
type: command
short-summary: Imports an image to an Azure Container Registry from another Container Registry. Import removes the need to docker pull, docker tag, docker push.
Expand Down
23 changes: 23 additions & 0 deletions src/azure-cli/azure/cli/command_modules/acr/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

# pylint: disable=line-too-long
import argparse
import os.path
import platform

from argcomplete.completers import FilesCompleter
from knack.arguments import CLIArgumentType

Expand Down Expand Up @@ -258,6 +261,11 @@ def load_arguments(self, _): # pylint: disable=too-many-statements
c.positional('chart_package', help="The helm chart package.", completer=FilesCompleter())
c.argument('force', help='Overwrite the existing chart package.', action='store_true')

with self.argument_context('acr helm install-cli') as c:
c.argument('client_version', help='The target Helm CLI version. (Attention: Currently, Helm 3 does not work with "az acr helm" commands) ')
c.argument('install_location', help='Path at which to install Helm CLI (Existing one at the same path will be overwritten)', default=_get_helm_default_install_location())
c.argument('yes', help='Agree to the license of Helm, and do not prompt for confirmation.')

with self.argument_context('acr network-rule') as c:
c.argument('subnet', help='Name or ID of subnet. If name is supplied, `--vnet-name` must be supplied.')
c.argument('vnet_name', help='Name of a virtual network.')
Expand Down Expand Up @@ -307,3 +315,18 @@ def load_arguments(self, _): # pylint: disable=too-many-statements
with self.argument_context('acr token credential delete') as c:
c.argument('password1', options_list=['--password1'], help='Flag indicating if first password should be deleted', action='store_true', required=False)
c.argument('password2', options_list=['--password2'], help='Flag indicating if second password should be deleted.', action='store_true', required=False)


def _get_helm_default_install_location():
exe_name = 'helm'
system = platform.system()
if system == 'Windows':
home_dir = os.environ.get('USERPROFILE')
if not home_dir:
return None
install_location = os.path.join(home_dir, r'.azure-{0}\{0}.exe'.format(exe_name))
elif system in ('Linux', 'Darwin'):
install_location = '/usr/local/bin/{}'.format(exe_name)
else:
install_location = None
return install_location
1 change: 1 addition & 0 deletions src/azure-cli/azure/cli/command_modules/acr/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def load_command_table(self, _): # pylint: disable=too-many-statements
g.command('delete', 'acr_helm_delete')
g.command('push', 'acr_helm_push')
g.command('repo add', 'acr_helm_repo_add')
g.command('install-cli', 'acr_helm_install_cli', is_preview=True)

with self.command_group('acr network-rule', acr_network_rule_util) as g:
g.command('list', 'acr_network_rule_list')
Expand Down
173 changes: 173 additions & 0 deletions src/azure-cli/azure/cli/command_modules/acr/helm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os
import platform
from six.moves.urllib.request import urlopen # pylint: disable=import-error

from knack.util import CLIError
from knack.log import get_logger

from azure.cli.core.util import in_cloud_console

from ._utils import user_confirmation

from ._docker_utils import (
get_access_credentials,
request_data_from_registry,
Expand Down Expand Up @@ -175,6 +182,77 @@ def acr_helm_repo_add(cmd,
p.wait()


def acr_helm_install_cli(client_version='2.16.3', install_location=None, yes=False):
"""Install Helm command-line tool."""

if client_version >= '3':
logger.warning('Please note that "az acr helm" commands do not work with Helm 3, '
'but you can still push Helm chart to ACR using a different command flow. '
'For more information, please check out '
'https://docs.microsoft.com/en-us/azure/container-registry/container-registry-helm-repos')

install_location, install_dir, cli = _process_helm_install_location_info(install_location)

client_version = "v%s" % client_version
source_url = 'https://get.helm.sh/{}'
package, folder = _get_helm_package_name(client_version)
download_path = ''

if not package:
raise CLIError('No prebuilt binary for current system.')

try:
import tempfile
with tempfile.TemporaryDirectory() as tmp_dir:
download_path = os.path.join(tmp_dir, package)
_urlretrieve(source_url.format(package), download_path)
_unzip(download_path, tmp_dir)

sub_dir = os.path.join(tmp_dir, folder)
# Ask user to check license
if not yes:
with open(os.path.join(sub_dir, 'LICENSE')) as f:
text = f.read()
logger.warning(text)
user_confirmation('Before proceeding with the installation, '
'please confirm that you have read and agreed the above license.')

# Move files from temporary location to specified location
import shutil
import stat
for f in os.scandir(sub_dir):
# Rename helm to specified name
target_path = install_location if os.path.splitext(f.name)[0] == 'helm' \
else os.path.join(install_dir, f.name)
logger.debug('Moving %s to %s', f.path, target_path)
shutil.move(f.path, target_path)

if os.path.splitext(f.name)[0] in ('helm', 'tiller'):
os.chmod(target_path, os.stat(target_path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
except IOError as e:
import traceback
logger.debug(traceback.format_exc())
raise CLIError('Error while installing {} to {}: {}'.format(cli, install_dir, e))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this cause the original stack trace to lost? Otherwise I suggest we catch a particular line which might throw this exception, or just start with uncaught.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can log the stack trace so that it won't lose and users won't get disturbed


logger.warning('Successfully installed %s to %s.', cli, install_dir)
# Remind user to add to path
system = platform.system()
if system == 'Windows': # be verbose, as the install_location likely not in Windows's search PATHs
env_paths = os.environ['PATH'].split(';')
found = next((x for x in env_paths if x.lower().rstrip('\\') == install_dir.lower()), None)
if not found:
# pylint: disable=logging-format-interpolation
logger.warning('Please add "{0}" to your search PATH so the `{1}` can be found. 2 options: \n'
' 1. Run "set PATH=%PATH%;{0}" or "$env:path += \'{0}\'" for PowerShell. '
'This is good for the current command session.\n'
' 2. Update system PATH environment variable by following '
'"Control Panel->System->Advanced->Environment Variables", and re-open the command window. '
'You only need to do it once'.format(install_dir, cli))
else:
logger.warning('Please ensure that %s is in your search PATH, so the `%s` command can be found.',
install_dir, cli)


def get_helm_command(is_diagnostics_context=False):
from ._errors import HELM_COMMAND_ERROR
helm_command = 'helm'
Expand Down Expand Up @@ -230,3 +308,98 @@ def _get_chart_package_name(chart, version, prov=False):
return '{}.prov'.format(chart_package_name)

return chart_package_name


def _process_helm_install_location_info(install_location):
if not install_location or install_location.isspace():
raise CLIError('Invalid install location.')

install_dir, cli = os.path.dirname(install_location), os.path.basename(install_location)
if not install_dir:
# Use current working directory
install_dir = os.getcwd()
install_location = os.path.join(install_dir, cli)
# Ensure installation directory exists
if not os.path.exists(install_dir):
os.makedirs(install_dir)
if not cli:
system = platform.system()
cli = 'helm.exe' if system == 'Windows' else 'helm'
install_location = os.path.join(install_dir, cli)

return install_location, install_dir, cli


def _get_helm_package_name(client_version):
package_template = 'helm-{}-{}-{}.{}'
folder_template = '{}-{}'
package = ''
folder = ''

# Reference: https://github.com/helm/helm/blob/master/scripts/get
archs = {
'armv5': 'armv5',
'armv6': 'armv6',
'armv7': 'arm',
'aarch64': 'arm64',
'x86': '386',
'x86_64': 'amd64',
'i686': '386',
'i386': '386',
'AMD64': 'amd64',
'ppc64le': 'ppc64le',
's390x': 's390x'
}
machine = platform.machine()
if machine not in archs:
return None, None
arch = archs[machine]

system = platform.system().lower()
if system == 'windows':
package = package_template.format(client_version, system, arch, 'zip')
elif system in ('linux', 'darwin'):
package = package_template.format(client_version, system, arch, 'tar.gz')
else:
return None, None

folder = folder_template.format(system, arch)
return package, folder


def _ssl_context():
import sys
import ssl

if sys.version_info < (3, 4) or (in_cloud_console() and platform.system() == 'Windows'):
try:
return ssl.SSLContext(ssl.PROTOCOL_TLS) # added in python 2.7.13 and 3.6
except AttributeError:
return ssl.SSLContext(ssl.PROTOCOL_TLSv1)

return ssl.create_default_context()


def _urlretrieve(url, path):
logger.warning('Downloading client from %s, it may take a long time...', url)
with urlopen(url, context=_ssl_context()) as response:
logger.debug('Start downloading from %s to %s', url, path)
# Open for writing in binary mode
with open(path, "wb") as f:
f.write(response.read())
logger.debug('Successfully downloaded from %s to %s', url, path)


def _unzip(src, dest):
logger.debug('Extracting %s to %s.', src, dest)
system = platform.system()
if system == 'Windows':
import zipfile
with zipfile.ZipFile(src, 'r') as zipObj:
zipObj.extractall(dest)
elif system in ('Linux', 'Darwin'):
import tarfile
with tarfile.open(src, 'r') as tarObj:
tarObj.extractall(dest)
else:
raise CLIError('The current system is not supported.')