Merge branch 'staging' into azure-initial-mgmt-grp

This commit is contained in:
tomdds 2020-02-10 17:44:19 -05:00 committed by GitHub
commit ccbc61f34f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
163 changed files with 637 additions and 3814 deletions

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null
},
"generated_at": "2020-01-27T19:24:43Z",
"generated_at": "2020-02-10T21:40:38Z",
"plugins_used": [
{
"base64_limit": 4.5,
@ -82,7 +82,7 @@
"hashed_secret": "afc848c316af1a89d49826c5ae9d00ed769415f3",
"is_secret": false,
"is_verified": false,
"line_number": 32,
"line_number": 33,
"type": "Secret Keyword"
}
],

View File

@ -101,5 +101,7 @@ RUN mkdir /var/run/uwsgi && \
chown -R atst:atat /var/run/uwsgi && \
chown -R atst:atat "${APP_DIR}"
RUN update-ca-certificates
# Run as the unprivileged APP user
USER atst

View File

@ -0,0 +1,29 @@
"""add last_sent column to clins and pdf_last_sent to task_orders
Revision ID: 567bfb019a87
Revises: 0039308c6351
Create Date: 2020-01-31 14:06:21.926019
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '567bfb019a87' # pragma: allowlist secret
down_revision = '0039308c6351' # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('clins', sa.Column('last_sent_at', sa.DateTime(), nullable=True))
op.add_column('task_orders', sa.Column('pdf_last_sent_at', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('task_orders', 'pdf_last_sent_at')
op.drop_column('clins', 'last_sent_at')
# ### end Alembic commands ###

View File

@ -233,12 +233,18 @@ def make_config(direct_config=None):
config.set("default", "DATABASE_URI", database_uri)
# Assemble REDIS_URI value
redis_use_tls = config["default"].getboolean("REDIS_TLS")
redis_uri = "redis{}://{}:{}@{}".format( # pragma: allowlist secret
("s" if config["default"].getboolean("REDIS_TLS") else ""),
("s" if redis_use_tls else ""),
(config.get("default", "REDIS_USER") or ""),
(config.get("default", "REDIS_PASSWORD") or ""),
config.get("default", "REDIS_HOST"),
)
if redis_use_tls:
tls_mode = config.get("default", "REDIS_SSLMODE")
tls_mode_str = tls_mode.lower() if tls_mode else "none"
redis_uri = f"{redis_uri}/?ssl_cert_reqs={tls_mode_str}"
config.set("default", "REDIS_URI", redis_uri)
return map_config(config)

View File

@ -1,6 +1,5 @@
import json
from secrets import token_urlsafe
from typing import Any, Dict
from uuid import uuid4
from atst.utils import sha256_hex
@ -1067,12 +1066,10 @@ class AzureCloudProvider(CloudProviderInterface):
def update_tenant_creds(self, tenant_id, secret: KeyVaultCredentials):
hashed = sha256_hex(tenant_id)
new_secrets = secret.dict()
curr_secrets = self._source_tenant_creds(tenant_id)
updated_secrets: Dict[str, Any] = {**curr_secrets.dict(), **new_secrets}
us = KeyVaultCredentials(**updated_secrets)
self.set_secret(hashed, json.dumps(us.dict()))
return us
updated_secrets = curr_secrets.merge_credentials(secret)
self.set_secret(hashed, json.dumps(updated_secrets.dict()))
return updated_secrets
def _source_tenant_creds(self, tenant_id) -> KeyVaultCredentials:
hashed = sha256_hex(tenant_id)
@ -1101,7 +1098,7 @@ class AzureCloudProvider(CloudProviderInterface):
"timeframe": "Custom",
"timePeriod": {"from": payload.from_date, "to": payload.to_date,},
"dataset": {
"granularity": "Daily",
"granularity": "Monthly",
"aggregation": {"totalCost": {"name": "PreTaxCost", "function": "Sum"}},
"grouping": [{"type": "Dimension", "name": "InvoiceId"}],
},

View File

@ -1,4 +1,5 @@
from uuid import uuid4
import pendulum
from .cloud_provider_interface import CloudProviderInterface
from .exceptions import (
@ -486,15 +487,26 @@ class MockCloudProvider(CloudProviderInterface):
self._maybe_raise(self.UNAUTHORIZED_RATE, self.AUTHORIZATION_EXCEPTION)
object_id = str(uuid4())
start_of_month = pendulum.today(tz="utc").start_of("month").replace(tzinfo=None)
this_month = start_of_month.to_atom_string()
last_month = start_of_month.subtract(months=1).to_atom_string()
two_months_ago = start_of_month.subtract(months=2).to_atom_string()
properties = CostManagementQueryProperties(
**dict(
columns=[
{"name": "PreTaxCost", "type": "Number"},
{"name": "UsageDate", "type": "Number"},
{"name": "BillingMonth", "type": "Datetime"},
{"name": "InvoiceId", "type": "String"},
{"name": "Currency", "type": "String"},
],
rows=[],
rows=[
[1.0, two_months_ago, "", "USD"],
[500.0, two_months_ago, "e05009w9sf", "USD"],
[50.0, last_month, "", "USD"],
[1000.0, last_month, "e0500a4qhw", "USD"],
[500.0, this_month, "", "USD"],
],
)
)

View File

@ -442,6 +442,15 @@ class KeyVaultCredentials(BaseModel):
return values
def merge_credentials(
self, new_creds: "KeyVaultCredentials"
) -> "KeyVaultCredentials":
updated_creds = {k: v for k, v in new_creds.dict().items() if v}
old_creds = self.dict()
old_creds.update(updated_creds)
return KeyVaultCredentials(**old_creds)
class SubscriptionCreationCSPPayload(BaseCSPPayload):
display_name: str

View File

@ -43,10 +43,12 @@ class AzureFileService(FileService):
from azure.storage.common import CloudStorageAccount
from azure.storage.blob import BlobSasPermissions
from azure.storage.blob.models import BlobPermissions
from azure.storage.blob.blockblobservice import BlockBlobService
self.CloudStorageAccount = CloudStorageAccount
self.BlobSasPermissions = BlobSasPermissions
self.BlobPermissions = BlobPermissions
self.BlockBlobService = BlockBlobService
def get_token(self):
@ -72,20 +74,22 @@ class AzureFileService(FileService):
return ({"token": sas_token}, object_name)
def generate_download_link(self, object_name, filename):
account = self.CloudStorageAccount(
block_blob_service = self.BlockBlobService(
account_name=self.account_name, account_key=self.storage_key
)
bbs = account.create_block_blob_service()
sas_token = bbs.generate_blob_shared_access_signature(
self.container_name,
object_name,
permission=self.BlobSasPermissions(read=True),
sas_token = block_blob_service.generate_blob_shared_access_signature(
container_name=self.container_name,
blob_name=object_name,
permission=self.BlobPermissions(read=True),
expiry=datetime.utcnow() + self.timeout,
content_disposition=f"attachment; filename={filename}",
protocol="https",
)
return bbs.make_blob_url(
self.container_name, object_name, protocol="https", sas_token=sas_token
return block_blob_service.make_blob_url(
container_name=self.container_name,
blob_name=object_name,
protocol="https",
sas_token=sas_token,
)
def download_task_order(self, object_name):

View File

@ -1,6 +1,6 @@
from collections import defaultdict
import json
from decimal import Decimal
import pendulum
def load_fixture_data():
@ -11,128 +11,25 @@ def load_fixture_data():
class MockReportingProvider:
FIXTURE_SPEND_DATA = load_fixture_data()
@classmethod
def get_portfolio_monthly_spending(cls, portfolio):
"""
returns an array of application and environment spending for the
portfolio. Applications and their nested environments are sorted in
alphabetical order by name.
[
{
name
this_month
last_month
total
environments [
{
name
this_month
last_month
total
}
]
}
]
"""
fixture_apps = cls.FIXTURE_SPEND_DATA.get(portfolio.name, {}).get(
"applications", []
)
def prepare_azure_reporting_data(rows: list):
"""
Returns a dict representing invoiced and estimated funds for a portfolio given
a list of rows from CostManagementQueryCSPResult.properties.rows
{
invoiced: Decimal,
estimated: Decimal
}
"""
for application in portfolio.applications:
if application.name not in [app["name"] for app in fixture_apps]:
fixture_apps.append({"name": application.name, "environments": []})
estimated = []
while rows:
if pendulum.parse(rows[-1][1]) >= pendulum.now(tz="utc").start_of("month"):
estimated.append(rows.pop())
else:
break
return sorted(
[
cls._get_application_monthly_totals(portfolio, fixture_app)
for fixture_app in fixture_apps
if fixture_app["name"]
in [application.name for application in portfolio.applications]
],
key=lambda app: app["name"],
)
@classmethod
def _get_environment_monthly_totals(cls, environment):
"""
returns a dictionary that represents spending totals for an environment e.g.
{
name
this_month
last_month
total
}
"""
return {
"name": environment["name"],
"this_month": sum(environment["spending"]["this_month"].values()),
"last_month": sum(environment["spending"]["last_month"].values()),
"total": sum(environment["spending"]["total"].values()),
}
@classmethod
def _get_application_monthly_totals(cls, portfolio, fixture_app):
"""
returns a dictionary that represents spending totals for an application
and its environments e.g.
{
name
this_month
last_month
total
environments: [
{
name
this_month
last_month
total
}
]
}
"""
application_envs = [
env
for env in portfolio.all_environments
if env.application.name == fixture_app["name"]
]
environments = [
cls._get_environment_monthly_totals(env)
for env in fixture_app["environments"]
if env["name"] in [e.name for e in application_envs]
]
for env in application_envs:
if env.name not in [env["name"] for env in environments]:
environments.append({"name": env.name})
return {
"name": fixture_app["name"],
"this_month": sum(env.get("this_month", 0) for env in environments),
"last_month": sum(env.get("last_month", 0) for env in environments),
"total": sum(env.get("total", 0) for env in environments),
"environments": sorted(environments, key=lambda env: env["name"]),
}
@classmethod
def get_spending_by_JEDI_clin(cls, portfolio):
"""
returns an dictionary of spending per JEDI CLIN for a portfolio
{
jedi_clin: {
invoiced
estimated
},
}
"""
if portfolio.name in cls.FIXTURE_SPEND_DATA:
CLIN_spend_dict = defaultdict(lambda: defaultdict(Decimal))
for application in cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]:
for environment in application["environments"]:
for clin, spend in environment["spending"]["this_month"].items():
CLIN_spend_dict[clin]["estimated"] += Decimal(spend)
for clin, spend in environment["spending"]["total"].items():
CLIN_spend_dict[clin]["invoiced"] += Decimal(spend)
return CLIN_spend_dict
return {}
return dict(
invoiced=Decimal(sum([row[0] for row in rows])),
estimated=Decimal(sum([row[0] for row in estimated])),
)

View File

@ -1,12 +1,13 @@
from flask import current_app
from itertools import groupby
from atst.domain.csp.cloud.models import (
ReportingCSPPayload,
CostManagementQueryCSPResult,
)
from atst.domain.csp.reports import prepare_azure_reporting_data
import pendulum
class Reports:
@classmethod
def monthly_spending(cls, portfolio):
return current_app.csp.reports.get_portfolio_monthly_spending(portfolio)
@classmethod
def expired_task_orders(cls, portfolio):
return [
@ -14,31 +15,19 @@ class Reports:
]
@classmethod
def obligated_funds_by_JEDI_clin(cls, portfolio):
clin_spending = current_app.csp.reports.get_spending_by_JEDI_clin(portfolio)
active_clins = portfolio.active_clins
for jedi_clin, clins in groupby(
active_clins, key=lambda clin: clin.jedi_clin_type
):
if not clin_spending.get(jedi_clin.name):
clin_spending[jedi_clin.name] = {}
clin_spending[jedi_clin.name]["obligated"] = sum(
clin.obligated_amount for clin in clins
)
def get_portfolio_spending(cls, portfolio):
# TODO: Extend this function to make from_date and to_date configurable
from_date = pendulum.now().subtract(years=1).add(days=1).format("YYYY-MM-DD")
to_date = pendulum.now().format("YYYY-MM-DD")
rows = []
output = []
for clin in clin_spending.keys():
invoiced = clin_spending[clin].get("invoiced", 0)
estimated = clin_spending[clin].get("estimated", 0)
obligated = clin_spending[clin].get("obligated", 0)
remaining = obligated - (invoiced + estimated)
output.append(
{
"name": clin,
"invoiced": invoiced,
"estimated": estimated,
"obligated": obligated,
"remaining": remaining,
}
if portfolio.csp_data:
payload = ReportingCSPPayload(
from_date=from_date, to_date=to_date, **portfolio.csp_data
)
return output
response: CostManagementQueryCSPResult = current_app.csp.cloud.get_reporting_data(
payload
)
rows = response.properties.rows
return prepare_azure_reporting_data(rows)

View File

@ -1,4 +1,5 @@
import datetime
from datetime import datetime
from sqlalchemy import or_
from atst.database import db
from atst.models.clin import CLIN
@ -40,7 +41,7 @@ class TaskOrders(BaseDomainClass):
@classmethod
def sign(cls, task_order, signer_dod_id):
task_order.signer_dod_id = signer_dod_id
task_order.signed_at = datetime.datetime.now()
task_order.signed_at = datetime.now()
db.session.add(task_order)
db.session.commit()
@ -76,3 +77,17 @@ class TaskOrders(BaseDomainClass):
task_order = TaskOrders.get(task_order_id)
db.session.delete(task_order)
db.session.commit()
@classmethod
def get_for_send_task_order_files(cls):
return (
db.session.query(TaskOrder)
.join(CLIN)
.filter(
or_(
TaskOrder.pdf_last_sent_at < CLIN.last_sent_at,
TaskOrder.pdf_last_sent_at.is_(None),
)
)
.all()
)

View File

@ -5,7 +5,7 @@ from flask import render_template
from jinja2 import contextfilter
from jinja2.exceptions import TemplateNotFound
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
from decimal import DivisionByZero as DivisionByZeroException
from decimal import DivisionByZero as DivisionByZeroException, InvalidOperation
def iconSvg(name):
@ -43,7 +43,7 @@ def obligatedFundingGraphWidth(values):
numerator, denominator = values
try:
return (numerator / denominator) * 100
except DivisionByZeroException:
except (DivisionByZeroException, InvalidOperation):
return 0

View File

@ -1,5 +1,7 @@
import pendulum
from flask import current_app as app
from smtplib import SMTPException
from azure.core.exceptions import AzureError
from atst.database import db
from atst.domain.application_roles import ApplicationRoles
@ -14,8 +16,10 @@ from atst.domain.csp.cloud.models import (
from atst.domain.environments import Environments
from atst.domain.portfolios import Portfolios
from atst.models import JobFailure
from atst.domain.task_orders import TaskOrders
from atst.models.utils import claim_for_update, claim_many_for_update
from atst.queue import celery
from atst.utils.localization import translate
class RecordFailure(celery.Task):
@ -43,8 +47,8 @@ class RecordFailure(celery.Task):
@celery.task(ignore_result=True)
def send_mail(recipients, subject, body):
app.mailer.send(recipients, subject, body)
def send_mail(recipients, subject, body, attachments=[]):
app.mailer.send(recipients, subject, body, attachments)
@celery.task(ignore_result=True)
@ -193,3 +197,39 @@ def dispatch_create_environment(self):
pendulum.now()
):
create_environment.delay(environment_id=environment_id)
@celery.task(bind=True)
def dispatch_create_atat_admin_user(self):
for environment_id in Environments.get_environments_pending_atat_user_creation(
pendulum.now()
):
create_atat_admin_user.delay(environment_id=environment_id)
@celery.task(bind=True)
def dispatch_send_task_order_files(self):
task_orders = TaskOrders.get_for_send_task_order_files()
recipients = [app.config.get("MICROSOFT_TASK_ORDER_EMAIL_ADDRESS")]
for task_order in task_orders:
subject = translate(
"email.task_order_sent.subject", {"to_number": task_order.number}
)
body = translate("email.task_order_sent.body", {"to_number": task_order.number})
try:
file = app.csp.files.download_task_order(task_order.pdf.object_name)
file["maintype"] = "application"
file["subtype"] = "pdf"
send_mail(
recipients=recipients, subject=subject, body=body, attachments=[file]
)
except (AzureError, SMTPException) as err:
app.logger.exception(err)
continue
task_order.pdf_last_sent_at = pendulum.now()
db.session.add(task_order)
db.session.commit()

View File

@ -1,5 +1,13 @@
from enum import Enum
from sqlalchemy import Column, Date, Enum as SQLAEnum, ForeignKey, Numeric, String
from sqlalchemy import (
Column,
Date,
DateTime,
Enum as SQLAEnum,
ForeignKey,
Numeric,
String,
)
from sqlalchemy.orm import relationship
from datetime import date
@ -29,6 +37,7 @@ class CLIN(Base, mixins.TimestampsMixin):
total_amount = Column(Numeric(scale=2), nullable=False)
obligated_amount = Column(Numeric(scale=2), nullable=False)
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=False)
last_sent_at = Column(DateTime)
#
# NOTE: For now obligated CLINS are CLIN 1 + CLIN 3

View File

@ -61,6 +61,10 @@ class Environment(
def portfolio_id(self):
return self.application.portfolio_id
@property
def is_pending(self):
return self.cloud_id is None
def __repr__(self):
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
self.name,

View File

@ -89,6 +89,12 @@ class Portfolio(
def active_task_orders(self):
return [task_order for task_order in self.task_orders if task_order.is_active]
@property
def total_obligated_funds(self):
return sum(
(task_order.total_obligated_funds for task_order in self.active_task_orders)
)
@property
def funding_duration(self):
"""

