Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,39 @@ Configure AWS CLI with your credentials as shown here - https://docs.aws.amazon.

`gdk component publish`

6. Deploy the component to a target thing or thing group.

First, add the deploy configuration to your `gdk-config.json`:
```json
{
"component": {
"com.example.PythonHelloWorld": {
"author": "J. Doe",
"version": "NEXT_PATCH",
"build": {
"build_system": "zip"
},
"publish": {
"bucket": "my-s3-bucket",
"region": "us-east-1"
},
"deploy": {
"target_arn": "arn:aws:iot:us-east-1:123456789012:thing/MyGreengrassCore"
}
}
},
"gdk_version": "1.0.0"
}
```

Then run:

`gdk component deploy`

Or specify the target directly via CLI:

`gdk component deploy --target-arn arn:aws:iot:us-east-1:123456789012:thinggroup/MyGroup`


<br />

Expand Down
51 changes: 51 additions & 0 deletions gdk/aws_clients/Greengrassv2Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,54 @@ def create_gg_component(self, file_path) -> None:
except Exception:
logging.error("Failed to create a private version of the component using the recipe at '%s'.", file_path)
raise

def create_deployment(
self,
target_arn: str,
component_name: str,
component_version: str,
deployment_name: str = None,
deployment_policies: dict = None,
) -> dict:
"""
Create a Greengrass deployment to the specified target.

Returns CreateDeployment API response containing deploymentId.
"""
params = {
"targetArn": target_arn,
"components": {
component_name: {"componentVersion": component_version}
},
}

if deployment_name:
params["deploymentName"] = deployment_name

if deployment_policies:
params["deploymentPolicies"] = deployment_policies

try:
response = self.client.create_deployment(**params)
logging.debug("CreateDeployment response: %s", response)
return response
except Exception:
logging.error(
"Failed to create deployment for component '%s' to target '%s'.",
component_name,
target_arn,
)
raise

def component_version_exists(self, component_version_arn: str) -> bool:
"""
Check if a specific component version exists in the account.
"""
try:
self.client.get_component(arn=component_version_arn)
return True
except self.client.exceptions.ResourceNotFoundException:
return False
except Exception:
logging.error("Error checking component version: %s", component_version_arn)
raise
95 changes: 95 additions & 0 deletions gdk/commands/component/DeployCommand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import logging
from botocore.exceptions import ClientError
from gdk.commands.Command import Command
from gdk.commands.component.config.ComponentDeployConfiguration import ComponentDeployConfiguration
from gdk.aws_clients.Greengrassv2Client import Greengrassv2Client


class DeployCommand(Command):
"""Command handler for 'gdk component deploy'."""

def __init__(self, command_args: dict) -> None:
super().__init__(command_args, "deploy")
self.project_config = ComponentDeployConfiguration(command_args)
self.greengrass_client = Greengrassv2Client(self.project_config.region)

def run(self) -> None:
"""Execute the deployment workflow."""
try:
logging.info(
"Deploying component '%s' version '%s' to target '%s'",
self.project_config.component_name,
self.project_config.component_version,
self.project_config.target_arn,
)

self._validate_component_published()
response = self._create_deployment()
self._log_success(response)

except ClientError as e:
self._handle_client_error(e)
raise
except ValueError as e:
logging.error("Configuration error: %s", str(e))
raise
except Exception:
logging.error(
"Failed to deploy component '%s' version '%s' to target '%s'.",
self.project_config.component_name,
self.project_config.component_version,
self.project_config.target_arn,
)
raise

def _validate_component_published(self) -> None:
"""Verify the component version exists in the account."""
component_version_arn = self._get_component_version_arn()
if not self.greengrass_client.component_version_exists(component_version_arn):
raise ValueError(
f"Component version '{self.project_config.component_version}' not found. "
"Run 'gdk component publish' first."
)
logging.debug("Component version verified: %s", component_version_arn)

def _get_component_version_arn(self) -> str:
"""Construct the full component version ARN."""
config = self.project_config
partition = config._get_aws_partition()
return (
f"arn:{partition}:greengrass:{config.region}:{config.account_num}:"
f"components:{config.component_name}:versions:{config.component_version}"
)

def _create_deployment(self) -> dict:
"""Create the deployment and return the response."""
config = self.project_config
return self.greengrass_client.create_deployment(
target_arn=config.target_arn,
component_name=config.component_name,
component_version=config.component_version,
deployment_name=config.deployment_name,
deployment_policies=config.deployment_policies,
)

def _log_success(self, response: dict) -> None:
"""Log successful deployment details."""
deployment_id = response.get("deploymentId", "unknown")
logging.info("Deployment created successfully. Deployment ID: %s", deployment_id)

def _handle_client_error(self, error: ClientError) -> None:
"""Handle AWS API errors with appropriate messages."""
error_code = error.response.get("Error", {}).get("Code", "")
error_message = error.response.get("Error", {}).get("Message", str(error))

if error_code == "ResourceNotFoundException":
logging.error(
"Target '%s' not found. Verify the thing or thing group exists.",
self.project_config.target_arn,
)
elif error_code == "AccessDeniedException":
logging.error(
"Permission denied. Ensure IAM role has greengrass:CreateDeployment permission."
)
else:
logging.error("Deployment failed: %s", error_message)
6 changes: 6 additions & 0 deletions gdk/commands/component/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ def list(d_args):
from gdk.commands.component.ListCommand import ListCommand

ListCommand(d_args).run()


def deploy(d_args):
from gdk.commands.component.DeployCommand import DeployCommand

