Skip to content

Commit

Permalink
Merge pull request #1290 from awslabs/develop
Browse files Browse the repository at this point in the history
v0.19.0
  • Loading branch information
sriram-mv committed Jul 31, 2019
2 parents dad3ab8 + 67edf91 commit bdc7abf
Show file tree
Hide file tree
Showing 40 changed files with 1,919 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
.idea/**/tasks.xml
.idea/dictionaries
.idea
.vscode

# Sensitive or high-churn files:
.idea/**/dataSources/
Expand Down
12 changes: 12 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,15 @@ also forces commands implementations to be modular, reusable, and highly
customizable. When RC files are implemented, new commands can be added
or existing commands can be removed, with simple a configuration in the
RC file.

Internal Environment Variables
==============================

SAM CLI uses the following internal, undocumented, environment variables
for development purposes. They should *not* be used by customers:

- `__SAM_CLI_APP_DIR`: Path to application directory to be used in place
of `~/.aws-sam` directory.

- `__SAM_CLI_TELEMETRY_ENDPOINT_URL`: HTTP Endpoint where the Telemetry
metrics will be published to
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Default value for environment variable. Can be overridden by setting the
# environment variable.
SAM_CLI_TELEMETRY ?= 0

init:
SAM_CLI_DEV=1 pip install -e '.[dev]'

Expand All @@ -8,10 +12,12 @@ test:

integ-test:
# Integration tests don't need code coverage
@echo Telemetry Status: $(SAM_CLI_TELEMETRY)
SAM_CLI_DEV=1 pytest tests/integration

func-test:
# Verify function test coverage only for `samcli.local` package
@echo Telemetry Status: $(SAM_CLI_TELEMETRY)
pytest --cov samcli.local --cov samcli.commands.local --cov-report term-missing tests/functional

flake:
Expand Down
2 changes: 1 addition & 1 deletion samcli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
SAM CLI version
"""

__version__ = '0.18.0'
__version__ = '0.19.0'
60 changes: 60 additions & 0 deletions samcli/cli/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
Context information passed to each CLI command
"""

import uuid
import logging
import boto3
import click


class Context(object):
Expand All @@ -26,6 +28,7 @@ def __init__(self):
self._debug = False
self._aws_region = None
self._aws_profile = None
self._session_id = str(uuid.uuid4())

@property
def debug(self):
Expand Down Expand Up @@ -68,6 +71,63 @@ def profile(self, value):
self._aws_profile = value
self._refresh_session()

@property
def session_id(self):
"""
Returns the ID of this command session. This is a randomly generated UUIDv4 which will not change until the
command terminates.
"""
return self._session_id

@property
def command_path(self):
"""
Returns the full path of the command as invoked ex: "sam local generate-event s3 put". Wrapper to
https://click.palletsprojects.com/en/7.x/api/#click.Context.command_path
Returns
-------
str
Full path of the command invoked
"""

# Uses Click's Core Context. Note, this is different from this class, also confusingly named `Context`.
# Click's Core Context object is the one that contains command path information.
click_core_ctx = click.get_current_context()
if click_core_ctx:
return click_core_ctx.command_path

@staticmethod
def get_current_context():
"""
Get the current Context object from Click's context stacks. This method is safe to run within the
actual command's handler that has a ``@pass_context`` annotation. Outside of the handler, you run
the risk of creating a new Context object which is entirely different from the Context object used by your
command.
.. code:
@pass_context
def my_command_handler(ctx):
# You will get the right context from within the command handler. This will also work from any
# downstream method invoked as part of the handler.
this_context = Context.get_current_context()
assert ctx == this_context
Returns
-------
samcli.cli.context.Context
Instance of this object, if we are running in a Click command. None otherwise.
"""

# Click has the concept of Context stacks. Think of them as linked list containing custom objects that are
# automatically accessible at different levels. We start from the Core Click context and discover the
# SAM CLI command-specific Context object which contains values for global options used by all commands.
#
# https://click.palletsprojects.com/en/7.x/complex/#ensuring-object-creation
#

click_core_ctx = click.get_current_context()
if click_core_ctx:
return click_core_ctx.find_object(Context) or click_core_ctx.ensure_object(Context)

