Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for mesh ingress #14640

Merged
merged 70 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
5ad1418
Add support for inbound hop nodes
fosterseth Oct 3, 2023
adefd14
Connect from controlplane node to mesh ingress
TheRealHaoLiu Oct 3, 2023
78bb08c
Add ReceptorAddress to root urls
fosterseth Oct 4, 2023
6b49808
Update receptor conf when address changes
fosterseth Oct 5, 2023
6f7a32b
Add peers_from_control_nodes to ReceptorAddress
fosterseth Oct 11, 2023
be656d0
Add install bundle support
fosterseth Oct 12, 2023
bb24c3e
Register_peers support for receptor_addresses
fosterseth Nov 2, 2023
fa6ece3
Temp change to aid dev
TheRealHaoLiu Nov 3, 2023
9380c18
Add API validation when creating ReceptorAddress
fosterseth Nov 9, 2023
4c74de2
Add validation when setting peers
fosterseth Nov 13, 2023
4a6c17e
Add functional and unit tests
fosterseth Nov 14, 2023
759e502
Update awx_collection to support ReceptorAddress
fosterseth Nov 15, 2023
62b0a15
Add search fields to views
fosterseth Nov 16, 2023
5ebd82d
Remove unused variables and imports
fosterseth Nov 16, 2023
ccdac7a
Add choices to module protocol field
fosterseth Nov 16, 2023
5c9eda8
Fix inconsistent tab width
fosterseth Nov 16, 2023
8142a47
Fix proper indent to instance module
fosterseth Nov 16, 2023
d6ed68e
UI Updates for receptor peering
dmzoneill Dec 14, 2023
46d6051
Add canonical receptor address
fosterseth Dec 18, 2023
872a96b
Fix lint trailing whitespace
dmzoneill Dec 19, 2023
9049f26
Mesh UI support
dmzoneill Jan 4, 2024
e8e5e24
Fix provision instance not respecting protocol
TheRealHaoLiu Jan 8, 2024
059c179
Cleanup
dmzoneill Jan 10, 2024
d99801e
Comment unused dependency
CFSNM Jan 12, 2024
1544841
Adjust migration names and dependencies
CFSNM Jan 12, 2024
499d748
Remove CRUD for Receptor Addresses
fosterseth Jan 15, 2024
5859f6c
Join across the InstanceLink.target to the underlying Instance
jbradberry Jan 15, 2024
7d2d759
Rename migration dependency
fosterseth Jan 16, 2024
19e87bc
Make canonical field default to False
fosterseth Jan 16, 2024
2626317
Remove receptor_address module from collection
fosterseth Jan 16, 2024
ae91ea3
Fix lint error, remove unused import
TheRealHaoLiu Jan 16, 2024
12fd1d6
Fix ui-lint error
TheRealHaoLiu Jan 16, 2024
b0ef9ff
Add canonical=True when creating ReceptorAddress in tests
fosterseth Jan 16, 2024
e671582
Update bootstrap_development.sh
TheRealHaoLiu Jan 16, 2024
d6d4665
Ensure register_peers target is ReceptorAddress
fosterseth Jan 16, 2024
0084d6c
Remove unused warnings import
fosterseth Jan 16, 2024
896531e
Add protocol to receptor address serializer
fosterseth Jan 17, 2024
4c545e6
Fix condition for creating receptor_address
TheRealHaoLiu Jan 17, 2024
f360b1b
Only create receptor address if port is defined
fosterseth Jan 18, 2024
cb56f89
Add migration to support InstanceLink changes
fosterseth Jan 19, 2024
d337b14
Updates for receptor reaslese to ui for protocol and is_managed
dmzoneill Jan 19, 2024
03280d4
Fix remaning tests, removed unused code
dmzoneill Jan 19, 2024
73a9329
InstanceAdd sends null for port_listener
dmzoneill Jan 19, 2024
1fb06f1
Add management command to remove address
fosterseth Jan 19, 2024
60b89b5
Reconstitute migration file
jbradberry Jan 19, 2024
cd61745
Make InstanceLink target non-nullable
fosterseth Jan 19, 2024
e999509
Template the listener protocol into the receptor install bundle (#14792)
jbradberry Jan 22, 2024
2f2e8df
Update requirements.yml
TheRealHaoLiu Jan 22, 2024
08f94ed
Support wss as ws-listener in the Receptor config
jbradberry Jan 22, 2024
0c9f4a4
Form hardening and node type exclusion
dmzoneill Jan 23, 2024
598b33c
Prevent modifying peers on managed node
fosterseth Jan 23, 2024
75ce00b
Require receptor collection 2.0.3
fosterseth Jan 23, 2024
2f1815b
Peers_from_control_nodes requires listener port
fosterseth Jan 23, 2024
71450b7
The listener port cannot be disabled when setting peers_from_control_…
jbradberry Jan 24, 2024
2bcc83e
'managed' is a read-only field on InstanceSerializer
jbradberry Jan 24, 2024
3ebdb1b
Use Counter to find duplicate peer relationships
jbradberry Jan 25, 2024
b4141df
Placeholder FIXMEs for things of concern
jbradberry Jan 25, 2024
1a448e6
Use a select_related to build the peers queryset in the install bundle
jbradberry Jan 25, 2024
302b181
Make the peer validation more compact
jbradberry Jan 25, 2024
a558dea
Break out peer validation into its own method
jbradberry Jan 25, 2024
03a2c21
Write tests around the two special instance serializer fields
jbradberry Jan 25, 2024
0d659a6
Test inspect_established_receptor_connections
fosterseth Jan 29, 2024
a806576
If managed, cannot modify peers_from_control_nodes
fosterseth Jan 30, 2024
97982b7
Remove redundant tests
fosterseth Jan 30, 2024
c713478
Prevent duplicating instance links
fosterseth Jan 31, 2024
79eb82a
Protocol blank if no canonical address
fosterseth Jan 31, 2024
286cdd1
Fix UI lint by running npm prettier
fosterseth Jan 31, 2024
be95756
Disable health check button if managed
fosterseth Jan 31, 2024
c7d0b19
InstanceLink unique constraint source and target
fosterseth Jan 31, 2024
08b1e58
UI rename Endpoints to Listener Addresses
fosterseth Feb 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 147 additions & 55 deletions awx/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import json
import logging
import re
from collections import OrderedDict
from collections import Counter, OrderedDict
from datetime import timedelta
from uuid import uuid4

Expand Down Expand Up @@ -82,6 +82,7 @@
Project,
ProjectUpdate,
ProjectUpdateEvent,
ReceptorAddress,
RefreshToken,
Role,
Schedule,
Expand Down Expand Up @@ -636,7 +637,7 @@ def validate(self, attrs):
exclusions = self.get_validation_exclusions(self.instance)
obj = self.instance or self.Meta.model()
for k, v in attrs.items():
if k not in exclusions:
if k not in exclusions and k != 'canonical_address_port':
setattr(obj, k, v)
obj.full_clean(exclude=exclusions)
# full_clean may modify values on the instance; copy those changes
Expand Down Expand Up @@ -5458,24 +5459,55 @@ def validate(self, attrs):
class InstanceLinkSerializer(BaseSerializer):
class Meta:
model = InstanceLink
fields = ('id', 'url', 'related', 'source', 'target', 'link_state')
fields = ('id', 'related', 'source', 'target', 'target_full_address', 'link_state')

source = serializers.SlugRelatedField(slug_field="hostname", queryset=Instance.objects.all())
target = serializers.SlugRelatedField(slug_field="hostname", queryset=Instance.objects.all())

target = serializers.SerializerMethodField()
target_full_address = serializers.SerializerMethodField()

def get_related(self, obj):
res = super(InstanceLinkSerializer, self).get_related(obj)
res['source_instance'] = self.reverse('api:instance_detail', kwargs={'pk': obj.source.id})
res['target_instance'] = self.reverse('api:instance_detail', kwargs={'pk': obj.target.id})
res['target_address'] = self.reverse('api:receptor_address_detail', kwargs={'pk': obj.target.id})
return res

def get_target(self, obj):
return obj.target.instance.hostname

def get_target_full_address(self, obj):
return obj.target.get_full_address()


class InstanceNodeSerializer(BaseSerializer):
class Meta:
model = Instance
fields = ('id', 'hostname', 'node_type', 'node_state', 'enabled')


class ReceptorAddressSerializer(BaseSerializer):
full_address = serializers.SerializerMethodField()

class Meta:
model = ReceptorAddress
fields = (
'id',
'url',
'address',
'port',
'protocol',
'websocket_path',
'is_internal',
'canonical',
'instance',
'peers_from_control_nodes',
'full_address',
)

def get_full_address(self, obj):
return obj.get_full_address()


class InstanceSerializer(BaseSerializer):
show_capabilities = ['edit']

Expand All @@ -5484,11 +5516,17 @@ class InstanceSerializer(BaseSerializer):
jobs_running = serializers.IntegerField(help_text=_('Count of jobs in the running or waiting state that are targeted for this instance'), read_only=True)
jobs_total = serializers.IntegerField(help_text=_('Count of all jobs that target this instance'), read_only=True)
health_check_pending = serializers.SerializerMethodField()
peers = serializers.SlugRelatedField(many=True, required=False, slug_field="hostname", queryset=Instance.objects.all())
peers = serializers.PrimaryKeyRelatedField(
help_text=_('Primary keys of receptor addresses to peer to.'), many=True, required=False, queryset=ReceptorAddress.objects.all()
)
reverse_peers = serializers.SerializerMethodField()
listener_port = serializers.IntegerField(source='canonical_address_port', required=False, allow_null=True)
peers_from_control_nodes = serializers.BooleanField(source='canonical_address_peers_from_control_nodes', required=False)
protocol = serializers.SerializerMethodField()

class Meta:
model = Instance
read_only_fields = ('ip_address', 'uuid', 'version')
read_only_fields = ('ip_address', 'uuid', 'version', 'managed', 'reverse_peers')
fields = (
'id',
'hostname',
Expand Down Expand Up @@ -5519,10 +5557,13 @@ class Meta:
'managed_by_policy',
'node_type',
'node_state',
'managed',
'ip_address',
'listener_port',
'peers',
'reverse_peers',
'listener_port',
'peers_from_control_nodes',
'protocol',
)
extra_kwargs = {
'node_type': {'initial': Instance.Types.EXECUTION, 'default': Instance.Types.EXECUTION},
Expand All @@ -5544,16 +5585,54 @@ class Meta:

def get_related(self, obj):
res = super(InstanceSerializer, self).get_related(obj)
res['receptor_addresses'] = self.reverse('api:instance_receptor_addresses_list', kwargs={'pk': obj.pk})
res['jobs'] = self.reverse('api:instance_unified_jobs_list', kwargs={'pk': obj.pk})
res['peers'] = self.reverse('api:instance_peers_list', kwargs={"pk": obj.pk})
res['instance_groups'] = self.reverse('api:instance_instance_groups_list', kwargs={'pk': obj.pk})
if obj.node_type in [Instance.Types.EXECUTION, Instance.Types.HOP]:
res['install_bundle'] = self.reverse('api:instance_install_bundle', kwargs={'pk': obj.pk})
res['peers'] = self.reverse('api:instance_peers_list', kwargs={"pk": obj.pk})
if self.context['request'].user.is_superuser or self.context['request'].user.is_system_auditor:
if obj.node_type == 'execution':
res['health_check'] = self.reverse('api:instance_health_check', kwargs={'pk': obj.pk})
return res

def create_or_update(self, validated_data, obj=None, create=True):
# create a managed receptor address if listener port is defined
port = validated_data.pop('listener_port', -1)
peers_from_control_nodes = validated_data.pop('peers_from_control_nodes', -1)

# delete the receptor address if the port is explicitly set to None
if obj and port == None:
obj.receptor_addresses.filter(address=obj.hostname).delete()

if create:
instance = super(InstanceSerializer, self).create(validated_data)
else:
instance = super(InstanceSerializer, self).update(obj, validated_data)
instance.refresh_from_db() # instance canonical address lookup is deferred, so needs to be reloaded

# only create or update if port is defined in validated_data or already exists in the
# canonical address
# this prevents creating a receptor address if peers_from_control_nodes is in
# validated_data but a port is not set
if (port != None and port != -1) or instance.canonical_address_port:
kwargs = {}
if port != -1:
kwargs['port'] = port
if peers_from_control_nodes != -1:
kwargs['peers_from_control_nodes'] = peers_from_control_nodes
if kwargs:
kwargs['canonical'] = True
instance.receptor_addresses.update_or_create(address=instance.hostname, defaults=kwargs)

return instance

def create(self, validated_data):
return self.create_or_update(validated_data, create=True)

def update(self, obj, validated_data):
return self.create_or_update(validated_data, obj, create=False)

def get_summary_fields(self, obj):
summary = super().get_summary_fields(obj)

Expand All @@ -5563,6 +5642,16 @@ def get_summary_fields(self, obj):

return summary

def get_reverse_peers(self, obj):
return Instance.objects.prefetch_related('peers').filter(peers__in=obj.receptor_addresses.all()).values_list('id', flat=True)

def get_protocol(self, obj):
# note: don't create a different query for receptor addresses, as this is prefetched on the View for optimization
for addr in obj.receptor_addresses.all():
if addr.canonical:
return addr.protocol
return ""

def get_consumed_capacity(self, obj):
return obj.consumed_capacity

Expand All @@ -5576,47 +5665,20 @@ def get_health_check_pending(self, obj):
return obj.health_check_pending

def validate(self, attrs):
def get_field_from_model_or_attrs(fd):
return attrs.get(fd, self.instance and getattr(self.instance, fd) or None)

def check_peers_changed():
'''
return True if
- 'peers' in attrs
- instance peers matches peers in attrs
'''
return self.instance and 'peers' in attrs and set(self.instance.peers.all()) != set(attrs['peers'])
# Oddly, using 'source' on a DRF field populates attrs with the source name, so we should rename it back
if 'canonical_address_port' in attrs:
attrs['listener_port'] = attrs.pop('canonical_address_port')
if 'canonical_address_peers_from_control_nodes' in attrs:
attrs['peers_from_control_nodes'] = attrs.pop('canonical_address_peers_from_control_nodes')

if not self.instance and not settings.IS_K8S:
raise serializers.ValidationError(_("Can only create instances on Kubernetes or OpenShift."))

node_type = get_field_from_model_or_attrs("node_type")
peers_from_control_nodes = get_field_from_model_or_attrs("peers_from_control_nodes")
listener_port = get_field_from_model_or_attrs("listener_port")
peers = attrs.get('peers', [])

if peers_from_control_nodes and node_type not in (Instance.Types.EXECUTION, Instance.Types.HOP):
raise serializers.ValidationError(_("peers_from_control_nodes can only be enabled for execution or hop nodes."))

if node_type in [Instance.Types.CONTROL, Instance.Types.HYBRID]:
if check_peers_changed():
raise serializers.ValidationError(
_("Setting peers manually for control nodes is not allowed. Enable peers_from_control_nodes on the hop and execution nodes instead.")
)

if not listener_port and peers_from_control_nodes:
raise serializers.ValidationError(_("Field listener_port must be a valid integer when peers_from_control_nodes is enabled."))

if not listener_port and self.instance and self.instance.peers_from.exists():
raise serializers.ValidationError(_("Field listener_port must be a valid integer when other nodes peer to it."))

for peer in peers:
if peer.listener_port is None:
raise serializers.ValidationError(_("Field listener_port must be set on peer ") + peer.hostname + ".")

if not settings.IS_K8S:
if check_peers_changed():
raise serializers.ValidationError(_("Cannot change peers."))
# cannot enable peers_from_control_nodes if listener_port is not set
if attrs.get('peers_from_control_nodes'):
port = attrs.get('listener_port', -1) # -1 denotes missing, None denotes explicit null
if (port is None) or (port == -1 and self.instance and self.instance.canonical_address is None):
raise serializers.ValidationError(_("Cannot enable peers_from_control_nodes if listener_port is not set."))

return super().validate(attrs)

Expand All @@ -5636,8 +5698,8 @@ def validate_node_state(self, value):
raise serializers.ValidationError(_("Can only change the state on Kubernetes or OpenShift."))
if value != Instance.States.DEPROVISIONING:
raise serializers.ValidationError(_("Can only change instances to the 'deprovisioning' state."))
if self.instance.node_type not in (Instance.Types.EXECUTION, Instance.Types.HOP):
raise serializers.ValidationError(_("Can only deprovision execution or hop nodes."))
if self.instance.managed:
raise serializers.ValidationError(_("Cannot deprovision managed nodes."))
else:
if value and value != Instance.States.INSTALLED:
raise serializers.ValidationError(_("Can only create instances in the 'installed' state."))
Expand All @@ -5656,18 +5718,48 @@ def validate_hostname(self, value):
def validate_listener_port(self, value):
"""
Cannot change listener port, unless going from none to integer, and vice versa
If instance is managed, cannot change listener port at all
"""
if value and self.instance and self.instance.listener_port and self.instance.listener_port != value:
raise serializers.ValidationError(_("Cannot change listener port."))
if self.instance:
canonical_address_port = self.instance.canonical_address_port
if value and canonical_address_port and canonical_address_port != value:
raise serializers.ValidationError(_("Cannot change listener port."))
if self.instance.managed and value != canonical_address_port:
raise serializers.ValidationError(_("Cannot change listener port for managed nodes."))
return value

def validate_peers(self, value):
# cannot peer to an instance more than once
peers_instances = Counter(p.instance_id for p in value)
if any(count > 1 for count in peers_instances.values()):
raise serializers.ValidationError(_("Cannot peer to the same instance more than once."))

if self.instance:
instance_addresses = set(self.instance.receptor_addresses.all())
setting_peers = set(value)
peers_changed = set(self.instance.peers.all()) != setting_peers

if not settings.IS_K8S and peers_changed:
raise serializers.ValidationError(_("Cannot change peers."))

if self.instance.managed and peers_changed:
raise serializers.ValidationError(_("Setting peers manually for managed nodes is not allowed."))

# cannot peer to self
if instance_addresses & setting_peers:
raise serializers.ValidationError(_("Instance cannot peer to its own address."))

# cannot peer to an instance that is already peered to this instance
if instance_addresses:
for p in setting_peers:
if set(p.instance.peers.all()) & instance_addresses:
raise serializers.ValidationError(_(f"Instance {p.instance.hostname} is already peered to this instance."))

return value

def validate_peers_from_control_nodes(self, value):
"""
Can only enable for K8S based deployments
"""
if value and not settings.IS_K8S:
raise serializers.ValidationError(_("Can only be enabled on Kubernetes or Openshift."))
if self.instance and self.instance.managed and self.instance.canonical_address_peers_from_control_nodes != value:
raise serializers.ValidationError(_("Cannot change peers_from_control_nodes for managed nodes."))

return value

Expand Down
11 changes: 5 additions & 6 deletions awx/api/templates/instance_install_bundle/group_vars/all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,18 @@ custom_worksign_public_keyfile: receptor/work_public_key.pem
custom_tls_certfile: receptor/tls/receptor.crt
custom_tls_keyfile: receptor/tls/receptor.key
custom_ca_certfile: receptor/tls/ca/mesh-CA.crt
receptor_protocol: 'tcp'
{% if instance.listener_port %}
{% if listener_port %}
receptor_protocol: {{ listener_protocol }}
receptor_listener: true
receptor_port: {{ instance.listener_port }}
receptor_port: {{ listener_port }}
{% else %}
receptor_listener: false
{% endif %}
{% if peers %}
receptor_peers:
{% for peer in peers %}
- host: {{ peer.host }}
port: {{ peer.port }}
protocol: tcp
- address: {{ peer.address }}
protocol: {{ peer.protocol }}
{% endfor %}
{% endif %}
{% verbatim %}
Expand Down
2 changes: 1 addition & 1 deletion awx/api/templates/instance_install_bundle/requirements.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
collections:
- name: ansible.receptor
version: 2.0.2
version: 2.0.3
2 changes: 2 additions & 0 deletions awx/api/urls/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
InstanceInstanceGroupsList,
InstanceHealthCheck,
InstancePeersList,
InstanceReceptorAddressesList,
)
from awx.api.views.instance_install_bundle import InstanceInstallBundle

Expand All @@ -21,6 +22,7 @@
re_path(r'^(?P<pk>[0-9]+)/instance_groups/$', InstanceInstanceGroupsList.as_view(), name='instance_instance_groups_list'),
re_path(r'^(?P<pk>[0-9]+)/health_check/$', InstanceHealthCheck.as_view(), name='instance_health_check'),
re_path(r'^(?P<pk>[0-9]+)/peers/$', InstancePeersList.as_view(), name='instance_peers_list'),
re_path(r'^(?P<pk>[0-9]+)/receptor_addresses/$', InstanceReceptorAddressesList.as_view(), name='instance_receptor_addresses_list'),
re_path(r'^(?P<pk>[0-9]+)/install_bundle/$', InstanceInstallBundle.as_view(), name='instance_install_bundle'),
]