DeployCommand(d_args).run()
140 changes: 140 additions & 0 deletions gdk/commands/component/config/ComponentDeployConfiguration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import re
import logging
from typing import Optional
import boto3
from gdk.common.config.GDKProject import GDKProject
from gdk.aws_clients.Greengrassv2Client import Greengrassv2Client


TARGET_ARN_PATTERN = re.compile(
r"^arn:(aws|aws-cn|aws-us-gov):iot:[a-z0-9-]+:[0-9]+:(thing|thinggroup)/.+$"
)


class ComponentDeployConfiguration(GDKProject):
"""Configuration handler for the deploy command."""

def __init__(self, args: dict) -> None:
super().__init__()
self._args = args
self._deploy_config = self.component_config.get("deploy", {})
self._publish_config = self.component_config.get("publish", {})

self.region = self._get_region()
self.account_num = self._get_account_number()
self.target_arn = self._get_target_arn()
self.deployment_name = self._get_deployment_name()
self.component_version = self._get_component_version()
self.deployment_policies = self._get_deployment_policies()

def _get_region(self) -> str:
"""Get region from publish config (reuse publish region)."""
region = self._publish_config.get("region", "")
if not region:
raise ValueError("Region not configured. Please set region in publish configuration.")
return region

def _get_account_number(self) -> str:
"""Get AWS account number using STS."""
try:
sts_client = boto3.client("sts")
account_num = sts_client.get_caller_identity().get("Account")
logging.debug("Identified account number as '%s'.", account_num)
return account_num
except Exception:
logging.error("Error while fetching account number from credentials.")
raise

def _get_target_arn(self) -> str:
"""Get target ARN from CLI args or config, validate format."""
target_arn = self._args.get("target_arn")
if not target_arn:
target_arn = self._deploy_config.get("target_arn")

if not target_arn:
raise ValueError(
"Target ARN is required. Provide --target-arn or set deploy.target_arn in gdk-config.json"
)

if not self._validate_target_arn_format(target_arn):
raise ValueError(
f"Invalid target ARN format: {target_arn}. Expected IoT thing or thing group ARN."
)

return target_arn

def _get_deployment_name(self) -> Optional[str]:
"""Get optional deployment name from CLI args or config."""
deployment_name = self._args.get("deployment_name")
if not deployment_name:
deployment_name = self._deploy_config.get("deployment_name")
return deployment_name

def _get_component_version(self) -> str:
"""Get component version, resolving NEXT_PATCH if needed."""
version = self._args.get("component_version")
if not version:
version = self.component_config.get("version")

if not version:
raise ValueError("Component version is required.")

if version == "NEXT_PATCH":
logging.debug("Resolving NEXT_PATCH to latest published version.")
return self._resolve_latest_version()

return version

def _resolve_latest_version(self) -> str:
"""Resolve the latest published version of the component."""
try:
component_arn = self._get_component_arn()
client = Greengrassv2Client(self.region)
latest_version = client.get_highest_cloud_component_version(component_arn)
if not latest_version:
raise ValueError(
f"No published version found for component '{self.component_name}'. "
"Run 'gdk component publish' first."
)
logging.info("Resolved latest version: %s", latest_version)
return latest_version
except Exception as e:
logging.error("Failed to resolve latest component version: %s", e)
raise

def _get_component_arn(self) -> str:
"""Construct the component ARN."""
partition = self._get_aws_partition()
return f"arn:{partition}:greengrass:{self.region}:{self.account_num}:components:{self.component_name}"

def _get_aws_partition(self) -> str:
"""Get AWS partition for the region."""
session = boto3.Session()
return session.get_partition_for_region(region_name=self.region)

def _get_deployment_policies(self) -> Optional[dict]:
"""Get deployment policies from config if present."""
policies = self._deploy_config.get("deployment_policies")
if not policies:
return None

api_policies = {}

if "failure_handling_policy" in policies:
api_policies["failureHandlingPolicy"] = policies["failure_handling_policy"]

if "component_update_policy" in policies:
update_policy = policies["component_update_policy"]
api_update_policy = {}
if "timeout_in_seconds" in update_policy:
api_update_policy["timeoutInSeconds"] = update_policy["timeout_in_seconds"]
if "action" in update_policy:
api_update_policy["action"] = update_policy["action"]
if api_update_policy:
api_policies["componentUpdatePolicy"] = api_update_policy

return api_policies if api_policies else None

def _validate_target_arn_format(self, arn: str) -> bool:
"""Validate ARN matches IoT thing or thing group pattern."""
return bool(TARGET_ARN_PATTERN.match(arn))
4 changes: 4 additions & 0 deletions gdk/commands/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def _gdk_component_list(d_args):
component.list(d_args)


def _gdk_component_deploy(d_args):
component.deploy(d_args)


def _gdk_config_update(d_args):
config.update(d_args)

Expand Down
26 changes: 26 additions & 0 deletions gdk/static/cli_model.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,32 @@
"repository"
]
]
},
"deploy": {
"help": "Deploy a GreengrassV2 component to a target core device or thing group.",
"arguments": {
"target_arn": {
"name": [
"-t",
"--target-arn"
],
"help": "ARN of the target IoT thing or thing group. Overrides gdk-config.json."
},
"deployment_name": {
"name": [
"-n",
"--deployment-name"
],
"help": "Optional name for the deployment."
},
"component_version": {
"name": [
"-cv",
"--component-version"
],
"help": "Component version to deploy. Defaults to version in gdk-config.json."
}
}
}
},
"help": "Initialize, build and publish GreengrassV2 components using this command."
Expand Down
Loading