Skip to content

Commit

Permalink
feat: add POC datasource for Ec2 / Kubernetes
Browse files Browse the repository at this point in the history
How it works
------------
This datasource uses the default EC2 datasource to get the bootscript that is
provided by cluster-api-provider-aws[1].

This bootscript retrieves retrieves user-data from AWS SSM Parameter Store
using the aws cli and writes user-data to the file /etc/secret-userdata.txt[2].

Cloud-init manually processes the bootscript and then it loads the contents of
the file /etc/secret-userdata.txt and passes that off to cloud-init as the
user-data, which is then processed in later stages.

How to use it
-------------
This datasource may be used as a drop-in datasource. To use it on Ubuntu, for
example, this file must be placed in the datasource directory[3]. Then the
following configuration must be set:

```
echo "datasource_list: [ Ec2Kubernetes ]" > /etc/cloud/cloud.cfg.d/90_dpkg.cfg
```

This configuration tells cloud-init to use the new drop-in datasource on
first boot.

After making these changes, create a new image from a snapshot. First boot of
the image should use the custom datasource.

[1] https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/v2.4.2/pkg/cloud/services/ssm/secret_fetch_script.go
[2] As written, the bootscript also tries to restart cloud-init's network stage,
    however this datasource uses a custom boothook part handler to manually disable
    the restart command (which will be broken on Oracular++).
[3] /usr/lib/python3/dist-packages/cloudinit/sources/DataSourceEc2Kubernetes.py
  • Loading branch information
holmanb committed Oct 9, 2024
1 parent 4de61f3 commit f2796cd
Showing 1 changed file with 108 additions and 0 deletions.
108 changes: 108 additions & 0 deletions cloudinit/sources/DataSourceEc2Kubernetes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# This file is part of cloud-init. See LICENSE file for license information.

import logging

from cloudinit import handlers, helpers, sources, util
from cloudinit.handlers.boot_hook import BootHookPartHandler
from cloudinit.handlers.jinja_template import JinjaTemplatePartHandler
from cloudinit.handlers.cloud_config import CloudConfigPartHandler
from cloudinit.handlers.shell_script import ShellScriptPartHandler
from cloudinit.settings import PER_ALWAYS
from cloudinit.sources import DataSourceEc2

LOG = logging.getLogger(__name__)


class BootHookPartHandlerModified(BootHookPartHandler):
def __init__(self, paths, datasource, **_kwargs):
super().__init__(paths, datasource)
self.output = None

def handle_part(self, data, ctype, filename, payload, frequency):
"""Save the output of the script"""
if ctype in handlers.CONTENT_SIGNALS:
return

# modify the payload to not restart cloud-init
# TODO: work with upstream to remove this restart
restart_index = payload.find("systemctl restart cloud-init")
if -1 != restart_index:
LOG.warning(
"Kubernetes is trying to restart cloud-init. This is no "
"longer necessary and is temporarily circumvented by "
"cloud-init. This will be a hard error in the future."
)
payload = payload[:restart_index] + "#" + payload[restart_index:]
super().handle_part(data, ctype, filename, payload, frequency)


class DataSourceEc2Kubernetes(DataSourceEc2.DataSourceEc2):
def _get_data(self):
super()._get_data()

# Get initial user-data
user_data_msg = self.get_userdata(True)
LOG.info("User-data received:[\n%s]", user_data_msg)

# This is required to get path of the instance
self.paths.datasource = self

# Boilerplate handler setup
c_handlers = helpers.ContentHandlers()
cloudconfig_handler = CloudConfigPartHandler(self.paths)
shellscript_handler = ShellScriptPartHandler(self.paths)
boothook_handler = BootHookPartHandlerModified(self.paths, self)
jinja_handler = JinjaTemplatePartHandler(
self.paths,
sub_handlers=[
cloudconfig_handler,
shellscript_handler,
boothook_handler,
],
)
c_handlers.register(boothook_handler, overwrite=False)
c_handlers.register(jinja_handler, overwrite=False)
LOG.debug(
"Registered handlers %s and %s", boothook_handler, jinja_handler
)

# Walk the user data MIME
handlers.walk(
user_data_msg,
handlers.walker_callback,
data={
"handlers": c_handlers,
"handlerdir": self.paths.get_ipath("handlers"),
"data": None,
"frequency": PER_ALWAYS,
"handlercount": 0,
"excluded": [],
},
)

# Get the boothook output, save it as user-data
# TODO: work with upstream to put this somewhere more sensible like:
# /var/lib/cloud/instances/{{v1.instance_id}}/ec2-kubernetes-userdata.txt
self.userdata_raw = util.load_text_file("/etc/secret-userdata.txt")
LOG.info("Secret user-data:[\n%s]", self.userdata_raw)
return True


class DataSourceEc2KubernetesLocal(DataSourceEc2Kubernetes):
perform_dhcp_setup = True # Use dhcp before querying metadata


# Used to match classes to dependencies
datasources = [
(
# Run at init-local
DataSourceEc2KubernetesLocal,
(sources.DEP_FILESYSTEM,),
),
(DataSourceEc2Kubernetes, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
]


# Return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return sources.list_from_depends(depends, datasources)

0 comments on commit f2796cd

Please sign in to comment.