Expand Down
17 changes: 17 additions & 0 deletions awx/api/urls/receptor_address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) 2017 Ansible, Inc.
# All Rights Reserved.

from django.urls import re_path

from awx.api.views import (
ReceptorAddressesList,
ReceptorAddressDetail,
)


urls = [
re_path(r'^$', ReceptorAddressesList.as_view(), name='receptor_addresses_list'),
re_path(r'^(?P<pk>[0-9]+)/$', ReceptorAddressDetail.as_view(), name='receptor_address_detail'),
]

__all__ = ['urls']
2 changes: 2 additions & 0 deletions awx/api/urls/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
from .workflow_approval_template import urls as workflow_approval_template_urls
from .workflow_approval import urls as workflow_approval_urls
from .analytics import urls as analytics_urls
from .receptor_address import urls as receptor_address_urls

v2_urls = [
re_path(r'^$', ApiV2RootView.as_view(), name='api_v2_root_view'),
Expand Down Expand Up @@ -155,6 +156,7 @@
re_path(r'^bulk/host_create/$', BulkHostCreateView.as_view(), name='bulk_host_create'),
re_path(r'^bulk/host_delete/$', BulkHostDeleteView.as_view(), name='bulk_host_delete'),
re_path(r'^bulk/job_launch/$', BulkJobLaunchView.as_view(), name='bulk_job_launch'),
re_path(r'^receptor_addresses/', include(receptor_address_urls)),
]


Expand Down
Loading
Loading