Merge branch 'staging' of github-DDS:dod-ccpo/atst into gi-updates-wo-20191216

This commit is contained in:
Jay R. Newlin (PromptWorks) 2019-12-20 10:37:43 -05:00
commit 007291da0a
72 changed files with 877 additions and 701 deletions

View File

@ -3,7 +3,7 @@
"files": "^.secrets.baseline$|^.*pgsslrootcert.yml$",
"lines": null
},
"generated_at": "2019-12-13T20:38:57Z",
"generated_at": "2019-12-18T15:29:41Z",
"plugins_used": [
{
"base64_limit": 4.5,
@ -170,7 +170,7 @@
"hashed_secret": "e4f14805dfd1e6af030359090c535e149e6b4207",
"is_secret": false,
"is_verified": false,
"line_number": 659,
"line_number": 665,
"type": "Hex High Entropy String"
}
]

View File

@ -0,0 +1,26 @@
"""add uniqueness contraint to environment within an application
Revision ID: 08f2a640e9c2
Revises: c487d91f1a26
Create Date: 2019-12-16 10:43:12.331095
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = '08f2a640e9c2' # pragma: allowlist secret
down_revision = 'c487d91f1a26' # pragma: allowlist secret
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint('environments_name_application_id_key', 'environments', ['name', 'application_id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('environments_name_application_id_key', 'environments', type_='unique')
# ### end Alembic commands ###

View File

@ -11,7 +11,7 @@ from atst.models import (
ApplicationRoleStatus,
EnvironmentRole,
)
from atst.utils import first_or_none, update_or_raise_already_exists_error
from atst.utils import first_or_none, commit_or_raise_already_exists_error
class Applications(BaseDomainClass):
@ -28,7 +28,7 @@ class Applications(BaseDomainClass):
if environment_names:
Environments.create_many(user, application, environment_names)
update_or_raise_already_exists_error(message="application")
commit_or_raise_already_exists_error(message="application")
return application
@classmethod
@ -55,7 +55,7 @@ class Applications(BaseDomainClass):
)
db.session.add(application)
update_or_raise_already_exists_error(message="application")
commit_or_raise_already_exists_error(message="application")
return application
@classmethod

View File

@ -12,6 +12,7 @@ from atst.models import (
CLIN,
)
from atst.domain.environment_roles import EnvironmentRoles
from atst.utils import commit_or_raise_already_exists_error
from .exceptions import NotFoundError, DisabledError
@ -21,7 +22,7 @@ class Environments(object):
def create(cls, user, application, name):
environment = Environment(application=application, name=name, creator=user)
db.session.add(environment)
db.session.commit()
commit_or_raise_already_exists_error(message="environment")
return environment
@classmethod
@ -39,7 +40,8 @@ class Environments(object):
if name is not None:
environment.name = name
db.session.add(environment)
db.session.commit()
commit_or_raise_already_exists_error(message="environment")
return environment
@classmethod
def get(cls, environment_id):

View File

@ -4,7 +4,7 @@ from atst.database import db
from atst.models.clin import CLIN
from atst.models.task_order import TaskOrder, SORT_ORDERING
from . import BaseDomainClass
from atst.utils import update_or_raise_already_exists_error
from atst.utils import commit_or_raise_already_exists_error
class TaskOrders(BaseDomainClass):
@ -15,7 +15,7 @@ class TaskOrders(BaseDomainClass):
def create(cls, portfolio_id, number, clins, pdf):
task_order = TaskOrder(portfolio_id=portfolio_id, number=number, pdf=pdf)
db.session.add(task_order)
update_or_raise_already_exists_error(message="task_order")
commit_or_raise_already_exists_error(message="task_order")
TaskOrders.create_clins(task_order.id, clins)
return task_order
@ -34,7 +34,7 @@ class TaskOrders(BaseDomainClass):
task_order.number = number
db.session.add(task_order)
update_or_raise_already_exists_error(message="task_order")
commit_or_raise_already_exists_error(message="task_order")
return task_order
@classmethod

View File

@ -151,3 +151,6 @@ class SignatureForm(BaseForm):
translate("task_orders.sign.digital_signature_description"),
validators=[Required()],
)
confirm = BooleanField(
translate("task_orders.sign.confirmation_description"), validators=[Required()],
)

View File

@ -1,4 +1,4 @@
from sqlalchemy import Column, ForeignKey, String, TIMESTAMP
from sqlalchemy import Column, ForeignKey, String, TIMESTAMP, UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import JSONB
from enum import Enum
@ -38,6 +38,12 @@ class Environment(
primaryjoin="and_(EnvironmentRole.environment_id == Environment.id, EnvironmentRole.deleted == False)",
)
__table_args__ = (
UniqueConstraint(
"name", "application_id", name="environments_name_application_id_key"
),
)
class ProvisioningStatus(Enum):
PENDING = "pending"
COMPLETED = "completed"

View File

@ -1,9 +1,7 @@
from flask import redirect, render_template, request as http_request, url_for, g
from flask import redirect, render_template, request as http_request, url_for
from .blueprint import applications_bp
from atst.domain.applications import Applications
from atst.domain.exceptions import AlreadyExistsError
from atst.domain.portfolios import Portfolios
from atst.forms.application import NameAndDescriptionForm, EnvironmentsForm
from atst.domain.authz.decorator import user_can_access_decorator as user_can
from atst.models.permissions import Permissions
@ -13,6 +11,7 @@ from atst.routes.applications.settings import (
get_new_member_form,
handle_create_member,
handle_update_member,
handle_update_application,
)
@ -38,31 +37,6 @@ def render_new_application_form(
return render_template(template, **render_args)
def update_application(form, application_id=None, portfolio_id=None):
if form.validate():
application = None
try:
if application_id:
application = Applications.get(application_id)
application = Applications.update(application, form.data)
flash("application_updated", application_name=application.name)
else:
portfolio = Portfolios.get_for_update(portfolio_id)
application = Applications.create(
g.current_user, portfolio, **form.data
)
flash("application_created", application_name=application.name)
return application
except AlreadyExistsError:
flash("application_name_error", name=form.data["name"])
return False
else:
return False
@applications_bp.route("/portfolios/<portfolio_id>/applications/new")
@applications_bp.route("/applications/<application_id>/new/step_1")
@user_can(Permissions.CREATE_APPLICATION, message="view create new application form")
@ -90,7 +64,7 @@ def create_or_update_new_application_step_1(portfolio_id=None, application_id=No
form = get_new_application_form(
{**http_request.form}, NameAndDescriptionForm, application_id
)
application = update_application(form, application_id, portfolio_id)
application = handle_update_application(form, application_id, portfolio_id)
if application:
return redirect(

View File

@ -1,4 +1,10 @@
from flask import redirect, render_template, request as http_request, url_for, g
from flask import (
redirect,
render_template,
request as http_request,
url_for,
g,
)
from .blueprint import applications_bp
from atst.domain.exceptions import AlreadyExistsError
@ -10,6 +16,7 @@ from atst.domain.csp.cloud import GeneralCSPException
from atst.domain.common import Paginator
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.invitations import ApplicationInvitations
from atst.domain.portfolios import Portfolios
from atst.forms.application_member import NewForm as NewMemberForm, UpdateMemberForm
from atst.forms.application import NameAndDescriptionForm, EditEnvironmentForm
from atst.forms.data import ENV_ROLE_NO_ACCESS as NO_ACCESS
@ -245,16 +252,59 @@ def handle_update_member(application_id, application_role_id, form_data):
# TODO: flash error message
def handle_update_environment(form, application=None, environment=None):
if form.validate():
try:
if environment:
environment = Environments.update(
environment=environment, name=form.name.data
)
flash("application_environments_updated")
else:
environment = Environments.create(
g.current_user, application=application, name=form.name.data
)
flash("environment_added", environment_name=form.name.data)
return environment
except AlreadyExistsError:
flash("application_environments_name_error", name=form.name.data)
return False
else:
return False
def handle_update_application(form, application_id=None, portfolio_id=None):
if form.validate():
application = None
try:
if application_id:
application = Applications.get(application_id)
application = Applications.update(application, form.data)
flash("application_updated", application_name=application.name)
else:
portfolio = Portfolios.get_for_update(portfolio_id)
application = Applications.create(
g.current_user, portfolio, **form.data
)
flash("application_created", application_name=application.name)
return application
except AlreadyExistsError:
flash("application_name_error", name=form.data["name"])
return False
@applications_bp.route("/applications/<application_id>/settings")
@user_can(Permissions.VIEW_APPLICATION, message="view application edit form")
def settings(application_id):
application = Applications.get(application_id)
return render_settings_page(
application=application,
active_toggler=http_request.args.get("active_toggler"),
active_toggler_section=http_request.args.get("active_toggler_section"),
)
return render_settings_page(application=application,)
@applications_bp.route("/environments/<environment_id>/edit", methods=["POST"])
@ -264,31 +314,21 @@ def update_environment(environment_id):
application = environment.application
env_form = EditEnvironmentForm(obj=environment, formdata=http_request.form)
updated_environment = handle_update_environment(
form=env_form, application=application, environment=environment
)
if env_form.validate():
Environments.update(environment=environment, name=env_form.name.data)
flash("application_environments_updated")
if updated_environment:
return redirect(
url_for(
"applications.settings",
application_id=application.id,
fragment="application-environments",
_anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="edit",
)
)
else:
return (
render_settings_page(
application=application,
active_toggler=environment.id,
active_toggler_section="edit",
),
400,
)
return (render_settings_page(application=application, show_flash=True), 400)
@applications_bp.route(
@ -298,14 +338,9 @@ def update_environment(environment_id):
def new_environment(application_id):
application = Applications.get(application_id)
env_form = EditEnvironmentForm(formdata=http_request.form)
environment = handle_update_environment(form=env_form, application=application)
if env_form.validate():
Environments.create(
g.current_user, application=application, name=env_form.name.data
)
flash("environment_added", environment_name=env_form.data["name"])
if environment:
return redirect(
url_for(
"applications.settings",
@ -315,7 +350,7 @@ def new_environment(application_id):
)
)
else:
return (render_settings_page(application=application), 400)
return (render_settings_page(application=application, show_flash=True), 400)
@applications_bp.route("/applications/<application_id>/edit", methods=["POST"])
@ -323,10 +358,9 @@ def new_environment(application_id):
def update(application_id):
application = Applications.get(application_id)
form = NameAndDescriptionForm(http_request.form)
if form.validate():
application_data = form.data
Applications.update(application, application_data)
updated_application = handle_update_application(form, application_id)
if updated_application:
return redirect(
url_for(
"applications.portfolio_applications",
@ -334,22 +368,10 @@ def update(application_id):
)
)
else:
return render_settings_page(application=application, application_form=form)
@applications_bp.route("/applications/<application_id>/delete", methods=["POST"])
@user_can(Permissions.DELETE_APPLICATION, message="delete application")
def delete(application_id):
application = Applications.get(application_id)
Applications.delete(application)
flash("application_deleted", application_name=application.name)
return redirect(
url_for(
"applications.portfolio_applications", portfolio_id=application.portfolio_id
return (
render_settings_page(application=application, show_flash=True),
400,
)
)
@applications_bp.route("/environments/<environment_id>/delete", methods=["POST"])

View File

@ -56,13 +56,3 @@ def reports(portfolio_id):
monthly_spending=Reports.monthly_spending(portfolio),
retrieved=datetime.now(), # mocked datetime of reporting data retrival
)
@portfolios_bp.route("/portfolios/<portfolio_id>/destroy", methods=["POST"])
@user_can(Permissions.ARCHIVE_PORTFOLIO, message="archive portfolio")
def delete_portfolio(portfolio_id):
Portfolios.delete(portfolio=g.portfolio)
flash("portfolio_deleted", portfolio_name=g.portfolio.name)
return redirect(url_for("atst.home"))

View File

@ -30,7 +30,7 @@ def pick(keys, dct):
return {k: v for (k, v) in dct.items() if k in _keys}
def update_or_raise_already_exists_error(message):
def commit_or_raise_already_exists_error(message):
try:
db.session.commit()
except IntegrityError:

View File

@ -29,6 +29,11 @@ MESSAGES = {
""",
"category": "success",
},
"application_environments_name_error": {
"title_template": "",
"message_template": """{{ 'flash.application.env_name_error.message' | translate({ 'name': name }) }}""",
"category": "error",
},
"application_environments_updated": {
"title_template": "Application environments updated",
"message_template": "Application environments have been updated",

View File

@ -72,15 +72,18 @@ $CONTAINER_IMAGE \
# Use curl to wait for application container to become available
docker pull curlimages/curl:latest
echo "Waiting for application container to become available"
docker run --network atat \
curlimages/curl:latest \
curl --connect-timeout 3 \
curl \
--silent \
--connect-timeout 3 \
--max-time 5 \
--retry $CONTAINER_TIMEOUT \
--retry-connrefused \
--retry-delay 1 \
--retry-max-time $CONTAINER_TIMEOUT \
test-atat:8000
test-atat:8000 >/dev/null
# Run Ghost Inspector tests
docker pull ghostinspector/test-runner-standalone:latest

View File

@ -1,7 +1,8 @@
// Form Grid
.form-row {
margin: ($gap * 4) 0;
&--separated {
&--bordered {
border-bottom: $color-gray-lighter 1px solid;
}

View File

@ -88,5 +88,9 @@ p {
hr {
border: 0;
border-bottom: 1px solid $color-gray-light;
margin: ($gap * 3) ($site-margins * -4);
margin: ($gap * 3) 0;
&.full-width {
margin: ($gap * 3) ($site-margins * -4);
}
}

View File

@ -17,6 +17,7 @@ $usa-banner-height: 2.8rem;
$sidenav-expanded-width: 25rem;
$sidenav-collapsed-width: 10rem;
$max-panel-width: 80rem;
$home-pg-icon-width: 6rem;
/*
* USWDS Variables

View File

@ -46,7 +46,7 @@
background: white;
right: 0;
padding-right: $gap * 4;
border-top: 1px solid $color-gray-light;
border-top: 1px solid $color-gray-lighter;
width: 100%;
z-index: 1;
}

View File

@ -94,4 +94,19 @@
&--primary {
@include icon-color($color-primary);
}
&--home-pg-badge {
@include icon-size(27);
@include icon-color($color-white);
background-color: $color-primary;
height: $home-pg-icon-width;
width: $home-pg-icon-width;
border-radius: 100%;
display: inline-flex;
svg {
margin: auto;
}
}
}

View File

@ -165,6 +165,15 @@
margin-top: 0;
margin-bottom: 0;
}
label {
margin-left: 3rem;
&:before {
position: absolute;
left: -3rem;
}
}
}
select {

View File

@ -1,8 +1,3 @@
@mixin sidenav__header {
padding: $gap ($gap * 2);
font-weight: bold;
}
.sidenav-container {
box-shadow: $box-shadow;
overflow: hidden;
@ -26,34 +21,58 @@
margin: 0px;
}
&__title {
@include sidenav__header;
&__header {
padding: $gap ($gap * 2);
font-weight: bold;
border-bottom: 1px solid $color-gray-lighter;
font-size: $h3-font-size;
&--minimized {
@extend .sidenav__header;
padding: $gap;
width: $sidenav-collapsed-width;
}
}
&__title {
font-size: $h6-font-size;
text-transform: uppercase;
width: 50%;
color: $color-gray-dark;
opacity: 0.54;
white-space: nowrap;
padding: $gap;
display: inline-flex;
align-items: center;
}
&__toggle {
@include sidenav__header;
font-size: $small-font-size;
line-height: 2.8rem;
float: right;
color: $color-blue-darker;
color: $color-blue;
text-decoration: none;
padding: $gap;
display: inline-flex;
align-items: center;
.toggle-arrows {
vertical-align: middle;
@include icon-size(20);
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
ul {
&.sidenav__list--padded {
margin-top: 4 * $gap;
margin-top: 3 * $gap;
margin-bottom: $footer-height;
padding-bottom: $gap;
padding-bottom: ($gap * 2);
position: fixed;
overflow-y: scroll;
top: $topbar-height + $usa-banner-height + 4rem;
@ -69,6 +88,7 @@
li {
margin: 0;
display: block;
color: $color-black-light;
}
}
@ -89,100 +109,19 @@
&__link {
display: block;
padding: $gap ($gap * 2);
color: $color-black;
text-decoration: underline;
white-space: nowrap;
overflow: hidden;
color: $color-black-light;
text-decoration: none;
text-overflow: ellipsis;
&-icon {
margin-left: -($gap * 0.5);
}
&--disabled {
color: $color-shadow;
pointer-events: none;
}
&--add {
color: $color-blue;
font-size: $small-font-size;
.icon {
@include icon-color($color-blue);
@include icon-size(14);
}
}
&--active {
@include h4;
color: $color-primary;
background-color: $color-aqua-lightest;
box-shadow: inset ($gap / 2) 0 0 0 $color-primary;
.sidenav__link-icon {
@include icon-style-active;
}
box-shadow: inset ($gap / 2) 0 0 0 $color-primary-darker;
position: relative;
&_indicator .icon {
@include icon-color($color-primary);
position: absolute;
right: 0;
}
+ ul {
background-color: $color-primary;
.sidenav__link {
color: $color-white;
background-color: $color-primary;
&:hover {
background-color: $color-blue-darker;
}
&--active {
@include h5;
color: $color-white;
background-color: $color-primary;
box-shadow: none;
}
.icon {
@include icon-color($color-white);
}
}
}
}
+ ul {
li {
.sidenav__link {
@include h5;
padding: $gap * 0.75;
padding-left: 4.5rem;
border: 0;
font-weight: normal;
.sidenav__link-icon {
@include icon-size(12);
flex-shrink: 0;
margin-right: 1.5rem;
margin-left: -3rem;
}
.sidenav__link-label {
padding-left: 0;
}
}
}
color: $color-primary-darker;
}
&:hover {

View File

@ -1,49 +1,25 @@
.home {
margin: $gap * 3;
.sticky-cta {
margin: -1.6rem -1.6rem 0 -1.6rem;
}
}
.about-cloud {
margin: 4rem auto;
max-width: 900px;
}
&__content {
margin: 4rem;
max-width: 900px;
.your-project {
margin-top: 3rem;
padding: 3rem;
background-color: $color-gray-lightest;
&--descriptions {
.col {
margin-left: $home-pg-icon-width;
padding: ($gap * 2) ($gap * 4);
position: relative;
h2 {
margin-top: 0;
}
.links {
justify-content: flex-start;
.icon-link {
padding: $gap ($gap * 4);
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
&:hover {
background-color: transparent;
color: $color-gray-dark;
.svg * {
fill: $color-gray-dark;
.icon--home-pg-badge {
position: absolute;
left: -$home-pg-icon-width;
top: $gap * 3;
}
}
&.active:hover {
color: $color-blue;
}
}
}
}
@ -112,8 +88,3 @@
}
}
}
#jedi-heirarchy {
max-width: 65rem;
margin-top: $gap * 8;
}

View File

@ -20,10 +20,7 @@
}
&__header {
.h2,
p {
margin-bottom: $gap * 0.5;
}
margin-bottom: $gap * 6;
}
.col {
@ -155,6 +152,10 @@
}
}
}
&__confirmation {
margin-left: $gap * 8;
}
}
.task-order__modal-cancel {

View File

@ -100,7 +100,7 @@
{{ CheckboxInput(form.perms_env_mgmt, classes="input__inline-fields", key=env_mgmt, id=env_mgmt, optional=True) }}
{{ CheckboxInput(form.perms_del_env, classes="input__inline-fields", key=del_env, id=del_env, optional=True) }}
</div>
<hr>
<hr class="full-width">
<div class="environment_roles environment-roles-new">
<h2>{{ "portfolios.applications.members.form.env_access.title" | translate }}</h2>
<p class='usa-input__help subtitle'>

View File

@ -40,7 +40,7 @@
{% call Modal(modal_name, classes="form-content--app-mem") %}
<div class="modal__form--header">
<h1>{{ Icon('avatar') }} {{ "portfolios.applications.members.form.edit_access_header" | translate({ "user": member.user_name }) }}</h1>
<hr>
<hr class="full-width">
</div>
<base-form inline-template>
<form id='{{ modal_name }}' method="POST" action="{{ url_for(action_update, application_id=application.id, application_role_id=member.role_id,) }}">
@ -59,7 +59,7 @@
{% call Modal(resend_invite_modal, classes="form-content--app-mem") %}
<div class="modal__form--header">
<h1>{{ "portfolios.applications.members.new.verify" | translate }}</h1>
<hr>
<hr class="full-width">
</div>
<base-form inline-template :enable-save="true">
<form id='{{ resend_invite_modal }}' method="POST" action="{{ url_for('applications.resend_invite', application_id=application.id, application_role_id=member.role_id) }}">

View File

@ -2,7 +2,7 @@
{% import "applications/fragments/member_form_fields.html" as member_fields %}
{% macro MemberFormTemplate(title=None, next_button=None, previous=True) %}
<hr>
<hr class="full-width">
{% if title %} <h1>{{ title }}</h1> {% endif %}
{{ caller() }}

View File

@ -30,7 +30,7 @@
{{ ('portfolios.applications.new.step_1_form_help_text.name' | translate | safe) }}
</div>
</div>
<hr class="panel__break">
<hr>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ TextInput(form.description, paragraph=True, optional=True) }}

View File

@ -19,7 +19,7 @@
<p>
{{ 'portfolios.applications.new.step_2_description' | translate }}
</p>
<hr class="panel__break">
<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">
<div class="subheading">{{ 'portfolios.applications.environments_heading' | translate }}</div>

View File

@ -15,7 +15,7 @@
<p>
{{ ('portfolios.applications.new.step_3_description' | translate) }}
</p>
<hr class="panel__break">
<hr>
{{ MemberManagementTemplate(
application,

View File

@ -13,6 +13,9 @@
{% block application_content %}
{% if show_flash -%}
{% include "fragments/flash.html" %}
{%- endif %}
<h3>{{ 'portfolios.applications.settings.name_description' | translate }}</h3>
{% if user_can(permissions.EDIT_APPLICATION) %}
@ -59,59 +62,8 @@
environments_obj,
new_env_form) }}
{% if user_can(permissions.DELETE_APPLICATION) %}
{% set env_count = application.environments | length %}
{% if env_count == 1 %}
{% set pluralized_env = "environment" %}
{% else %}
{% set pluralized_env = "environments" %}
{% endif %}
<h3>
{{ "portfolios.applications.delete.subheading" | translate }}
</h3>
<div class="form-row">
<div class="form-col form-col--two-thirds">
{{ "portfolios.applications.delete.text" | translate({"application_name": application.name}) | safe }}
</div>
<div class="form-col form-col--third">
<div class="usa-input">
<input
id="delete-application"
type="button"
v-on:click="openModal('delete-application')"
class='usa-button--outline button-danger-outline'
value="{{ 'portfolios.applications.delete.button' | translate }}"
>
</div>
</div>
</div>
{% call Modal(name="delete-application") %}
<h1>{{ "portfolios.applications.delete.header" | translate }}</h1>
<hr>
{{
Alert(
title=("components.modal.destructive_title" | translate),
message=("portfolios.applications.delete.alert.message" | translate),
level="warning"
)
}}
{{
DeleteConfirmation(
modal_id="delete_application",
delete_text=('portfolios.applications.delete.button' | translate),
delete_action= url_for('applications.delete', application_id=application.id),
form=application_form
)
}}
{% endcall %}
{% endif %}
<hr>
{% if user_can(permissions.VIEW_APPLICATION_ACTIVITY_LOG) and config.get("USE_AUDIT_LOG", False) %}
<hr>
{% include "fragments/audit_events_log.html" %}
{{ Pagination(audit_events, url=url_for('applications.settings', application_id=application.id)) }}
{% endif %}

View File

@ -21,9 +21,7 @@
{% include 'navigation/topbar.html' %}
<div class='global-layout'>
{% if portfolios %}
{% include 'navigation/global_sidenav.html' %}
{% endif %}
{% include 'navigation/global_sidenav.html' %}
<div class='global-panel-container'>
{% block sidenav %}{% endblock %}

View File

@ -1,35 +1,11 @@
{% from "components/icon.html" import Icon %}
{% macro SidenavItem(label, href, active=False, icon=None, subnav=None) -%}
{% macro SidenavItem(label, href, active=False) -%}
<li>
<a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}">
{% if icon %}
{{ Icon(icon, classes="sidenav__link-icon") }}
{% endif %}
<span class="sidenav__link-label">
{{label}}
</span>
{% if active %}
<span class="sidenav__link-active_indicator">
{{ Icon("caret_right") }}
<a class="sidenav__link {% if active %}sidenav__link--active{% endif %}" href="{{href}}" title="{{label}}">
<span class="sidenav__link-label">
{{label}}
</span>
{% endif %}
</a>
{% if subnav and active %}
<ul>
{% for item in subnav %}
<li>
<a class="sidenav__link {% if item["active"] %}sidenav__link--active{% endif %}" href="{{item["href"]}}" title="{{item["label"]}}">
{% if "icon" in item %}
{{ Icon(item["icon"], classes="sidenav__link-icon") }}
{% endif %}
<span class="sidenav__link-label">{{item["label"]}}</span>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</a>
</li>
{%- endmacro %}

View File

@ -1,8 +1,7 @@
{% extends "base.html" %}
{% from "components/sticky_cta.html" import StickyCTA %}
{% from "components/icon.html" import Icon %}
{% from "components/semi_collapsible_text.html" import SemiCollapsibleText %}
{% from "components/sticky_cta.html" import StickyCTA %}
{% block content %}
@ -13,88 +12,56 @@
{% set sticky_header = "home.get_started" | translate %}
{% endif %}
{% call StickyCTA(sticky_header) %}
<a href="{{ url_for("portfolios.new_portfolio_step_1") }}" class="usa-button-primary">
{{ "home.add_portfolio_button_text" | translate }}
</a>
{% endcall %}
<div class="about-cloud">
<div class="home__content">
{% include "fragments/flash.html" %}
<h1>{{ "home.head" | translate }}</h1>
<h3>Set up a Portfolio</h3>
<h4>New Portfolios will be visible in the left side bar of this page. </h4>
<p>All TOs associated to a specific Application or set of related Applications will be entered at the Portfolio level. Funding is applied and managed at the Portfolio level as well.</p>
<hr>
{{ SemiCollapsibleText(first_half=("home.about_cloud.part1"|translate), second_half=("home.about_cloud.part2"|translate)) }}
<div class="your-project">
<h2 class="h3">{{ "home.your_project" | translate }}</h2>
<p>{{ "home.your_project_descrip" | translate }}</p>
<hr>
{% macro Link(icon, text, section, default=False) %}
{% if default %}
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}" || selectedSection === null}' v-on:click="toggleSection('{{ section }}')">
{% else %}
<div v-bind:class='{"icon-link": true, active: selectedSection === "{{ section }}"}' v-on:click="toggleSection('{{ section }}')">
{% endif %}
<div class="col">
<div class='icon-link--icon'>{{ Icon(icon) }}</div>
<div class='icon-link--name'>{{ text }}</div>
</div>
<div class="home__content--descriptions">
<div class="row">
<div class="col col--half col--pad">
{{ Icon('funding', classes="icon--home-pg-badge") }}
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.funding" | translate }}</h4>
<p>
{{ "home.funding_descrip" | translate }}
</p>
</div>
{% endmacro %}
<toggler inline-template v-bind:initial-selected-section="'funding'">
<div>
<div class="portfolio-header">
<div class="links row">
{{ Link(
icon='funding',
section='funding',
text='navigation.portfolio_navigation.breadcrumbs.funding' | translate,
default=True
) }}
{{ Link(
icon='applications',
section='applications',
text='navigation.portfolio_navigation.breadcrumbs.applications' | translate,
) }}
{{ Link(
icon='chart-pie',
section='reports',
text='navigation.portfolio_navigation.breadcrumbs.reports' | translate,
) }}
{{ Link(
icon='cog',
section='admin',
text='navigation.portfolio_navigation.breadcrumbs.admin' | translate,
) }}
</div>
</div>
{% macro Description(section, default=False) %}
{% if default %}
<p v-show="selectedSection === '{{ section }}' || selectedSection === null">
{% else %}
<p v-show="selectedSection === '{{ section }}'">
{% endif %}
<strong>
{{ "navigation.portfolio_navigation.breadcrumbs.%s" | format(section) | translate }}
</strong>
{{ "home.%s_descrip" | format(section) | translate }}
</p>
{% endmacro %}
<div class="project-section-descriptions">
{{ Description('funding', default=True) }}
{{ Description('applications') }}
{{ Description('reports') }}
{{ Description('admin') }}
</div>
<div class="col col--half col--pad">
{{ Icon('chart-pie', classes="icon--home-pg-badge") }}
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.reports" | translate }}</h4>
<p>
{{ "home.reports_descrip" | translate }}
</p>
</div>
</toggler>
</div>
<div class="row">
<div class="col col--half col--pad">
{{ Icon('applications', classes="icon--home-pg-badge") }}
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.applications" | translate }}</h4>
<p>
{{ "home.applications_descrip" | translate }}
</p>
</div>
<div class="col col--half col--pad">
{{ Icon('cog', classes="icon--home-pg-badge") }}
<h4>{{ "navigation.portfolio_navigation.breadcrumbs.admin" | translate }}</h4>
<p>
{{ "home.admin_descrip" | translate }}
</p>
</div>
</div>
</div>
<div class="empty-state">
<div class="empty-state__footer">
<a href="{{ url_for("portfolios.new_portfolio_step_1") }}" class="usa-button usa-button-primary">
{{ "home.add_portfolio_button_text" | translate }}
</a>
</div>
</div>
<img id='jedi-heirarchy' src="{{ url_for("static", filename="img/JEDIhierarchyDiagram.png")}}" alt="JEDI heirarchy diagram">
</div>
</main>

View File

@ -7,29 +7,29 @@
<div v-bind:class="{'sidenav-container': props.isVisible, 'sidenav-container--minimized': !props.isVisible}">
<div class="sidenav-container__fixed">
<div v-bind:class="{'sidenav': props.isVisible, 'sidenav--minimized': !props.isVisible}">
<a href="#" v-on:click="props.toggle" class="sidenav__toggle">
<div v-bind:class="{'sidenav__header': props.isVisible, 'sidenav__header--minimized': !props.isVisible}" class="row">
<template v-if="props.isVisible">
{{ Icon('angle-double-left-solid', classes="toggle-arrows icon--blue") }}
Hide
<span class="sidenav__title col col--grow">My Portfolios</span>
<a href="#" v-on:click="props.toggle" class="sidenav__toggle col">
{{ Icon('angle-double-left-solid', classes="toggle-arrows icon--primary") }}
<span>Hide</span>
</a>
</template>
<template v-else>
Show
{{ Icon('angle-double-right-solid', classes="toggle-arrows icon--blue") }}
<a href="#" v-on:click="props.toggle" class="sidenav__toggle col">
<span>Show</span>
{{ Icon('angle-double-right-solid', classes="toggle-arrows icon--primary") }}
</a>
</template>
</a>
</div>
<div v-if="props.isVisible">
<div class="sidenav__title">Portfolios</div>
<ul class="sidenav__list--padded">
{% if portfolios %}
{% for other_portfolio in portfolios|sort(attribute='name') %}
{{ SidenavItem(other_portfolio.name,
href=url_for("applications.portfolio_applications", portfolio_id=other_portfolio.id),
active=(other_portfolio.id | string) == request.view_args.get('portfolio_id')
) }}
{% endfor %}
{% else %}
<li><span class="sidenav__text">You have no portfolios yet</span></li>
{% endif %}
{% for other_portfolio in portfolios|sort(attribute='name') %}
{{ SidenavItem(other_portfolio.name,
href=url_for("applications.portfolio_applications", portfolio_id=other_portfolio.id),
active=(other_portfolio.id | string) == request.view_args.get('portfolio_id')
) }}
{% endfor %}
</ul>
</div>
</div>

View File

@ -56,10 +56,6 @@
{% include "portfolios/fragments/primary_point_of_contact.html" %}
{% endif %}
{% if user_can(permissions.ARCHIVE_PORTFOLIO) %}
{% include "portfolios/fragments/delete_portfolio.html" %}
{% endif %}
{% if user_can(permissions.VIEW_PORTFOLIO_USERS) %}
{% include "portfolios/fragments/portfolio_members.html" %}
{% endif %}

View File

@ -16,7 +16,7 @@
{% endmacro %}
{% set step_one %}
<hr>
<hr class="full-width">
<h1>Invite new portfolio member</h1>
<div class='form-row'>
<div class='form-col form-col--half'>
@ -52,7 +52,7 @@
</div>
{% endset %}
{% set step_two %}
<hr>
<hr class="full-width">
<h1>Assign member permissions</h1>
<a class='icon-link'>
{{ Icon('info') }}

View File

@ -5,7 +5,7 @@
{% from "components/options_input.html" import OptionsInput %}
{% set step_one %}
<hr>
<hr class="full-width">
<h1>{{ "fragments.ppoc.update_ppoc_title" | translate }}</h1>
{{
@ -42,7 +42,7 @@
{% endset %}
{% set step_two %}
<hr>
<hr class="full-width">
<h1>{{ "fragments.ppoc.update_ppoc_confirmation_title" | translate }}</h1>
{{

View File

@ -1,42 +0,0 @@
{% from "components/delete_confirmation.html" import DeleteConfirmation %}
{% from "components/alert.html" import Alert %}
{% from "components/modal.html" import Modal %}
<section id="primary-point-of-contact" class="panel">
<div class="panel__content">
<h2>{{ "fragments.delete_portfolio.title" | translate }}</h2>
<p>{{ "fragments.delete_portfolio.subtitle" | translate }}</p>
<div
class="usa-button-primary {% if applications_count == 0 %}button-danger{% else %}usa-button-disabled{% endif %}"
{% if applications_count == 0 %}v-on:click="openModal('delete_portfolio')"{% endif %}
>
{{ "common.deactivate" | translate }}
</div>
</div>
</section>
{% call Modal(name="delete_portfolio") %}
<h1>
{{ 'fragments.delete_portfolio.title' | translate }}
</h1>
<hr>
{{
Alert(
level="warning",
title=('components.modal.destructive_title' | translate),
message=('components.modal.destructive_message' | translate({"resource": "portfolio"})),
)
}}
{{
DeleteConfirmation(
modal_id='delete_portfolio',
delete_text='Deactivate',
delete_action=url_for('portfolios.delete_portfolio', portfolio_id=portfolio.id),
form=portfolio_form,
confirmation_text="deactivate",
)
}}
{% endcall %}

View File

@ -16,36 +16,36 @@
</div>
{{ StickyCTA(text="Create New Portfolio") }}
<base-form inline-template>
<form id="portfolio-create" action="{{ url_for('portfolios.create_portfolio') }}" method="POST">
{{ form.csrf_token }}
<div class="form-row form-row--separated">
<div class="form-col">
{{ TextInput(form.name, optional=False) }}
{{"forms.portfolio.name.help_text" | translate | safe }}
<div class="row">
<form id="portfolio-create" class="col" 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, optional=False, classes="form-col") }}
{{"forms.portfolio.name.help_text" | translate | safe }}
</div>
</div>
</div>
<div class="form-row form-row--separated">
<div class="form-col">
{{ TextInput(form.description, paragraph=True) }}
{{"forms.portfolio.description.help_text" | translate | safe }}
<div class="form-row form-row--bordered">
<div class="form-col">
{{ TextInput(form.description, paragraph=True) }}
{{"forms.portfolio.description.help_text" | translate | safe }}
</div>
</div>
</div>
<div class="form-row">
<div class="form-col">
{{ MultiCheckboxInput(form.defense_component, optional=False) }}
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
<div class="form-row">
<div class="form-col">
{{ MultiCheckboxInput(form.defense_component, optional=False) }}
{{ "forms.portfolio.defense_component.help_text" | translate | safe }}
</div>
</div>
</div>
<div class='action-group'>
{{
SaveButton(
text=('common.save' | translate),
form="portfolio-create",
element="input",
)
}}
</div>
</form>
<div class='action-group-footer'>
{% block next_button %}
{{ SaveButton(text=('common.save' | translate), form="portfolio-create", element="input") }}
{% endblock %}
<a href="{{ url_for('applications.portfolio_applications', portfolio_id=portfolio.id) }}">
Cancel
</a>
</form>
</div>
</base-form>
</main>
{% endblock %}

View File

@ -8,8 +8,26 @@
<form id="to_form" action='{{ action }}' method="POST" autocomplete="off" enctype="multipart/form-data">
{{ form.csrf_token }}
{% call StickyCTA(text=('task_orders.form.sticky_header_text' | translate({"step": step}) )) %}
<span class="action-group">
{{ StickyCTA(
text='task_orders.form.sticky_header_text' | translate,
context=('task_orders.form.sticky_header_context' | translate({"step": step}) )) }}
{% call Modal(name='cancel', dismissable=True) %}
<div class="task-order__modal-cancel">
<h1>Do you want to save this draft?</h1>
<div class="action-group">
<button formaction="{{ cancel_discard_url }}" class="usa-button usa-button-primary" type="submit">No, delete it</button>
<button formaction="{{ cancel_save_url }}" class="usa-button usa-button-primary" type="submit">Yes, save for later</button>
</div>
</div>
{% endcall %}
{% include "fragments/flash.html" %}
<div class="task-order">
{% block to_builder_form_field %}{% endblock %}
</div>
<span class="action-group-footer">
{% block next_button %}
<input
type="submit"
@ -32,23 +50,6 @@
{{ "common.cancel" | translate }}
</a>
</span>
{% endcall %}
{% call Modal(name='cancel', dismissable=True) %}
<div class="task-order__modal-cancel">
<h1>Do you want to save this draft?</h1>
<div class="action-group">
<button formaction="{{ cancel_discard_url }}" class="usa-button usa-button-primary" type="submit">No, delete it</button>
<button formaction="{{ cancel_save_url }}" class="usa-button usa-button-primary" type="submit">Yes, save for later</button>
</div>
</div>
{% endcall %}
{% include "fragments/flash.html" %}
<div class="task-order">
{% block to_builder_form_field %}{% endblock %}
</div>
</form>
</to-form>

View File

@ -1,8 +1,10 @@
{% macro TOFormStepHeader(title, description, to_number=None) %}
{% macro TOFormStepHeader(description, title=None, to_number=None) %}
<div class="task-order__header">
<div class="h2">
{{ title }}
</div>
{% if title -%}
<div class="h2">
{{ title }}
</div>
{%- endif %}
{% if to_number %}
<p>
<strong>Task Order Number:</strong> {{ to_number }}

View File

@ -1,7 +1,6 @@
{% extends "task_orders/builder_base.html" %}
{% from 'components/icon.html' import Icon %}
{% from "components/sticky_cta.html" import StickyCTA %}
{% from "task_orders/form_header.html" import TOFormStepHeader %}
{% from 'components/upload_input.html' import UploadInput %}

View File

@ -10,11 +10,16 @@
{% set step = "5" %}
{% block to_builder_form_field %}
{{ TOFormStepHeader('task_orders.form.step_5.title' | translate, 'task_orders.form.step_5.description' | translate, task_order.number) }}
{% call Alert('',
message="task_orders.form.step_5.alert_message" | translate
) %}
{{ TOFormStepHeader('task_orders.form.step_5.description' | translate, to_number=task_order.number) }}
<div class="task-order__confirmation">
{{ CheckboxInput(form.signature) }}
{% endcall %}
{{ CheckboxInput(form.confirm) }}
</div>
<hr>
<h5>
{{ "task_orders.sign.acknowledge.title" | translate }}
</h5>
<p>
{{ "task_orders.sign.acknowledge.text" | translate }}
</p>
{% endblock %}

View File

@ -0,0 +1,31 @@
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
}
}

View File

View File

@ -0,0 +1,31 @@
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"
}

View File

@ -0,0 +1,13 @@
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]
}

View File

@ -0,0 +1,37 @@
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
}

View File

@ -0,0 +1,22 @@
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
}
}

View File

View File

@ -0,0 +1,19 @@
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

@ -0,0 +1,24 @@
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
redis_configuration {
enable_authentication = var.enable_authentication
}
tags = {
environment = var.environment
owner = var.owner
}
}

View File

View File

@ -0,0 +1,60 @@
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)"
}

View File

@ -37,7 +37,7 @@ resource "azurerm_subnet" "subnet" {
# See https://github.com/terraform-providers/terraform-provider-azurerm/issues/3471
lifecycle {
ignore_changes = [route_table_id]
ignore_changes = [route_table_id]
}
#delegation {
# name = "acctestdelegation"
@ -57,16 +57,58 @@ resource "azurerm_route_table" "route_table" {
}
resource "azurerm_subnet_route_table_association" "route_table" {
for_each = var.networks
subnet_id = azurerm_subnet.subnet[each.key].id
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"
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
route_table_name = azurerm_route_table.route_table[each.key].name
address_prefix = "0.0.0.0/0"
next_hop_type = each.value
}
# Required for the gateway
resource "azurerm_subnet" "gateway" {
name = "GatewaySubnet"
resource_group_name = azurerm_resource_group.vpc.name
virtual_network_name = azurerm_virtual_network.vpc.name
address_prefix = var.gateway_subnet
}
resource "azurerm_public_ip" "vpn_ip" {
name = "test"
location = azurerm_resource_group.vpc.location
resource_group_name = azurerm_resource_group.vpc.name
allocation_method = "Dynamic"
}
resource "azurerm_virtual_network_gateway" "vnet_gateway" {
name = "test"
location = azurerm_resource_group.vpc.location
resource_group_name = azurerm_resource_group.vpc.name
type = "Vpn"
vpn_type = "RouteBased"
active_active = false
enable_bgp = false
sku = "Standard"
ip_configuration {
name = "vnetGatewayConfig"
public_ip_address_id = azurerm_public_ip.vpn_ip.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.gateway.id
}
vpn_client_configuration {
address_space = ["172.16.1.0/24"]
vpn_client_protocols = ["OpenVPN"]
}
}

View File

@ -41,3 +41,8 @@ variable "route_tables" {
type = map
description = "A map with the route tables to create"
}
variable "gateway_subnet" {
type = string
description = "The Subnet CIDR that we'll use for the virtual_network_gateway 'GatewaySubnet'"
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -0,0 +1,50 @@
@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

@ -0,0 +1,40 @@
@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

@ -9,3 +9,10 @@ module "k8s" {
vnet_subnet_id = module.vpc.subnets #FIXME - output from module.vpc.subnets should be map
}
module "lb" {
source = "../../modules/lb"
region = var.region
name = var.name
environment = var.environment
owner = var.owner
}

View File

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

View File

@ -7,6 +7,11 @@ variable "region" {
}
variable "backup_region" {
default = "westus2"
}
variable "owner" {
default = "dev"
}
@ -31,6 +36,12 @@ variable "networks" {
}
}
variable "gateway_subnet" {
type = string
default = "10.1.20.0/24"
}
variable "route_tables" {
description = "Route tables and their default routes"
type = map

View File

@ -4,6 +4,7 @@ module "vpc" {
region = var.region
virtual_network = var.virtual_network
networks = var.networks
gateway_subnet = var.gateway_subnet
route_tables = var.route_tables
owner = var.owner
name = var.name

View File

@ -4,7 +4,7 @@ from uuid import uuid4
from atst.domain.environments import Environments
from atst.domain.environment_roles import EnvironmentRoles
from atst.domain.exceptions import NotFoundError, DisabledError
from atst.domain.exceptions import AlreadyExistsError, DisabledError, NotFoundError
from atst.models.environment_role import CSPRole, EnvironmentRole
from tests.factories import (
@ -100,6 +100,27 @@ def test_update_environment():
assert environment.name == "name 2"
def test_create_does_not_duplicate_names_within_application():
application = ApplicationFactory.create()
name = "Your Environment"
user = application.portfolio.owner
assert Environments.create(user, application, name)
with pytest.raises(AlreadyExistsError):
Environments.create(user, application, name)
def test_update_does_not_duplicate_names_within_application():
application = ApplicationFactory.create()
name = "Your Environment"
environment = EnvironmentFactory.create(application=application, name=name)
dupe_env = EnvironmentFactory.create(application=application)
user = application.portfolio.owner
with pytest.raises(AlreadyExistsError):
Environments.update(dupe_env, name)
class EnvQueryTest:
@property
def NOW(self):

View File

@ -52,8 +52,6 @@ def test_updating_application_environments_success(client, user_session):
_external=True,
fragment="application-environments",
_anchor="application-environments",
active_toggler=environment.id,
active_toggler_section="edit",
)
assert environment.name == "new name a"
@ -78,6 +76,24 @@ def test_update_environment_failure(client, user_session):
assert environment.name == "original name"
def test_enforces_unique_env_name(client, user_session, session):
application = ApplicationFactory.create()
user = application.portfolio.owner
name = "New Environment"
environment = EnvironmentFactory.create(application=application, name=name)
form_data = {"name": name}
user_session(user)
session.begin_nested()
response = client.post(
url_for("applications.new_environment", application_id=application.id),
data=form_data,
)
session.rollback()
assert response.status_code == 400
def test_application_settings(client, user_session):
portfolio = PortfolioFactory.create()
application = Applications.create(
@ -258,6 +274,23 @@ def test_user_without_permission_cannot_update_application(client, user_session)
assert application.description == "Cool stuff happening here!"
def test_update_application_enforces_unique_name(client, user_session, session):
portfolio = PortfolioFactory.create()
name = "Test Application"
application = ApplicationFactory.create(portfolio=portfolio, name=name)
dupe_application = ApplicationFactory.create(portfolio=portfolio)
user_session(portfolio.owner)
session.begin_nested()
response = client.post(
url_for("applications.update", application_id=dupe_application.id),
data={"name": name, "description": dupe_application.description},
)
session.rollback()
assert response.status_code == 400
def test_user_can_only_access_apps_in_their_portfolio(client, user_session):
portfolio = PortfolioFactory.create()
other_portfolio = PortfolioFactory.create(
@ -288,41 +321,6 @@ def test_user_can_only_access_apps_in_their_portfolio(client, user_session):
assert time_updated == other_application.time_updated
def test_delete_application(client, user_session):
user = UserFactory.create()
port = PortfolioFactory.create(
owner=user,
applications=[
{
"name": "mos eisley",
"environments": [
{"name": "bar"},
{"name": "booth"},
{"name": "band stage"},
],
}
],
)
application = port.applications[0]
user_session(user)
response = client.post(
url_for("applications.delete", application_id=application.id)
)
# appropriate response and redirect
assert response.status_code == 302
assert response.location == url_for(
"applications.portfolio_applications", portfolio_id=port.id, _external=True
)
# appropriate flash message
message = get_flashed_messages()[0]
assert "deleted" in message["message"]
assert application.name in message["message"]
# app and envs are soft deleted
assert len(port.applications) == 0
assert len(application.environments) == 0
def test_new_environment(client, user_session):
user = UserFactory.create()
portfolio = PortfolioFactory(owner=user)

View File

@ -84,35 +84,3 @@ def test_portfolio_reports_with_mock_portfolio(client, user_session):
response = client.get(url_for("portfolios.reports", portfolio_id=portfolio.id))
assert response.status_code == 200
assert portfolio.name in response.data.decode()
def test_delete_portfolio_success(client, user_session):
portfolio = PortfolioFactory.create()
owner = portfolio.owner
user_session(owner)
assert len(Portfolios.for_user(user=owner)) == 1
response = client.post(
url_for("portfolios.delete_portfolio", portfolio_id=portfolio.id)
)
assert response.status_code == 302
assert url_for("atst.home") in response.location
assert len(Portfolios.for_user(user=owner)) == 0
def test_delete_portfolio_failure(no_debug_client, user_session):
portfolio = PortfolioFactory.create()
application = ApplicationFactory.create(portfolio=portfolio)
owner = portfolio.owner
user_session(owner)
assert len(Portfolios.for_user(user=owner)) == 1
response = no_debug_client.post(
url_for("portfolios.delete_portfolio", portfolio_id=portfolio.id)
)
assert response.status_code == 500
assert len(Portfolios.for_user(user=owner)) == 1

View File

@ -343,40 +343,6 @@ def test_portfolios_invite_member_access(post_url_assert_status):
post_url_assert_status(rando, url, 404)
# applications.delete
def test_applications_delete_access(post_url_assert_status, monkeypatch):
ccpo = UserFactory.create_ccpo()
owner = user_with()
app_admin = user_with()
rando = user_with()
portfolio = PortfolioFactory.create(
owner=owner, applications=[{"name": "mos eisley"}]
)
application = portfolio.applications[0]
ApplicationRoleFactory.create(
user=app_admin,
application=application,
permission_sets=PermissionSets.get_many(
[
PermissionSets.VIEW_APPLICATION,
PermissionSets.EDIT_APPLICATION_ENVIRONMENTS,
PermissionSets.EDIT_APPLICATION_TEAM,
PermissionSets.DELETE_APPLICATION_ENVIRONMENTS,
]
),
)
monkeypatch.setattr("atst.domain.applications.Applications.delete", lambda *a: True)
url = url_for("applications.delete", application_id=application.id)
post_url_assert_status(app_admin, url, 404)
post_url_assert_status(rando, url, 404)
post_url_assert_status(owner, url, 302)
post_url_assert_status(ccpo, url, 302)
# applications.settings
def test_application_settings_access(get_url_assert_status):
ccpo = user_with(PermissionSets.VIEW_PORTFOLIO_APPLICATION_MANAGEMENT)
@ -538,10 +504,16 @@ def test_applications_update_access(post_url_assert_status):
)
app = portfolio.applications[0]
def _form_data():
return {
"name": "Test Application %s" % (random.randrange(1, 1000)),
"description": "This is only a test",
}
url = url_for("applications.update", application_id=app.id)
post_url_assert_status(dev, url, 200)
post_url_assert_status(ccpo, url, 200)
post_url_assert_status(rando, url, 404)
post_url_assert_status(dev, url, 302, data=_form_data())
post_url_assert_status(ccpo, url, 302, data=_form_data())
post_url_assert_status(rando, url, 404, data=_form_data())
# applications.update_environments
@ -699,34 +671,3 @@ def test_task_orders_new_post_routes(post_url_assert_status):
post_url_assert_status(owner, url, 302, data=data)
post_url_assert_status(ccpo, url, 302, data=data)
post_url_assert_status(rando, url, 404, data=data)
def test_portfolio_delete_access(post_url_assert_status):
rando = UserFactory.create()
owner = UserFactory.create()
ccpo = UserFactory.create_ccpo()
post_url_assert_status(
ccpo,
url_for(
"portfolios.delete_portfolio", portfolio_id=PortfolioFactory.create().id
),
302,
)
post_url_assert_status(
owner,
url_for(
"portfolios.delete_portfolio",
portfolio_id=PortfolioFactory.create(owner=owner).id,
),
302,
)
post_url_assert_status(
rando,
url_for(
"portfolios.delete_portfolio", portfolio_id=PortfolioFactory.create().id
),
404,
)

View File

@ -22,13 +22,13 @@ home:
add_portfolio_button_text: Add New Portfolio
new_portfolio: New Portfolio
get_started: Get Started
head: About Cloud Services
head: JEDI Cloud Services
your_project: Your Project
your_project_descrip: Your portfolio is where all task orders pertaining to a specific project or set of related projects live. In JEDI, every task order in your portfolio has four components.
funding_descrip: is information about all approved task orders associated to your portfolio.
applications_descrip: ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
reports_descrip: enim ad minim veniam, quis nostrud exercitation ullamco
admin_descrip: aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
funding_descrip: The Task Orders section allows you to enter, manage, and edit awarded TOs associated to a specific Portfolio.
applications_descrip: The Applications section allows you to easily create and define new Applications within a Portfolio, as well as manage user permissions and Environments.
reports_descrip: The Reports section allows you to view and monitor funding usage within a specific Portfolio.
admin_descrip: Within the Settings section, you can manage your Portfolio name and description, as well as add, edit, and delete Portfolio managers.
ccpo:
users_title: CCPO Users
add_user: Add new CCPO user
@ -116,6 +116,8 @@ flash:
deleted: 'You have successfully deleted the {application_name} application. To view the retained activity log, visit the portfolio administration page.'
name_error:
message: 'The application name {name} has already been used in this portfolio. Please enter a unique name.'
env_name_error:
message: 'The environment name {name} has already been used in this application. Please enter a unique name.'
delete_member_success: 'You have successfully deleted {member_name} from the portfolio.'
deleted_member: Portfolio member deleted
environment_added: 'The environment "{env_name}" has been added to the application.'
@ -523,10 +525,11 @@ task_orders:
next_button: 'Next: Review Task Order'
step_5:
title: Confirm Signature
description: Finally, plase confirm that your uploaded document representing the information you've entered contains the required signature from your Contracting Officer. You will be informed as soon as CCPO completes their review.
description: Prior to submitting the Task Order, you must acknowledge, by marking the appropriate box below, that the uploaded Task Order is signed by an appropriate, duly warranted Contracting Officer who has the authority to execute the uploaded Task Order on your Agencys behalf and has authorized you to upload the Task Order in accordance with Agency policy and procedures. You must further acknowledge, by marking the appropriate box below, that all information entered herein matches that of the submitted Task Order.
alert_message: All task orders require a Contracting Officer signature.
next_button: 'Confirm & Submit'
sticky_header_text: 'Add Task Order (step {step} of 5)'
sticky_header_text: 'Add Task Order'
sticky_header_context: 'Step {step} of 5'
empty_state:
header: Add approved task orders
message: Upload your approved Task Order here. You are required to confirm you have the appropriate signature. You will have the ability to add additional approved Task Orders with more funding to this Portfolio in the future.
@ -539,7 +542,11 @@ task_orders:
subtitle: Who will be involved in the work funded by this task order?
team_title: Your team
sign:
digital_signature_description: I acknowledge that the uploaded task order contains the required KO signature.
digital_signature_description: I confirm the uploaded Task Order is signed by the appropriate, duly warranted Agency Contracting Officer who authorized me to upload the Task Order.
confirmation_description: I confirm that the information entered here in matches that of the submitted Task Order.
acknowledge:
title: Acknowledge Statement
text: I acknowledge, by executing the confirmation above and submitting this verification, that I am subject to potential penalties that may include fines, imprisonment, or both, under the U.S. law and regulations for any false statement or misrepresentation in association with this Task Order submission or on any accompanying documentation.
status_empty_state: 'This Portfolio has no {status} Task Orders.'
status_list_title: '{status} Task Orders'
JEDICLINType:

View File

@ -58,6 +58,5 @@ NGROK_TOKEN=<token> GI_API_KEY=<api key> GI_SUITE=<suite> CONTAINER_IMAGE=atat:b
- If you get errors regarding ports being in use, make sure you don't have instances of the Flask app, Postgres, or Redis running locally using those ports.
- If the curl command used to wait for the application container times out and fails, you can increase the timeout by setting a CONTAINER_TIMEOUT environment variable. It defaults to 200 in the script.
- The curl command will print errors until it successfully connects to the application container. These are normal and expected. When it finally connects, it will print the ATAT home page HTML to STDOUT.
- You may see errors like "No such container". The script attempts to clean up any previous incarnations of the containers before it starts, and it may print errors when it doesn't find them. This is fine.
- The script is, for the most part, a series of docker commands, so try running the commands individually and debugging that way.