def _refresh_session(self):
"""
Update boto3's default session by creating a new session based on values set in the context. Some properties of
Expand Down
205 changes: 205 additions & 0 deletions samcli/cli/global_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
"""
Provides global configuration helpers.
"""

import json
import logging
import uuid
import os

import click

try:
from pathlib import Path
except ImportError: # pragma: no cover
from pathlib2 import Path # pragma: no cover

LOG = logging.getLogger(__name__)

CONFIG_FILENAME = "metadata.json"
INSTALLATION_ID_KEY = "installationId"
TELEMETRY_ENABLED_KEY = "telemetryEnabled"


class GlobalConfig(object):
"""
Contains helper methods for global configuration files and values. Handles
configuration file creation, updates, and fetching in a platform-neutral way.
Generally uses '~/.aws-sam/' or 'C:\\Users\\<user>\\AppData\\Roaming\\AWS SAM' as
the base directory, depending on platform.
"""

def __init__(self, config_dir=None, installation_id=None, telemetry_enabled=None):
"""
Initializes the class, with options provided to assist with testing.
:param config_dir: Optional, overrides the default config directory path.
:param installation_id: Optional, will use this installation id rather than checking config values.
"""
self._config_dir = config_dir
self._installation_id = installation_id
self._telemetry_enabled = telemetry_enabled

@property
def config_dir(self):
if not self._config_dir:
# Internal Environment variable to customize SAM CLI App Dir. Currently used only by integ tests.
app_dir = os.getenv("__SAM_CLI_APP_DIR")
self._config_dir = Path(app_dir) if app_dir else Path(click.get_app_dir('AWS SAM', force_posix=True))

return Path(self._config_dir)

@property
def installation_id(self):
"""
Returns the installation UUID for this AWS SAM CLI installation. If the
installation id has not yet been set, it will be set before returning.
Examples
--------
>>> gc = GlobalConfig()
>>> gc.installation_id
"7b7d4db7-2f54-45ba-bf2f-a2cbc9e74a34"
>>> gc = GlobalConfig()
>>> gc.installation_id
None
Returns
-------
A string containing the installation UUID, or None in case of an error.
"""
if self._installation_id:
return self._installation_id
try:
self._installation_id = self._get_or_set_uuid(INSTALLATION_ID_KEY)
return self._installation_id
except (ValueError, IOError):
return None

@property
def telemetry_enabled(self):
"""
Check if telemetry is enabled for this installation. Default value of
False. It first tries to get value from SAM_CLI_TELEMETRY environment variable. If its not set,
then it fetches the value from config file.
To enable telemetry, set SAM_CLI_TELEMETRY environment variable equal to integer 1 or string '1'.
All other values including words like 'True', 'true', 'false', 'False', 'abcd' etc will disable Telemetry
Examples
--------
>>> gc = GlobalConfig()
>>> gc.telemetry_enabled
True
Returns
-------
Boolean flag value. True if telemetry is enabled for this installation,
False otherwise.
"""
if self._telemetry_enabled is not None:
return self._telemetry_enabled

# If environment variable is set, its value takes precedence over the value from config file.
env_name = "SAM_CLI_TELEMETRY"
if env_name in os.environ:
return os.getenv(env_name) in ('1', 1)

try:
self._telemetry_enabled = self._get_value(TELEMETRY_ENABLED_KEY)
return self._telemetry_enabled
except (ValueError, IOError) as ex:
LOG.debug("Error when retrieving telemetry_enabled flag", exc_info=ex)
return False

@telemetry_enabled.setter
def telemetry_enabled(self, value):
"""
Sets the telemetry_enabled flag to the provided boolean value.
Examples
--------
>>> gc = GlobalConfig()
>>> gc.telemetry_enabled
False
>>> gc.telemetry_enabled = True
>>> gc.telemetry_enabled
True
Raises
------
IOError
If there are errors opening or writing to the global config file.
JSONDecodeError
If the config file exists, and is not valid JSON.
"""
self._set_value("telemetryEnabled", value)
self._telemetry_enabled = value

def _get_value(self, key):
cfg_path = self._get_config_file_path(CONFIG_FILENAME)
if not cfg_path.exists():
return None
with open(str(cfg_path)) as fp:
body = fp.read()
json_body = json.loads(body)
return json_body.get(key)

def _set_value(self, key, value):
cfg_path = self._get_config_file_path(CONFIG_FILENAME)
if not cfg_path.exists():
return self._set_json_cfg(cfg_path, key, value)
with open(str(cfg_path)) as fp:
body = fp.read()
try:
json_body = json.loads(body)
except ValueError as ex:
LOG.debug("Failed to decode JSON in {cfg_path}", exc_info=ex)
raise ex
return self._set_json_cfg(cfg_path, key, value, json_body)

def _create_dir(self):
self.config_dir.mkdir(mode=0o700, parents=True, exist_ok=True)

def _get_config_file_path(self, filename):
self._create_dir()
filepath = self.config_dir.joinpath(filename)
return filepath

def _get_or_set_uuid(self, key):
"""
Special logic method for when we want a UUID to always be present, this
method behaves as a getter with side effects. Essentially, if the value
is not present, we will set it with a generated UUID.
If we have multiple such values in the future, a possible refactor is
to just be _get_or_set_value, where we also take a default value as a
parameter.
"""
cfg_value = self._get_value(key)
if cfg_value is not None:
return cfg_value
return self._set_value(key, str(uuid.uuid4()))

def _set_json_cfg(self, filepath, key, value, json_body=None):
"""
Special logic method to add a value to a JSON configuration file. This
method will write a new version of the file in question, so it will
either write a new file with only the first config value, or if a JSON
body is provided, it will upsert starting from that JSON body.
"""
json_body = json_body or {}
json_body[key] = value
file_body = json.dumps(json_body, indent=4) + "\n"
try:
with open(str(filepath), 'w') as f:
f.write(file_body)
except IOError as ex:
LOG.debug("Error writing to {filepath}", exc_info=ex)
raise ex
return value
Loading

0 comments on commit bdc7abf

Please sign in to comment.