diff --git a/.trivyignore b/.trivyignore index 9ee7883c..2d0b1b3f 100644 --- a/.trivyignore +++ b/.trivyignore @@ -7,6 +7,7 @@ CVE-2023-27899 CVE-2023-27900 CVE-2023-27901 CVE-2023-35141 +CVE-2023-2976 # Jenkins plugin manager CVEs CVE-2022-45688 CVE-2023-20862 diff --git a/tests/conftest.py b/tests/conftest.py index 8ae44843..f379bcbd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,4 +15,4 @@ def pytest_addoption(parser: pytest.Parser): # The Jenkins image name:tag. parser.addoption("--jenkins-image", action="store", default="") # The number of jenkins agents to deploy and relate. - parser.addoption("--num-units", action="store", default="1") + parser.addoption("--num-units", action="store", default="3") diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 507572fe..4647902e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -24,6 +24,7 @@ from pytest_operator.plugin import OpsTest import jenkins +import state from .types_ import ModelAppUnit @@ -63,12 +64,18 @@ async def application_fixture( # Deploy the charm and wait for active/idle status application = await model.deploy(charm, resources=resources, series="jammy") await model.wait_for_idle( - apps=[application.name], status="active", raise_on_blocked=True, timeout=20 * 60 + apps=[application.name], + wait_for_active=True, + raise_on_blocked=True, + timeout=20 * 60, + idle_period=30, ) - yield application + # slow down update-status so that it doesn't intervene currently running tests + async with ops_test.fast_forward(fast_interval="1h"): + yield application - await model.remove_application(application.name, block_until_done=True) + await model.remove_application(application.name, force=True, block_until_done=True) @pytest_asyncio.fixture(scope="module", name="unit_ip") @@ -89,14 +96,38 @@ async def web_address_fixture(unit_ip: str): return f"http://{unit_ip}:8080" +@pytest.fixture(scope="function", name="app_suffix") +def app_suffix_fixture(): + """Get random 4 char length application suffix.""" + # secrets random hex cannot be used because it has chances to generate numeric only suffix + # which will return " is not a valid application tag" + app_suffix = "".join(random.choices(string.ascii_lowercase, k=4)) # nosec + return app_suffix + + @pytest_asyncio.fixture(scope="function", name="jenkins_k8s_agent") async def jenkins_k8s_agent_fixture( - model: Model, num_units: int + model: Model, app_suffix: str ) -> typing.AsyncGenerator[Application, None]: """The Jenkins k8s agent.""" - # secrets random hex cannot be used because it has chances to generate numeric only suffix - # which will return " is not a valid application tag" - app_suffix = "".join(random.choices(string.ascii_lowercase, k=4)) # nosec + agent_app: Application = await model.deploy( + "jenkins-agent-k8s", + config={"jenkins_agent_labels": "k8s"}, + channel="latest/edge", + application_name=f"jenkins-agentk8s-{app_suffix}", + ) + await model.wait_for_idle(apps=[agent_app.name], status="blocked") + + yield agent_app + + await model.remove_application(agent_app.name, force=True) + + +@pytest_asyncio.fixture(scope="function", name="new_relation_k8s_agents") +async def new_relation_k8s_agents_fixture( + model: Model, num_units: int, app_suffix: str +) -> typing.AsyncGenerator[Application, None]: + """The Jenkins k8s agent to be used for new agent relation with multiple units.""" agent_app: Application = await model.deploy( "jenkins-agent-k8s", config={"jenkins_agent_labels": "k8s"}, @@ -111,6 +142,23 @@ async def jenkins_k8s_agent_fixture( await model.remove_application(agent_app.name, force=True) +@pytest_asyncio.fixture(scope="function", name="new_relation_k8s_agents_related") +async def new_relation_k8s_agents_related_fixture( + model: Model, + new_relation_k8s_agents: Application, + application: Application, +): + """The Jenkins-k8s server charm related to Jenkins-k8s agent charm through agent relation.""" + await application.relate( + state.AGENT_RELATION, f"{new_relation_k8s_agents.name}:{state.AGENT_RELATION}" + ) + await model.wait_for_idle( + apps=[application.name, new_relation_k8s_agents.name], wait_for_active=True + ) + + return application + + @pytest_asyncio.fixture(scope="module", name="jenkins_client") async def jenkins_client_fixture( application: Application, @@ -185,16 +233,71 @@ async def machine_model_fixture( @pytest_asyncio.fixture(scope="function", name="jenkins_machine_agent") -async def jenkins_machine_agent_fixture(machine_model: Model) -> Application: +async def jenkins_machine_agent_fixture( + machine_model: Model, app_suffix: str +) -> typing.AsyncGenerator[Application, None]: """The jenkins machine agent.""" # 2023-06-02 use the edge version of jenkins agent until the changes have been promoted to # stable. app = await machine_model.deploy( - "jenkins-agent", channel="latest/edge", config={"labels": "machine"} + "jenkins-agent", + channel="latest/edge", + config={"labels": "machine"}, + application_name=f"jenkins-agent-{app_suffix}", ) + await machine_model.create_offer(f"{app.name}:slave") await machine_model.wait_for_idle(apps=[app.name], status="blocked", timeout=1200) - return app + yield app + + await machine_model.remove_offer(f"admin/{machine_model.name}.{app.name}", force=True) + await machine_model.remove_application(app.name, force=True) + + +@pytest_asyncio.fixture(scope="function", name="new_relation_machine_agents") +async def new_relation_machine_agents_fixture( + machine_model: Model, num_units: int, app_suffix: str +) -> typing.AsyncGenerator[Application, None]: + """The jenkins machine agent with 3 units to be used for new agent relation.""" + # 2023-06-02 use the edge version of jenkins agent until the changes have been promoted to + # stable. + app: Application = await machine_model.deploy( + "jenkins-agent", + channel="latest/edge", + config={"labels": "machine"}, + application_name=f"jenkins-agent-{app_suffix}", + num_units=num_units, + ) + await machine_model.create_offer(f"{app.name}:{state.AGENT_RELATION}") + await machine_model.wait_for_idle( + apps=[app.name], status="blocked", idle_period=30, timeout=1200 + ) + + yield app + + await machine_model.remove_offer(f"admin/{machine_model.name}.{app.name}", force=True) + await machine_model.remove_application(app.name, force=True, block_until_done=True) + + +@pytest_asyncio.fixture(scope="function", name="new_relation_agent_related") +async def new_relation_agents_related_fixture( + model: Model, + new_relation_machine_agents: Application, + application: Application, +): + """The Jenkins-k8s server charm related to Jenkins agent charm through agent relation.""" + machine_model: Model = new_relation_machine_agents.model + await machine_model.create_offer(f"{new_relation_machine_agents.name}:{state.AGENT_RELATION}") + await model.relate( + f"{application.name}:{state.AGENT_RELATION}", + f"localhost:admin/{machine_model.name}.{new_relation_machine_agents.name}", + ) + await machine_model.wait_for_idle( + apps=[new_relation_machine_agents.name], wait_for_active=True + ) + await model.wait_for_idle(apps=[application.name], wait_for_active=True) + + return application @pytest.fixture(scope="module", name="jenkins_version") @@ -287,18 +390,3 @@ def update_status_env_fixture(model: Model, unit: Unit) -> typing.Iterable[str]: f"JUJU_MODEL_NAME={model.name}", f"JUJU_UNIT_NAME={unit.name}", ) - - -@pytest_asyncio.fixture(scope="function", name="jenkins_k8s_agent_related") -async def jenkins_k8s_agent_related_fixture( - model: Model, - jenkins_k8s_agent: Application, - application: Application, -): - """The Jenkins-k8s server charm related to Jenkins-k8s agent charm through agent relation.""" - await application.relate("agent", f"{jenkins_k8s_agent.name}:agent") - await model.wait_for_idle( - apps=[application.name, jenkins_k8s_agent.name], wait_for_active=True - ) - - return application diff --git a/tests/integration/test_agent.py b/tests/integration/test_agent.py index dca91ce8..1d26c5fe 100644 --- a/tests/integration/test_agent.py +++ b/tests/integration/test_agent.py @@ -32,7 +32,7 @@ async def test_jenkins_wizard_bypass(web_address: str): assert "Welcome to Jenkins!" in str(response.content) -async def test_jenkins_deprecated_agent_relation( +async def test_jenkins_k8s_deprecated_agent_relation( model: Model, application: Application, jenkins_k8s_agent: Application, @@ -45,7 +45,9 @@ async def test_jenkins_deprecated_agent_relation( assert: the relation succeeds and the agent is able to run jobs successfully. """ await application.relate(state.DEPRECATED_AGENT_RELATION, f"{jenkins_k8s_agent.name}") - await model.wait_for_idle(status="active") + await model.wait_for_idle( + apps=[application.name, jenkins_k8s_agent.name], wait_for_active=True + ) nodes = jenkins_client.get_nodes() assert any( @@ -66,22 +68,22 @@ async def test_jenkins_machine_deprecated_agent_relation( gen_jenkins_test_job_xml: typing.Callable[[str], str], ): """ - arrange: given a cross controller cross model jenkins machine agent. - act: when the offer is created and relation is setup through the offer. + arrange: given a cross controller cross model jenkins machine agent with an offer. + act: when the relation is setup through the offer. assert: the relation succeeds and the agent is able to run jobs successfully. """ - machine_model: Model = jenkins_machine_agent.model - await machine_model.create_offer(f"{jenkins_machine_agent.name}:slave") model: Model = application.model - await model.relate( - f"{application.name}:{state.DEPRECATED_AGENT_RELATION}", + machine_model: Model = jenkins_machine_agent.model + await application.relate( + state.DEPRECATED_AGENT_RELATION, f"localhost:admin/{machine_model.name}.{jenkins_machine_agent.name}", ) - await model.wait_for_idle(status="active", timeout=1200) + await model.wait_for_idle(apps=[application.name], wait_for_active=True) + await machine_model.wait_for_idle(apps=[jenkins_machine_agent.name], wait_for_active=True) nodes = jenkins_client.get_nodes() assert any( - ("jenkins-agent-0" in key for key in nodes.keys()) + (jenkins_machine_agent.name in key for key in nodes.keys()) ), "Jenkins agent node not registered." job = jenkins_client.create_job( @@ -96,7 +98,7 @@ async def test_jenkins_machine_deprecated_agent_relation( async def test_jenkins_k8s_agent_relation( model: Model, application: Application, - jenkins_k8s_agent: Application, + new_relation_k8s_agents: Application, jenkins_client: jenkinsapi.jenkins.Jenkins, gen_jenkins_test_job_xml: typing.Callable[[str], str], ): @@ -105,15 +107,17 @@ async def test_jenkins_k8s_agent_relation( act: when the server charm is related to the k8s agent charm. assert: the relation succeeds and the agent is able to run jobs successfully. """ - await application.relate("agent", f"{jenkins_k8s_agent.name}") - await model.wait_for_idle(status="active") + await application.relate(state.AGENT_RELATION, new_relation_k8s_agents.name) + await model.wait_for_idle( + apps=[application.name, new_relation_k8s_agents.name], wait_for_active=True + ) nodes = jenkins_client.get_nodes() assert any( - (jenkins_k8s_agent.name in key for key in nodes.keys()) + (new_relation_k8s_agents.name in key for key in nodes.keys()) ), "Jenkins k8s agent node not registered." - job = jenkins_client.create_job(jenkins_k8s_agent.name, gen_jenkins_test_job_xml("k8s")) + job = jenkins_client.create_job(new_relation_k8s_agents.name, gen_jenkins_test_job_xml("k8s")) queue_item = job.invoke() queue_item.block_until_complete() build: jenkinsapi.build.Build = queue_item.get_build() @@ -122,8 +126,8 @@ async def test_jenkins_k8s_agent_relation( async def test_jenkins_k8s_agent_relation_removed( model: Model, - jenkins_k8s_agent_related: Application, - jenkins_k8s_agent: Application, + new_relation_k8s_agents_related: Application, + new_relation_k8s_agents: Application, jenkins_client: jenkinsapi.jenkins.Jenkins, ): """ @@ -131,8 +135,68 @@ async def test_jenkins_k8s_agent_relation_removed( act: when the relation is removed. assert: no agent nodes remain registered. """ - await jenkins_k8s_agent_related.remove_relation("agent", f"{jenkins_k8s_agent.name}:agent") - await model.wait_for_idle() + await new_relation_k8s_agents_related.remove_relation( + state.AGENT_RELATION, f"{new_relation_k8s_agents.name}:{state.AGENT_RELATION}" + ) + await model.wait_for_idle( + apps=[new_relation_k8s_agents_related.name, new_relation_k8s_agents.name] + ) + + nodes = jenkins_client.get_nodes() + assert not any((new_relation_k8s_agents.name in key for key in nodes.keys())) + + +async def test_jenkins_machine_agent_relation( + model: Model, + application: Application, + new_relation_machine_agents: Application, + jenkins_client: jenkinsapi.jenkins.Jenkins, + gen_jenkins_test_job_xml: typing.Callable[[str], str], +): + """ + arrange: given a cross controller cross model jenkins machine agent with an offer. + act: when the relation is setup through the offer. + assert: the relation succeeds and the agent is able to run jobs successfully. + """ + machine_model: Model = new_relation_machine_agents.model + await application.relate( + state.AGENT_RELATION, + f"localhost:admin/{machine_model.name}.{new_relation_machine_agents.name}", + ) + await model.wait_for_idle(apps=[application.name], wait_for_active=True) + await machine_model.wait_for_idle( + apps=[new_relation_machine_agents.name], wait_for_active=True + ) + + nodes = jenkins_client.get_nodes() + assert any( + (new_relation_machine_agents.name in key for key in nodes.keys()) + ), "Jenkins agent nodes not registered." + + job = jenkins_client.create_job( + new_relation_machine_agents.name, gen_jenkins_test_job_xml("machine") + ) + queue_item = job.invoke() + queue_item.block_until_complete() + build: jenkinsapi.build.Build = queue_item.get_build() + assert build.get_status() == "SUCCESS" + + +async def test_jenkins_machine_agent_relation_removed( + model: Model, + new_relation_agent_related: Application, + new_relation_machine_agents: Application, + jenkins_client: jenkinsapi.jenkins.Jenkins, +): + """ + arrange: given jenkins server charm related to jenkins-agent machine charm via agent relation. + act: when the relation is removed. + assert: no agent nodes remain registered. + """ + await new_relation_agent_related.remove_relation( + state.AGENT_RELATION, f"{new_relation_machine_agents.name}:{state.AGENT_RELATION}" + ) + await model.wait_for_idle(apps=[new_relation_agent_related.name]) nodes = jenkins_client.get_nodes() - assert not any((jenkins_k8s_agent.name in key for key in nodes.keys())) + assert not any((new_relation_machine_agents.name in key for key in nodes.keys()))