Merge pull request #709 from thekad/feature/connection-plugin
Some checks are pending
Run integration tests for the cloud.google collection / integration (stable-2.16) (push) Waiting to run
Run integration tests for the cloud.google collection / integration (stable-2.17) (push) Waiting to run
Run integration tests for the cloud.google collection / integration (stable-2.18) (push) Waiting to run
Run integration tests for the cloud.google collection / integration (stable-2.19) (push) Waiting to run

IAP connection plugin
This commit is contained in:
Chris Hawk 2025-09-08 10:39:52 -07:00 committed by GitHub
commit 091d11fd63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1064 additions and 1 deletions

View file

@ -0,0 +1,73 @@
# Identity Aware Proxy Connection Plugin
This plugin uses the gcloud cli [start-iap-tunnel](https://cloud.google.com/sdk/gcloud/reference/compute/start-iap-tunnel)
method to prepare TCP forwarding to your compute instances, and then uses the
builtin ansible SSH connection plugin to communicate ansible commands to the
target nodes.
This makes it possible to start using ansible without the need to expose your
instances to the open web, or configure stringent firewall rules to ensure no
bad actors can potentially login to your infrastructure.
## Requisites
1. The [gcloud cli tool](https://cloud.google.com/sdk/gcloud?authuser=0) installed
2. Firewall rules in places for [IAP TCP Forwarding](https://cloud.google.com/iap/docs/using-tcp-forwarding)
## Configuring the connection plugin
The connection plugin can be configured by setting some values in the
`[gcloud]` section of your ansible.cfg, here's an example:
```ini
[gcloud]
account = my-service-account@my-project.iam.gserviceaccount.com
project = my-project
region = us-central1
zone = us-central1-a
```
With the above, you can now connect to all your instances in a single
`us-central1-a` zone via IAP.
You can also couple this with the GCP dynamic inventory like so:
```yaml
plugin: google.cloud.gcp_compute
zones:
- us-central1-a
- us-central1-b
- us-central1-c
- us-central1-f
projects:
- my-project
service_account_file: /path/to/my/service-account.json
auth_kind: serviceaccount
scopes:
- 'https://www.googleapis.com/auth/cloud-platform'
- 'https://www.googleapis.com/auth/compute.readonly'
# Create groups from labels e.g.
keyed_groups:
- prefix: gcp
key: labels.gcp_role
# inventory_hostname needs to be the actual name of the instance
hostnames:
- name
# fetch zone dynamically to feed IAP plugin
compose:
ansible_gcloud_zone: zone
# maybe add some filters
filters:
- 'status = RUNNING'
- 'labels.my-special-label:some-value'
```
with the above, you don't need to statically set the zone, they will be
populated accordingly.
The rest of the connection behavior can be configured just like the builtin SSH
ansible plugin, e.g. remote user, etc.

775
plugins/connection/iap.py Normal file
View file

@ -0,0 +1,775 @@
# Copyright (c) 2025 Red Hat
# GNU General Public License v3.0+ https://www.gnu.org/licenses/gpl-3.0.txt
from __future__ import annotations
# I had to duplicate (almost?) all of the documentation found in the
# ansible.plugins.connection.ssh plugin, due to how ansible-doc and ansible-test sanity
# work, they look at the lexical structure of the code. I had initially done:
# 1. load ssh.py DOCUMENTATION string into a yaml object
# 2. modify the yaml object to change defaults / add my options
# 3. set this plugin's DOCUMENTATION to the yaml.dump() of the modified object
# but that doesn't work with how the AST evaluation is done.
# here are the changes from upstream:
# 1. Changed default private_key_file default to ~/.ssh/google_compute_engine
# 2. Make host_key_checking default to False
# 3. Added known_hosts_file option pointing to ~/.ssh/google_compute_known_hosts
DOCUMENTATION = """
name: iap
short_description: connect via SSH through Google Cloud's Identity Aware Proxy (IAP)
description:
- This connection plugin behaves almost like the stock SSH plugin, but it creates
a new IAP process per host in the inventory so connections are tunneled through
it.
author:
- Jorge A Gallegos (@thekad)
notes:
- This plugin requires you to have configured gcloud authentication prior to using
it. You can change the active configuration used, but the plugin won't auth
for you.
- This plugin is mostly a wrapper to the ``ssh`` CLI utility and the exact behavior
of the options depends on this tool. This means that the documentation provided
here is subject to be overridden by the CLI tool itself.
- Many options default to V(None) here but that only means we do not override the
SSH tool's defaults and/or configuration. For example, if you specify the port
in this plugin it will override any C(Port) entry in your C(.ssh/config).
- The ssh CLI tool uses return code 255 as a 'connection error', this can conflict
with commands/tools that also return 255 as an error code and will look like an
'unreachable' condition or 'connection error' to this plugin.
extends_documentation_fragment:
- connection_pipelining
options:
gcloud_executable:
description:
- Path to the gcloud executable, defaults to whatever is found in the PATH
environment variable.
type: string
vars:
- name: ansible_gcloud_executable
ini:
- section: gcloud
key: executable
gcloud_configuration:
description:
- If set, points to non-standard gcloud configuration.
type: string
vars:
- name: ansible_gcloud_configuration
ini:
- section: gcloud
key: configuration
env:
- name: CLOUDSDK_ACTIVE_CONFIG_NAME
gcloud_project:
description:
- The Google Cloud project ID to use for this invocation.
- If omitted, then the current active project is assumed.
type: string
vars:
- name: ansible_gcloud_project
ini:
- section: gcloud
key: project
env:
- name: CLOUDSDK_CORE_PROJECT
gcloud_account:
description:
- Google cloud account to use for invocation.
type: string
vars:
- name: ansible_gcloud_account
ini:
- section: gcloud
key: account
env:
- name: CLOUDSDK_CORE_ACCOUNT
gcloud_zone:
description:
- The Google Cloud zone to use for the instance(s).
type: string
vars:
- name: ansible_gcloud_zone
ini:
- section: gcloud
key: zone
env:
- name: CLOUDSDK_COMPUTE_ZONE
gcloud_access_token_file:
description:
- A file to read the access token from.
- The credentials of the active account (if exists) will be ignored.
type: string
vars:
- name: ansible_access_token_file
ini:
- section: gcloud
key: access_token_file
env:
- name: CLOUDSDK_AUTH_ACCESS_TOKEN_FILE
host:
description: Google Cloud instance name to connect to.
default: inventory_hostname
type: string
vars:
- name: inventory_hostname
- name: ansible_host
- name: ansible_gcloud_host
known_hosts_file:
description: Path to the UserKnownHosts file storing SSH fingerprints. Defaults
to the same file used by `gcloud compute ssh`
type: string
default: ~/.ssh/google_compute_known_hosts
ini:
- section: ssh_connection
key: known_hosts_file
vars:
- name: ansible_known_hosts_file
- name: ansible_ssh_known_hosts_file
host_key_checking:
description: Determines if SSH should reject or not a connection after checking
host keys.
default: false
type: boolean
ini:
- section: defaults
key: host_key_checking
- section: ssh_connection
key: host_key_checking
env:
- name: ANSIBLE_HOST_KEY_CHECKING
- name: ANSIBLE_SSH_HOST_KEY_CHECKING
vars:
- name: ansible_host_key_checking
- name: ansible_ssh_host_key_checking
password:
description:
- Authentication password for the O(remote_user). Can be supplied as CLI option.
type: string
vars:
- name: ansible_password
- name: ansible_ssh_pass
- name: ansible_ssh_password
password_mechanism:
description: Mechanism to use for handling ssh password prompt
type: string
default: ssh_askpass
choices:
- ssh_askpass
- sshpass
- disable
env:
- name: ANSIBLE_SSH_PASSWORD_MECHANISM
ini:
- section: ssh_connection
key: password_mechanism
vars:
- name: ansible_ssh_password_mechanism
sshpass_prompt:
description:
- Password prompt that C(sshpass)/C(SSH_ASKPASS) should search for.
- Supported by sshpass 1.06 and up when O(password_mechanism) set to V(sshpass).
- Defaults to C(Enter PIN for) when pkcs11_provider is set.
- Defaults to C(assword) when O(password_mechanism) set to V(ssh_askpass).
default: ''
type: string
ini:
- section: ssh_connection
key: 'sshpass_prompt'
env:
- name: ANSIBLE_SSHPASS_PROMPT
vars:
- name: ansible_sshpass_prompt
ssh_args:
description: arguments to pass to all ssh cli tools.
type: string
default: '-C -o controlmaster=auto -o controlpersist=60s'
ini:
- section: ssh_connection
key: ssh_args
env:
- name: ansible_ssh_args
vars:
- name: ansible_ssh_args
ssh_common_args:
description: common extra args for all ssh cli tools.
type: string
default: ''
ini:
- section: ssh_connection
key: ssh_common_args
env:
- name: ANSIBLE_SSH_COMMON_ARGS
vars:
- name: ansible_ssh_common_args
cli:
- name: ssh_common_args
ssh_executable:
description:
- This defines the location of the SSH binary. It defaults to V(ssh) which will
use the first SSH binary available in $PATH.
- This option is usually not required, it might be useful when access to system
SSH is restricted, or when using SSH wrappers to connect to remote hosts.
type: string
default: ssh
ini:
- key: ssh_executable
section: ssh_connection
env:
- name: ANSIBLE_SSH_EXECUTABLE
vars:
- name: ansible_ssh_executable
sftp_executable:
description:
- This defines the location of the sftp binary. It defaults to V(sftp) which
will use the first binary available in $PATH.
type: string
default: sftp
ini:
- key: sftp_executable
section: ssh_connection
env:
- name: ANSIBLE_SFTP_EXECUTABLE
vars:
- name: ansible_sftp_executable
scp_executable:
description:
- This defines the location of the scp binary. It defaults to V(scp) which will
use the first binary available in $PATH.
type: string
default: scp
ini:
- section: ssh_connection
key: scp_executable
env:
- name: ANSIBLE_SCP_EXECUTABLE
vars:
- name: ansible_scp_executable
scp_extra_args:
description: Extra exclusive to the C(scp) CLI
type: string
default: ''
ini:
- section: ssh_connection
key: scp_extra_args
env:
- name: ANSIBLE_SCP_EXTRA_ARGS
vars:
- name: ansible_scp_extra_args
cli:
- name: scp_extra_args
sftp_extra_args:
description: Extra exclusive to the C(sftp) CLI
type: string
default: ''
ini:
- section: ssh_connection
key: sftp_extra_args
vars:
- name: ansible_sftp_extra_args
env:
- name: ANSIBLE_SFTP_EXTRA_ARGS
cli:
- name: sftp_extra_args
ssh_extra_args:
description: Extra exclusive to the SSH CLI.
type: string
default: ''
vars:
- name: ansible_ssh_extra_args
env:
- name: ANSIBLE_SSH_EXTRA_ARGS
ini:
- section: ssh_connection
key: ssh_extra_args
cli:
- name: ssh_extra_args
reconnection_retries:
description:
- Number of attempts to connect.
- Ansible retries connections only if it gets an SSH error with a return code
of 255.
- Any errors with return codes other than 255 indicate an issue with program
execution.
type: integer
default: 0
env:
- name: ANSIBLE_SSH_RETRIES
ini:
- section: connection
key: retries
- section: ssh_connection
key: retries
vars:
- name: ansible_ssh_retries
port:
description: Remote port to connect to.
type: int
ini:
- section: defaults
key: remote_port
env:
- name: ANSIBLE_REMOTE_PORT
vars:
- name: ansible_port
- name: ansible_ssh_port
keyword:
- name: port
remote_user:
description:
- User name with which to login to the remote server, normally set by the
remote_user keyword.
- If no user is supplied, Ansible will let the SSH client binary choose the
user as it normally.
type: string
ini:
- section: defaults
key: remote_user
env:
- name: ANSIBLE_REMOTE_USER
vars:
- name: ansible_user
- name: ansible_ssh_user
cli:
- name: user
keyword:
- name: remote_user
private_key_file:
description:
- Path to private key file to use for authentication.
type: string
default: '~/.ssh/google_compute_engine'
ini:
- section: defaults
key: private_key_file
- section: gcloud
key: private_key_file
env:
- name: ANSIBLE_PRIVATE_KEY_FILE
vars:
- name: ansible_private_key_file
- name: ansible_ssh_private_key_file
- name: ansible_gcloud_private_key_file
cli:
- name: private_key_file
option: '--private-key'
private_key:
description:
- Private key contents in PEM format. Requires the C(SSH_AGENT) configuration
to be enabled.
type: string
env:
- name: ANSIBLE_PRIVATE_KEY
vars:
- name: ansible_private_key
- name: ansible_ssh_private_key
private_key_passphrase:
description:
- Private key passphrase, dependent on O(private_key).
- This does NOT have any effect when used with O(private_key_file).
type: string
env:
- name: ANSIBLE_PRIVATE_KEY_PASSPHRASE
vars:
- name: ansible_private_key_passphrase
- name: ansible_ssh_private_key_passphrase
control_path:
description:
- This is the location to save SSH's ControlPath sockets, it uses SSH's variable
substitution.
- Be aware that this setting is ignored if C(-o ControlPath) is set in ssh args.
type: string
env:
- name: ANSIBLE_SSH_CONTROL_PATH
ini:
- section: ssh_connection
key: control_path
vars:
- name: ansible_control_path
control_path_dir:
default: ~/.ansible/cp
description:
- This sets the directory to use for ssh control path if the control path
setting is null.
- Also, provides the ``%(directory)s`` variable for the control path setting.
type: string
env:
- name: ANSIBLE_SSH_CONTROL_PATH_DIR
ini:
- section: ssh_connection
key: control_path_dir
vars:
- name: ansible_control_path_dir
sftp_batch_mode:
description:
- When set to C(True), sftp will be run in batch mode, allowing detection of
transfer errors.
- When set to C(False), sftp will not be run in batch mode, preventing detection
of transfer errors.
type: bool
default: true
env:
- name: ANSIBLE_SFTP_BATCH_MODE
ini:
- section: ssh_connection
key: sftp_batch_mode
vars:
- name: ansible_sftp_batch_mode
ssh_transfer_method:
description: Preferred method to use when transferring files over ssh
type: string
default: smart
choices:
- sftp
- scp
- piped
- smart
env:
- name: ANSIBLE_SSH_TRANSFER_METHOD
ini:
- section: ssh_connection
key: transfer_method
vars:
- name: ansible_ssh_transfer_method
use_tty:
description: add -tt to ssh commands to force tty allocation.
type: bool
default: true
env:
- name: ANSIBLE_SSH_USETTY
ini:
- section: ssh_connection
key: usetty
vars:
- name: ansible_ssh_use_tty
timeout:
description:
- This is the default amount of time we will wait while establishing an SSH
connection.
- It also controls how long we can wait to access reading the connection once
established (select on the socket).
type: integer
default: 10
env:
- name: ANSIBLE_TIMEOUT
- name: ANSIBLE_SSH_TIMEOUT
ini:
- section: defaults
key: timeout
- section: ssh_connection
key: timeout
vars:
- name: ansible_ssh_timeout
cli:
- name: timeout
pkcs11_provider:
description:
- PKCS11 SmartCard provider such as opensc, e.g. /usr/local/lib/opensc-pkcs11.so
type: string
default: ''
env:
- name: ANSIBLE_PKCS11_PROVIDER
ini:
- section: ssh_connection
key: pkcs11_provider
vars:
- name: ansible_ssh_pkcs11_provider
verbosity:
description:
- Requested verbosity level for the SSH CLI.
default: 0
type: int
env:
- name: ANSIBLE_SSH_VERBOSITY
ini:
- section: ssh_connection
key: verbosity
vars:
- name: ansible_ssh_verbosity
"""
import os
import re
import pty
import shlex
import select
import shutil
import subprocess
import threading
import time
import tempfile
import typing as T
from os import path as ospath
from ansible.plugins.connection import ssh as sshconn
from ansible import errors
from ansible.utils import display
D = display.Display()
DEFAULT_GCLOUD: T.Optional[str] = shutil.which("gcloud")
DEFAULT_SSH_PORT: int = 22
PORT_REGEX = re.compile(r"\d+")
class IAP:
host: str
local_port: int
remote_port: int
master_fd: int
up: bool = False
process: T.Optional[subprocess.Popen] = None
thread: T.Optional[threading.Thread] = None
ready: threading.Event = threading.Event()
output: T.List[str] = []
def __init__(
self,
gcloud_bin: str,
host: str,
remote_port: int,
project: T.Optional[str],
account: T.Optional[str],
zone: T.Optional[str],
config: T.Optional[str] = None,
token_file: T.Optional[str] = None,
) -> None:
self.host = host
self.remote_port = remote_port
cmd: T.List[str] = [
gcloud_bin,
"compute",
"start-iap-tunnel",
host,
str(self.remote_port),
]
if config is not None:
cmd.extend(
[
"--configuration",
shlex.quote(ospath.realpath(ospath.expanduser(config.strip()))),
]
)
if project is not None:
cmd.extend(
[
"--project",
shlex.quote(project.strip()),
]
)
if account is not None:
cmd.extend(
[
"--account",
shlex.quote(account.strip()),
]
)
if zone is not None:
cmd.extend(
[
"--zone",
shlex.quote(zone.strip()),
]
)
if token_file is not None:
cmd.extend(
[
"--access-token-file",
shlex.quote(token_file.strip()),
]
)
D.vvv(f"IAP: CMD {' '.join(cmd)}", host=self.host)
try:
# start-iap-tunnel prints 2 lines:
# - Picking local unused port [$PORT].
# - Testing if tunnel connection works.
# and only when the terminal is a pty, a 3rd line:
# - Listening on port [$PORT].
# The last line only displayed after the tunnel has been tested,
# that's why we use a PTY for the subprocess
self.master_fd, slave_fd = pty.openpty()
self.process = subprocess.Popen(
cmd, stdout=slave_fd, stderr=slave_fd, text=True, close_fds=True
)
os.close(slave_fd)
self.thread = threading.Thread(target=self._monitor, daemon=True)
self.thread.start()
D.vvvvv("started IAP thread", host=self.host)
except Exception as e:
self.process = None
raise Exception from e
def _monitor(self) -> None:
"""Monitor the thread handling the IAP subprocess until it is 'up'"""
while self.process is not None and self.process.poll() is None:
# pylint: disable=disallowed-name
rlist, _, _ = select.select([self.master_fd], [], [], 0.1)
if rlist is not None:
try:
output = os.read(self.master_fd, 1024).decode("utf-8")
if output:
for line in output.splitlines():
self.output.append(line)
if line.startswith("Listening on port"):
m = PORT_REGEX.search(line)
if m is not None:
self.local_port = int(m.group())
self.up = True
D.vvv(
f"IAP: LOCAL PORT {self.local_port}",
host=self.host,
)
except OSError: # pty is closed
break
if self.up: # no need to monitor if already up
break
if not self.ready.is_set():
self.ready.set()
os.close(self.master_fd)
def terminate(self) -> None:
"""Gracefully terminate the IAP subprocess"""
D.vvv("IAP: STOPPING TUNNEL", host=self.host)
if self.process is not None and self.process.poll() is None:
try:
self.process.terminate()
self.process.wait(timeout=5) # wait up to 5 seconds to terminate IAP
except subprocess.TimeoutExpired:
self.process.kill()
D.vvvvv("terminated/killed IAP", host=self.host)
if self.thread is not None and self.thread.is_alive():
self.thread.join(timeout=1) # joining thread back should be quick
class Connection(sshconn.Connection):
"""
This is pretty much the same as the upstream ssh plugin, just overloads
the connection handling to start/stop the IAP tunnel with gcloud as appropriate
"""
iaps: dict[str, IAP] = {}
lock: threading.Lock = threading.Lock()
gcloud_executable: T.Optional[str] = None
ssh_config: str
transport = "gcloud-iap" # type: ignore[override]
def __init__(self, *args: T.Any, **kwargs: T.Any) -> None:
super(Connection, self).__init__(*args, **kwargs)
# If the gcloud binary isn't found/configured, bail out immediately
exec: T.Optional[str] = self.get_option("gcloud_executable")
if exec is None:
self.gcloud_executable = DEFAULT_GCLOUD
else:
self.gcloud_executable = exec
if self.gcloud_executable is None:
raise errors.AnsiblePluginError(
"Plugin Error: no gcloud binary found in $PATH and "
"no executable defined in ansible config"
)
def _connect(self) -> Connection:
"""Upstream ssh is empty, overload with the stuff starting the IAP tunnel"""
host: T.Optional[str] = self.get_option("host")
project: T.Optional[str] = self.get_option("gcloud_project")
account: T.Optional[str] = self.get_option("gcloud_account")
zone: T.Optional[str] = self.get_option("gcloud_zone")
token_file: T.Optional[str] = self.get_option("gcloud_access_token_file")
config: T.Optional[str] = self.get_option("gcloud_configuration")
port: T.Optional[int] = self.get_option("port")
timeout: T.Optional[int] = self.get_option("timeout")
# this shouldn't happen, but still.
if host is None:
raise errors.AnsibleAssertionError("No host defined")
with self.lock:
if host not in self.iaps:
self.iaps[host] = IAP(
str(self.gcloud_executable),
host=host,
remote_port=int(port or DEFAULT_SSH_PORT),
project=project,
zone=zone,
account=account,
config=config,
token_file=token_file,
)
success = self.iaps[host].ready.wait(timeout=timeout)
is_up: bool = False
for _ in range(3): # pylint: disable=disallowed-name
is_up = self.iaps[host].up
if success and is_up:
D.vvv("IAP: TUNNEL IS UP", host=host)
is_up = True
break
else:
time.sleep(0.5)
if not is_up:
D.vvv("IAP: TUNNEL FAILURE", host=host)
for line in self.iaps[host].output:
D.vvvvv(line, host=host)
raise errors.AnsibleRuntimeError("Failure when starting IAP tunnel")
# override port with the random IAP port
self.set_option("port", self.iaps[host].local_port)
# read path to the supplied known hosts file
ukhf: str = ospath.abspath(
ospath.expanduser(str(self.get_option("known_hosts_file")))
)
# have to trick SSH to connect to localhost instead of the instances
fd, self.ssh_config = tempfile.mkstemp(
suffix="ssh_config", prefix="ansible_gcloud", text=True
)
with open(fd, "w") as fp:
fp.write("Host *\n")
fp.write(" HostName localhost\n") # trick
fp.write(" HostKeyAlias {}\n".format(host)) # avoid multiple entries
fp.write(" UserKnownHostsFile {}\n".format(ukhf)) # as defined in opts
# prepend our generated ssh config to all ssh_args if not already present
if self.ssh_config not in str(self.get_option("ssh_args")):
self.set_option(
"ssh_args", f"-F {self.ssh_config} " + str(self.get_option("ssh_args"))
)
self._connected = True
return self
def close(self) -> None:
"""
Upstream only marks the connection as closed, we have to terminate
all IAP tunnels as well
"""
# Terminate IAP
with self.lock:
for iap in self.iaps.values():
iap.terminate()
self.iaps.clear()
# remove ssh config
os.unlink(self.ssh_config)
self._connected = False

View file

@ -0,0 +1 @@
cloud/gcp

View file

@ -0,0 +1,66 @@
---
- name: Setup test suite
hosts: localhost
connection: local
gather_facts: false
vars_files:
- ../vars.yml
environment:
GCP_SERVICE_ACCOUNT_FILE: "{{ gcp_cred_file }}"
GCP_AUTH_KIND: "{{ gcp_cred_kind }}"
GCP_PROJECT: "{{ gcp_project }}"
tasks:
- name: SETUP | Create SSH key pair
community.crypto.openssh_keypair:
path: "{{ ansible_ssh_private_key_file }}"
type: ed25519
register: _keypair
- name: SETUP | Create network
google.cloud.gcp_compute_network:
name: "{{ prefix }}"
auto_create_subnetworks: true
state: present
register: _network
- name: SETUP | Allow SSH through IAP
google.cloud.gcp_compute_firewall:
name: all-iap
state: present
source_ranges:
- 35.235.240.0/20
allowed:
- ip_protocol: tcp
ports:
- 22
network: "{{ _network }}"
- name: SETUP | Create instances
google.cloud.gcp_compute_instance:
name: "{{ prefix }}-{{ item.name }}"
machine_type: "{{ gcp_machine_type }}"
disks:
- auto_delete: true
boot: true
initialize_params:
source_image: "{{ gcp_disk_image }}"
disk_type: pd-standard
network_interfaces:
- network: "{{ _network }}"
metadata:
ssh-keys: "{{ ansible_ssh_user }}:{{ _keypair.public_key }}"
labels: "{{ item.labels | default({}) }}"
hostname: "{{ item.hostname | default(omit) }}"
zone: "{{ gcp_zone }}"
state: present
loop: "{{ sut }}"
- name: SETUP | Render dynamic inventory file
ansible.builtin.copy:
dest: ../test.gcp_compute.yml
content: "{{ lookup('template', '../templates/inventory.yml.j2') }}"
mode: preserve
- name: SETUP | Give time for instances to be up
ansible.builtin.pause:
seconds: 30

View file

@ -0,0 +1,34 @@
---
- name: Teardown test suite
hosts: localhost
connection: local
gather_facts: false
vars_files:
- ../vars.yml
environment:
GCP_SERVICE_ACCOUNT_FILE: "{{ gcp_cred_file }}"
GCP_AUTH_KIND: "{{ gcp_cred_kind }}"
GCP_PROJECT: "{{ gcp_project }}"
tasks:
- name: TEARDOWN | Destroy instances # noqa: ignore-errors
google.cloud.gcp_compute_instance:
name: "{{ prefix }}-{{ item.name }}"
machine_type: "{{ gcp_machine_type }}"
zone: "{{ gcp_zone }}"
state: absent
loop: "{{ sut }}"
ignore_errors: true
- name: TEARDOWN | Remove IAP firewall rule # noqa: ignore-errors
google.cloud.gcp_compute_firewall:
name: all-iap
state: absent
network:
selfLink: "networks/{{ prefix }}"
ignore_errors: true
- name: TEARDOWN | Destroy network # noqa: ignore-errors
google.cloud.gcp_compute_network:
name: "{{ prefix }}"
state: absent
ignore_errors: true

View file

@ -0,0 +1,25 @@
---
- name: Test IAP connection plugin
hosts: gcp_cluster_web:gcp_cluster_db
connection: google.cloud.iap
gather_facts: false
vars_files:
- ../vars.yml
tasks:
- name: TEST | Ping
ansible.builtin.ping:
- name: TEST | Copy
ansible.builtin.copy:
content: "Test file test"
dest: "/tmp/{{ prefix }}.txt"
mode: "0644"
- name: TEST | Slurp
ansible.builtin.slurp:
src: "/tmp/{{ prefix }}.txt"
register: _content
- name: TEST | Debug
ansible.builtin.debug:
msg: "{{ _content['content'] | b64decode }}"

View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -eux
# test infra
ansible-playbook playbooks/setup.yml "$@"
export ANSIBLE_INVENTORY=test.gcp_compute.yml
ansible-inventory --graph
RC=0
# we want to run teardown regardless of playbook exit status, so catch the
# exit code of ansible-playbook manually
set +e
ansible-playbook -vvvvv playbooks/test.yml "$@"
RC=$?
set -e
unset ANSIBLE_INVENTORY
# delete test infra
ansible-playbook playbooks/teardown.yml "$@"
exit $RC

View file

@ -0,0 +1,31 @@
---
plugin: google.cloud.gcp_compute
zones:
{{ gcp_zones | to_nice_yaml }}
projects:
- {{ gcp_project }}
auth_kind: {{ gcp_cred_kind }}
service_account_file: {{ gcp_cred_file }}
scopes:
- 'https://www.googleapis.com/auth/cloud-platform'
- 'https://www.googleapis.com/auth/compute.readonly'
keyed_groups:
- prefix: gcp
key: labels
filters:
- 'labels.test:{{ prefix }}'
hostnames:
- name
# set variables for the connection plugin
compose:
ansible_gcloud_zone: zone
ansible_gcloud_project: project

View file

@ -0,0 +1 @@
# placeholder

View file

@ -0,0 +1,32 @@
---
prefix: "{{ resource_prefix | default('d3adb33f') }}"
gcp_region: us-central1
gcp_zones:
- "{{ gcp_region }}-a"
- "{{ gcp_region }}-b"
- "{{ gcp_region }}-c"
- "{{ gcp_region }}-f"
gcp_zone: "{{ gcp_zones | last }}"
gcp_disk_image: projects/centos-cloud/global/images/family/centos-stream-9
gcp_machine_type: g1-small
sut:
- name: vm1
labels:
test: "{{ prefix }}"
cluster: web
- name: vm2
labels:
test: "{{ prefix }}"
cluster: web
- name: vm3
labels:
test: "{{ prefix }}"
cluster: db
ansible_python_interpreter: /usr/bin/python3
# these are only useful when connection != local
ansible_ssh_user: cloud-user
ansible_ssh_private_key_file: "{{ playbook_dir }}/ssh_key"

View file

@ -6,7 +6,7 @@ gcp_zones:
- "{{ gcp_region }}-c"
- "{{ gcp_region }}-f"
gcp_zone: "{{ gcp_zones | first }}"
gcp_disk_image: projects/centos-cloud/global/images/centos-stream-9-v20250513
gcp_disk_image: projects/centos-cloud/global/images/family/centos-stream-9
prefix: "{{ resource_prefix | default('d3adb33f') }}"
sut: