Merge "Add label aliases"

This commit is contained in:
Zuul
2026-06-27 00:05:41 +00:00
committed by Gerrit Code Review
15 changed files with 534 additions and 6 deletions
+9
View File
@@ -127,3 +127,12 @@ The attributes available to top-level label objects are:
instance is retained for at least the minimum billing interval.
This setting takes precedence over `max-[ready-]age`.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
+18
View File
@@ -1178,6 +1178,15 @@
Values set on individual labels may still override the
values set here.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
.. attr:: az
:type: str
@@ -1399,6 +1408,15 @@
A list of labels associated with this provider.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
.. attr:: az
:type: str
+18
View File
@@ -812,6 +812,15 @@
Values set on individual labels may still override the
values set here.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
.. attr:: boot-timeout
:type: int
:default: 300
@@ -989,6 +998,15 @@
A list of labels associated with this provider.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
.. attr:: boot-timeout
:type: int
:default: 300
@@ -498,6 +498,15 @@
Values set on individual labels may still override the
values set here.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
.. attr:: annotations
:type: dict
@@ -647,6 +656,15 @@
A list of labels associated with this provider.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
.. attr:: annotations
:type: dict
+18
View File
@@ -621,6 +621,15 @@
Values set on individual labels may still override the
values set here.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
.. attr:: auto-floating-ip
:type: bool
:default: True
@@ -777,6 +786,15 @@
A list of labels associated with this provider.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
.. attr:: auto-floating-ip
:type: bool
:default: True
+18
View File
@@ -498,6 +498,15 @@
Values set on individual labels may still override the
values set here.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
.. attr:: boot-timeout
:type: int
:default: 300
@@ -611,6 +620,15 @@
A list of labels associated with this provider.
.. attr:: aliases
:type: str
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
.. attr:: boot-timeout
:type: int
:default: 300
@@ -0,0 +1,6 @@
---
features:
- |
A new attribute, :attr:`label.aliases` is available for indicating
to Zuul that if an existing node of other labels is ready
for use, it may be used to satisfy a request for the label.
@@ -0,0 +1,90 @@
- pipeline:
name: check
manager: independent
trigger:
gerrit:
- event: patchset-created
success:
gerrit:
Verified: 1
failure:
gerrit:
Verified: -1
- job:
name: base
parent: null
run: playbooks/base.yaml
nodeset:
nodes:
- label: debian-normal
name: controller
- job:
name: check-job
run: playbooks/check.yaml
- project:
name: org/project
check:
jobs:
- check-job
- image:
name: debian
type: cloud
- flavor:
name: normal
- flavor:
name: spot
- label:
name: debian-normal
image: debian
flavor: normal
min-ready: 1
- label:
name: debian-spot
image: debian
flavor: spot
aliases: debian-normal
min-ready: 1
- section:
name: aws-base
abstract: true
connection: aws
launch-timeout: 600
launch-attempts: 2
label-defaults:
host-key-checking: false
boot-timeout: 120
key-name: zuul
flavors:
- name: normal
instance-type: t3.medium
- name: spot
instance-type: t3.medium
market-type: spot
images:
- name: debian
image-id: ami-1e749f67
- section:
name: aws-us-east-1
parent: aws-base
region: us-east-1
- provider:
name: aws-us-east-1-main
section: aws-us-east-1
labels:
- name: debian-normal
reuse: true
max-age: 600
- name: debian-spot
reuse: true
max-age: 600
+90
View File
@@ -0,0 +1,90 @@
- pipeline:
name: check
manager: independent
trigger:
gerrit:
- event: patchset-created
success:
gerrit:
Verified: 1
failure:
gerrit:
Verified: -1
- job:
name: base
parent: null
run: playbooks/base.yaml
nodeset:
nodes:
- label: debian-normal
name: controller
- job:
name: check-job
run: playbooks/check.yaml
- project:
name: org/project
check:
jobs:
- check-job
- image:
name: debian
type: cloud
- flavor:
name: normal
- flavor:
name: spot
- label:
name: debian-normal
image: debian
flavor: normal
aliases: debian-spot
min-ready: 1
- label:
name: debian-spot
image: debian
flavor: spot
min-ready: 1
- section:
name: aws-base
abstract: true
connection: aws
launch-timeout: 600
launch-attempts: 2
label-defaults:
host-key-checking: false
boot-timeout: 120
key-name: zuul
flavors:
- name: normal
instance-type: t3.medium
- name: spot
instance-type: t3.medium
market-type: spot
images:
- name: debian
image-id: ami-1e749f67
- section:
name: aws-us-east-1
parent: aws-base
region: us-east-1
- provider:
name: aws-us-east-1-main
section: aws-us-east-1
labels:
- name: debian-normal
reuse: true
max-age: 600
- name: debian-spot
reuse: true
max-age: 600
+88
View File
@@ -0,0 +1,88 @@
- pipeline:
name: check
manager: independent
trigger:
gerrit:
- event: patchset-created
success:
gerrit:
Verified: 1
failure:
gerrit:
Verified: -1
- job:
name: base
parent: null
run: playbooks/base.yaml
nodeset:
nodes:
- label: debian-normal
name: controller
- job:
name: check-job
run: playbooks/check.yaml
- project:
name: org/project
check:
jobs:
- check-job
- image:
name: debian
type: cloud
- flavor:
name: normal
- flavor:
name: spot
- label:
name: debian-normal
image: debian
flavor: normal
- label:
name: debian-spot
image: debian
flavor: spot
- section:
name: aws-base
abstract: true
connection: aws
launch-timeout: 600
launch-attempts: 2
label-defaults:
host-key-checking: false
boot-timeout: 120
key-name: zuul
flavors:
- name: normal
instance-type: t3.medium
- name: spot
instance-type: t3.medium
market-type: spot
images:
- name: debian
image-id: ami-1e749f67
- section:
name: aws-us-east-1
parent: aws-base
region: us-east-1
- provider:
name: aws-us-east-1-main
section: aws-us-east-1
labels:
- name: debian-normal
reuse: true
max-age: 600
aliases: debian-spot
- name: debian-spot
reuse: true
max-age: 600
+115
View File
@@ -3191,6 +3191,48 @@ class TestMinReadyLauncher(LauncherBaseTestCase):
nodes_by_label = self._nodes_by_label()
self.assertEqual(1, len(nodes_by_label['debian-emea']))
@simple_layout('layouts/nodepool-min-ready-aliases.yaml',
enable_nodepool=True)
def test_min_ready_aliases(self):
for _ in iterate_timeout(60, "nodes to be ready"):
nodes = self.launcher.api.nodes_cache.getItems()
if len(nodes) != 1:
continue
if all(n.state == n.State.READY for n in nodes):
break
# NOTE: This test relies on the min-ready algorithm processing
# labels in the order they appear in the tenant config, but
# that is not something we want to codify; it is a convenient
# implementation detail that is okay to change.
# We create the aliased debian-normal instance first, so it
# can be reused by debian-spot.
self.waitUntilSettled()
nodes = self.launcher.api.nodes_cache.getItems()
self.assertEqual(1, len(nodes))
@simple_layout('layouts/nodepool-min-ready-aliases-reverse.yaml',
enable_nodepool=True)
def test_min_ready_aliases_reverse(self):
for _ in iterate_timeout(60, "nodes to be ready"):
nodes = self.launcher.api.nodes_cache.getItems()
if len(nodes) != 2:
continue
if all(n.state == n.State.READY for n in nodes):
break
# NOTE: This test relies on the min-ready algorithm processing
# labels in the order they appear in the tenant config, but
# that is not something we want to codify; it is a convenient
# implementation detail that is okay to change.
# We create the unaliased debian-normal instance first, and it
# is not reusable by debian-spot, so we end up with two nodes.
self.waitUntilSettled()
nodes = self.launcher.api.nodes_cache.getItems()
self.assertEqual(2, len(nodes))
class TestMinReadyTenantVariant(LauncherBaseTestCase):
tenant_config_file = "config/launcher-min-ready/tenant-variant.yaml"
@@ -4130,6 +4172,79 @@ class TestSubnodesAndReuse(LauncherBaseTestCase):
except NoNodeError:
break
@simple_layout('layouts/nodepool-reuse-aliases.yaml', enable_nodepool=True)
def test_reuse_aliases(self):
# Test requesting an alias of a reusable node
ctx = self.createZKContext(None)
request = self.requestNodes(['debian-normal'])
self.assertEqual(request.State.FULFILLED, request.state)
nodes = self.launcher.api.nodes_cache.getItems()
self.assertEqual(1, len(nodes))
node = nodes[0]
# Get a copy so we're not modifying the launcher's
node = model.ProviderNode.fromZK(ctx, path=node.getPath())
with node.locked(ctx):
with node.activeContext(ctx):
node.unassign(ctx)
node.assign(ctx, request_id="dne", tenant_name="test")
# Modify the request so that the max zxid of the
# request cache is later than the node assignment
# (since the "dne" request does not exist).
with request.locked(ctx), request.activeContext(ctx):
request.priority += 1
node.setState(node.State.USED)
for _ in iterate_timeout(10, "node to be recycled"):
node.refresh(ctx)
if node.state == node.State.READY:
break
# The normal label can also be requested as a spot label
request = self.requestNodes(['debian-spot'])
self.assertEqual(request.State.FULFILLED, request.state)
nodes = self.launcher.api.nodes_cache.getItems()
self.assertEqual(1, len(nodes))
@simple_layout('layouts/nodepool-reuse-aliases.yaml', enable_nodepool=True)
def test_reuse_aliases_reverse(self):
# Test the reverse of the above test: that label aliases are
# one-way
ctx = self.createZKContext(None)
request = self.requestNodes(['debian-spot'])
self.assertEqual(request.State.FULFILLED, request.state)
nodes = self.launcher.api.nodes_cache.getItems()
self.assertEqual(1, len(nodes))
node = nodes[0]
# Get a copy so we're not modifying the launcher's
node = model.ProviderNode.fromZK(ctx, path=node.getPath())
with node.locked(ctx):
with node.activeContext(ctx):
node.unassign(ctx)
node.assign(ctx, request_id="dne", tenant_name="test")
# Modify the request so that the max zxid of the
# request cache is later than the node assignment
# (since the "dne" request does not exist).
with request.locked(ctx), request.activeContext(ctx):
request.priority += 1
node.setState(node.State.USED)
for _ in iterate_timeout(10, "node to be recycled"):
node.refresh(ctx)
if node.state == node.State.READY:
break
# The spot label may not be requested as a normal label
request = self.requestNodes(['debian-normal'])
self.assertEqual(request.State.FULFILLED, request.state)
nodes = self.launcher.api.nodes_cache.getItems()
self.assertEqual(2, len(nodes))
@simple_layout('layouts/nodepool-reuse.yaml', enable_nodepool=True)
@okay_tracebacks('_checkNodescanRequest')
def test_reuse_unreachable(self):
+3 -1
View File
@@ -448,6 +448,7 @@ class LabelParser(object):
'max-age': int,
'min-retention-time': int,
'max-nodes': int,
'aliases': to_list(str),
}
schema = vs.Schema(label)
@@ -462,7 +463,8 @@ class LabelParser(object):
conf.get('description'), conf.get('min-ready'),
conf.get('max-ready-age'), conf.get('max-age'),
conf.get('max-nodes'),
conf.get('min-retention-time'))
conf.get('min-retention-time'),
as_list(conf.get('aliases')))
label.source_context = conf.get('_source_context')
label.start_mark = conf.get('_start_mark')
return label
+20 -3
View File
@@ -1720,12 +1720,14 @@ class Launcher:
node_uuid = static_node.node_id
connection_port = static_node.connection_port
state = model.ProviderNode.State.BUILDING
label_aliases = static_node.aliases
label_aliases = set(static_node.aliases)
else:
node_uuid = uuid.uuid4().hex
connection_port = image.connection_port
state = model.ProviderNode.State.REQUESTED
label_aliases = []
label_aliases = set()
label_aliases |= set(label.aliases)
label_aliases = list(label_aliases)
args = dict(
uuid=node_uuid,
state=state,
@@ -2504,9 +2506,14 @@ class Launcher:
label_scores,
candidate_launchers):
continue
# We collect all label variants to determin if
# We collect all label variants to determine if
# min-ready is satisfied based on the config hashes
tenant_labels[tenant_name][label.name].append(label)
# Also collect all aliases
for alias in label.aliases:
if aliased_label := tenant_provider.labels.get(alias):
tenant_labels[tenant_name][
aliased_label.name].append(label)
unassigned_hashes = self._getUnassignedNodeLabelHashes()
for tenant_name, min_ready_labels in tenant_labels.items():
@@ -2524,7 +2531,13 @@ class Launcher:
provider = random.choice(label_providers)
label = provider.labels[label_name]
yield label, provider
# Add the node we just added to the node list for its label
unassigned_hashes[label.name].append(label.config_hash)
# And also all of its aliases
for alias in label.aliases:
if aliased_label := provider.labels.get(alias):
unassigned_hashes[alias].append(
label.config_hash)
def _hasHighestMinReadyScore(
self, label_cname, label_scores, candidate_launchers):
@@ -2547,6 +2560,10 @@ class Launcher:
if node.request_id is not None:
continue
unassigned_nodes[node.label].append(node.label_config_hash)
# Append our config hash for all other aliases that are
# equivalent
for alias in node.label_aliases:
unassigned_nodes[alias].append(node.label_config_hash)
return unassigned_nodes
def _getUnassignedNodes(self):
+12 -2
View File
@@ -1753,7 +1753,8 @@ class Label(ConfigObject):
"""
def __init__(self, name, image, flavor, description, min_ready,
max_ready_age, max_age, max_nodes, min_retention_time):
max_ready_age, max_age, max_nodes, min_retention_time,
aliases):
super().__init__()
self.name = name
self.image = image
@@ -1764,6 +1765,7 @@ class Label(ConfigObject):
self.max_age = max_age
self.max_nodes = max_nodes
self.min_retention_time = min_retention_time
self.aliases = aliases
@property
def project_canonical_name(self):
@@ -1793,7 +1795,8 @@ class Label(ConfigObject):
self.max_ready_age == other.max_ready_age and
self.max_age == other.max_age and
self.max_nodes == other.max_nodes and
self.min_retention_time == other.min_retention_time)
self.min_retention_time == other.min_retention_time and
self.aliases == other.aliases)
def toDict(self):
return {
@@ -1807,6 +1810,7 @@ class Label(ConfigObject):
'max_age': self.max_age,
'max_nodes': self.max_nodes,
'min_retention_time': self.min_retention_time,
'aliases': self.aliases,
}
def toConfig(self):
@@ -1820,6 +1824,7 @@ class Label(ConfigObject):
'max-age': self.max_age,
# max-nodes is only permitted at the top level
'min-retention-time': self.min_retention_time,
'aliases': self.aliases,
}
def validateReferences(self, layout):
@@ -1831,6 +1836,11 @@ class Label(ConfigObject):
raise Exception(
f'The label "{self.name}" references an unknown flavor '
f'"{self.flavor}"')
for alias in self.aliases:
if not layout.labels.get(alias):
raise Exception(
f'The label "{self.name}" references an unknown label '
f'"{alias}"')
class Section(ConfigObject):
+11
View File
@@ -19,6 +19,7 @@
import voluptuous as vs
from zuul.lib.voluputil import (
AsList,
Constant,
Nullable,
Optional,
@@ -79,6 +80,16 @@ common_label = vs.Schema({
doc="""\
Should the node be reused (True) or deleted (False) after use."""
): bool,
Optional(
'aliases',
doc="""\
A list of label aliases to associate with nodes created from
this label. These labels may be used to request existing
ready nodes that were previously created from this label, but
none of their attributes will be used to define
characteristics.
""",
default=[]): AsList(str),
Optional(
'executor-zone',
doc="""\