View File

@ -39,6 +39,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
pdf_attachment_id = Column(ForeignKey("attachments.id"))
_pdf = relationship("Attachment", foreign_keys=[pdf_attachment_id])
pdf_last_sent_at = Column(DateTime)
number = Column(String, unique=True,) # Task Order Number
signer_dod_id = Column(String)
signed_at = Column(DateTime)

View File

@ -39,6 +39,7 @@ def get_environments_obj_for_app(application):
{
"id": env.id,
"name": env.name,
"pending": env.is_pending,
"edit_form": EditEnvironmentForm(obj=env),
"member_count": len(env.roles),
"members": sorted(

View File

@ -34,25 +34,25 @@ def create_portfolio():
@user_can(Permissions.VIEW_PORTFOLIO_REPORTS, message="view portfolio reports")
def reports(portfolio_id):
portfolio = Portfolios.get(g.current_user, portfolio_id)
spending = Reports.get_portfolio_spending(portfolio)
obligated = portfolio.total_obligated_funds
remaining = obligated - (spending["invoiced"] + spending["estimated"])
current_obligated_funds = Reports.obligated_funds_by_JEDI_clin(portfolio)
current_obligated_funds = {
**spending,
"obligated": obligated,
"remaining": remaining,
}
if any(map(lambda clin: clin["remaining"] < 0, current_obligated_funds)):
if current_obligated_funds["remaining"] < 0:
flash("insufficient_funds")
# wrapped in str() because the sum of obligated funds returns a Decimal object
total_portfolio_value = str(
sum(
task_order.total_obligated_funds
for task_order in portfolio.active_task_orders
)
)
return render_template(
"portfolios/reports/index.html",
portfolio=portfolio,
total_portfolio_value=total_portfolio_value,
# wrapped in str() because the sum of obligated funds returns a Decimal object
total_portfolio_value=str(portfolio.total_obligated_funds),
current_obligated_funds=current_obligated_funds,
expired_task_orders=Reports.expired_task_orders(portfolio),
monthly_spending=Reports.monthly_spending(portfolio),
retrieved=datetime.now(), # mocked datetime of reporting data retrival
)

View File

@ -19,6 +19,9 @@ from atst.models import (
def get_resources_from_context(view_args):
query = None
if view_args is None:
view_args = {}
if "portfolio_token" in view_args:
query = (
db.session.query(Portfolio)

View File

@ -38,6 +38,7 @@ PGUSER = postgres
PORT=8000
REDIS_HOST=localhost:6379
REDIS_PASSWORD
REDIS_SSLMODE
REDIS_TLS=False
REDIS_USER
SECRET_KEY = change_me_into_something_secret

View File

@ -5,6 +5,13 @@ export default {
mixins: [ToggleMixin],
props: {
defaultVisible: {
type: Boolean,
default: false,
},
},
methods: {
toggle: function(e) {
if (this.$el.contains(e.target)) {

View File

@ -17,7 +17,7 @@ export default {
filename: {
type: String,
},
objectName: {
initialObjectName: {
type: String,
},
initialErrors: {
@ -42,6 +42,7 @@ export default {
filenameError: false,
downloadLink: '',
fileSizeLimit: this.sizeLimit,
objectName: this.initialObjectName,
}
},
@ -72,6 +73,7 @@ export default {
const response = await uploader.upload(file)
if (uploadResponseOkay(response)) {
this.attachment = e.target.value
this.objectName = uploader.objectName
this.$refs.attachmentFilename.value = file.name
this.$refs.attachmentObjectName.value = response.objectName
this.$refs.attachmentInput.disabled = true

View File

@ -9,6 +9,12 @@ export default {
unmask: [],
validationError: 'Please enter a response',
},
applicationName: {
mask: false,
match: /^[A-Za-z0-9\-_,'".\s]{4,100}$$/,
unmask: [],
validationError: 'Application names can be between 4-100 characters',
},
clinNumber: {
mask: false,
match: /^\d{4}$/,
@ -42,14 +48,14 @@ export default {
},
defaultStringField: {
mask: false,
match: /^[A-Za-z0-9\-_ \.]{1,100}$/,
match: /^[A-Za-z0-9\-_,'".\s]{1,1000}$/,
unmask: [],
validationError:
'Please enter a response of no more than 100 alphanumeric characters',
},
defaultTextAreaField: {
mask: false,
match: /^[A-Za-z0-9\-_ \.]{1,1000}$/,
match: /^[A-Za-z0-9\-_,'".\s]{1,1000}$/,
unmask: [],
validationError:
'Please enter a response of no more than 1000 alphanumeric characters',
@ -94,7 +100,7 @@ export default {
},
portfolioName: {
mask: false,
match: /^.{4,100}$/,
match: /^[A-Za-z0-9\-_,'".\s]{4,100}$$/,
unmask: [],
validationError: 'Portfolio names can be between 4-100 characters',
},

View File

@ -15,6 +15,7 @@ export default {
return {
changed: this.hasChanges,
valid: false,
submitted: false,
}
},
@ -36,15 +37,16 @@ export default {
handleSubmit: function(event) {
if (!this.valid) {
event.preventDefault()
this.submitted = true
}
},
},
computed: {
canSave: function() {
if (this.changed && this.valid) {
if (this.changed && this.valid && !this.submitted) {
return true
} else if (this.enableSave && this.valid) {
} else if (this.enableSave && this.valid && !this.submitted) {
return true
} else {
return false

View File

@ -98,3 +98,7 @@ hr {
.usa-section {
padding: 0;
}
.form {
margin-bottom: $action-footer-height + $large-spacing;
}

View File

@ -20,6 +20,7 @@ $max-panel-width: 90rem;
$home-pg-icon-width: 6rem;
$large-spacing: 4rem;
$max-page-width: $max-panel-width + $sidenav-expanded-width + $large-spacing;
$action-footer-height: 6rem;
/*
* USWDS Variables

View File

@ -42,6 +42,7 @@
border-top: 1px solid $color-gray-lighter;
z-index: 1;
width: 100%;
height: $action-footer-height;
&.action-group-footer--expand-offset {
padding-left: $sidenav-expanded-width;

View File

@ -228,6 +228,7 @@
&--validation {
&--anything,
&--applicationName,
&--portfolioName,
&--requiredField,
&--defaultStringField,

View File

@ -1,6 +1,5 @@
.task-order {
margin-top: $gap * 4;
margin-bottom: $footer-height;
width: 900px;
&__amount {

View File

@ -47,7 +47,7 @@
<span>
{{ env['name'] }}
</span>
{{ Label(type="pending_creation", classes='label--below')}}
{{ Label(type="pending_creation")}}
{%- endif %}
{% if user_can(permissions.EDIT_ENVIRONMENT) -%}
{{

View File

@ -14,7 +14,7 @@
action_new,
action_update) %}
<h3 id="application-members">
<h3 id="application-members">
{{ 'portfolios.applications.settings.team_members' | translate }}
</h3>
@ -22,7 +22,7 @@
{% include "fragments/flash.html" %}
{% endif %}
<div class="panel">
<div class="panel{% if new_member_form%} form{% endif %}">
{% if not application.members %}
<div class='empty-state empty-state--centered empty-state--white panel__content'>
<p class='empty-state__message'>

View File

@ -22,11 +22,11 @@
{% include "fragments/flash.html" %}
<base-form inline-template :enable-save="true">
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit">
<form method="POST" action="{{ action }}" v-on:submit="handleSubmit" class="form">
{{ form.csrf_token }}
<div class="form-row">
<div class="form-col">
{{ TextInput(form.name, validation="name", optional=False) }}
{{ TextInput(form.name, validation="applicationName", optional=False) }}
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
</div>
</div>

View File

@ -21,7 +21,7 @@
</p>
<hr>
<application-environments inline-template v-bind:initial-data='{{ form.data|tojson }}'>
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit">
<form method="POST" action="{{ url_for('applications.update_new_application_step_2', portfolio_id=portfolio.id, application_id=application.id) }}" v-on:submit="handleSubmit" class="form">
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>
<div class="panel">
<div class="panel__content">

View File

@ -22,7 +22,7 @@
<base-form inline-template>
<form method="POST" action="{{ url_for('applications.update', application_id=application.id) }}" class="col col--half">
{{ application_form.csrf_token }}
{{ TextInput(application_form.name, validation="name", optional=False) }}
{{ TextInput(application_form.name, validation="applicationName", optional=False) }}
{{ TextInput(application_form.description, validation="defaultTextAreaField", paragraph=True, optional=True, showOptional=False) }}
<div class="action-group action-group--tight">
{{ SaveButton(text='common.save_changes'|translate) }}

View File

@ -5,7 +5,7 @@
inline-template
{% if not field.errors %}
v-bind:filename='{{ field.filename.data | tojson }}'
v-bind:object-name='{{ field.object_name.data | tojson }}'
v-bind:initial-object-name='{{ field.object_name.data | tojson }}'
{% else %}
v-bind:initial-errors='true'
{% endif %}
@ -46,7 +46,7 @@
v-bind:value="attachment"
type="file">
<input type="hidden" name="{{ field.filename.name }}" id="{{ field.filename.name }}" ref="attachmentFilename">
<input type="hidden" name="{{ field.object_name.name }}" id="{{ field.object_name.name }}" ref="attachmentObjectName">
<input type="hidden" name="{{ field.object_name.name }}" id="{{ field.object_name.name }}" ref="attachmentObjectName" v-bind:value='objectName'>
</div>
<template v-if="uploadError">
<span class="usa-input__message">{{ "forms.task_order.upload_error" | translate }}</span>

View File

@ -19,11 +19,11 @@
{{ StickyCTA(text="portfolios.new.cta_step_1"|translate, context=("portfolios.new.sticky_header_context"|translate({"step": "1"}) )) }}
<base-form inline-template>
<div class="row">
<form id="portfolio-create" class="col" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
<form id="portfolio-create" class="col form" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
{{ form.csrf_token }}
<div class="form-row form-row--bordered">
<div class="form-col">
{{ TextInput(form.name, validation="name", optional=False, classes="form-col") }}
{{ TextInput(form.name, validation="portfolioName", optional=False, classes="form-col") }}
{{"forms.portfolio.name.help_text" | translate | safe }}
</div>
</div>

View File

@ -16,13 +16,12 @@
<th>PoP</th>
<th>CLIN Value</th>
<th>Amount Obligated</th>
<th>Amount Unspent</th>
</tr>
</thead>
<tbody>
{% for task_order in expired_task_orders %}
<tr>
<td colspan="5">
<td colspan="4">
<span class="h4 reporting-expended-funding__header">Task Order</span> <a href="{{ url_for("task_orders.view_task_order", task_order_id=task_order.id) }}">
{{ task_order.number }} {{ Icon("caret_right", classes="icon--tiny icon--blue" ) }}
</a>
@ -39,9 +38,8 @@
-
{{ clin.end_date | formattedDate(formatter="%b %d, %Y") }}
</td>
<td>{{ clin.total_amount | dollars }}</td>
<td>{{ clin.obligated_amount | dollars }}</td>
<td>{{ 0 | dollars }}</td>
<td class="table-cell--align-right">{{ clin.total_amount | dollars }}</td>
<td class="table-cell--align-right">{{ clin.obligated_amount | dollars }}</td>
<tr>
{% endfor %}
{% endfor %}

View File

@ -13,7 +13,5 @@
<hr>
{% include "portfolios/reports/obligated_funds.html" %}
{% include "portfolios/reports/expired_task_orders.html" %}
<hr>
{% include "portfolios/reports/application_and_env_spending.html" %}
</div>
{% endblock %}

View File

@ -7,61 +7,56 @@
</header>
<div class='panel'>
<div class='panel__content jedi-clin-funding'>
{% for JEDI_clin in current_obligated_funds | sort(attribute='name')%}
<div class="jedi-clin-funding__clin-wrapper">
<h3 class="h5 jedi-clin-funding__header">
{{ "JEDICLINType.{}".format(JEDI_clin.name) | translate }}
</h3>
<p class="jedi-clin-funding__subheader">Total obligated amount: {{ JEDI_clin.obligated | dollars }}</p>
<div class="jedi-clin-funding__graph">
{% if JEDI_clin.remaining < 0 %}
<span style="width:100%" class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--insufficient"></span>
{% else %}
{% set invoiced_width = (JEDI_clin.invoiced, JEDI_clin.obligated) | obligatedFundingGraphWidth %}
{% if invoiced_width %}
<span style="width:{{ invoiced_width }}%"
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--invoiced">
</span>
{% endif %}
{% set estimated_width = (JEDI_clin.estimated, JEDI_clin.obligated) | obligatedFundingGraphWidth %}
{% if estimated_width %}
<span style="width:{{ (JEDI_clin.estimated, JEDI_clin.obligated) | obligatedFundingGraphWidth }}%"
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--estimated">
</span>
{% endif %}
<span style="width:{{ (JEDI_clin.remaining, JEDI_clin.obligated) | obligatedFundingGraphWidth }}%"
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--remaining">
<div class="jedi-clin-funding__clin-wrapper">
<h3 class="h5 jedi-clin-funding__header">
Total obligated amount: {{ current_obligated_funds.obligated | dollars }}
</h3>
<div class="jedi-clin-funding__graph">
{% if current_obligated_funds.remaining < 0 %}
<span style="width:100%" class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--insufficient"></span>
{% else %}
{% set invoiced_width = (current_obligated_funds.invoiced, current_obligated_funds.obligated) | obligatedFundingGraphWidth %}
{% if invoiced_width %}
<span style="width:{{ invoiced_width }}%"
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--invoiced">
</span>
{% endif %}
{% set estimated_width = (current_obligated_funds.estimated, current_obligated_funds.obligated) | obligatedFundingGraphWidth %}
{% if estimated_width %}
<span style="width:{{ (current_obligated_funds.estimated, current_obligated_funds.obligated) | obligatedFundingGraphWidth }}%"
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--estimated">
</span>
{% endif %}
<span style="width:{{ (current_obligated_funds.remaining, current_obligated_funds.obligated) | obligatedFundingGraphWidth }}%"
class="jedi-clin-funding__graph-bar jedi-clin-funding__graph-bar--remaining">
</span>
{% endif %}
</div>
<div class="jedi-clin-funding__graph-values">
<div class="jedi-clin-funding__meta">
<p class="jedi-clin-funding__meta-header">
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--invoiced"></span>
Invoiced expended funds:
</p>
<p class="h3 jedi-clin-funding__meta-value">{{ current_obligated_funds.invoiced | dollars }}</p>
</div>
<div class="jedi-clin-funding__graph-values">
<div class="jedi-clin-funding__meta">
<p class="jedi-clin-funding__meta-header">
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--invoiced"></span>
Invoiced expended funds:
</p>
<p class="h3 jedi-clin-funding__meta-value">{{ JEDI_clin.invoiced | dollars }}</p>
</div>
<div class="jedi-clin-funding__meta">
<p class="jedi-clin-funding__meta-header">
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--estimated"></span>
Estimated expended funds:
</p>
<p class="h3 jedi-clin-funding__meta-value">{{ JEDI_clin.estimated | dollars }}</p>
</div>
<div class="jedi-clin-funding__meta">
<p class="jedi-clin-funding__meta-header">
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--{{"remaining" if JEDI_clin.remaining > 0 else "insufficient"}}"></span>
Remaining funds:
</p>
<p class="h3 jedi-clin-funding__meta-value {% if JEDI_clin.remaining < 0 %}text-danger{% endif %}">{{ JEDI_clin.remaining | dollars }}</p>
</div>
<div class="jedi-clin-funding__meta">
<p class="jedi-clin-funding__meta-header">
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--estimated"></span>
Estimated expended funds:
</p>
<p class="h3 jedi-clin-funding__meta-value">{{ current_obligated_funds.estimated | dollars }}</p>
</div>
<div class="jedi-clin-funding__meta">
<p class="jedi-clin-funding__meta-header">
<span class="jedi-clin-funding__meta-key jedi-clin-funding__meta-key--{{"remaining" if current_obligated_funds.remaining > 0 else "insufficient"}}"></span>
Remaining funds:
</p>
<p class="h3 jedi-clin-funding__meta-value {% if current_obligated_funds.remaining < 0 %}text-danger{% endif %}">{{ current_obligated_funds.remaining | dollars }}</p>
</div>
</div>
{% endfor %}
</div>
<div class="jedi-clin-funding__active-task-orders">
<h3 class="h4">
Active Task Orders

View File

@ -28,7 +28,7 @@
{% include "fragments/flash.html" %}
<div class="task-order">
<div class="task-order form">
{% block to_builder_form_field %}{% endblock %}
</div>
<div

View File

@ -1,2 +0,0 @@
.terraform
.vscode/

View File

@ -1,305 +0,0 @@
# ATAT Terraform
Welcome! You've found the ATAT IaC configurations.
ATAT is configured using terraform and a wrapper script called `secrets-tool`. With `terraform` we can configure infrastructure in a programatic way and ensure consistency across environments.
## Directory Structure
**modules/** - Terraform modules. These are modules that can be re-used for multiple environments.
**providers/** - Specific environment configurations. (dev,production, etc)
# Setup
Install the following requirements.
I highly recommend [tfenv](https://github.com/tfutils/tfenv) which will help you manage versions of TF and install new ones as needed. It gives you the ability to switch back and forth between versions as necessary, especially when doing upgrades and managing multiple environments. Think of it like `pyenv`.
Python is required for the `secrets-tool`. It is used to wrap terraform and pass secrets in to terraform from Azure KeyVault. This approach avoids leaving secrets on the filesystem in any way and allow for restricting access to secrets to specific operators.
Azure CLI is necessary for creating some intial resources, but is also used by the Python Azure SDK to make calls in some cases.
Requirements:
- [tfenv](https://github.com/tfutils/tfenv)
- Python 3.7
- Python pip
- Python virtualenv # FIXME: Switch to `pipenv`
- [azure cli](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest)
- [powershell](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-6) See below
# tfenv
`tfenv` will allow you to install TF versions. For example.
```
tfenv install 0.12.18
```
_0.12.18 at time of writing_
To select a version to use
```
tfenv use 0.12.18
```
# Powershell
Some things you need to use powershell. Specifically getting client profiles for the VPN.
## Install powershell on Linux
Powershell on recent versions of Ubuntu is available through snap.
For Ubuntu 19.10
```
snap install powershell --classic
```
# Preview Features
To create all the resources we need for this environment we'll need to enable some _Preview_ features.
This registers the specific feature for _SystemAssigned_ principals
```
az feature register --namespace Microsoft.ContainerService --name MSIPreview
az feature register --namespace Microsoft.ContainerService --name NodePublicIPPreview
```
To apply the registration, run the following
```
az provider register -n Microsoft.ContainerService
```
# Running Terraform
First, you'll need to log in to Azure. With the Azure CLI installed, you can run the following.
```
az login
```
Next, you'll need to initialize the environment. This process pulls down the terraform provider module from github as well as pulls in the modules that will be used by this provider/environment setup.
```
cd providers/dev/
terraform init
```
Once initialized, you can run a plan. A `plan` compares the terraform definitions you have configured in the provider directory (Ex. `providers/dev`) with what is in the shared state file in the Azure Object Storage (which all providers are currently configured for). This then also compares it to the state of the services which are running in Azure.
If nothing has been applied, you'll see all the resources defined in terraform as all new with a `+` next to the resource name. If the resource exists, but has changed, you'll see a `~` next to the resource and the delta of the change to be applied.
If you're plan looks good, you can run the apply.
```
terraform apply
```
Check the output for errors. Sometimes the syntax is valid, but some of the configuration may be wrong and only rejected by the Azure API at run time. If this is the case, fix your mistake, and re-run.
# After running TF (Manual Steps)
## VM Scale Set
After running terraform, we need to make a manual change to the VM Scale Set that is used in the kubernetes. Terraform has a bug that is not applying this as of `v1.40` of the `azurerm` provider.
In order to get the `SystemAssigned` identity to be set, it needs to be set manually in the console.
Navigate to the VM Scale Set for the k8s cluster you're managing (in the console).
![SystemAssigned Identity](images/system-assigned.png)
_Just click the `Status` to `On`_
## KeyVault Policy
There is a bug (missing feature really) in the `azurerm` terraform provider which exposes the wrong `object_id/principal_id` in the `azurerm_kubernetes_cluster` output. The `id` that it exposes is the `object_id` of the cluster itself, and _not_ the Virtual Machine Scale Set SystemAssigned identity. This needs to be updated manually after running terraform for the first time.
To update, just edit the `keyvault.tf`. Set the `principal_id` to the `object_id` of the Virtual Machine Scale set. This can be found in the Azure portal, or via cli.
```
az vmss list
```
In that list, find the scale set for the k8s cluster you're working on. You'll want the value of `principal_id`.
The error looks like the following
```
Warning FailedMount 8s (x6 over 25s) kubelet, aks-default-54410534-vmss000001 MountVolume.SetUp failed for volume "flask-secret" : mount command failed, status: Failure, reason: /etc/kubernetes/volumeplugins/azure~kv/azurekeyvault-flex
volume failed, Access denied. Caller was not found on any access policy. r nCaller: appid=e6651156-7127-432d-9617-4425177c48f1;oid=f9bcbe58-8b73-4957-aee2-133dc3e58063;numgroups=0;iss=https://sts.windows.net/b5ab0e1e-09f8-4258-afb7-fb17654bc5
b3/ r nVault: cloudzero-dev-keyvault;location=eastus2 InnerError={code:AccessDenied}
```
Final configuration will look like this.
**keyvault.tf**
```
module "keyvault" {
source = "../../modules/keyvault"
name = var.name
region = var.region
owner = var.owner
environment = var.environment
tenant_id = var.tenant_id
principal_id = "f9bcbe58-8b73-4957-aee2-133dc3e58063"
}
```
## Setting the Redis key in KeyVault
Redis auth is provided by a simple key that is randomly generated by Azure. This is a simple task for `secrets-tool`.
First, get the key from the portal. You can navigate to the redis cluster, and click on either "Show Keys", or "Access Keys"
![Redis Keys](images/redis-keys.png)
In order to set the secret, make sure you specify the keyvault that is used by the application. In dev, its simply called "keyvault", where the operator keyvault has a different name.
```
secrets-tool secrets --keyvault https://cloudzero-dev-keyvault.vault.azure.net/ create --key REDIS-PASSWORD --value "<redis key>"
```
You'll see output similar to the following if it was successful
```
2020-01-17 14:04:42,996 - utils.keyvault.secrets - DEBUG - Set value for key: REDIS-PASSWORD
```
## Setting the Azure Storage Key
Azure storage is very similar to how Redis has a generated key. This generated key is what is used at the time of writing this doc.
Grab the key from the "Access Keys" tab on the cloud storage bucket
![Cloud Storage Keys](images/azure-storage.png)
Now create the secret in KeyVault. This secret should also be in the application specific KeyVault.
```
secrets-tool secrets --keyvault https://cloudzero-dev-keyvault.vault.azure.net/ create --key AZURE-STORAGE-KEY --value "<storage key>"
```
You'll see output similar to the following if it was successful
```
2020-01-17 14:14:59,426 - utils.keyvault.secrets - DEBUG - Set value for key: AZURE-STORAGE-KEY
```
# Shutting down and environment
To shutdown and remove an environment completely as to not incur any costs you would need to run a `terraform destroy`.
```
terraform destroy
```
**This will destroy all resources defined in the provider so use with caution!! This will include things like KeyVault, Postgres, and so on. You may lose data!!**
# Advanced Terraform
## Targeted Apply
Sometimes you're writing a new module and don't want to make changes to anything else. In this case you can limit what TF changes.
```
terraform plan -target=module.vpc
```
In the above example, this will only run a plan (plan/apply/destroy) on the specific module. This can be a module, or resource. You can get a list of module and resources by running `terraform show`.
# VPN Setup
[Configure OpenVPN clients for Azure VPN Gateway](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-openvpn-clients#before-you-begin)
[About P2S VPN client profiles](https://docs.microsoft.com/en-us/azure/vpn-gateway/about-vpn-profile-download)
[Configure a VPN client for P2S OpenVPN protocol connections: Azure AD authentication (Preview)](https://docs.microsoft.com/en-us/azure/vpn-gateway/openvpn-azure-ad-client)
[Create an Azure Active Directory tenant for P2S OpenVPN protocol connections](https://docs.microsoft.com/en-us/azure/vpn-gateway/openvpn-azure-ad-tenant)
The docs above should help with client configuration. The last doc (Create an Azure Active Directory..) is necessary to run the command to add the VPN app for AD.
Copied here for convenience. Just enter this in your browser.
```
# For Public Azure - Government has a different URL, see doc above
https://login.microsoftonline.com/common/oauth2/authorize?client_id=41b23e61-6c1e-4545-b367-cd054e0ed4b4&response_type=code&redirect_uri=https://portal.azure.com&nonce=1234&prompt=admin_consent
```
## Adding a client
TODO
## Downloading a client profile
TODO
# Quick Steps
Copy paste (mostly)
*Register Preview features*
See [Registering Features](#Preview_Features)
*Edit provider.tf and turn off remote bucket temporarily (comment out backend {} section)*
```
provider "azurerm" {
version = "=1.40.0"
}
provider "azuread" {
# Whilst version is optional, we /strongly recommend/ using it to pin the version of the Provider being used
version = "=0.7.0"
}
terraform {
#backend "azurerm" {
#resource_group_name = "cloudzero-dev-tfstate"
#storage_account_name = "cloudzerodevtfstate"
#container_name = "tfstate"
#key = "dev.terraform.tfstate"
#}
}
```
`terraform init`
`terraform plan -target=module.tf_state`
Ensure the state bucket is created.
*create the container in the portal (or cli).*
This simply involves going to the bucket in the azure portal and creating the container.
Now is the tricky part. For this, we will be switching from local state (files) to remote state (stored in the azure bucket)
Uncomment the `backend {}` section in the `provider.tf` file. Once uncommented, we will re-run the init. This will attempt to copy the local state to the remote bucket.
`terraform init`
*Say `yes` to the question*
Now we need to update the Update `variables.tf` with the principals for the users in `admin_users` variable map. If these are not defined yet, just leave it as an empty set.
Next, we'll create the operator keyvault.
`terraform plan -target=module.operator_keyvault`
Next, we'll pre-populate some secrets using the secrets-tool. Follow the install/setup section in the README.md first. Then populate the secrets with a definition file as described in the following link.
https://github.com/dod-ccpo/atst/tree/staging/terraform/secrets-tool#populating-secrets-from-secrets-definition-file
*Create service principal for AKS*
```
az ad sp create-for-rbac
```
Take note of the output, you'll need it in the next step to store the secret and `client_id` in keyvault.
This also involves using secrets-tool. Substitute your keyvault url.
```
secrets-tool secrets --keyvault https://ops-jedidev-keyvault.vault.azure.net/ create --key k8s-client-id --value [value]
secrets-tool secrets --keyvault https://ops-jedidev-keyvault.vault.azure.net/ create --key k8s-client-secret --value [value]
```
*Next we'll apply the rest of the TF configuration*
`terraform plan` # Make sure this looks correct
`terraform apply`
*[Configure AD for MFA](https://docs.microsoft.com/en-us/azure/vpn-gateway/openvpn-azure-ad-mfa)*
*Then we need an instance of the container*
Change directories to the repo root. Ensure that you've checked out the staging or master branch:
`docker build . --build-arg CSP=azure -f ./Dockerfile -t atat:latest`
*Create secrets for ATAT database user*
Change directories back to terraform/secrets-tool. There is a sample file there. Make sure you know the URL for the aplication Key Vault (distinct from the operator Key Vault). Run:
`secrets-tool secrets --keyvault [application key vault URL] load -f ./postgres-user.yaml
*Create the database, database user, schema, and initial data set*
This is discussed in more detail [here](https://github.com/dod-ccpo/atst/tree/staging/terraform/secrets-tool#setting-up-the-initial-atat-database). Be sure to read the requirements section.
```
secrets-tool database --keyvault [operator key vault URL] provision --app-keyvault [application key vault URL] --dbname jedidev-atat --dbhost [database host name] --ccpo-users /full/path/to/users.yml
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

View File

@ -1,40 +0,0 @@
resource "azurerm_resource_group" "bucket" {
name = "${var.name}-${var.environment}-${var.service_name}"
location = var.region
}
resource "azurerm_storage_account" "bucket" {
name = var.service_name
resource_group_name = azurerm_resource_group.bucket.name
location = azurerm_resource_group.bucket.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "azurerm_storage_account_network_rules" "acls" {
resource_group_name = azurerm_resource_group.bucket.name
storage_account_name = azurerm_storage_account.bucket.name
default_action = var.policy
# Azure Storage CIDR ACLs do not accept /32 CIDR ranges.
ip_rules = [
for cidr in values(var.whitelist) : cidr
]
virtual_network_subnet_ids = var.subnet_ids
bypass = ["AzureServices"]
}
resource "azurerm_storage_container" "bucket" {
name = "content"
storage_account_name = azurerm_storage_account.bucket.name
container_access_type = var.container_access_type
}
# Added until requisite TF bugs are fixed. Typically this would be configured in the
# storage_account resource
resource "null_resource" "retention" {
provisioner "local-exec" {
command = "az storage logging update --account-name ${azurerm_storage_account.bucket.name} --log rwd --services bqt --retention 90"
}
}

View File

@ -1,48 +0,0 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}
variable "container_access_type" {
default = "private"
description = "Access type for the container (Default: private)"
type = string
}
variable "service_name" {
description = "Name of the service using this bucket"
type = string
}
variable "subnet_ids" {
description = "List of subnet_ids that will have access to this service"
type = list
}
variable "policy" {
description = "The default policy for the network access rules (Allow/Deny)"
default = "Deny"
type = string
}
variable "whitelist" {
type = map
description = "A map of whitelisted IPs and CIDR ranges. For single IPs, Azure expects just the IP, NOT a /32."
default = {}
}

View File

@ -1,43 +0,0 @@
resource "random_id" "server" {
keepers = {
azi_id = 1
}
byte_length = 8
}
resource "azurerm_resource_group" "cdn" {
name = "${var.name}-${var.environment}-cdn"
location = var.region
}
resource "azurerm_cdn_profile" "cdn" {
name = "${var.name}-${var.environment}-profile"
location = azurerm_resource_group.cdn.location
resource_group_name = azurerm_resource_group.cdn.name
sku = var.sku
}
resource "azurerm_cdn_endpoint" "cdn" {
name = "${var.name}-${var.environment}-${random_id.server.hex}"
profile_name = azurerm_cdn_profile.cdn.name
location = azurerm_resource_group.cdn.location
resource_group_name = azurerm_resource_group.cdn.name
origin {
name = "${var.name}-${var.environment}-origin"
host_name = var.origin_host_name
}
}
resource "azurerm_monitor_diagnostic_setting" "acr_diagnostic" {
name = "${var.name}-${var.environment}-acr-diag"
target_resource_id = azurerm_cdn_endpoint.cdn.id
log_analytics_workspace_id = var.workspace_id
log {
category = "CoreAnalytics"
retention_policy {
enabled = true
}
}
}

View File

@ -1,35 +0,0 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}
variable "sku" {
type = string
description = "SKU of which CDN to use"
default = "Standard_Verizon"
}
variable "origin_host_name" {
type = string
description = "Subdomain to use for the origin in requests to the CDN"
}
variable "workspace_id" {
description = "Log Analytics Workspace ID for sending logs generated by this resource"
type = string
}

View File

@ -1,67 +0,0 @@
locals {
whitelist = values(var.whitelist)
}
resource "azurerm_resource_group" "acr" {
name = "${var.name}-${var.environment}-acr"
location = var.region
}
resource "azurerm_container_registry" "acr" {
name = "${var.name}${var.environment}registry" # Alpha Numeric Only
resource_group_name = azurerm_resource_group.acr.name
location = azurerm_resource_group.acr.location
sku = var.sku
admin_enabled = var.admin_enabled
#georeplication_locations = [azurerm_resource_group.acr.location, var.backup_region]
network_rule_set {
default_action = var.policy
ip_rule = [
for cidr in values(var.whitelist) : {
action = "Allow"
ip_range = cidr
}
]
# Dynamic rule should work, but doesn't - See https://github.com/hashicorp/terraform/issues/22340#issuecomment-518779733
#dynamic "ip_rule" {
# for_each = values(var.whitelist)
# content {
# action = "Allow"
# ip_range = ip_rule.value
# }
#}
virtual_network = [
for subnet in var.subnet_ids : {
action = "Allow"
subnet_id = subnet
}
]
}
}
resource "azurerm_monitor_diagnostic_setting" "acr_diagnostic" {
name = "${var.name}-${var.environment}-acr-diag"
target_resource_id = azurerm_container_registry.acr.id
log_analytics_workspace_id = var.workspace_id
log {
category = "ContainerRegistryRepositoryEvents"
retention_policy {
enabled = true
}
}
log {
category = "ContainerRegistryLoginEvents"
retention_policy {
enabled = true
}
}
metric {
category = "AllMetrics"
retention_policy {
enabled = true
}
}
}

View File

@ -1,59 +0,0 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}
variable "backup_region" {
type = string
description = "Backup region for georeplicating the container registry"
}
variable "sku" {
type = string
description = "SKU to use for the container registry service"
default = "Premium"
}
variable "admin_enabled" {
type = string
description = "Admin enabled? (true/false default: false)"
default = false
}
variable "subnet_ids" {
description = "List of subnet_ids that will have access to this service"
type = list
}
variable "policy" {
description = "The default policy for the network access rules (Allow/Deny)"
default = "Deny"
type = string
}
variable "whitelist" {
type = map
description = "A map of whitelisted IPs and CIDR ranges. For single IPs, Azure expects just the IP, NOT a /32."
default = {}
}
variable "workspace_id" {
description = "The Log Analytics Workspace ID"
type = string
}

View File

@ -1,89 +0,0 @@
resource "azurerm_resource_group" "k8s" {
name = "${var.name}-${var.environment}-vpc"
location = var.region
}
resource "azurerm_kubernetes_cluster" "k8s" {
name = "${var.name}-${var.environment}-k8s"
location = azurerm_resource_group.k8s.location
resource_group_name = azurerm_resource_group.k8s.name
dns_prefix = var.k8s_dns_prefix
service_principal {
client_id = var.client_id
client_secret = var.client_secret
}
default_node_pool {
name = "default"
vm_size = "Standard_D1_v2"
os_disk_size_gb = 30
vnet_subnet_id = var.vnet_subnet_id
enable_node_public_ip = true # Nodes need a public IP for external resources. FIXME: Switch to NAT Gateway if its available in our subscription
enable_auto_scaling = var.enable_auto_scaling
max_count = var.max_count # FIXME: if auto_scaling disabled, set to 0
min_count = var.min_count # FIXME: if auto_scaling disabled, set to 0
}
identity {
type = "SystemAssigned"
}
lifecycle {
ignore_changes = [
default_node_pool.0.node_count
]
}
tags = {
environment = var.environment
owner = var.owner
}
}
resource "azurerm_monitor_diagnostic_setting" "k8s_diagnostic-1" {
name = "${var.name}-${var.environment}-k8s-diag"
target_resource_id = azurerm_kubernetes_cluster.k8s.id
log_analytics_workspace_id = var.workspace_id
log {
category = "kube-apiserver"
retention_policy {
enabled = true
}
}
log {
category = "kube-controller-manager"
retention_policy {
enabled = true
}
}
log {
category = "kube-scheduler"
retention_policy {
enabled = true
}
}
log {
category = "kube-audit"
retention_policy {
enabled = true
}
}
log {
category = "cluster-autoscaler"
retention_policy {
enabled = true
}
}
metric {
category = "AllMetrics"
retention_policy {
enabled = true
}
}
}
resource "azurerm_role_assignment" "k8s_network_contrib" {
scope = var.vnet_id
role_definition_name = "Network Contributor"
principal_id = azurerm_kubernetes_cluster.k8s.identity[0].principal_id
}

View File

@ -1,3 +0,0 @@
output "principal_id" {
value = azurerm_kubernetes_cluster.k8s.identity[0].principal_id
}

View File

@ -1,74 +0,0 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}
variable "k8s_dns_prefix" {
type = string
description = "A DNS prefix"
}
variable "k8s_node_size" {
type = string
description = "The size of the instance to use in the node pools for k8s"
default = "Standard_A1_v2"
}
variable "vnet_subnet_id" {
description = "Subnet to use for the default k8s pool"
type = string
}
variable "enable_auto_scaling" {
default = false
type = string
description = "Enable or disable autoscaling (Default: false)"
}
variable "max_count" {
default = 1
type = string
description = "Maximum number of nodes to use in autoscaling. This requires `enable_auto_scaling` to be set to true"
}
variable "min_count" {
default = 1
type = string
description = "Minimum number of nodes to use in autoscaling. This requires `enable_auto_scaling` to be set to true"
}
variable "client_id" {
type = string
description = "The client ID for the Service Principal associated with the AKS cluster."
}
variable "client_secret" {
type = string
description = "The client secret for the Service Principal associated with the AKS cluster."
}
variable "workspace_id" {
description = "Log Analytics workspace for this resource to log to"
type = string
}
variable "vnet_id" {
description = "The ID of the VNET that the AKS cluster app registration needs to provision load balancers in"
type = string
}

View File

@ -1,101 +0,0 @@
data "azurerm_client_config" "current" {}
resource "azurerm_resource_group" "keyvault" {
name = "${var.name}-${var.environment}-keyvault"
location = var.region
}
resource "azurerm_key_vault" "keyvault" {
name = "${var.name}-${var.environment}-keyvault"
location = azurerm_resource_group.keyvault.location
resource_group_name = azurerm_resource_group.keyvault.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "premium"
network_acls {
default_action = var.policy
bypass = "AzureServices"
virtual_network_subnet_ids = var.subnet_ids
ip_rules = values(var.whitelist)
}
tags = {
environment = var.environment
owner = var.owner
}
}
resource "azurerm_key_vault_access_policy" "keyvault_k8s_policy" {
count = length(var.principal_id) > 0 ? 1 : 0
key_vault_id = azurerm_key_vault.keyvault.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.principal_id
key_permissions = [
"get",
]
secret_permissions = [
"get",
]
}
# Admin Access
resource "azurerm_key_vault_access_policy" "keyvault_admin_policy" {
for_each = var.admin_principals
key_vault_id = azurerm_key_vault.keyvault.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = each.value
key_permissions = [
"get",
"list",
"create",
"update",
"delete",
]
secret_permissions = [
"get",
"list",
"set",
]
# backup create delete deleteissuers get getissuers import list listissuers managecontacts manageissuers purge recover restore setissuers update
certificate_permissions = [
"get",
"list",
"create",
"import",
"listissuers",
"manageissuers",
"deleteissuers",
"backup",
"update",
]
}
resource "azurerm_monitor_diagnostic_setting" "keyvault_diagnostic" {
name = "${var.name}-${var.environment}-keyvault-diag"
target_resource_id = azurerm_key_vault.keyvault.id
log_analytics_workspace_id = var.workspace_id
log {
category = "AuditEvent"
enabled = true
retention_policy {
enabled = true
}
}
metric {
category = "AllMetrics"
retention_policy {
enabled = true
}
}
}

View File

@ -1,7 +0,0 @@
output "id" {
value = azurerm_key_vault.keyvault.id
}
output "url" {
value = azurerm_key_vault.keyvault.vault_uri
}

View File

@ -1,57 +0,0 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of this environment"
}
variable "tenant_id" {
type = string
description = "The Tenant ID"
}
variable "principal_id" {
type = string
description = "The service principal_id of the k8s cluster"
}
variable "admin_principals" {
type = map
description = "A list of user principals who need access to manage the keyvault"
}
variable "subnet_ids" {
description = "List of subnet_ids that will have access to this service"
type = list
}
variable "policy" {
description = "The default policy for the network access rules (Allow/Deny)"
default = "Deny"
type = string
}
variable "whitelist" {
type = map
description = "A map of whitelisted IPs and CIDR ranges. For single IPs, Azure expects just the IP, NOT a /32."
default = {}
}
variable "workspace_id" {
description = "Log Analytics Workspace ID for sending logs generated by this resource"
type = string
}

View File

@ -1,27 +0,0 @@
resource "azurerm_resource_group" "lb" {
name = "${var.name}-${var.environment}-lb"
location = var.region
}
resource "azurerm_public_ip" "lb" {
name = "${var.name}-${var.environment}-ip"
location = var.region
resource_group_name = azurerm_resource_group.lb.name
allocation_method = "Static"
}
resource "azurerm_lb" "lb" {
name = "${var.name}-${var.environment}-lb"
location = var.region
resource_group_name = azurerm_resource_group.lb.name
frontend_ip_configuration {
name = "${var.name}-${var.environment}-ip"
public_ip_address_id = azurerm_public_ip.lb.id
}
tags = {
owner = var.owner
environment = var.environment
}
}

View File

@ -1,19 +0,0 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}

View File

@ -1,15 +0,0 @@
resource "azurerm_resource_group" "log_workspace" {
name = "${var.name}-${var.environment}-log-workspace"
location = var.region
}
resource "azurerm_log_analytics_workspace" "log_workspace" {
name = "${var.name}-${var.environment}-log-workspace"
location = azurerm_resource_group.log_workspace.location
resource_group_name = azurerm_resource_group.log_workspace.name
sku = "Premium"
tags = {
environment = var.environment
owner = var.owner
}
}

View File

@ -1,3 +0,0 @@
output "workspace_id" {
value = azurerm_log_analytics_workspace.log_workspace.id
}

View File

@ -1,19 +0,0 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}

View File

@ -1,20 +0,0 @@
resource "azurerm_resource_group" "identity" {
name = "${var.name}-${var.environment}-${var.identity}"
location = var.region
}
resource "azurerm_user_assigned_identity" "identity" {
resource_group_name = azurerm_resource_group.identity.name
location = azurerm_resource_group.identity.location
name = "${var.name}-${var.environment}-${var.identity}"
}
data "azurerm_subscription" "primary" {}
resource "azurerm_role_assignment" "roles" {
count = length(var.roles)
scope = data.azurerm_subscription.primary.id
role_definition_name = var.roles[count.index]
principal_id = azurerm_user_assigned_identity.identity.principal_id
}

View File

@ -1,11 +0,0 @@
output "id" {
value = azurerm_user_assigned_identity.identity.id
}
output "principal_id" {
value = azurerm_user_assigned_identity.identity.principal_id
}
output "client_id" {
value = azurerm_user_assigned_identity.identity.client_id
}

View File

@ -1,29 +0,0 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}
variable "identity" {
type = string
description = "Name of the managed identity to create"
}
variable "roles" {
type = list
description = "List of roles by name"
}

View File

@ -1,67 +0,0 @@
resource "azurerm_resource_group" "sql" {
name = "${var.name}-${var.environment}-postgres"
location = var.region
}
resource "azurerm_postgresql_server" "sql" {
name = "${var.name}-${var.environment}-sql"
location = azurerm_resource_group.sql.location
resource_group_name = azurerm_resource_group.sql.name
sku {
name = var.sku_name
capacity = var.sku_capacity
tier = var.sku_tier
family = var.sku_family
}
storage_profile {
storage_mb = var.storage_mb
backup_retention_days = var.storage_backup_retention_days
geo_redundant_backup = var.storage_geo_redundant_backup
auto_grow = var.storage_auto_grow
}
administrator_login = var.administrator_login
administrator_login_password = var.administrator_login_password
version = var.postgres_version
ssl_enforcement = var.ssl_enforcement
}
resource "azurerm_postgresql_virtual_network_rule" "sql" {
name = "${var.name}-${var.environment}-rule"
resource_group_name = azurerm_resource_group.sql.name
server_name = azurerm_postgresql_server.sql.name
subnet_id = var.subnet_id
ignore_missing_vnet_service_endpoint = true
}
resource "azurerm_postgresql_database" "db" {
name = "${var.name}-${var.environment}-atat"
resource_group_name = azurerm_resource_group.sql.name
server_name = azurerm_postgresql_server.sql.name
charset = "UTF8"
collation = "en-US"
}
resource "azurerm_monitor_diagnostic_setting" "postgresql_diagnostic" {
name = "${var.name}-${var.environment}-postgresql-diag"
target_resource_id = azurerm_postgresql_server.sql.id
log_analytics_workspace_id = var.workspace_id
log {
category = "PostgreSQLLogs"
enabled = true
retention_policy {
enabled = true
}
}
metric {
category = "AllMetrics"
retention_policy {
enabled = true
}
}
}

View File

@ -1,100 +0,0 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}
variable "subnet_id" {
type = string
description = "Subnet the SQL server should run"
}
variable "sku_name" {
type = string
description = "SKU name"
default = "GP_Gen5_2"
}
variable "sku_capacity" {
type = string
description = "SKU Capacity"
default = "2"
}
variable "sku_tier" {
type = string
description = "SKU Tier"
default = "GeneralPurpose"
}
variable "sku_family" {
type = string
description = "SKU Family"
default = "Gen5"
}
variable "storage_mb" {
type = string
description = "Size in MB of the storage used for the sql server"
default = "5120"
}
variable "storage_backup_retention_days" {
type = string
description = "Storage backup retention (days)"
default = "7"
}
variable "storage_geo_redundant_backup" {
type = string
description = "Geographic redundant backup (Enabled/Disabled)"
default = "Disabled"
}
variable "storage_auto_grow" {
type = string
description = "Auto Grow? (Enabled/Disabled)"
default = "Enabled"
}
variable "administrator_login" {
type = string
description = "Administrator login"
}
variable "administrator_login_password" {
type = string
description = "Administrator password"
}
variable "postgres_version" {
type = string
description = "Postgres version to use"
default = "10"
}
variable "ssl_enforcement" {
type = string
description = "Enforce SSL (Enabled/Disable)"
default = "Enabled"
}
variable "workspace_id" {
description = "Log Analytics workspace for this resource to log to"
type = string
}

View File

@ -1,38 +0,0 @@
resource "azurerm_resource_group" "redis" {
name = "${var.name}-${var.environment}-redis"
location = var.region
}
# NOTE: the Name used for Redis needs to be globally unique
resource "azurerm_redis_cache" "redis" {
name = "${var.name}-${var.environment}-redis"
location = azurerm_resource_group.redis.location
resource_group_name = azurerm_resource_group.redis.name
capacity = var.capacity
family = var.family
sku_name = var.sku_name
enable_non_ssl_port = var.enable_non_ssl_port
minimum_tls_version = var.minimum_tls_version
subnet_id = var.subnet_id
redis_configuration {
enable_authentication = var.enable_authentication
}
tags = {
environment = var.environment
owner = var.owner
}
}
resource "azurerm_monitor_diagnostic_setting" "redis_diagnostic" {
name = "${var.name}-${var.environment}-redis-diag"
target_resource_id = azurerm_redis_cache.redis.id
log_analytics_workspace_id = var.workspace_id
metric {
category = "AllMetrics"
retention_policy {
enabled = true
}
}
}

View File

@ -1,65 +0,0 @@
variable "region" {
type = string
description = "Region this module and resources will be created in"
}
variable "name" {
type = string
description = "Unique name for the services in this module"
}
variable "environment" {
type = string
description = "Environment these resources reside (prod, dev, staging, etc)"
}
variable "owner" {
type = string
description = "Owner of the environment and resources created in this module"
}
variable "capacity" {
type = string
default = 2
description = "The capacity of the redis cache"
}
variable "family" {
type = string
default = "C"
description = "The subscription family for redis"
}
variable "sku_name" {
type = string
default = "Standard"
description = "The sku to use"
}
variable "enable_non_ssl_port" {
type = bool
default = false
description = "Enable non TLS port (default: false)"
}
variable "minimum_tls_version" {
type = string
default = "1.2"
description = "Minimum TLS version to use"
}
variable "enable_authentication" {
type = bool
default = true
description = "Enable or disable authentication (default: true)"
}
variable "subnet_id" {
type = string
description = "Subnet ID that the service_endpoint should reside"
}
variable "workspace_id" {
description = "Log Analytics workspace for this resource to log to"
type = string
}

View File

@ -1,74 +0,0 @@
resource "azurerm_resource_group" "vpc" {
name = "${var.name}-${var.environment}-vpc"
location = var.region
tags = {
environment = var.environment
owner = var.owner
}
}
resource "azurerm_network_ddos_protection_plan" "vpc" {
count = var.ddos_enabled
name = "${var.name}-${var.environment}-ddos"
location = azurerm_resource_group.vpc.location
resource_group_name = azurerm_resource_group.vpc.name
}
resource "azurerm_virtual_network" "vpc" {
name = "${var.name}-${var.environment}-network"
location = azurerm_resource_group.vpc.location
resource_group_name = azurerm_resource_group.vpc.name
address_space = ["${var.virtual_network}"]
dns_servers = var.dns_servers
tags = {
environment = var.environment
owner = var.owner
}
}
resource "azurerm_subnet" "subnet" {
for_each = var.networks
name = "${var.name}-${var.environment}-${each.key}"
resource_group_name = azurerm_resource_group.vpc.name
virtual_network_name = azurerm_virtual_network.vpc.name
address_prefix = element(split(",", each.value), 0)
# See https://github.com/terraform-providers/terraform-provider-azurerm/issues/3471
lifecycle {
ignore_changes = [route_table_id]
}
service_endpoints = split(",", var.service_endpoints[each.key])
#delegation {
# name = "acctestdelegation"
#
# service_delegation {
# name = "Microsoft.ContainerInstance/containerGroups"
# actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
# }
#}
}
resource "azurerm_route_table" "route_table" {
for_each = var.route_tables
name = "${var.name}-${var.environment}-${each.key}"
location = azurerm_resource_group.vpc.location
resource_group_name = azurerm_resource_group.vpc.name
}
resource "azurerm_subnet_route_table_association" "route_table" {
for_each = var.networks
subnet_id = azurerm_subnet.subnet[each.key].id
route_table_id = azurerm_route_table.route_table[each.key].id
}
resource "azurerm_route" "route" {
for_each = var.route_tables
name = "${var.name}-${var.environment}-default"
resource_group_name = azurerm_resource_group.vpc.name
route_table_name = azurerm_route_table.route_table[each.key].name
address_prefix = "0.0.0.0/0"
next_hop_type = each.value
}

View File

@ -1,13 +0,0 @@
output "subnets" {
value = azurerm_subnet.subnet["private"].id #FIXED: this is now legacy, use subnet_list
}
output "subnet_list" {
value = {
for k, id in azurerm_subnet.subnet : k => id
}
}
output "id" {
value = azurerm_virtual_network.vpc.id
}

View File

@ -1,48 +0,0 @@
variable "environment" {
description = "Environment (Prod,Dev,etc)"
}
variable "region" {
description = "Region (useast2, etc)"
}
variable "name" {
description = "Name or prefix to use for all resources created by this module"
}
variable "owner" {
description = "Owner of these resources"
}
variable "ddos_enabled" {
description = "Enable or disable DDoS Protection (1,0)"
default = "0"
}
variable "virtual_network" {
description = "The supernet used for this VPC a.k.a Virtual Network"
type = string
}
variable "networks" {
description = "A map of lists describing the network topology"
type = map
}
variable "dns_servers" {
description = "DNS Server IPs for internal and public DNS lookups (must be on a defined subnet)"
type = list
}
variable "route_tables" {
type = map
description = "A map with the route tables to create"
}
variable "service_endpoints" {
type = map
description = "A map of the service endpoints and its mapping to subnets"
}

View File

@ -1,29 +0,0 @@
# Task order bucket is required to be accessible publicly by the users.
# which is why the policy here is "Allow"
module "task_order_bucket" {
source = "../../modules/bucket"
service_name = "jeditasksatat"
owner = var.owner
name = var.name
environment = var.environment
region = var.region
policy = "Allow"
subnet_ids = [module.vpc.subnets]
whitelist = var.storage_admin_whitelist
}
# TF State should be restricted to admins only, but IP protected
# This has to be public due to a chicken/egg issue of VPN not
# existing until TF is run. If this bucket is private, you would
# not be able to access it when running TF without being on a VPN.
module "tf_state" {
source = "../../modules/bucket"
service_name = "jedidevtfstate"
owner = var.owner
name = var.name
environment = var.environment
region = var.region
policy = "Deny"
subnet_ids = []
whitelist = var.storage_admin_whitelist
}

View File

@ -1,9 +0,0 @@
module "cdn" {
source = "../../modules/cdn"
origin_host_name = "staging.atat.code.mil"
owner = var.owner
environment = var.environment
name = var.name
region = var.region
workspace_id = module.logs.workspace_id
}

View File

@ -1,12 +0,0 @@
module "container_registry" {
source = "../../modules/container_registry"
name = var.name
region = var.region
environment = var.environment
owner = var.owner
backup_region = var.backup_region
policy = "Deny"
subnet_ids = [module.vpc.subnet_list["private"].id]
whitelist = var.admin_user_whitelist
workspace_id = module.logs.workspace_id
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@ -1,50 +0,0 @@
@startuml USEAST Development Network
title USEAST Development Network
cloud Internet
cloud Azure {
[Azure Storage] as storage
[Azure CDN] as cdn
cdn --> storage : "HTTPS/443"
note as cdn_note
CDN and Azure storage are
managed by Azure and configured
for geographic failover
end note
}
frame "USEAST Virtual Network" as vnet {
frame "Public Route Table" as public_rt{
frame "Public Subnet" as public_subnet {
[ALB]
[Internet] --> ALB
note as public_useast
10.1.1.0/24
end note
}
}
frame "Private Route Table" as private_rt{
frame "Private Subnet" as private_subnet {
[AKS]
[Redis]
[Postgres]
[AzurePrivateStorage]
AKS --> Redis : "TLS:6379"
AKS --> Postgres : "TLS:5432"
AKS --> AzurePrivateStorage : "HTTPS/443"
[ALB] --> AKS : "HTTPS:443"
note as private_useast
10.1.2.0/24
end note
}
}
}
frame "US West Backup Region" as backupregion {
component "Backup Postgres" as pgbackup
[Postgres] --> pgbackup : "Private Peering / TLS:5432"
}
note right of [ALB] : Azure Load Balancer restricted to AKS only
@enduml

View File

@ -1,40 +0,0 @@
@startuml USWEST Development Network
title USWEST Development Network
cloud Internet
frame "USEAST Virtual Network" as vnet {
frame "Public Route Table" as public_rt{
frame "Public Subnet" as public_subnet {
[ALB]
[Internet] --> ALB
note as public_useast
10.2.1.0/24
end note
}
}
frame "Private Route Table" as private_rt{
frame "Private Subnet" as private_subnet {
[AKS]
[Redis]
[Postgres]
[AzurePrivateStorage]
AKS --> Redis : "TLS:6379"
AKS --> Postgres : "TLS:5432"
AKS --> AzurePrivateStorage : "HTTPS/443"
[ALB] --> AKS : "HTTPS:443"
note as private_useast
10.2.2.0/24
end note
}
}
}
frame "USEAST Primary Region " as primary_region{
component "Postgres" as pgbackup
[Postgres] --> pgbackup : "Private Peering / TLS:5432"
}
note right of [ALB] : Azure Load Balancer restricted to AKS only
@enduml

View File

@ -1,10 +0,0 @@
module "keyvault_reader_identity" {
source = "../../modules/managed_identity"
name = var.name
owner = var.owner
environment = var.environment
region = var.region
identity = "${var.name}-${var.environment}-vault-reader"
roles = ["Reader", "Managed Identity Operator"]
}

View File

@ -1,43 +0,0 @@
data "azurerm_key_vault_secret" "k8s_client_id" {
name = "k8s-client-id"
key_vault_id = module.operator_keyvault.id
}
data "azurerm_key_vault_secret" "k8s_client_secret" {
name = "k8s-client-secret"
key_vault_id = module.operator_keyvault.id
}
module "k8s" {
source = "../../modules/k8s"
region = var.region
name = var.name
environment = var.environment
owner = var.owner
k8s_dns_prefix = var.k8s_dns_prefix
k8s_node_size = var.k8s_node_size
vnet_subnet_id = module.vpc.subnets #FIXME - output from module.vpc.subnets should be map
enable_auto_scaling = true
max_count = 5
min_count = 3
client_id = data.azurerm_key_vault_secret.k8s_client_id.value
client_secret = data.azurerm_key_vault_secret.k8s_client_secret.value
workspace_id = module.logs.workspace_id
vnet_id = module.vpc.id
}
#module "main_lb" {
# source = "../../modules/lb"
# region = var.region
# name = "main-${var.name}"
# environment = var.environment
# owner = var.owner
#}
#module "auth_lb" {
# source = "../../modules/lb"
# region = var.region
# name = "auth-${var.name}"
# environment = var.environment
# owner = var.owner
#}

View File

@ -1,15 +0,0 @@
module "keyvault" {
source = "../../modules/keyvault"
name = "cz"
region = var.region
owner = var.owner
environment = var.environment
tenant_id = var.tenant_id
principal_id = "f9bcbe58-8b73-4957-aee2-133dc3e58063"
admin_principals = var.admin_users
policy = "Deny"
subnet_ids = [module.vpc.subnets]
whitelist = var.admin_user_whitelist
workspace_id = module.logs.workspace_id
}

View File

@ -1,8 +0,0 @@
module "logs" {
source = "../../modules/log_analytics"
owner = var.owner
environment = var.environment
region = var.region
name = var.name
}

View File

@ -1,21 +0,0 @@
data "azurerm_key_vault_secret" "postgres_username" {
name = "postgres-root-user"
key_vault_id = module.operator_keyvault.id
}
data "azurerm_key_vault_secret" "postgres_password" {
name = "postgres-root-password"
key_vault_id = module.operator_keyvault.id
}
module "sql" {
source = "../../modules/postgres"
name = var.name
owner = var.owner
environment = var.environment
region = var.region
subnet_id = module.vpc.subnet_list["private"].id
administrator_login = data.azurerm_key_vault_secret.postgres_username.value
administrator_login_password = data.azurerm_key_vault_secret.postgres_password.value
workspace_id = module.logs.workspace_id
}

View File

@ -1,17 +0,0 @@
provider "azurerm" {
version = "=1.40.0"
}
provider "azuread" {
# Whilst version is optional, we /strongly recommend/ using it to pin the version of the Provider being used
version = "=0.7.0"
}
terraform {
backend "azurerm" {
resource_group_name = "cloudzero-jedidev-jedidevtfstate"
storage_account_name = "jedidevtfstate"
container_name = "tfstate"
key = "dev.terraform.tfstate"
}
}

View File

@ -1,11 +0,0 @@
module "redis" {
source = "../../modules/redis"
owner = var.owner
environment = var.environment
region = var.region
name = var.name
subnet_id = module.vpc.subnet_list["redis"].id
sku_name = "Premium"
family = "P"
workspace_id = module.logs.workspace_id
}

View File

@ -1,14 +0,0 @@
module "operator_keyvault" {
source = "../../modules/keyvault"
name = "ops"
region = var.region
owner = var.owner
environment = var.environment
tenant_id = var.tenant_id
principal_id = ""
admin_principals = var.admin_users
policy = "Deny"
subnet_ids = [module.vpc.subnets]
whitelist = var.admin_user_whitelist
workspace_id = module.logs.workspace_id
}

View File

@ -1,111 +0,0 @@
variable "environment" {
default = "jedidev"
}
variable "region" {
default = "eastus"
}
variable "backup_region" {
default = "westus2"
}
variable "owner" {
default = "dev"
}
variable "name" {
default = "cloudzero"
}
variable "virtual_network" {
type = string
default = "10.1.0.0/16"
}
variable "networks" {
type = map
default = {
#format
#name = "CIDR, route table, Security Group Name"
public = "10.1.1.0/24,public" # LBs
private = "10.1.2.0/24,private" # k8s, postgres, keyvault
redis = "10.1.3.0/24,private" # Redis
apps = "10.1.4.0/24,private" # Redis
}
}
variable "service_endpoints" {
type = map
default = {
public = "Microsoft.ContainerRegistry" # Not necessary but added to avoid infinite state loop
private = "Microsoft.Storage,Microsoft.KeyVault,Microsoft.ContainerRegistry,Microsoft.Sql"
redis = "Microsoft.Storage,Microsoft.Sql" # FIXME: There is no Microsoft.Redis
apps = "Microsoft.Storage,Microsoft.KeyVault,Microsoft.ContainerRegistry,Microsoft.Sql"
}
}
variable "route_tables" {
description = "Route tables and their default routes"
type = map
default = {
public = "Internet"
private = "Internet" # TODO: Switch to FW
redis = "VnetLocal"
apps = "Internet" # TODO: Switch to FW
}
}
variable "dns_servers" {
type = list
default = []
}
variable "k8s_node_size" {
type = string
default = "Standard_A1_v2"
}
variable "k8s_dns_prefix" {
type = string
default = "atat"
}
variable "tenant_id" {
type = string
default = "47f616e9-6ff5-4736-9b9e-b3f62c93a915"
}
variable "admin_users" {
type = map
default = {
"Rob Gil" = "cef37d01-1acf-4085-96c8-da9d34d0237e"
"Dan Corrigan" = "7e852ceb-eb0d-49b1-b71e-e9dcd1082ffc"
}
}
variable "admin_user_whitelist" {
type = map
default = {
"Rob Gil" = "66.220.238.246/32"
"Dan Corrigan Work" = "108.16.207.173/32"
"Dan Corrigan Home" = "71.162.221.27/32"
}
}
variable "storage_admin_whitelist" {
type = map
default = {
"Rob Gil" = "66.220.238.246"
"Dan Corrigan Work" = "108.16.207.173"
"Dan Corrigan Home" = "71.162.221.27"
}
}
variable "vpn_client_cidr" {
type = list
default = ["172.16.255.0/24"]
}

View File

@ -1,12 +0,0 @@
module "vpc" {
source = "../../modules/vpc/"
environment = var.environment
region = var.region
virtual_network = var.virtual_network
networks = var.networks
route_tables = var.route_tables
owner = var.owner
name = var.name
dns_servers = var.dns_servers
service_endpoints = var.service_endpoints
}

View File

@ -1,4 +0,0 @@
bin/
include/
lib/

View File

@ -1,63 +0,0 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
requests = "==2.22.0"
adal = "==1.2.2"
antlr4-python3-runtime = "==4.7.2"
applicationinsights = "==0.11.9"
argcomplete = "==1.10.3"
astroid = "==2.3.3"
azure-cli-core = "==2.0.77"
azure-cli-nspkg = "==3.0.4"
azure-cli-telemetry = "==1.0.4"
azure-common = "==1.1.23"
azure-core = "==1.1.1"
azure-identity = "==1.1.0"
azure-keyvault = "==4.0.0"
azure-keyvault-keys = "==4.0.0"
azure-keyvault-secrets = "==4.0.0"
azure-mgmt-resource = "==4.0.0"
azure-nspkg = "==3.0.2"
bcrypt = "==3.1.7"
certifi = "==2019.11.28"
cffi = "==1.13.2"
chardet = "==3.0.4"
click = "==7.0"
colorama = "==0.4.3"
coloredlogs = "==10.0"
cryptography = "==2.8"
humanfriendly = "==4.18"
idna = "==2.8"
isodate = "==0.6.0"
isort = "==4.3.21"
jmespath = "==0.9.4"
knack = "==0.6.3"
lazy-object-proxy = "==1.4.3"
mccabe = "==0.6.1"
msal = "==1.0.0"
msal-extensions = "==0.1.3"
msrest = "==0.6.10"
msrestazure = "==0.6.2"
oauthlib = "==3.1.0"
paramiko = "==2.7.1"
portalocker = "==1.5.2"
pycparser = "==2.19"
Pygments = "==2.5.2"
PyJWT = "==1.7.1"
pylint = "==2.4.4"
PyNaCl = "==1.3.0"
pyOpenSSL = "==19.1.0"
python-dateutil = "==2.8.1"
PyYAML = "==5.2"
requests-oauthlib = "==1.3.0"
six = "==1.13.0"
tabulate = "==0.8.6"
typed-ast = "==1.4.0"
urllib3 = "==1.25.7"
wrapt = "==1.11.2"
[dev-packages]
pytest = "*"

View File

@ -1,670 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "83c0cc35bbf74c0b7620a5b7f4f9fab4324e86442e12284d1b9ffcc12a371696"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"adal": {
"hashes": [
"sha256:5a7f1e037c6290c6d7609cab33a9e5e988c2fbec5c51d1c4c649ee3faff37eaf",
"sha256:fd17e5661f60634ddf96a569b95d34ccb8a98de60593d729c28bdcfe360eaad1"
],
"index": "pypi",
"version": "==1.2.2"
},
"antlr4-python3-runtime": {
"hashes": [
"sha256:168cdcec8fb9152e84a87ca6fd261b3d54c8f6358f42ab3b813b14a7193bb50b"
],
"index": "pypi",
"markers": "python_version >= '3.0'",
"version": "==4.7.2"
},
"applicationinsights": {
"hashes": [
"sha256:30a11aafacea34f8b160fbdc35254c9029c7e325267874e3c68f6bdbcd6ed2c3",
"sha256:b88bc5a41385d8e516489128d5e63f8c52efe597a3579b1718d1ab2f7cf150a2"
],
"index": "pypi",
"version": "==0.11.9"
},
"argcomplete": {
"hashes": [
"sha256:a37f522cf3b6a34abddfedb61c4546f60023b3799b22d1cd971eacdc0861530a",
"sha256:d8ea63ebaec7f59e56e7b2a386b1d1c7f1a7ae87902c9ee17d377eaa557f06fa"
],
"index": "pypi",
"version": "==1.10.3"
},
"astroid": {
"hashes": [
"sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a",
"sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"
],
"index": "pypi",
"version": "==2.3.3"
},
"azure-cli-core": {
"hashes": [
"sha256:4281b71cf9a8278f665765c97eb3dae61fbf2dac916fc032c4acdf5ed2065210",
"sha256:d14a733dd6d6019c23dbd0b459026fef0978901d263382a1fdb71852293b9e6a"
],
"index": "pypi",
"version": "==2.0.77"
},
"azure-cli-nspkg": {
"hashes": [
"sha256:1bde56090f548c6435bd3093995cf88e4c445fb040604df8b5b5f70780d79181",
"sha256:9a1e4f3197183470e4afecfdd45c92320f6753555b06a70651f89972332ffaf6"
],
"index": "pypi",
"version": "==3.0.4"
},
"azure-cli-telemetry": {
"hashes": [
"sha256:1f239d544d309c29e827982cc20113eb57037dba16db6cdd2e0283e437e0e577",
"sha256:7b18d7520e35e134136a0f7de38403a7dbce7b1e835065bd9e965579815ddf2f"
],
"index": "pypi",
"version": "==1.0.4"
},
"azure-common": {
"hashes": [
"sha256:53b1195b8f20943ccc0e71a17849258f7781bc6db1c72edc7d6c055f79bd54e3",
"sha256:99ef36e74b6395329aada288764ce80504da16ecc8206cb9a72f55fb02e8b484"
],
"index": "pypi",
"version": "==1.1.23"
},
"azure-core": {
"hashes": [
"sha256:4d047fd4e46a958c9b63f9d5cb52e6bf7dfc5c2a1c2a81b968499335a94bb5cb",
"sha256:b44fe5b46d2bb0260cafb737ab5ee89a16d478fc1885dabe21c426c4df205502"
],
"index": "pypi",
"version": "==1.1.1"
},
"azure-identity": {
"hashes": [
"sha256:0c8e540e1b75d48c54e5cd8d599f7ea5ccf4dae260c35bebb0c8d34d22b7c4f6",
"sha256:75f4ad9abfd191bd5f3de4c6dc29980b138bf5dfbbef9bca5c548a6048473bde"
],
"index": "pypi",
"version": "==1.1.0"
},
"azure-keyvault": {
"hashes": [
"sha256:76f75cb83929f312a08616d426ad6f597f1beae180131cf445876fb88f2c8ef1",
"sha256:e85f5bd6cb4f10b3248b99bbf02e3acc6371d366846897027d4153f18025a2d7"
],
"index": "pypi",
"version": "==4.0.0"
},
"azure-keyvault-keys": {
"hashes": [
"sha256:2983fa42e20a0e6bf6b87976716129c108e613e0292d34c5b0f0c8dc1d488e89",
"sha256:38c27322637a2c52620a8b96da1942ad6a8d22d09b5a01f6fa257f7a51e52ed0"
],
"index": "pypi",
"version": "==4.0.0"
},
"azure-keyvault-secrets": {
"hashes": [
"sha256:2eae9264a8f6f59277e1a9bfdbc8b0a15969ee5a80d8efe403d7744805b4a481",
"sha256:97a602406a833e8f117c540c66059c818f4321a35168dd17365fab1e4527d718"
],
"index": "pypi",
"version": "==4.0.0"
},
"azure-mgmt-resource": {
"hashes": [
"sha256:2b909f137469c7bfa541554c3d22eb918e9191c07667a42f2c6fc684e24ac83f",
"sha256:5022263349e66dba5ddadd3bf36208d82a00e0f1bb3288e32822fc821ccd1f76"
],
"index": "pypi",
"version": "==4.0.0"
},
"azure-nspkg": {
"hashes": [
"sha256:1d0bbb2157cf57b1bef6c8c8e5b41133957364456c43b0a43599890023cca0a8",
"sha256:31a060caca00ed1ebd369fc7fe01a56768c927e404ebc92268f4d9d636435e28",
"sha256:e7d3cea6af63e667d87ba1ca4f8cd7cb4dfca678e4c55fc1cedb320760e39dd0"
],
"index": "pypi",
"version": "==3.0.2"
},
"bcrypt": {
"hashes": [
"sha256:0258f143f3de96b7c14f762c770f5fc56ccd72f8a1857a451c1cd9a655d9ac89",
"sha256:0b0069c752ec14172c5f78208f1863d7ad6755a6fae6fe76ec2c80d13be41e42",
"sha256:19a4b72a6ae5bb467fea018b825f0a7d917789bcfe893e53f15c92805d187294",
"sha256:5432dd7b34107ae8ed6c10a71b4397f1c853bd39a4d6ffa7e35f40584cffd161",
"sha256:6305557019906466fc42dbc53b46da004e72fd7a551c044a827e572c82191752",
"sha256:69361315039878c0680be456640f8705d76cb4a3a3fe1e057e0f261b74be4b31",
"sha256:6fe49a60b25b584e2f4ef175b29d3a83ba63b3a4df1b4c0605b826668d1b6be5",
"sha256:74a015102e877d0ccd02cdeaa18b32aa7273746914a6c5d0456dd442cb65b99c",
"sha256:763669a367869786bb4c8fcf731f4175775a5b43f070f50f46f0b59da45375d0",
"sha256:8b10acde4e1919d6015e1df86d4c217d3b5b01bb7744c36113ea43d529e1c3de",
"sha256:9fe92406c857409b70a38729dbdf6578caf9228de0aef5bc44f859ffe971a39e",
"sha256:a190f2a5dbbdbff4b74e3103cef44344bc30e61255beb27310e2aec407766052",
"sha256:a595c12c618119255c90deb4b046e1ca3bcfad64667c43d1166f2b04bc72db09",
"sha256:c9457fa5c121e94a58d6505cadca8bed1c64444b83b3204928a866ca2e599105",
"sha256:cb93f6b2ab0f6853550b74e051d297c27a638719753eb9ff66d1e4072be67133",
"sha256:ce4e4f0deb51d38b1611a27f330426154f2980e66582dc5f438aad38b5f24fc1",
"sha256:d7bdc26475679dd073ba0ed2766445bb5b20ca4793ca0db32b399dccc6bc84b7",
"sha256:ff032765bb8716d9387fd5376d987a937254b0619eff0972779515b5c98820bc"
],
"index": "pypi",
"version": "==3.1.7"
},
"certifi": {
"hashes": [
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
],
"index": "pypi",
"version": "==2019.11.28"
},
"cffi": {
"hashes": [
"sha256:0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42",
"sha256:0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04",
"sha256:135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5",
"sha256:19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54",
"sha256:2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba",
"sha256:291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57",
"sha256:2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396",
"sha256:2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12",
"sha256:32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97",
"sha256:3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43",
"sha256:415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db",
"sha256:42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3",
"sha256:4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b",
"sha256:4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579",
"sha256:599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346",
"sha256:5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159",
"sha256:5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652",
"sha256:62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e",
"sha256:6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a",
"sha256:6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506",
"sha256:71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f",
"sha256:74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d",
"sha256:7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c",
"sha256:7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20",
"sha256:7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858",
"sha256:8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc",
"sha256:aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a",
"sha256:ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3",
"sha256:d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e",
"sha256:d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410",
"sha256:dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25",
"sha256:e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b",
"sha256:fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"
],
"index": "pypi",
"version": "==1.13.2"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"index": "pypi",
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"index": "pypi",
"version": "==7.0"
},
"colorama": {
"hashes": [
"sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff",
"sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"
],
"index": "pypi",
"version": "==0.4.3"
},
"coloredlogs": {
"hashes": [
"sha256:34fad2e342d5a559c31b6c889e8d14f97cb62c47d9a2ae7b5ed14ea10a79eff8",
"sha256:b869a2dda3fa88154b9dd850e27828d8755bfab5a838a1c97fbc850c6e377c36"
],
"index": "pypi",
"version": "==10.0"
},
"cryptography": {
"hashes": [
"sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
"sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
"sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
"sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
"sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
"sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
"sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
"sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
"sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
"sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
"sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
"sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
"sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
"sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
"sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
"sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
"sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
"sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
"sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
"sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
"sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
],
"index": "pypi",
"version": "==2.8"
},
"humanfriendly": {
"hashes": [
"sha256:23057b10ad6f782e7bc3a20e3cb6768ab919f619bbdc0dd75691121bbde5591d",
"sha256:33ee8ceb63f1db61cce8b5c800c531e1a61023ac5488ccde2ba574a85be00a85"
],
"index": "pypi",
"version": "==4.18"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"index": "pypi",
"version": "==2.8"
},
"isodate": {
"hashes": [
"sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8",
"sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"
],
"index": "pypi",
"version": "==0.6.0"
},
"isort": {
"hashes": [
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
],
"index": "pypi",
"version": "==4.3.21"
},
"jmespath": {
"hashes": [
"sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6",
"sha256:bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c"
],
"index": "pypi",
"version": "==0.9.4"
},
"knack": {
"hashes": [
"sha256:b1ac92669641b902e1aef97138666a21b8852f65d83cbde03eb9ddebf82ce121",
"sha256:bd240163d4e2ce9fc8535f77519358da0afd6c0ca19f001c639c3160b57630a9"
],
"index": "pypi",
"version": "==0.6.3"
},
"lazy-object-proxy": {
"hashes": [
"sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d",
"sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449",
"sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08",
"sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a",
"sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50",
"sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd",
"sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239",
"sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb",
"sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea",
"sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e",
"sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156",
"sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142",
"sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442",
"sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62",
"sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db",
"sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531",
"sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383",
"sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a",
"sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357",
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
],
"index": "pypi",
"version": "==1.4.3"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"index": "pypi",
"version": "==0.6.1"
},
"msal": {
"hashes": [
"sha256:c944b833bf686dfbc973e9affdef94b77e616cb52ab397e76cde82e26b8a3373",
"sha256:ecbe3f5ac77facad16abf08eb9d8562af3bc7184be5d4d90c9ef4db5bde26340"
],
"index": "pypi",
"version": "==1.0.0"
},
"msal-extensions": {
"hashes": [
"sha256:59e171a9a4baacdbf001c66915efeaef372fb424421f1a4397115a3ddd6205dc",
"sha256:c5a32b8e1dce1c67733dcdf8aa8bebcff5ab123e779ef7bc14e416bd0da90037"
],
"index": "pypi",
"version": "==0.1.3"
},
"msrest": {
"hashes": [
"sha256:56b8b5b4556fb2a92cac640df267d560889bdc9e2921187772d4691d97bc4e8d",
"sha256:f5153bfe60ee757725816aedaa0772cbfe0bddb52cd2d6db4cb8b4c3c6c6f928"
],
"index": "pypi",
"version": "==0.6.10"
},
"msrestazure": {
"hashes": [
"sha256:63db9f646fffc9244b332090e679d1e5f283ac491ee0cc321f5116f9450deb4a",
"sha256:fecb6a72a3eb5483e4deff38210d26ae42d3f6d488a7a275bd2423a1a014b22c"
],
"index": "pypi",
"version": "==0.6.2"
},
"oauthlib": {
"hashes": [
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
],
"index": "pypi",
"version": "==3.1.0"
},
"paramiko": {
"hashes": [
"sha256:920492895db8013f6cc0179293147f830b8c7b21fdfc839b6bad760c27459d9f",
"sha256:9c980875fa4d2cb751604664e9a2d0f69096643f5be4db1b99599fe114a97b2f"
],
"index": "pypi",
"version": "==2.7.1"
},
"portalocker": {
"hashes": [
"sha256:6f57aabb25ba176462dc7c63b86c42ad6a9b5bd3d679a9d776d0536bfb803d54",
"sha256:dac62e53e5670cb40d2ee4cdc785e6b829665932c3ee75307ad677cf5f7d2e9f"
],
"index": "pypi",
"version": "==1.5.2"
},
"pycparser": {
"hashes": [
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
],
"index": "pypi",
"version": "==2.19"
},
"pygments": {
"hashes": [
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b",
"sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"
],
"index": "pypi",
"version": "==2.5.2"
},
"pyjwt": {
"hashes": [
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
],
"index": "pypi",
"version": "==1.7.1"
},
"pylint": {
"hashes": [
"sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd",
"sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"
],
"index": "pypi",
"version": "==2.4.4"
},
"pynacl": {
"hashes": [
"sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255",
"sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c",
"sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e",
"sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae",
"sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621",
"sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56",
"sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39",
"sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310",
"sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1",
"sha256:53126cd91356342dcae7e209f840212a58dcf1177ad52c1d938d428eebc9fee5",
"sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a",
"sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786",
"sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b",
"sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b",
"sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f",
"sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20",
"sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415",
"sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715",
"sha256:bf459128feb543cfca16a95f8da31e2e65e4c5257d2f3dfa8c0c1031139c9c92",
"sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1",
"sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"
],
"index": "pypi",
"version": "==1.3.0"
},
"pyopenssl": {
"hashes": [
"sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504",
"sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507"
],
"index": "pypi",
"version": "==19.1.0"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"index": "pypi",
"version": "==2.8.1"
},
"pyyaml": {
"hashes": [
"sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc",
"sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803",
"sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc",
"sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15",
"sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075",
"sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd",
"sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31",
"sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f",
"sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c",
"sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04",
"sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"
],
"index": "pypi",
"version": "==5.2"
},
"requests": {
"hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
],
"index": "pypi",
"version": "==2.22.0"
},
"requests-oauthlib": {
"hashes": [
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
],
"index": "pypi",
"version": "==1.3.0"
},
"six": {
"hashes": [
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
],
"index": "pypi",
"version": "==1.13.0"
},
"tabulate": {
"hashes": [
"sha256:5470cc6687a091c7042cee89b2946d9235fe9f6d49c193a4ae2ac7bf386737c8"
],
"index": "pypi",
"version": "==0.8.6"
},
"typed-ast": {
"hashes": [
"sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
"sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e",
"sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0",
"sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c",
"sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47",
"sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631",
"sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4",
"sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34",
"sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b",
"sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2",
"sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e",
"sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a",
"sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233",
"sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1",
"sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36",
"sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d",
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
"sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
],
"index": "pypi",
"markers": "implementation_name == 'cpython' and python_version < '3.8'",
"version": "==1.4.0"
},
"urllib3": {
"hashes": [
"sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
"sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
],
"index": "pypi",
"version": "==1.25.7"
},
"wheel": {
"hashes": [
"sha256:9515fe0a94e823fd90b08d22de45d7bde57c90edce705b22f5e1ecf7e1b653c8",
"sha256:e721e53864f084f956f40f96124a74da0631ac13fbbd1ba99e8e2b5e9cafdf64"
],
"version": "==0.30.0"
},
"wrapt": {
"hashes": [
"sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"
],
"index": "pypi",
"version": "==1.11.2"
}
},
"develop": {
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"importlib-metadata": {
"hashes": [
"sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
"sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"
],
"markers": "python_version < '3.8'",
"version": "==1.4.0"
},
"more-itertools": {
"hashes": [
"sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39",
"sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"
],
"version": "==8.1.0"
},
"packaging": {
"hashes": [
"sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb",
"sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"
],
"version": "==20.0"
},
"pluggy": {
"hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"version": "==0.13.1"
},
"py": {
"hashes": [
"sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
"sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
],
"version": "==1.8.1"
},
"pyparsing": {
"hashes": [
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
],
"version": "==2.4.6"
},
"pytest": {
"hashes": [
"sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa",
"sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4"
],
"index": "pypi",
"version": "==5.3.2"
},
"six": {
"hashes": [
"sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
"sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
],
"index": "pypi",
"version": "==1.13.0"
},
"wcwidth": {
"hashes": [
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
"sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
],
"version": "==0.1.8"
},
"zipp": {
"hashes": [
"sha256:8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
"sha256:d38fbe01bbf7a3593a32bc35a9c4453c32bc42b98c377f9bff7e9f8da157786c"
],
"version": "==1.0.0"
}
}
}

Some files were not shown because too many files have changed in this diff Show More