mirror of
https://github.com/ansible/awx.git
synced 2026-05-04 08:05:30 -02:30
Merge pull request #4189 from shanemcd/toc
Container Groups Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -22,6 +22,7 @@ from rest_framework.request import clone_request
|
||||
# AWX
|
||||
from awx.main.fields import JSONField, ImplicitRoleField
|
||||
from awx.main.models import InventorySource, NotificationTemplate
|
||||
from awx.main.scheduler.kubernetes import PodManager
|
||||
|
||||
|
||||
class Metadata(metadata.SimpleMetadata):
|
||||
@@ -200,6 +201,9 @@ class Metadata(metadata.SimpleMetadata):
|
||||
if not isinstance(meta, dict):
|
||||
continue
|
||||
|
||||
if field == "pod_spec_override":
|
||||
meta['default'] = PodManager().pod_definition
|
||||
|
||||
# Add type choices if available from the serializer.
|
||||
if field == 'type' and hasattr(serializer, 'get_type_choices'):
|
||||
meta['choices'] = serializer.get_type_choices()
|
||||
|
||||
@@ -116,7 +116,7 @@ SUMMARIZABLE_FK_FIELDS = {
|
||||
'project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'),
|
||||
'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'),
|
||||
'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
|
||||
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'kubernetes', 'credential_type_id'),
|
||||
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed', 'type'),
|
||||
'job_template': DEFAULT_SUMMARY_FIELDS,
|
||||
'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
|
||||
@@ -135,7 +135,7 @@ SUMMARIZABLE_FK_FIELDS = {
|
||||
'source_script': ('name', 'description'),
|
||||
'role': ('id', 'role_field'),
|
||||
'notification_template': DEFAULT_SUMMARY_FIELDS,
|
||||
'instance_group': {'id', 'name', 'controller_id'},
|
||||
'instance_group': ('id', 'name', 'controller_id', 'is_containerized'),
|
||||
'insights_credential': DEFAULT_SUMMARY_FIELDS,
|
||||
'source_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||
'target_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'),
|
||||
@@ -2515,7 +2515,7 @@ class CredentialSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Credential
|
||||
fields = ('*', 'organization', 'credential_type', 'inputs', 'kind', 'cloud')
|
||||
fields = ('*', 'organization', 'credential_type', 'inputs', 'kind', 'cloud', 'kubernetes')
|
||||
extra_kwargs = {
|
||||
'credential_type': {
|
||||
'label': _('Credential Type'),
|
||||
@@ -4730,6 +4730,11 @@ class InstanceGroupSerializer(BaseSerializer):
|
||||
'Isolated groups have a designated controller group.'),
|
||||
read_only=True
|
||||
)
|
||||
is_containerized = serializers.BooleanField(
|
||||
help_text=_('Indicates whether instances in this group are containerized.'
|
||||
'Containerized groups have a designated Openshift or Kubernetes cluster.'),
|
||||
read_only=True
|
||||
)
|
||||
# NOTE: help_text is duplicated from field definitions, no obvious way of
|
||||
# both defining field details here and also getting the field's help_text
|
||||
policy_instance_percentage = serializers.IntegerField(
|
||||
@@ -4755,8 +4760,9 @@ class InstanceGroupSerializer(BaseSerializer):
|
||||
fields = ("id", "type", "url", "related", "name", "created", "modified",
|
||||
"capacity", "committed_capacity", "consumed_capacity",
|
||||
"percent_capacity_remaining", "jobs_running", "jobs_total",
|
||||
"instances", "controller", "is_controller", "is_isolated",
|
||||
"policy_instance_percentage", "policy_instance_minimum", "policy_instance_list")
|
||||
"instances", "controller", "is_controller", "is_isolated", "is_containerized", "credential",
|
||||
"policy_instance_percentage", "policy_instance_minimum", "policy_instance_list",
|
||||
"pod_spec_override", "summary_fields")
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(InstanceGroupSerializer, self).get_related(obj)
|
||||
@@ -4764,6 +4770,9 @@ class InstanceGroupSerializer(BaseSerializer):
|
||||
res['instances'] = self.reverse('api:instance_group_instance_list', kwargs={'pk': obj.pk})
|
||||
if obj.controller_id:
|
||||
res['controller'] = self.reverse('api:instance_group_detail', kwargs={'pk': obj.controller_id})
|
||||
if obj.credential:
|
||||
res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential_id})
|
||||
|
||||
return res
|
||||
|
||||
def validate_policy_instance_list(self, value):
|
||||
@@ -4783,6 +4792,11 @@ class InstanceGroupSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError(_('tower instance group name may not be changed.'))
|
||||
return value
|
||||
|
||||
def validate_credential(self, value):
|
||||
if value and not value.kubernetes:
|
||||
raise serializers.ValidationError(_('Only Kubernetes credentials can be associated with an Instance Group'))
|
||||
return value
|
||||
|
||||
def get_capacity_dict(self):
|
||||
# Store capacity values (globally computed) in the context
|
||||
if 'capacity_map' not in self.context:
|
||||
|
||||
@@ -11,7 +11,9 @@ from django.conf import settings
|
||||
import ansible_runner
|
||||
|
||||
import awx
|
||||
from awx.main.utils import get_system_task_capacity
|
||||
from awx.main.utils import (
|
||||
get_system_task_capacity
|
||||
)
|
||||
from awx.main.queue import CallbackQueueDispatcher
|
||||
|
||||
logger = logging.getLogger('awx.isolated.manager')
|
||||
@@ -29,7 +31,7 @@ def set_pythonpath(venv_libdir, env):
|
||||
|
||||
class IsolatedManager(object):
|
||||
|
||||
def __init__(self, cancelled_callback=None, check_callback=None):
|
||||
def __init__(self, cancelled_callback=None, check_callback=None, pod_manager=None):
|
||||
"""
|
||||
:param cancelled_callback: a callable - which returns `True` or `False`
|
||||
- signifying if the job has been prematurely
|
||||
@@ -40,6 +42,24 @@ class IsolatedManager(object):
|
||||
self.idle_timeout = max(60, 2 * settings.AWX_ISOLATED_CONNECTION_TIMEOUT)
|
||||
self.started_at = None
|
||||
self.captured_command_artifact = False
|
||||
self.instance = None
|
||||
self.pod_manager = pod_manager
|
||||
|
||||
def build_inventory(self, hosts):
|
||||
if self.instance and self.instance.is_containerized:
|
||||
inventory = {'all': {'hosts': {}}}
|
||||
for host in hosts:
|
||||
inventory['all']['hosts'][host] = {
|
||||
"ansible_connection": "kubectl",
|
||||
"ansible_kubectl_config": self.pod_manager.kube_config
|
||||
}
|
||||
else:
|
||||
inventory = '\n'.join([
|
||||
'{} ansible_ssh_user={}'.format(host, settings.AWX_ISOLATED_USERNAME)
|
||||
for host in hosts
|
||||
])
|
||||
|
||||
return inventory
|
||||
|
||||
def build_runner_params(self, hosts, verbosity=1):
|
||||
env = dict(os.environ.items())
|
||||
@@ -69,17 +89,12 @@ class IsolatedManager(object):
|
||||
else:
|
||||
playbook_logger.info(runner_obj.stdout.read())
|
||||
|
||||
inventory = '\n'.join([
|
||||
'{} ansible_ssh_user={}'.format(host, settings.AWX_ISOLATED_USERNAME)
|
||||
for host in hosts
|
||||
])
|
||||
|
||||
return {
|
||||
'project_dir': os.path.abspath(os.path.join(
|
||||
os.path.dirname(awx.__file__),
|
||||
'playbooks'
|
||||
)),
|
||||
'inventory': inventory,
|
||||
'inventory': self.build_inventory(hosts),
|
||||
'envvars': env,
|
||||
'finished_callback': finished_callback,
|
||||
'verbosity': verbosity,
|
||||
@@ -153,6 +168,11 @@ class IsolatedManager(object):
|
||||
runner_obj = self.run_management_playbook('run_isolated.yml',
|
||||
self.private_data_dir,
|
||||
extravars=extravars)
|
||||
|
||||
if runner_obj.status == 'failed':
|
||||
self.instance.result_traceback = runner_obj.stdout.read()
|
||||
self.instance.save(update_fields=['result_traceback'])
|
||||
|
||||
return runner_obj.status, runner_obj.rc
|
||||
|
||||
def check(self, interval=None):
|
||||
@@ -175,6 +195,7 @@ class IsolatedManager(object):
|
||||
rc = None
|
||||
last_check = time.time()
|
||||
dispatcher = CallbackQueueDispatcher()
|
||||
|
||||
while status == 'failed':
|
||||
canceled = self.cancelled_callback() if self.cancelled_callback else False
|
||||
if not canceled and time.time() - last_check < interval:
|
||||
@@ -279,7 +300,6 @@ class IsolatedManager(object):
|
||||
|
||||
|
||||
def cleanup(self):
|
||||
# If the job failed for any reason, make a last-ditch effort at cleanup
|
||||
extravars = {
|
||||
'private_data_dir': self.private_data_dir,
|
||||
'cleanup_dirs': [
|
||||
@@ -393,6 +413,7 @@ class IsolatedManager(object):
|
||||
[instance.execution_node],
|
||||
verbosity=min(5, self.instance.verbosity)
|
||||
)
|
||||
|
||||
status, rc = self.dispatch(playbook, module, module_args)
|
||||
if status == 'successful':
|
||||
status, rc = self.check()
|
||||
|
||||
@@ -221,8 +221,9 @@ class InstanceGroupManager(models.Manager):
|
||||
elif t.status == 'running':
|
||||
# Subtract capacity from all groups that contain the instance
|
||||
if t.execution_node not in instance_ig_mapping:
|
||||
logger.warning('Detected %s running inside lost instance, '
|
||||
'may still be waiting for reaper.', t.log_format)
|
||||
if not t.is_containerized:
|
||||
logger.warning('Detected %s running inside lost instance, '
|
||||
'may still be waiting for reaper.', t.log_format)
|
||||
if t.instance_group:
|
||||
impacted_groups = [t.instance_group.name]
|
||||
else:
|
||||
|
||||
38
awx/main/migrations/0096_v360_container_groups.py
Normal file
38
awx/main/migrations/0096_v360_container_groups.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 2.2.4 on 2019-09-16 23:50
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
from awx.main.models import CredentialType
|
||||
from awx.main.utils.common import set_current_apps
|
||||
|
||||
|
||||
def create_new_credential_types(apps, schema_editor):
|
||||
set_current_apps(apps)
|
||||
CredentialType.setup_tower_managed_defaults()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('main', '0095_v360_increase_instance_version_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='instancegroup',
|
||||
name='credential',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='instancegroups', to='main.Credential'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='instancegroup',
|
||||
name='pod_spec_override',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='credentialtype',
|
||||
name='kind',
|
||||
field=models.CharField(choices=[('ssh', 'Machine'), ('vault', 'Vault'), ('net', 'Network'), ('scm', 'Source Control'), ('cloud', 'Cloud'), ('token', 'Personal Access Token'), ('insights', 'Insights'), ('external', 'External'), ('kubernetes', 'Kubernetes')], max_length=32),
|
||||
),
|
||||
migrations.RunPython(create_new_credential_types)
|
||||
]
|
||||
@@ -150,6 +150,14 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
|
||||
def supports_isolation(cls):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_containerized(self):
|
||||
return bool(self.instance_group and self.instance_group.is_containerized)
|
||||
|
||||
@property
|
||||
def can_run_containerized(self):
|
||||
return True
|
||||
|
||||
def get_absolute_url(self, request=None):
|
||||
return reverse('api:ad_hoc_command_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
|
||||
@@ -135,6 +135,10 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
def cloud(self):
|
||||
return self.credential_type.kind == 'cloud'
|
||||
|
||||
@property
|
||||
def kubernetes(self):
|
||||
return self.credential_type.kind == 'kubernetes'
|
||||
|
||||
def get_absolute_url(self, request=None):
|
||||
return reverse('api:credential_detail', kwargs={'pk': self.pk}, request=request)
|
||||
|
||||
@@ -325,6 +329,7 @@ class CredentialType(CommonModelNameNotUnique):
|
||||
('token', _('Personal Access Token')),
|
||||
('insights', _('Insights')),
|
||||
('external', _('External')),
|
||||
('kubernetes', _('Kubernetes')),
|
||||
)
|
||||
|
||||
kind = models.CharField(
|
||||
@@ -1118,6 +1123,38 @@ ManagedCredentialType(
|
||||
)
|
||||
|
||||
|
||||
ManagedCredentialType(
|
||||
namespace='kubernetes_bearer_token',
|
||||
kind='kubernetes',
|
||||
name=ugettext_noop('OpenShift or Kubernetes API Bearer Token'),
|
||||
inputs={
|
||||
'fields': [{
|
||||
'id': 'host',
|
||||
'label': ugettext_noop('OpenShift or Kubernetes API Endpoint'),
|
||||
'type': 'string',
|
||||
'help_text': ugettext_noop('The OpenShift or Kubernetes API Endpoint to authenticate with.')
|
||||
},{
|
||||
'id': 'bearer_token',
|
||||
'label': ugettext_noop('API authentication bearer token.'),
|
||||
'type': 'string',
|
||||
'secret': True,
|
||||
},{
|
||||
'id': 'verify_ssl',
|
||||
'label': ugettext_noop('Verify SSL'),
|
||||
'type': 'boolean',
|
||||
'default': True,
|
||||
},{
|
||||
'id': 'ssl_ca_cert',
|
||||
'label': ugettext_noop('Certificate Authority data'),
|
||||
'type': 'string',
|
||||
'secret': True,
|
||||
'multiline': True,
|
||||
}],
|
||||
'required': ['host', 'bearer_token'],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CredentialInputSource(PrimordialModel):
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -18,7 +18,7 @@ from awx import __version__ as awx_application_version
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.managers import InstanceManager, InstanceGroupManager
|
||||
from awx.main.fields import JSONField
|
||||
from awx.main.models.base import BaseModel, HasEditsMixin
|
||||
from awx.main.models.base import BaseModel, HasEditsMixin, prevent_search
|
||||
from awx.main.models.unified_jobs import UnifiedJob
|
||||
from awx.main.utils import get_cpu_capacity, get_mem_capacity, get_system_task_capacity
|
||||
from awx.main.models.mixins import RelatedJobsMixin
|
||||
@@ -176,6 +176,18 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin):
|
||||
null=True,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
credential = models.ForeignKey(
|
||||
'Credential',
|
||||
related_name='%(class)ss',
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
pod_spec_override = prevent_search(models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
))
|
||||
policy_instance_percentage = models.IntegerField(
|
||||
default=0,
|
||||
help_text=_("Percentage of Instances to automatically assign to this group")
|
||||
@@ -218,6 +230,10 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin):
|
||||
def is_isolated(self):
|
||||
return bool(self.controller)
|
||||
|
||||
@property
|
||||
def is_containerized(self):
|
||||
return bool(self.credential and self.credential.kubernetes)
|
||||
|
||||
'''
|
||||
RelatedJobsMixin
|
||||
'''
|
||||
@@ -271,7 +287,8 @@ def schedule_policy_task():
|
||||
@receiver(post_save, sender=InstanceGroup)
|
||||
def on_instance_group_saved(sender, instance, created=False, raw=False, **kwargs):
|
||||
if created or instance.has_policy_changes():
|
||||
schedule_policy_task()
|
||||
if not instance.is_containerized:
|
||||
schedule_policy_task()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Instance)
|
||||
@@ -282,7 +299,8 @@ def on_instance_saved(sender, instance, created=False, raw=False, **kwargs):
|
||||
|
||||
@receiver(post_delete, sender=InstanceGroup)
|
||||
def on_instance_group_deleted(sender, instance, using, **kwargs):
|
||||
schedule_policy_task()
|
||||
if not instance.is_containerized:
|
||||
schedule_policy_task()
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Instance)
|
||||
|
||||
@@ -718,6 +718,14 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
||||
return "$hidden due to Ansible no_log flag$"
|
||||
return artifacts
|
||||
|
||||
@property
|
||||
def can_run_containerized(self):
|
||||
return any([ig for ig in self.preferred_instance_groups if ig.is_containerized])
|
||||
|
||||
@property
|
||||
def is_containerized(self):
|
||||
return bool(self.instance_group and self.instance_group.is_containerized)
|
||||
|
||||
@property
|
||||
def preferred_instance_groups(self):
|
||||
if self.project is not None and self.project.organization is not None:
|
||||
|
||||
@@ -714,6 +714,10 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
def supports_isolation(cls):
|
||||
return False
|
||||
|
||||
@property
|
||||
def can_run_containerized(self):
|
||||
return False
|
||||
|
||||
def _get_parent_field_name(self):
|
||||
return 'unified_job_template' # Override in subclasses.
|
||||
|
||||
@@ -1425,3 +1429,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
|
||||
def is_isolated(self):
|
||||
return bool(self.controller_node)
|
||||
|
||||
@property
|
||||
def is_containerized(self):
|
||||
return False
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Copyright (c) 2017 Ansible, Inc.
|
||||
#
|
||||
|
||||
from awx.main.scheduler.task_manager import TaskManager # noqa
|
||||
from .task_manager import TaskManager
|
||||
|
||||
__all__ = ['TaskManager']
|
||||
|
||||
153
awx/main/scheduler/kubernetes.py
Normal file
153
awx/main/scheduler/kubernetes.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import os
|
||||
import stat
|
||||
import time
|
||||
import yaml
|
||||
import tempfile
|
||||
from base64 import b64encode
|
||||
|
||||
from django.conf import settings
|
||||
from kubernetes import client, config
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from awx.main.utils.common import parse_yaml_or_json
|
||||
|
||||
|
||||
class PodManager(object):
|
||||
|
||||
def __init__(self, task=None):
|
||||
self.task = task
|
||||
|
||||
def deploy(self):
|
||||
if not self.credential.kubernetes:
|
||||
raise RuntimeError('Pod deployment cannot occur without a Kubernetes credential')
|
||||
|
||||
|
||||
self.kube_api.create_namespaced_pod(body=self.pod_definition,
|
||||
namespace=self.namespace,
|
||||
_request_timeout=settings.AWX_CONTAINER_GROUP_DEFAULT_LAUNCH_TIMEOUT)
|
||||
|
||||
# We don't do any fancy timeout logic here because it is handled
|
||||
# at a higher level in the job spawning process. See
|
||||
# settings.AWX_ISOLATED_LAUNCH_TIMEOUT and settings.AWX_ISOLATED_CONNECTION_TIMEOUT
|
||||
while True:
|
||||
pod = self.kube_api.read_namespaced_pod(name=self.pod_name,
|
||||
namespace=self.namespace,
|
||||
_request_timeout=settings.AWX_CONTAINER_GROUP_DEFAULT_LAUNCH_TIMEOUT)
|
||||
if pod.status.phase != 'Pending':
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
if pod.status.phase == 'Running':
|
||||
return pod
|
||||
else:
|
||||
raise RuntimeError(f"Unhandled Pod phase: {pod.status.phase}")
|
||||
|
||||
|
||||
def delete(self):
|
||||
return self.kube_api.delete_namespaced_pod(name=self.pod_name,
|
||||
namespace=self.namespace,
|
||||
_request_timeout=settings.AWX_CONTAINER_GROUP_DEFAULT_LAUNCH_TIMEOUT)
|
||||
|
||||
@property
|
||||
def namespace(self):
|
||||
return self.pod_definition['metadata']['namespace']
|
||||
|
||||
@property
|
||||
def credential(self):
|
||||
return self.task.instance_group.credential
|
||||
|
||||
@cached_property
|
||||
def kube_config(self):
|
||||
return generate_tmp_kube_config(self.credential, self.namespace)
|
||||
|
||||
@cached_property
|
||||
def kube_api(self):
|
||||
my_client = config.new_client_from_config(config_file=self.kube_config)
|
||||
return client.CoreV1Api(api_client=my_client)
|
||||
|
||||
@property
|
||||
def pod_name(self):
|
||||
return f"job-{self.task.id}"
|
||||
|
||||
@property
|
||||
def pod_definition(self):
|
||||
default_pod_spec = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"namespace": settings.AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE
|
||||
},
|
||||
"spec": {
|
||||
"containers": [{
|
||||
"image": settings.AWX_CONTAINER_GROUP_DEFAULT_IMAGE,
|
||||
"tty": True,
|
||||
"stdin": True,
|
||||
"imagePullPolicy": "Always",
|
||||
"args": [
|
||||
'sleep', 'infinity'
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
pod_spec_override = {}
|
||||
if self.task and self.task.instance_group.pod_spec_override:
|
||||
pod_spec_override = parse_yaml_or_json(
|
||||
self.task.instance_group.pod_spec_override)
|
||||
pod_spec = {**default_pod_spec, **pod_spec_override}
|
||||
|
||||
if self.task:
|
||||
pod_spec['metadata']['name'] = self.pod_name
|
||||
pod_spec['spec']['containers'][0]['name'] = self.pod_name
|
||||
|
||||
return pod_spec
|
||||
|
||||
|
||||
def generate_tmp_kube_config(credential, namespace):
|
||||
host_input = credential.get_input('host')
|
||||
config = {
|
||||
"apiVersion": "v1",
|
||||
"kind": "Config",
|
||||
"preferences": {},
|
||||
"clusters": [
|
||||
{
|
||||
"name": host_input,
|
||||
"cluster": {
|
||||
"server": host_input
|
||||
}
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"name": host_input,
|
||||
"user": {
|
||||
"token": credential.get_input('bearer_token')
|
||||
}
|
||||
}
|
||||
],
|
||||
"contexts": [
|
||||
{
|
||||
"name": host_input,
|
||||
"context": {
|
||||
"cluster": host_input,
|
||||
"user": host_input,
|
||||
"namespace": namespace
|
||||
}
|
||||
}
|
||||
],
|
||||
"current-context": host_input
|
||||
}
|
||||
|
||||
if credential.get_input('verify_ssl'):
|
||||
config["clusters"][0]["cluster"]["certificate-authority-data"] = b64encode(
|
||||
credential.get_input('ssl_ca_cert').encode() # encode to bytes
|
||||
).decode() # decode the base64 data into a str
|
||||
else:
|
||||
config["clusters"][0]["cluster"]["insecure-skip-tls-verify"] = True
|
||||
|
||||
fd, path = tempfile.mkstemp(prefix='kubeconfig')
|
||||
with open(path, 'wb') as temp:
|
||||
temp.write(yaml.dump(config).encode())
|
||||
temp.flush()
|
||||
os.chmod(temp.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
||||
return path
|
||||
@@ -251,6 +251,8 @@ class TaskManager():
|
||||
task.controller_node = controller_node
|
||||
logger.debug('Submitting isolated {} to queue {} controlled by {}.'.format(
|
||||
task.log_format, task.execution_node, controller_node))
|
||||
elif rampart_group.is_containerized:
|
||||
task.instance_group = rampart_group
|
||||
else:
|
||||
task.instance_group = rampart_group
|
||||
if instance is not None:
|
||||
@@ -447,7 +449,7 @@ class TaskManager():
|
||||
for rampart_group in preferred_instance_groups:
|
||||
if idle_instance_that_fits is None:
|
||||
idle_instance_that_fits = rampart_group.find_largest_idle_instance()
|
||||
if self.get_remaining_capacity(rampart_group.name) <= 0:
|
||||
if not rampart_group.is_containerized and self.get_remaining_capacity(rampart_group.name) <= 0:
|
||||
logger.debug("Skipping group {} capacity <= 0".format(rampart_group.name))
|
||||
continue
|
||||
|
||||
@@ -456,10 +458,11 @@ class TaskManager():
|
||||
logger.debug("Starting dependent {} in group {} instance {}".format(
|
||||
task.log_format, rampart_group.name, execution_instance.hostname))
|
||||
elif not execution_instance and idle_instance_that_fits:
|
||||
execution_instance = idle_instance_that_fits
|
||||
logger.debug("Starting dependent {} in group {} on idle instance {}".format(
|
||||
task.log_format, rampart_group.name, execution_instance.hostname))
|
||||
if execution_instance:
|
||||
if not rampart_group.is_containerized:
|
||||
execution_instance = idle_instance_that_fits
|
||||
logger.debug("Starting dependent {} in group {} on idle instance {}".format(
|
||||
task.log_format, rampart_group.name, execution_instance.hostname))
|
||||
if execution_instance or rampart_group.is_containerized:
|
||||
self.graph[rampart_group.name]['graph'].add_job(task)
|
||||
tasks_to_fail = [t for t in dependency_tasks if t != task]
|
||||
tasks_to_fail += [dependent_task]
|
||||
@@ -492,10 +495,16 @@ class TaskManager():
|
||||
self.start_task(task, None, task.get_jobs_fail_chain(), None)
|
||||
continue
|
||||
for rampart_group in preferred_instance_groups:
|
||||
if task.can_run_containerized and rampart_group.is_containerized:
|
||||
self.graph[rampart_group.name]['graph'].add_job(task)
|
||||
self.start_task(task, rampart_group, task.get_jobs_fail_chain(), None)
|
||||
found_acceptable_queue = True
|
||||
break
|
||||
|
||||
if idle_instance_that_fits is None:
|
||||
idle_instance_that_fits = rampart_group.find_largest_idle_instance()
|
||||
remaining_capacity = self.get_remaining_capacity(rampart_group.name)
|
||||
if remaining_capacity <= 0:
|
||||
if not rampart_group.is_containerized and self.get_remaining_capacity(rampart_group.name) <= 0:
|
||||
logger.debug("Skipping group {}, remaining_capacity {} <= 0".format(
|
||||
rampart_group.name, remaining_capacity))
|
||||
continue
|
||||
@@ -505,10 +514,11 @@ class TaskManager():
|
||||
logger.debug("Starting {} in group {} instance {} (remaining_capacity={})".format(
|
||||
task.log_format, rampart_group.name, execution_instance.hostname, remaining_capacity))
|
||||
elif not execution_instance and idle_instance_that_fits:
|
||||
execution_instance = idle_instance_that_fits
|
||||
logger.debug("Starting {} in group {} instance {} (remaining_capacity={})".format(
|
||||
task.log_format, rampart_group.name, execution_instance.hostname, remaining_capacity))
|
||||
if execution_instance:
|
||||
if not rampart_group.is_containerized:
|
||||
execution_instance = idle_instance_that_fits
|
||||
logger.debug("Starting {} in group {} instance {} (remaining_capacity={})".format(
|
||||
task.log_format, rampart_group.name, execution_instance.hostname, remaining_capacity))
|
||||
if execution_instance or rampart_group.is_containerized:
|
||||
self.graph[rampart_group.name]['graph'].add_job(task)
|
||||
self.start_task(task, rampart_group, task.get_jobs_fail_chain(), execution_instance)
|
||||
found_acceptable_queue = True
|
||||
|
||||
@@ -251,6 +251,9 @@ def apply_cluster_membership_policies():
|
||||
# On a differential basis, apply instances to non-isolated groups
|
||||
with transaction.atomic():
|
||||
for g in actual_groups:
|
||||
if g.obj.is_containerized:
|
||||
logger.debug('Skipping containerized group {} for policy calculation'.format(g.obj.name))
|
||||
continue
|
||||
instances_to_add = set(g.instances) - set(g.prior_instances)
|
||||
instances_to_remove = set(g.prior_instances) - set(g.instances)
|
||||
if instances_to_add:
|
||||
@@ -878,7 +881,7 @@ class BaseTask(object):
|
||||
settings.AWX_PROOT_SHOW_PATHS
|
||||
|
||||
pi_path = settings.AWX_PROOT_BASE_PATH
|
||||
if not self.instance.is_isolated():
|
||||
if not self.instance.is_isolated() and not self.instance.is_containerized:
|
||||
pi_path = tempfile.mkdtemp(
|
||||
prefix='ansible_runner_pi_',
|
||||
dir=settings.AWX_PROOT_BASE_PATH
|
||||
@@ -1168,6 +1171,7 @@ class BaseTask(object):
|
||||
|
||||
try:
|
||||
isolated = self.instance.is_isolated()
|
||||
containerized = self.instance.is_containerized
|
||||
self.instance.send_notification_templates("running")
|
||||
private_data_dir = self.build_private_data_dir(self.instance)
|
||||
self.pre_run_hook(self.instance, private_data_dir)
|
||||
@@ -1261,7 +1265,7 @@ class BaseTask(object):
|
||||
if not params[v]:
|
||||
del params[v]
|
||||
|
||||
if self.instance.is_isolated() is True:
|
||||
if self.instance.is_isolated() or containerized:
|
||||
module_args = None
|
||||
if 'module_args' in params:
|
||||
# if it's adhoc, copy the module args
|
||||
@@ -1272,10 +1276,22 @@ class BaseTask(object):
|
||||
params.pop('inventory'),
|
||||
os.path.join(private_data_dir, 'inventory')
|
||||
)
|
||||
pod_manager = None
|
||||
if containerized:
|
||||
from awx.main.scheduler.kubernetes import PodManager # Avoid circular import
|
||||
params['envvars'].pop('HOME')
|
||||
pod_manager = PodManager(self.instance)
|
||||
self.cleanup_paths.append(pod_manager.kube_config)
|
||||
pod_manager.deploy()
|
||||
self.instance.execution_node = pod_manager.pod_name
|
||||
self.instance.save(update_fields=['execution_node'])
|
||||
|
||||
|
||||
ansible_runner.utils.dump_artifacts(params)
|
||||
isolated_manager_instance = isolated_manager.IsolatedManager(
|
||||
cancelled_callback=lambda: self.update_model(self.instance.pk).cancel_flag,
|
||||
check_callback=self.check_handler,
|
||||
pod_manager=pod_manager
|
||||
)
|
||||
status, rc = isolated_manager_instance.run(self.instance,
|
||||
private_data_dir,
|
||||
@@ -1600,6 +1616,8 @@ class RunJob(BaseTask):
|
||||
'''
|
||||
Return whether this task should use proot.
|
||||
'''
|
||||
if job.is_containerized:
|
||||
return False
|
||||
return getattr(settings, 'AWX_PROOT_ENABLED', False)
|
||||
|
||||
def pre_run_hook(self, job, private_data_dir):
|
||||
@@ -1660,6 +1678,7 @@ class RunJob(BaseTask):
|
||||
if job.is_isolated() is True:
|
||||
pu_ig = pu_ig.controller
|
||||
pu_en = settings.CLUSTER_HOST_ID
|
||||
|
||||
sync_metafields = dict(
|
||||
launch_type="sync",
|
||||
job_type='run',
|
||||
@@ -1720,8 +1739,13 @@ class RunJob(BaseTask):
|
||||
os.path.join(private_data_dir, 'artifacts', str(job.id), 'fact_cache'),
|
||||
fact_modification_times,
|
||||
)
|
||||
if isolated_manager_instance:
|
||||
if isolated_manager_instance and not job.is_containerized:
|
||||
isolated_manager_instance.cleanup()
|
||||
|
||||
if job.is_containerized:
|
||||
from awx.main.scheduler.kubernetes import PodManager # prevent circular import
|
||||
PodManager(job).delete()
|
||||
|
||||
try:
|
||||
inventory = job.inventory
|
||||
except Inventory.DoesNotExist:
|
||||
@@ -2537,6 +2561,8 @@ class RunAdHocCommand(BaseTask):
|
||||
'''
|
||||
Return whether this task should use proot.
|
||||
'''
|
||||
if ad_hoc_command.is_containerized:
|
||||
return False
|
||||
return getattr(settings, 'AWX_PROOT_ENABLED', False)
|
||||
|
||||
def final_run_hook(self, adhoc_job, status, private_data_dir, fact_modification_times, isolated_manager_instance=None):
|
||||
|
||||
@@ -203,6 +203,13 @@ def organization(instance):
|
||||
return Organization.objects.create(name="test-org", description="test-org-desc")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def credentialtype_kube():
|
||||
kube = CredentialType.defaults['kubernetes_bearer_token']()
|
||||
kube.save()
|
||||
return kube
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def credentialtype_ssh():
|
||||
ssh = CredentialType.defaults['ssh']()
|
||||
@@ -336,6 +343,12 @@ def other_external_credential(credentialtype_external):
|
||||
inputs={'url': 'http://testhost.com', 'token': 'secret2'})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kube_credential(credentialtype_kube):
|
||||
return Credential.objects.create(credential_type=credentialtype_kube, name='kube-cred',
|
||||
inputs={'host': 'my.cluster', 'bearer_token': 'my-token', 'verify_ssl': False})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def inventory(organization):
|
||||
return organization.inventories.create(name="test-inv")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import subprocess
|
||||
import yaml
|
||||
import base64
|
||||
|
||||
from unittest import mock # noqa
|
||||
import pytest
|
||||
|
||||
from awx.main.scheduler.kubernetes import PodManager
|
||||
from awx.main.utils import (
|
||||
create_temporary_fifo,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def containerized_job(default_instance_group, kube_credential, job_template_factory):
|
||||
default_instance_group.credential = kube_credential
|
||||
default_instance_group.save()
|
||||
objects = job_template_factory('jt', organization='org1', project='proj',
|
||||
inventory='inv', credential='cred',
|
||||
jobs=['my_job'])
|
||||
jt = objects.job_template
|
||||
jt.instance_groups.add(default_instance_group)
|
||||
|
||||
j1 = objects.jobs['my_job']
|
||||
j1.instance_group = default_instance_group
|
||||
j1.status = 'pending'
|
||||
j1.save()
|
||||
return j1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_containerized_job(containerized_job):
|
||||
assert containerized_job.is_containerized
|
||||
assert containerized_job.instance_group.is_containerized
|
||||
assert containerized_job.instance_group.credential.kubernetes
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_kubectl_ssl_verification(containerized_job):
|
||||
cred = containerized_job.instance_group.credential
|
||||
cred.inputs['verify_ssl'] = True
|
||||
key_material = subprocess.run('openssl genrsa 2> /dev/null',
|
||||
shell=True, check=True,
|
||||
stdout=subprocess.PIPE)
|
||||
key = create_temporary_fifo(key_material.stdout)
|
||||
cmd = f"""
|
||||
openssl req -x509 -sha256 -new -nodes \
|
||||
-key {key} -subj '/C=US/ST=North Carolina/L=Durham/O=Ansible/OU=AWX Development/CN=awx.localhost'
|
||||
"""
|
||||
cert = subprocess.run(cmd.strip(), shell=True, check=True, stdout=subprocess.PIPE)
|
||||
cred.inputs['ssl_ca_cert'] = cert.stdout
|
||||
cred.save()
|
||||
pm = PodManager(containerized_job)
|
||||
config = yaml.load(open(pm.kube_config), Loader=yaml.FullLoader)
|
||||
ca_data = config['clusters'][0]['cluster']['certificate-authority-data']
|
||||
assert cert.stdout == base64.b64decode(ca_data.encode())
|
||||
@@ -87,6 +87,7 @@ def test_default_cred_types():
|
||||
'hashivault_kv',
|
||||
'hashivault_ssh',
|
||||
'insights',
|
||||
'kubernetes_bearer_token',
|
||||
'net',
|
||||
'openstack',
|
||||
'rhv',
|
||||
|
||||
55
awx/main/tests/unit/scheduler/test_kubernetes.py
Normal file
55
awx/main/tests/unit/scheduler/test_kubernetes.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import pytest
|
||||
from unittest import mock
|
||||
from django.conf import settings
|
||||
|
||||
from awx.main.models import (
|
||||
InstanceGroup,
|
||||
Job,
|
||||
JobTemplate,
|
||||
Project,
|
||||
Inventory,
|
||||
)
|
||||
from awx.main.scheduler.kubernetes import PodManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def container_group():
|
||||
instance_group = mock.Mock(InstanceGroup(name='container-group'))
|
||||
|
||||
return instance_group
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def job(container_group):
|
||||
return Job(pk=1,
|
||||
id=1,
|
||||
project=Project(),
|
||||
instance_group=container_group,
|
||||
inventory=Inventory(),
|
||||
job_template=JobTemplate(id=1, name='foo'))
|
||||
|
||||
|
||||
def test_default_pod_spec(job):
|
||||
default_image = PodManager(job).pod_definition['spec']['containers'][0]['image']
|
||||
assert default_image == settings.AWX_CONTAINER_GROUP_DEFAULT_IMAGE
|
||||
|
||||
|
||||
def test_custom_pod_spec(job):
|
||||
job.instance_group.pod_spec_override = """
|
||||
spec:
|
||||
containers:
|
||||
- image: my-custom-image
|
||||
"""
|
||||
custom_image = PodManager(job).pod_definition['spec']['containers'][0]['image']
|
||||
assert custom_image == 'my-custom-image'
|
||||
|
||||
|
||||
def test_pod_manager_namespace_property(job):
|
||||
pm = PodManager(job)
|
||||
assert pm.namespace == settings.AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE
|
||||
|
||||
job.instance_group.pod_spec_override = """
|
||||
metadata:
|
||||
namespace: my-namespace
|
||||
"""
|
||||
assert PodManager(job).namespace == 'my-namespace'
|
||||
@@ -11,6 +11,7 @@ class FakeObject(object):
|
||||
|
||||
class Job(FakeObject):
|
||||
task_impact = 43
|
||||
is_containerized = False
|
||||
|
||||
def log_format(self):
|
||||
return 'job 382 (fake)'
|
||||
|
||||
@@ -20,12 +20,42 @@
|
||||
mode: pull
|
||||
delete: yes
|
||||
recursive: yes
|
||||
when: ansible_kubectl_config is not defined
|
||||
|
||||
- name: Copy daemon log from the isolated host
|
||||
synchronize:
|
||||
src: "{{src}}/daemon.log"
|
||||
dest: "{{src}}/daemon.log"
|
||||
mode: pull
|
||||
when: ansible_kubectl_config is not defined
|
||||
|
||||
- name: Copy artifacts from pod
|
||||
synchronize:
|
||||
src: "{{src}}/artifacts/"
|
||||
dest: "{{src}}/artifacts/"
|
||||
mode: pull
|
||||
delete: yes
|
||||
recursive: yes
|
||||
set_remote_user: no
|
||||
rsync_opts:
|
||||
- "--rsh=$RSH"
|
||||
environment:
|
||||
RSH: "oc rsh --config={{ ansible_kubectl_config }}"
|
||||
delegate_to: localhost
|
||||
when: ansible_kubectl_config is defined
|
||||
|
||||
- name: Copy daemon log from pod
|
||||
synchronize:
|
||||
src: "{{src}}/daemon.log"
|
||||
dest: "{{src}}/daemon.log"
|
||||
mode: pull
|
||||
set_remote_user: no
|
||||
rsync_opts:
|
||||
- "--rsh=$RSH"
|
||||
environment:
|
||||
RSH: "oc rsh --config={{ ansible_kubectl_config }}"
|
||||
delegate_to: localhost
|
||||
when: ansible_kubectl_config is defined
|
||||
|
||||
- name: Fail if previous check determined that process is not alive.
|
||||
fail:
|
||||
|
||||
@@ -11,12 +11,25 @@
|
||||
secret: "{{ lookup('pipe', 'cat ' + src + '/env/ssh_key') }}"
|
||||
|
||||
tasks:
|
||||
|
||||
- name: synchronize job environment with isolated host
|
||||
synchronize:
|
||||
copy_links: true
|
||||
src: "{{src}}"
|
||||
dest: "{{dest}}"
|
||||
copy_links: yes
|
||||
src: "{{ src }}"
|
||||
dest: "{{ dest }}"
|
||||
when: ansible_kubectl_config is not defined
|
||||
|
||||
- name: synchronize job environment with remote job container
|
||||
synchronize:
|
||||
copy_links: yes
|
||||
src: "{{ src }}"
|
||||
dest: "{{ dest }}"
|
||||
set_remote_user: no
|
||||
rsync_opts:
|
||||
- "--rsh=$RSH"
|
||||
environment:
|
||||
RSH: "oc rsh --config={{ ansible_kubectl_config }}"
|
||||
delegate_to: localhost
|
||||
when: ansible_kubectl_config is defined
|
||||
|
||||
- local_action: stat path="{{src}}/env/ssh_key"
|
||||
register: key
|
||||
|
||||
@@ -67,6 +67,10 @@ DATABASES = {
|
||||
}
|
||||
}
|
||||
|
||||
AWX_CONTAINER_GROUP_DEFAULT_LAUNCH_TIMEOUT = 10
|
||||
AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE = 'default'
|
||||
AWX_CONTAINER_GROUP_DEFAULT_IMAGE = 'ansible/ansible-runner'
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/dev/topics/i18n/
|
||||
#
|
||||
|
||||
@@ -83,8 +83,7 @@ AWX_PROOT_ENABLED = True
|
||||
|
||||
AWX_ISOLATED_USERNAME = 'root'
|
||||
AWX_ISOLATED_CHECK_INTERVAL = 1
|
||||
AWX_ISOLATED_PERIODIC_CHECK = 5
|
||||
AWX_ISOLATED_LAUNCH_TIMEOUT = 30
|
||||
AWX_ISOLATED_PERIODIC_CHECK = 30
|
||||
|
||||
# Disable Pendo on the UI for development/test.
|
||||
# Note: This setting may be overridden by database settings.
|
||||
|
||||
@@ -5,7 +5,7 @@ import jobsListController from '../jobsList.controller';
|
||||
const jobsListTemplate = require('~features/jobs/jobsList.view.html');
|
||||
const listContainerTemplate = require('~src/instance-groups/jobs/instanceGroupsJobsListContainer.partial.html');
|
||||
|
||||
export default {
|
||||
const instanceGroupJobsRoute = {
|
||||
name: 'instanceGroups.jobs',
|
||||
url: '/:instance_group_id/jobs',
|
||||
ncyBreadcrumb: {
|
||||
@@ -63,6 +63,69 @@ export default {
|
||||
SearchBasePath: [
|
||||
'$stateParams',
|
||||
($stateParams) => `api/v2/instance_groups/${$stateParams.instance_group_id}/jobs`
|
||||
]
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
const containerGroupJobsRoute = {
|
||||
name: 'instanceGroups.containerGroupJobs',
|
||||
url: '/container_groups/:instance_group_id/jobs',
|
||||
ncyBreadcrumb: {
|
||||
parent: 'instanceGroups.editContainerGroup',
|
||||
label: N_('JOBS')
|
||||
},
|
||||
params: {
|
||||
job_search: {
|
||||
value: {
|
||||
page_size: '10',
|
||||
order_by: '-finished'
|
||||
},
|
||||
dynamic: true
|
||||
}
|
||||
},
|
||||
views: {
|
||||
'containerGroupJobs@instanceGroups': {
|
||||
templateUrl: listContainerTemplate,
|
||||
controller: listContainerController,
|
||||
controllerAs: 'vm'
|
||||
},
|
||||
'jobsList@instanceGroups.containerGroupJobs': {
|
||||
templateUrl: jobsListTemplate,
|
||||
controller: jobsListController,
|
||||
controllerAs: 'vm'
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
resolvedModels: [
|
||||
'UnifiedJobModel',
|
||||
(UnifiedJob) => {
|
||||
const models = [
|
||||
new UnifiedJob(['options']),
|
||||
];
|
||||
return Promise.all(models);
|
||||
},
|
||||
],
|
||||
Dataset: [
|
||||
'$stateParams',
|
||||
'Wait',
|
||||
'QuerySet',
|
||||
($stateParams, Wait, qs) => {
|
||||
const groupId = $stateParams.instance_group_id;
|
||||
|
||||
const searchParam = $stateParams.job_search;
|
||||
|
||||
const searchPath = `api/v2/instance_groups/${groupId}/jobs`;
|
||||
|
||||
Wait('start');
|
||||
return qs.search(searchPath, searchParam)
|
||||
.finally(() => Wait('stop'));
|
||||
}
|
||||
],
|
||||
SearchBasePath: [
|
||||
'$stateParams',
|
||||
($stateParams) => `api/v2/instance_groups/${$stateParams.instance_group_id}/jobs`
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
export { instanceGroupJobsRoute, containerGroupJobsRoute };
|
||||
|
||||
@@ -553,12 +553,16 @@ function getInstanceGroupDetails () {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = strings.get('labels.INSTANCE_GROUP');
|
||||
const value = $filter('sanitize')(instanceGroup.name);
|
||||
const link = `/#/instance_groups/${instanceGroup.id}`;
|
||||
|
||||
let label = strings.get('labels.INSTANCE_GROUP');
|
||||
let link = `/#/instance_groups/${instanceGroup.id}`;
|
||||
if (instanceGroup.is_containerized) {
|
||||
label = strings.get('labels.CONTAINER_GROUP');
|
||||
link = `/#/instance_groups/container_group/${instanceGroup.id}`;
|
||||
}
|
||||
|
||||
let isolated = null;
|
||||
|
||||
if (instanceGroup.is_isolated) {
|
||||
isolated = strings.get('details.ISOLATED');
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ function OutputStrings (BaseString) {
|
||||
FORKS: t.s('Forks'),
|
||||
HOST_LIMIT_ERROR: t.s('Host Limit Error'),
|
||||
INSTANCE_GROUP: t.s('Instance Group'),
|
||||
CONTAINER_GROUP: t.s('Container Group'),
|
||||
INVENTORY: t.s('Inventory'),
|
||||
INVENTORY_SCM: t.s('Source Project'),
|
||||
JOB_EXPLANATION: t.s('Explanation'),
|
||||
|
||||
@@ -88,17 +88,14 @@ function AtInputLookupController (baseInputController, $q, $state) {
|
||||
scope.state._value = null;
|
||||
return vm.check({ isValid: true });
|
||||
}
|
||||
|
||||
searchParams = searchParams || { [search.key]: scope.state._displayValue };
|
||||
|
||||
return model.search(searchParams, search.config)
|
||||
.then(found => {
|
||||
if (!found) {
|
||||
vm.reset();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
scope[scope.state._resource] = model.get('id');
|
||||
scope.state._value = model.get('id');
|
||||
scope.state._displayValue = model.get('name');
|
||||
|
||||
@@ -241,6 +241,10 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.at-Row-rightSide{
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.at-Row-container--wrapped {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
<span class="atSwitch-slider"></span>
|
||||
<i class="fa fa-check"></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -2,6 +2,14 @@ function EditController ($rootScope, $state, models, strings) {
|
||||
const vm = this || {};
|
||||
const { instanceGroup } = models;
|
||||
|
||||
if (instanceGroup.get('is_containerized')) {
|
||||
return $state.go(
|
||||
'instanceGroups.editContainerGroup',
|
||||
{ instance_group_id: instanceGroup.get('id') },
|
||||
{ reload: true }
|
||||
);
|
||||
}
|
||||
|
||||
$rootScope.breadcrumb.instance_group_name = instanceGroup.get('name');
|
||||
|
||||
vm.mode = 'edit';
|
||||
@@ -51,4 +59,4 @@ EditController.$inject = [
|
||||
'InstanceGroupsStrings'
|
||||
];
|
||||
|
||||
export default EditController;
|
||||
export default EditController;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
function AddContainerGroupController(ToJSON, $scope, $state, models, strings, i18n, DataSet) {
|
||||
const vm = this || {};
|
||||
const {
|
||||
instanceGroup,
|
||||
credential
|
||||
} = models;
|
||||
|
||||
vm.mode = 'add';
|
||||
vm.strings = strings;
|
||||
vm.panelTitle = strings.get('state.ADD_CONTAINER_GROUP_BREADCRUMB_LABEL');
|
||||
vm.lookUpTitle = strings.get('container.LOOK_UP_TITLE');
|
||||
|
||||
vm.form = instanceGroup.createFormSchema('post');
|
||||
vm.form.name.required = true;
|
||||
|
||||
vm.form.credential = {
|
||||
type: 'field',
|
||||
label: i18n._('Credential'),
|
||||
id: 'credential'
|
||||
};
|
||||
vm.form.credential._resource = 'credential';
|
||||
vm.form.credential._route = "instanceGroups.addContainerGroup.credentials";
|
||||
vm.form.credential._model = credential;
|
||||
vm.form.credential._placeholder = strings.get('container.CREDENTIAL_PLACEHOLDER');
|
||||
vm.form.credential.required = true;
|
||||
|
||||
vm.form.extraVars = {
|
||||
label: strings.get('container.POD_SPEC_LABEL'),
|
||||
value: DataSet.data.actions.POST.pod_spec_override.default,
|
||||
name: 'extraVars',
|
||||
toggleLabel: strings.get('container.POD_SPEC_TOGGLE'),
|
||||
};
|
||||
|
||||
vm.tab = {
|
||||
details: { _active: true },
|
||||
instances: {_disabled: true },
|
||||
jobs: {_disabled: true }
|
||||
};
|
||||
|
||||
$scope.variables = vm.form.extraVars.value;
|
||||
$scope.name = vm.form.extraVars.name;
|
||||
vm.panelTitle = strings.get('container.PANEL_TITLE');
|
||||
|
||||
|
||||
$scope.$watch('credential', () => {
|
||||
if ($scope.credential) {
|
||||
vm.form.credential._idFromModal= $scope.credential;
|
||||
}
|
||||
});
|
||||
vm.form.save = (data) => {
|
||||
data.pod_spec_override = null;
|
||||
if (vm.form.extraVars.isOpen) {
|
||||
data.pod_spec_override = vm.form.extraVars.value;
|
||||
}
|
||||
return instanceGroup.request('post', { data: data }).then((res) => {
|
||||
$state.go('instanceGroups.editContainerGroup', { instance_group_id: res.data.id }, { reload: true });
|
||||
});
|
||||
};
|
||||
vm.form.extraVars.isOpen = false;
|
||||
vm.toggle = () => {
|
||||
if (vm.form.extraVars.isOpen === true) {
|
||||
vm.form.extraVars.isOpen = false;
|
||||
} else {
|
||||
vm.form.extraVars.isOpen = true;
|
||||
}
|
||||
return vm.form.extraVars.isOpen;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
AddContainerGroupController.$inject = [
|
||||
'ToJSON',
|
||||
'$scope',
|
||||
'$state',
|
||||
'resolvedModels',
|
||||
'InstanceGroupsStrings',
|
||||
'i18n',
|
||||
'DataSet'
|
||||
];
|
||||
|
||||
export default AddContainerGroupController;
|
||||
@@ -0,0 +1,44 @@
|
||||
<div ui-view="credentials"></div>
|
||||
<a class="containerGroups-messageBar-link"href="https://docs.ansible.com/ansible-tower/latest/html/userguide/instance_groups.html" target="_blank" style="color: white">
|
||||
<div class="Section-messageBar">
|
||||
<i class="Section-messageBar-warning fa fa-warning"></i>
|
||||
<span class="Section-messageBar-text">This feature is tech preview, and is subject to change in a future release. Click here for documentation.</span>
|
||||
</div>
|
||||
</a>
|
||||
<at-panel>
|
||||
<at-panel-heading title="{{:: vm.panelTitle }}"></at-panel-heading>
|
||||
<at-tab-group>
|
||||
<at-tab state="vm.tab.details">{{:: vm.strings.get('tab.DETAILS') }}</at-tab>
|
||||
<at-tab state="vm.tab.jobs">{{:: vm.strings.get('tab.JOBS') }}</at-tab>
|
||||
</at-tab-group>
|
||||
<at-panel-body>
|
||||
<at-form state="vm.form" autocomplete="off">
|
||||
<at-input-text col="4" tab="1" state="vm.form.name"></at-input-text>
|
||||
<at-input-lookup col="4" tab="3" state="vm.form.credential" id="containter_groups_credential"></at-input-lookup>
|
||||
<div class="at-Row-toggle Form-formGroup--fullWidth ">
|
||||
<label class="Form-inputLabelContainer" for="vm.form.extraVars.isOpen">
|
||||
<span class="Form-inputLabel" translate>
|
||||
{{ vm.form.extraVars.toggleLabel }}
|
||||
</span>
|
||||
</label>
|
||||
<div ng-class="{'ContainerGroups-codeMirror': vm.form.extraVars.isOpen }">
|
||||
<at-switch on-toggle="vm.toggle(instance)" switch-on="vm.form.extraVars.isOpen"
|
||||
switch-disabled="vm.rowAction.toggle._disabled"></at-switch>
|
||||
</div>
|
||||
<at-code-mirror
|
||||
ng-if="vm.form.extraVars.isOpen"
|
||||
col="4" tab="3"
|
||||
class="Form-formGroup--fullWidth"
|
||||
variables="vm.form.extraVars.value"
|
||||
label="{{ vm.form.extraVars.label }}"
|
||||
name="{{ vm.form.extraVars.name }}"
|
||||
>
|
||||
</at-code-mirror>
|
||||
</div>
|
||||
<at-action-group col="12" pos="right">
|
||||
<at-form-action type="cancel" to="instanceGroups"></at-form-action>
|
||||
<at-form-action type="save"></at-form-action>
|
||||
</at-action-group>
|
||||
</at-form>
|
||||
</at-panel-body>
|
||||
</at-panel>
|
||||
@@ -0,0 +1,137 @@
|
||||
function EditContainerGroupController($rootScope, $scope, $state, models, strings, i18n, EditContainerGroupDataset) {
|
||||
const vm = this || {};
|
||||
const {
|
||||
instanceGroup,
|
||||
credential
|
||||
} = models;
|
||||
|
||||
if (!instanceGroup.get('is_containerized')) {
|
||||
return $state.go(
|
||||
'instanceGroups.edit',
|
||||
{ instance_group_id: instanceGroup.get('id') },
|
||||
{ reload: true }
|
||||
);
|
||||
}
|
||||
|
||||
$rootScope.breadcrumb.instance_group_name = instanceGroup.get('name');
|
||||
|
||||
vm.mode = 'edit';
|
||||
vm.strings = strings;
|
||||
vm.panelTitle = EditContainerGroupDataset.data.name;
|
||||
vm.lookUpTitle = strings.get('container.LOOK_UP_TITLE');
|
||||
|
||||
vm.form = instanceGroup.createFormSchema('post');
|
||||
vm.form.name.required = true;
|
||||
vm.form.credential = {
|
||||
type: 'field',
|
||||
label: i18n._('Credential'),
|
||||
id: 'credential'
|
||||
};
|
||||
vm.form.credential._resource = 'credential';
|
||||
vm.form.credential._route = "instanceGroups.editContainerGroup.credentials";
|
||||
vm.form.credential._model = credential;
|
||||
vm.form.credential._displayValue = EditContainerGroupDataset.data.summary_fields.credential.name;
|
||||
vm.form.credential.required = true;
|
||||
vm.form.credential._value = EditContainerGroupDataset.data.summary_fields.credential.id;
|
||||
|
||||
vm.tab = {
|
||||
details: {
|
||||
_active: true,
|
||||
_go: 'instanceGroups.editContainerGroup',
|
||||
_params: { instance_group_id: instanceGroup.get('id') }
|
||||
},
|
||||
instances: {
|
||||
_go: 'instanceGroups.containerGroupInstances',
|
||||
_params: { instance_group_id: instanceGroup.get('id') }
|
||||
},
|
||||
jobs: {
|
||||
_go: 'instanceGroups.containerGroupJobs',
|
||||
_params: { instance_group_id: instanceGroup.get('id') }
|
||||
}
|
||||
};
|
||||
|
||||
vm.form.extraVars = {
|
||||
label: strings.get('container.POD_SPEC_LABEL'),
|
||||
value: EditContainerGroupDataset.data.pod_spec_override || instanceGroup.model.OPTIONS.actions.PUT.pod_spec_override.default,
|
||||
name: 'extraVars',
|
||||
toggleLabel: strings.get('container.POD_SPEC_TOGGLE')
|
||||
};
|
||||
|
||||
function sanitizeVars (str) {
|
||||
// Quick function to test if the host vars are a json-object-string,
|
||||
// by testing if they can be converted to a JSON object w/o error.
|
||||
function IsJsonString (varStr) {
|
||||
try {
|
||||
JSON.parse(varStr);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof str === 'undefined') {
|
||||
return '---';
|
||||
}
|
||||
if (typeof str !== 'string') {
|
||||
const yamlStr = jsyaml.safeDump(str);
|
||||
// jsyaml.safeDump doesn't process an empty object correctly
|
||||
if (yamlStr === '{}\n') {
|
||||
return '---';
|
||||
}
|
||||
return yamlStr;
|
||||
}
|
||||
if (str === '' || str === '{}') {
|
||||
return '---';
|
||||
} else if (IsJsonString(str)) {
|
||||
str = JSON.parse(str);
|
||||
return jsyaml.safeDump(str);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
const podSpecValue = sanitizeVars(EditContainerGroupDataset.data.pod_spec_override);
|
||||
const defaultPodSpecValue = sanitizeVars(instanceGroup.model.OPTIONS.actions.PUT.pod_spec_override.default);
|
||||
|
||||
if ((podSpecValue !== '---') && podSpecValue && podSpecValue.trim() !== defaultPodSpecValue.trim()) {
|
||||
vm.form.extraVars.isOpen = true;
|
||||
} else {
|
||||
vm.form.extraVars.isOpen = false;
|
||||
}
|
||||
|
||||
$scope.$watch('credential', () => {
|
||||
if ($scope.credential) {
|
||||
vm.form.credential._idFromModal= $scope.credential;
|
||||
}
|
||||
});
|
||||
vm.form.save = (data) => {
|
||||
if (!vm.form.extraVars.isOpen) {
|
||||
data.pod_spec_override = null;
|
||||
} else {
|
||||
data.pod_spec_override = vm.form.extraVars.value;
|
||||
}
|
||||
return instanceGroup.request('put', { data: data }).then((res) => {
|
||||
$state.go('instanceGroups.editContainerGroup', { instance_group_id: res.data.id }, { reload: true });
|
||||
} );
|
||||
};
|
||||
|
||||
vm.toggle = () => {
|
||||
if (vm.form.extraVars.isOpen === true) {
|
||||
vm.form.extraVars.isOpen = false;
|
||||
} else {
|
||||
vm.form.extraVars.isOpen = true;
|
||||
}
|
||||
return vm.form.extraVars.isOpen;
|
||||
};
|
||||
}
|
||||
|
||||
EditContainerGroupController.$inject = [
|
||||
'$rootScope',
|
||||
'$scope',
|
||||
'$state',
|
||||
'resolvedModels',
|
||||
'InstanceGroupsStrings',
|
||||
'i18n',
|
||||
'EditContainerGroupDataset'
|
||||
];
|
||||
|
||||
export default EditContainerGroupController;
|
||||
@@ -1,4 +1,18 @@
|
||||
.InstanceGroups {
|
||||
.at-Row-actions{
|
||||
justify-content: flex-start;
|
||||
width: 300px;
|
||||
& > capacity-bar:only-child{
|
||||
margin-left: 0px;
|
||||
margin-top: 5px
|
||||
}
|
||||
}
|
||||
.at-RowAction{
|
||||
margin: 0;
|
||||
}
|
||||
.at-Row-links{
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.BreadCrumb-menuLinkImage:hover {
|
||||
color: @default-link;
|
||||
@@ -74,4 +88,47 @@
|
||||
color: @at-white;
|
||||
}
|
||||
}
|
||||
.at-Row-toggle{
|
||||
margin-top: 20px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.ContainerGroups-codeMirror{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.at-Row-container{
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.containerGroups-messageBar-link:hover{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1060px) and (min-width: 769px){
|
||||
.at-Row-links {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1061px){
|
||||
.at-Row-actions{
|
||||
justify-content: flex-end;
|
||||
& > capacity-bar:only-child {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
.instanceGroupsList-details{
|
||||
display: flex;
|
||||
}
|
||||
.at-Row-links {
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
width: 445px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<aw-limit-panels max-panels="2" panel-container="instance-groups-panel"></aw-limit-panels>
|
||||
<div ui-view="add"></div>
|
||||
<div ui-view="edit"></div>
|
||||
<div ui-view="addContainerGroup"></div>
|
||||
<div ui-view="editContainerGroup"></div>
|
||||
<div ui-view="containerGroupInstances"></div>
|
||||
<div ui-view="containerGroupJobs"></div>
|
||||
|
||||
<div ui-view="instanceJobsContainer"></div>
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
function InstanceGroupsStrings (BaseString) {
|
||||
function InstanceGroupsStrings(BaseString) {
|
||||
BaseString.call(this, 'instanceGroups');
|
||||
|
||||
const { t } = this;
|
||||
const {
|
||||
t
|
||||
} = this;
|
||||
const ns = this.instanceGroups;
|
||||
|
||||
ns.state = {
|
||||
INSTANCE_GROUPS_BREADCRUMB_LABEL: t.s('INSTANCE GROUPS'),
|
||||
INSTANCES_BREADCRUMB_LABEL: t.s('INSTANCES'),
|
||||
ADD_BREADCRUMB_LABEL: t.s('CREATE INSTANCE GROUP')
|
||||
ADD_BREADCRUMB_LABEL: t.s('CREATE INSTANCE GROUP'),
|
||||
ADD_CONTAINER_GROUP_BREADCRUMB_LABEL: t.s('CREATE CONTAINER GROUP')
|
||||
};
|
||||
|
||||
ns.list = {
|
||||
@@ -33,7 +36,8 @@ function InstanceGroupsStrings (BaseString) {
|
||||
};
|
||||
|
||||
ns.instance = {
|
||||
PANEL_TITLE: t.s('SELECT INSTANCE')
|
||||
PANEL_TITLE: t.s('SELECT INSTANCE'),
|
||||
BADGE_TEXT: t.s('Instance Group')
|
||||
};
|
||||
|
||||
ns.capacityBar = {
|
||||
@@ -62,6 +66,15 @@ function InstanceGroupsStrings (BaseString) {
|
||||
ns.alert = {
|
||||
MISSING_PARAMETER: t.s('Instance Group parameter is missing.'),
|
||||
};
|
||||
ns.container = {
|
||||
PANEL_TITLE: t.s('Add Container Group'),
|
||||
LOOK_UP_TITLE: t.s('Add Credential'),
|
||||
CREDENTIAL_PLACEHOLDER: t.s('SELECT A CREDENTIAL'),
|
||||
POD_SPEC_LABEL: t.s('Pod Spec Override'),
|
||||
BADGE_TEXT: t.s('Container Group'),
|
||||
POD_SPEC_TOGGLE: t.s('Customize Pod Spec')
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
InstanceGroupsStrings.$inject = ['BaseStringService'];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function InstanceModalController ($scope, $state, Dataset, models, strings, ProcessErrors, Wait) {
|
||||
function InstanceModalController ($scope, $state, Dataset, models, strings, ProcessErrors, Wait, routeData) {
|
||||
const { instanceGroup } = models;
|
||||
const vm = this || {};
|
||||
let relatedInstanceIds = [];
|
||||
@@ -116,11 +116,11 @@ function InstanceModalController ($scope, $state, Dataset, models, strings, Proc
|
||||
};
|
||||
|
||||
vm.onSaveSuccess = () => {
|
||||
$state.go('instanceGroups.instances', {}, {reload: 'instanceGroups.instances'});
|
||||
$state.go(`${routeData}`, {}, {reload: `${routeData}`});
|
||||
};
|
||||
|
||||
vm.dismiss = () => {
|
||||
$state.go('instanceGroups.instances');
|
||||
$state.go(`${routeData}`);
|
||||
};
|
||||
|
||||
vm.toggleRow = (row) => {
|
||||
@@ -163,7 +163,8 @@ InstanceModalController.$inject = [
|
||||
'resolvedModels',
|
||||
'InstanceGroupsStrings',
|
||||
'ProcessErrors',
|
||||
'Wait'
|
||||
'Wait',
|
||||
'routeData'
|
||||
];
|
||||
|
||||
export default InstanceModalController;
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<div class="at-ActionGroup">
|
||||
<div class="pull-right">
|
||||
<button class="btn at-ButtonHollow--default"
|
||||
ng-click="$state.go('instanceGroups.instances')">
|
||||
ng-click=vm.dismiss()>
|
||||
{{:: vm.strings.get('CANCEL') }}
|
||||
</button>
|
||||
<button class="btn at-Button--success"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="at-List-toolbarAction">
|
||||
<button
|
||||
type="button"
|
||||
ng-click="$state.go('instanceGroups.instances.modal.add')"
|
||||
ng-click= "vm.addInstances()"
|
||||
class="at-Button--add"
|
||||
id="button-add"
|
||||
ng-show="vm.isSuperuser"
|
||||
|
||||
@@ -8,6 +8,41 @@ function InstancesController ($scope, $state, $http, $transitions, models, strin
|
||||
vm.policy_instance_list = instanceGroup.get('policy_instance_list');
|
||||
vm.isSuperuser = $scope.$root.user_is_superuser;
|
||||
|
||||
let tabs = {};
|
||||
let addInstancesRoute ="";
|
||||
if ($state.is("instanceGroups.instances")) {
|
||||
tabs={ state: {
|
||||
details: {
|
||||
_go: 'instanceGroups.edit'
|
||||
},
|
||||
instances: {
|
||||
_active: true,
|
||||
_go: 'instanceGroups.instances'
|
||||
},
|
||||
jobs: {
|
||||
_go: 'instanceGroups.jobs'
|
||||
}
|
||||
}
|
||||
};
|
||||
addInstancesRoute = 'instanceGroups.instances.modal.add';
|
||||
} else if ($state.is("instanceGroups.containerGroupInstances")) {
|
||||
tabs={
|
||||
state: {
|
||||
details: {
|
||||
_go: 'instanceGroups.editContainerGroup'
|
||||
},
|
||||
instances: {
|
||||
_active: true,
|
||||
_go: 'instanceGroups.containerGroupInstances'
|
||||
},
|
||||
jobs: {
|
||||
_go: 'instanceGroups.containerGroupJobs'
|
||||
}
|
||||
}
|
||||
};
|
||||
addInstancesRoute = 'instanceGroups.containerGroupInstances.modal.add';
|
||||
}
|
||||
|
||||
vm.list = {
|
||||
name: 'instances',
|
||||
iterator: 'instance',
|
||||
@@ -21,6 +56,12 @@ function InstancesController ($scope, $state, $http, $transitions, models, strin
|
||||
value: 'hostname'
|
||||
};
|
||||
|
||||
vm.addInstances = () => {
|
||||
|
||||
return $state.go(`${addInstancesRoute}`);
|
||||
};
|
||||
|
||||
|
||||
vm.toolbarSortValue = toolbarSortDefault;
|
||||
vm.toolbarSortOptions = [
|
||||
toolbarSortDefault,
|
||||
@@ -64,21 +105,14 @@ function InstancesController ($scope, $state, $http, $transitions, models, strin
|
||||
}, { notify: false, location: 'replace' });
|
||||
};
|
||||
|
||||
vm.tab = {
|
||||
details: {
|
||||
_go: 'instanceGroups.edit',
|
||||
_params: { instance_group_id: vm.instance_group_id }
|
||||
},
|
||||
instances: {
|
||||
_active: true,
|
||||
_go: 'instanceGroups.instances',
|
||||
_params: { instance_group_id: vm.instance_group_id }
|
||||
},
|
||||
jobs: {
|
||||
_go: 'instanceGroups.jobs',
|
||||
_params: { instance_group_id: vm.instance_group_id }
|
||||
}
|
||||
};
|
||||
const tabObj = {};
|
||||
const params = { instance_group_id: instanceGroup.get('id') };
|
||||
|
||||
tabObj.details = { _go: tabs.state.details._go, _params: params };
|
||||
tabObj.instances = { _go: tabs.state.instances._go, _params: params, _active: true };
|
||||
tabObj.jobs = { _go: tabs.state.jobs._go, _params: params };
|
||||
vm.tab = tabObj;
|
||||
|
||||
|
||||
vm.tooltips = {
|
||||
add: strings.get('tooltips.ASSOCIATE_INSTANCES')
|
||||
@@ -107,7 +141,6 @@ function InstancesController ($scope, $state, $http, $transitions, models, strin
|
||||
url: instance.url,
|
||||
data
|
||||
};
|
||||
|
||||
$http(req).then(vm.onSaveSuccess)
|
||||
.catch(({data, status}) => {
|
||||
ProcessErrors($scope, data, status, null, {
|
||||
@@ -157,7 +190,7 @@ InstancesController.$inject = [
|
||||
'resolvedModels',
|
||||
'InstanceGroupsStrings',
|
||||
'Dataset',
|
||||
'ProcessErrors'
|
||||
'ProcessErrors',
|
||||
];
|
||||
|
||||
export default InstancesController;
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
|
||||
function InstanceGroupJobsContainerController ($scope, strings, $state) {
|
||||
const vm = this || {};
|
||||
|
||||
const instanceGroupId = $state.params.instance_group_id;
|
||||
|
||||
let tabs = {};
|
||||
if ($state.is('instanceGroups.jobs')) {
|
||||
tabs = {
|
||||
state: {
|
||||
details: {
|
||||
_go: 'instanceGroups.edit'
|
||||
},
|
||||
instances: {
|
||||
_go: 'instanceGroups.instances'
|
||||
},
|
||||
jobs: {
|
||||
_active: true,
|
||||
_go: 'instanceGroups.jobs'
|
||||
}
|
||||
}
|
||||
};
|
||||
} else if ($state.is('instanceGroups.containerGroupJobs')) {
|
||||
tabs = {
|
||||
state: {
|
||||
details: {
|
||||
_go: 'instanceGroups.editContainerGroup'
|
||||
},
|
||||
instances: {
|
||||
_go: 'instanceGroups.containerGroupInstances'
|
||||
},
|
||||
jobs: {
|
||||
_active: true,
|
||||
_go: 'instanceGroups.containerGroupJobs'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
vm.panelTitle = strings.get('jobs.PANEL_TITLE');
|
||||
vm.strings = strings;
|
||||
const tabObj = {};
|
||||
|
||||
vm.tab = {
|
||||
details: {
|
||||
_go: 'instanceGroups.edit',
|
||||
_params: { instance_group_id: instanceGroupId },
|
||||
_label: strings.get('tab.DETAILS')
|
||||
},
|
||||
instances: {
|
||||
_go: 'instanceGroups.instances',
|
||||
_params: { instance_group_id: instanceGroupId },
|
||||
_label: strings.get('tab.INSTANCES')
|
||||
},
|
||||
jobs: {
|
||||
_active: true,
|
||||
_params: { instance_group_id: instanceGroupId },
|
||||
_label: strings.get('tab.JOBS')
|
||||
}
|
||||
};
|
||||
tabObj.details = { _go: tabs.state.details._go, _params: { instance_group_id: parseInt(instanceGroupId) } };
|
||||
tabObj.instances = { _go: tabs.state.instances._go, _params: { instance_group_id: parseInt(instanceGroupId) } };
|
||||
tabObj.jobs = { _go: tabs.state.jobs._go, _params: { instance_group_id: parseInt(instanceGroupId) }, _active: true };
|
||||
vm.tab = tabObj;
|
||||
|
||||
$scope.$on('updateCount', (e, count) => {
|
||||
if (typeof count === 'number') {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<at-tab-group class="at-TabGroup--padBelow">
|
||||
<at-tab state="vm.tab.details">{{:: vm.strings.get('tab.DETAILS') }}</at-tab>
|
||||
<at-tab state="vm.tab.instances">{{:: vm.strings.get('tab.INSTANCES') }}</at-tab>
|
||||
<at-tab ng-if="$state.is('instanceGroups.jobs')" state="vm.tab.instances">{{:: vm.strings.get('tab.INSTANCES') }}</at-tab>
|
||||
<at-tab state="vm.tab.jobs">{{:: vm.strings.get('tab.JOBS') }}</at-tab>
|
||||
</at-tab-group>
|
||||
<div ui-view="jobsList"></div>
|
||||
|
||||
@@ -18,19 +18,20 @@
|
||||
search-tags="searchTags">
|
||||
</smart-search>
|
||||
<div class="at-List-toolbarAction">
|
||||
<button
|
||||
type="button"
|
||||
ui-sref="instanceGroups.add"
|
||||
class="at-Button--add"
|
||||
id="button-add"
|
||||
ng-show="vm.isSuperuser"
|
||||
aw-tool-tip="{{vm.tooltips.add}}"
|
||||
data-placement="top"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<button type="button" class="at-Button--add" id="button-add" ng-show="vm.isSuperuser"
|
||||
data-toggle="dropdown" data-placement="top" aria-haspopup="true" aria-expanded="false">
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="button-add">
|
||||
<a class="dropdown-item" ui-sref="instanceGroups.add">
|
||||
{{:: vm.strings.get('state.ADD_BREADCRUMB_LABEL') }}
|
||||
</a>
|
||||
<a class="dropdown-item" ui-sref="instanceGroups.addContainerGroup">
|
||||
{{:: vm.strings.get('state.ADD_CONTAINER_GROUP_BREADCRUMB_LABEL') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<at-list-toolbar
|
||||
ng-if="instance_groups.length > 0"
|
||||
sort-only="true"
|
||||
@@ -40,46 +41,72 @@
|
||||
</at-list-toolbar>
|
||||
<at-list results="instance_groups">
|
||||
<at-row ng-repeat="instance_group in instance_groups"
|
||||
ng-class="{'at-Row--active': (instance_group.id === vm.activeId)}">
|
||||
|
||||
ng-class="{'at-Row--active': (instance_group.id === vm.activeId)}" >
|
||||
<div class="at-Row-items">
|
||||
<at-row-item
|
||||
header-value="{{ instance_group.name }}"
|
||||
header-state="instanceGroups.edit({instance_group_id:{{instance_group.id}}})"
|
||||
header-tag="{{ instance_group.is_isolated ? vm.strings.get('list.ROW_ITEM_LABEL_ISOLATED') : '' }}"
|
||||
>
|
||||
</at-row-item>
|
||||
|
||||
<div class="at-Row--inline">
|
||||
<at-row-item
|
||||
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_INSTANCES') }}"
|
||||
label-state="instanceGroups.instances({instance_group_id: {{ instance_group.id }}})"
|
||||
value="{{ instance_group.instances }}"
|
||||
inline="true"
|
||||
badge="true">
|
||||
</at-row-item>
|
||||
<at-row-item
|
||||
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_RUNNING_JOBS') }}"
|
||||
label-state="instanceGroups.jobs({instance_group_id: {{ instance_group.id }}, job_search: {status__in: ['running,waiting']}})"
|
||||
value="{{ instance_group.jobs_running }}"
|
||||
inline="true"
|
||||
badge="true">
|
||||
</at-row-item>
|
||||
<at-row-item
|
||||
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_TOTAL_JOBS') }}"
|
||||
label-state="instanceGroups.jobs({instance_group_id: {{ instance_group.id }}})"
|
||||
value="{{ instance_group.jobs_total }}"
|
||||
inline="true"
|
||||
badge="true">
|
||||
</at-row-item>
|
||||
<div class="at-Row-container">
|
||||
<div class="at-Row-content">
|
||||
<at-row-item
|
||||
ng-if="!instance_group.credential"
|
||||
header-value="{{ instance_group.name }}"
|
||||
header-state="instanceGroups.edit({instance_group_id:{{instance_group.id}}})"
|
||||
header-tag="{{ instance_group.is_isolated ? vm.strings.get('list.ROW_ITEM_LABEL_ISOLATED') : '' }}"
|
||||
>
|
||||
</at-row-item>
|
||||
<at-row-item
|
||||
ng-if="instance_group.credential"
|
||||
header-value="{{ instance_group.name }}"
|
||||
header-state="instanceGroups.editContainerGroup({instance_group_id:{{instance_group.id}}})"
|
||||
header-tag="{{ instance_group.is_isolated ? vm.strings.get('list.ROW_ITEM_LABEL_ISOLATED') : '' }}"
|
||||
>
|
||||
</at-row-item>
|
||||
<div class="at-RowItem--labels" ng-if="instance_group.credential">
|
||||
<div class="LabelList-tagContainer">
|
||||
<div class="LabelList-tag" ng-class="{'LabelList-tag--deletable' : (showDelete && template.summary_fields.user_capabilities.edit)}">
|
||||
<span class="LabelList-name">{{vm.strings.get('container.BADGE_TEXT') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="at-RowItem--labels" ng-if="!instance_group.credential">
|
||||
<div class="LabelList-tagContainer">
|
||||
<div class="LabelList-tag" ng-class="{'LabelList-tag--deletable' : (showDelete && template.summary_fields.user_capabilities.edit)}">
|
||||
<span class="LabelList-name">{{vm.strings.get('instance.BADGE_TEXT') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="instanceGroupsList-details">
|
||||
<div class="at-Row-links">
|
||||
<at-row-item
|
||||
ng-if="!instance_group.credential"
|
||||
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_INSTANCES') }}"
|
||||
label-state="instanceGroups.instances({instance_group_id: {{ instance_group.id }}})"
|
||||
value="{{ instance_group.instances }}"
|
||||
inline="true"
|
||||
badge="true">
|
||||
</at-row-item>
|
||||
<at-row-item
|
||||
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_RUNNING_JOBS') }}"
|
||||
label-state="instanceGroups.jobs({instance_group_id: {{ instance_group.id }}, job_search: {status__in: ['running,waiting']}})"
|
||||
value="{{ instance_group.jobs_running }}"
|
||||
inline="true"
|
||||
badge="true">
|
||||
</at-row-item>
|
||||
<at-row-item
|
||||
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_TOTAL_JOBS') }}"
|
||||
label-state="instanceGroups.jobs({instance_group_id: {{ instance_group.id }}})"
|
||||
value="{{ instance_group.jobs_total }}"
|
||||
inline="true"
|
||||
badge="true">
|
||||
</at-row-item>
|
||||
</div>
|
||||
<div class="at-Row-actions" >
|
||||
<capacity-bar ng-show="!instance_group.credential" label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_USED_CAPACITY') }}" capacity="instance_group.consumed_capacity" total-capacity="instance_group.capacity"></capacity-bar>
|
||||
<at-row-action ng-class="{'at-Row-actions-noCredential': !instance_group.credential}" icon="fa-trash" ng-click="vm.deleteInstanceGroup(instance_group)" ng-if="vm.rowAction.trash(instance_group)">
|
||||
</at-row-action>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="at-Row-actions">
|
||||
<capacity-bar label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_USED_CAPACITY') }}" capacity="instance_group.consumed_capacity" total-capacity="instance_group.capacity"></capacity-bar>
|
||||
<at-row-action icon="fa-trash" ng-click="vm.deleteInstanceGroup(instance_group)" ng-if="vm.rowAction.trash(instance_group)">
|
||||
</at-row-action>
|
||||
</div>
|
||||
</at-row>
|
||||
</at-list>
|
||||
</at-panel-body>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { templateUrl } from '../shared/template-url/template-url.factory';
|
||||
import {
|
||||
templateUrl
|
||||
} from '../shared/template-url/template-url.factory';
|
||||
import CapacityAdjuster from './capacity-adjuster/capacity-adjuster.directive';
|
||||
import AddContainerGroup from './container-groups/add-container-group.view.html';
|
||||
import EditContainerGroupController from './container-groups/edit-container-group.controller';
|
||||
import AddContainerGroupController from './container-groups/add-container-group.controller';
|
||||
import CapacityBar from './capacity-bar/capacity-bar.directive';
|
||||
import instanceGroupsMultiselect from '../shared/instance-groups-multiselect/instance-groups.directive';
|
||||
import instanceGroupsModal from '../shared/instance-groups-multiselect/instance-groups-modal/instance-groups-modal.directive';
|
||||
@@ -22,37 +27,58 @@ import service from './instance-groups.service';
|
||||
|
||||
import InstanceGroupsStrings from './instance-groups.strings';
|
||||
|
||||
import instanceGroupJobsRoute from '~features/jobs/routes/instanceGroupJobs.route.js';
|
||||
import {instanceGroupJobsRoute, containerGroupJobsRoute} from '~features/jobs/routes/instanceGroupJobs.route.js';
|
||||
import instanceJobsRoute from '~features/jobs/routes/instanceJobs.route.js';
|
||||
|
||||
|
||||
const MODULE_NAME = 'instanceGroups';
|
||||
|
||||
function InstanceGroupsResolve ($q, $stateParams, InstanceGroup, Instance, ProcessErrors, strings) {
|
||||
function InstanceGroupsResolve($q, $stateParams, InstanceGroup, Credential, Instance, ProcessErrors, strings) {
|
||||
const instanceGroupId = $stateParams.instance_group_id;
|
||||
const instanceId = $stateParams.instance_id;
|
||||
let promises = {};
|
||||
|
||||
if (!instanceGroupId && !instanceId) {
|
||||
promises.instanceGroup = new InstanceGroup(['get', 'options']);
|
||||
promises.credential = new Credential(['get', 'options']);
|
||||
return $q.all(promises);
|
||||
}
|
||||
|
||||
if (instanceGroupId && instanceId) {
|
||||
promises.instance = new Instance(['get', 'options'], [instanceId, instanceId])
|
||||
.then((instance) => instance.extend('get', 'jobs', {params: {page_size: "10", order_by: "-finished"}}));
|
||||
.then((instance) => instance.extend('get', 'jobs', {
|
||||
params: {
|
||||
page_size: "10",
|
||||
order_by: "-finished"
|
||||
}
|
||||
}));
|
||||
return $q.all(promises);
|
||||
}
|
||||
|
||||
promises.instanceGroup = new InstanceGroup(['get', 'options'], [instanceGroupId, instanceGroupId])
|
||||
.then((instanceGroup) => instanceGroup.extend('get', 'jobs', {params: {page_size: "10", order_by: "-finished"}}))
|
||||
.then((instanceGroup) => instanceGroup.extend('get', 'instances'));
|
||||
.then((instanceGroup) => instanceGroup.extend('get', 'jobs', {
|
||||
params: {
|
||||
page_size: "10",
|
||||
order_by: "-finished"
|
||||
}
|
||||
}))
|
||||
.then((instanceGroup) => instanceGroup.extend('get', 'instances'));
|
||||
|
||||
promises.credential = new Credential();
|
||||
|
||||
return $q.all(promises)
|
||||
.then(models => models)
|
||||
.catch(({ data, status, config }) => {
|
||||
.catch(({
|
||||
data,
|
||||
status,
|
||||
config
|
||||
}) => {
|
||||
ProcessErrors(null, data, status, null, {
|
||||
hdr: strings.get('error.HEADER'),
|
||||
msg: strings.get('error.CALL', { path: `${config.url}`, status })
|
||||
msg: strings.get('error.CALL', {
|
||||
path: `${config.url}`,
|
||||
status
|
||||
})
|
||||
});
|
||||
return $q.reject();
|
||||
});
|
||||
@@ -62,12 +88,13 @@ InstanceGroupsResolve.$inject = [
|
||||
'$q',
|
||||
'$stateParams',
|
||||
'InstanceGroupModel',
|
||||
'CredentialModel',
|
||||
'InstanceModel',
|
||||
'ProcessErrors',
|
||||
'InstanceGroupsStrings'
|
||||
];
|
||||
|
||||
function InstanceGroupsRun ($stateExtender, strings) {
|
||||
function InstanceGroupsRun($stateExtender, strings) {
|
||||
$stateExtender.addState({
|
||||
name: 'instanceGroups',
|
||||
url: '/instance_groups',
|
||||
@@ -100,7 +127,7 @@ function InstanceGroupsRun ($stateExtender, strings) {
|
||||
resolve: {
|
||||
resolvedModels: InstanceGroupsResolve,
|
||||
Dataset: ['InstanceGroupList', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||
function(list, qs, $stateParams, GetBasePath) {
|
||||
function (list, qs, $stateParams, GetBasePath) {
|
||||
let path = GetBasePath(list.basePath) || GetBasePath(list.name);
|
||||
return qs.search(path, $stateParams[`${list.iterator}_search`]);
|
||||
}
|
||||
@@ -143,6 +170,157 @@ function InstanceGroupsRun ($stateExtender, strings) {
|
||||
]
|
||||
}
|
||||
});
|
||||
$stateExtender.addState({
|
||||
name: 'instanceGroups.addContainerGroup',
|
||||
url: '/container_group',
|
||||
views: {
|
||||
'addContainerGroup@instanceGroups': {
|
||||
templateUrl: AddContainerGroup,
|
||||
controller: AddContainerGroupController,
|
||||
controllerAs: 'vm'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
resolvedModels: InstanceGroupsResolve,
|
||||
DataSet: ['Rest', 'GetBasePath', (Rest, GetBasePath) => {
|
||||
Rest.setUrl(`${GetBasePath('instance_groups')}`);
|
||||
return Rest.options();
|
||||
}]
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: strings.get('state.ADD_CONTAINER_GROUP_BREADCRUMB_LABEL')
|
||||
},
|
||||
});
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'instanceGroups.addContainerGroup.credentials',
|
||||
url: '/credential?selected',
|
||||
searchPrefix: 'credential',
|
||||
params: {
|
||||
credential_search: {
|
||||
value: {
|
||||
credential_type__kind: 'kubernetes',
|
||||
order_by: 'name',
|
||||
page_size: 5,
|
||||
},
|
||||
dynamic: true,
|
||||
squash: ''
|
||||
}
|
||||
},
|
||||
data: {
|
||||
basePath: 'credentials',
|
||||
formChildState: true
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
skip: true
|
||||
},
|
||||
views: {
|
||||
'credentials@instanceGroups.addContainerGroup': {
|
||||
templateProvider: (ListDefinition, generateList) => {
|
||||
const html = generateList.build({
|
||||
mode: 'lookup',
|
||||
list: ListDefinition,
|
||||
input_type: 'radio'
|
||||
});
|
||||
return `<lookup-modal>${html}</lookup-modal>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
ListDefinition: ['CredentialList', list => list],
|
||||
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', (list, qs, $stateParams, GetBasePath) => {
|
||||
|
||||
|
||||
const searchPath = GetBasePath('credentials');
|
||||
return qs.search(
|
||||
searchPath,
|
||||
$stateParams[`${list.iterator}_search`]
|
||||
);
|
||||
}]
|
||||
},
|
||||
onExit ($state) {
|
||||
if ($state.transition) {
|
||||
$('#form-modal').modal('hide');
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'instanceGroups.editContainerGroup',
|
||||
url: '/container_group/:instance_group_id',
|
||||
views: {
|
||||
'editContainerGroup@instanceGroups': {
|
||||
templateUrl: AddContainerGroup,
|
||||
controller: EditContainerGroupController,
|
||||
controllerAs: 'vm'
|
||||
}
|
||||
},
|
||||
|
||||
resolve: {
|
||||
resolvedModels: InstanceGroupsResolve,
|
||||
EditContainerGroupDataset: ['GetBasePath', 'QuerySet', '$stateParams',
|
||||
function (GetBasePath, qs, $stateParams) {
|
||||
let path = `${GetBasePath('instance_groups')}${$stateParams.instance_group_id}`;
|
||||
return qs.search(path, $stateParams);
|
||||
}
|
||||
],
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: '{{breadcrumb.instance_group_name}}'
|
||||
},
|
||||
});
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'instanceGroups.editContainerGroup.credentials',
|
||||
url: '/credential?selected',
|
||||
searchPrefix: 'credential',
|
||||
params: {
|
||||
credential_search: {
|
||||
value: {
|
||||
credential_type__kind: 'kubernetes',
|
||||
order_by: 'name',
|
||||
page_size: 5,
|
||||
},
|
||||
dynamic: true,
|
||||
squash: ''
|
||||
}
|
||||
},
|
||||
data: {
|
||||
basePath: 'credentials',
|
||||
formChildState: true
|
||||
},
|
||||
views: {
|
||||
'credentials@instanceGroups.editContainerGroup': {
|
||||
templateProvider: (ListDefinition, generateList) => {
|
||||
const html = generateList.build({
|
||||
mode: 'lookup',
|
||||
list: ListDefinition,
|
||||
input_type: 'radio'
|
||||
});
|
||||
return `<lookup-modal>${html}</lookup-modal>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
ListDefinition: ['CredentialList', list => list],
|
||||
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath', (list, qs, $stateParams, GetBasePath) => {
|
||||
const searchPath = GetBasePath('credentials');
|
||||
return qs.search(
|
||||
searchPath,
|
||||
$stateParams[`${list.iterator}_search`]
|
||||
);
|
||||
}]
|
||||
},
|
||||
onExit ($state) {
|
||||
if ($state.transition) {
|
||||
$('#form-modal').modal('hide');
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$stateExtender.addState({
|
||||
name: 'instanceGroups.edit',
|
||||
@@ -207,11 +385,11 @@ function InstanceGroupsRun ($stateExtender, strings) {
|
||||
resolve: {
|
||||
resolvedModels: InstanceGroupsResolve,
|
||||
Dataset: ['GetBasePath', 'QuerySet', '$stateParams',
|
||||
function(GetBasePath, qs, $stateParams) {
|
||||
function (GetBasePath, qs, $stateParams) {
|
||||
let path = `${GetBasePath('instance_groups')}${$stateParams.instance_group_id}/instances`;
|
||||
return qs.search(path, $stateParams[`instance_search`]);
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
@@ -255,21 +433,26 @@ function InstanceGroupsRun ($stateExtender, strings) {
|
||||
resolve: {
|
||||
resolvedModels: InstanceGroupsResolve,
|
||||
Dataset: ['GetBasePath', 'QuerySet', '$stateParams',
|
||||
function(GetBasePath, qs, $stateParams) {
|
||||
function (GetBasePath, qs, $stateParams) {
|
||||
let path = `${GetBasePath('instances')}`;
|
||||
return qs.search(path, $stateParams[`add_instance_search`]);
|
||||
}
|
||||
]
|
||||
],
|
||||
routeData: [function () {
|
||||
return "instanceGroups.instances";
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
$stateExtender.addState(instanceJobsRoute);
|
||||
$stateExtender.addState(instanceGroupJobsRoute);
|
||||
$stateExtender.addState(containerGroupJobsRoute);
|
||||
}
|
||||
|
||||
InstanceGroupsRun.$inject = [
|
||||
'$stateExtender',
|
||||
'InstanceGroupsStrings'
|
||||
'InstanceGroupsStrings',
|
||||
'Rest'
|
||||
];
|
||||
|
||||
angular.module(MODULE_NAME, [])
|
||||
|
||||
@@ -74,7 +74,7 @@ export default ['templateUrl', 'i18n', function(templateUrl, i18n) {
|
||||
}
|
||||
}
|
||||
|
||||
$scope.saveForm = function() {
|
||||
$scope.saveForm = function () {
|
||||
eventService.remove(listeners);
|
||||
let list = $scope.list;
|
||||
if($scope.currentSelection.name !== null) {
|
||||
@@ -89,7 +89,7 @@ export default ['templateUrl', 'i18n', function(templateUrl, i18n) {
|
||||
$state.go('^');
|
||||
};
|
||||
|
||||
$scope.toggle_row = function(selectedRow) {
|
||||
$scope.toggle_row = function (selectedRow) {
|
||||
let list = $scope.list;
|
||||
let count = 0;
|
||||
$scope[list.name].forEach(function(row) {
|
||||
|
||||
Reference in New Issue
Block a user