{"id":48084455,"url":"https://github.com/upbound/function-external-name-backup-restore","last_synced_at":"2026-04-04T15:07:12.130Z","repository":{"id":327969664,"uuid":"1030427649","full_name":"upbound/function-external-name-backup-restore","owner":"upbound","description":"Function for storing and recovering external-names","archived":false,"fork":false,"pushed_at":"2026-01-23T13:29:50.000Z","size":256,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-24T04:51:59.132Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/upbound.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-01T16:10:36.000Z","updated_at":"2026-01-24T00:46:48.000Z","dependencies_parsed_at":"2025-12-10T22:14:33.799Z","dependency_job_id":null,"html_url":"https://github.com/upbound/function-external-name-backup-restore","commit_stats":null,"previous_names":["upbound/function-external-name-backup-restore"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/upbound/function-external-name-backup-restore","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/upbound%2Ffunction-external-name-backup-restore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/upbound%2Ffunction-external-name-backup-restore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/upbound%2Ffunction-external-name-backup-restore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/upbound%2Ffunction-external-name-backup-restore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/upbound","download_url":"https://codeload.github.com/upbound/function-external-name-backup-restore/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/upbound%2Ffunction-external-name-backup-restore/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31403952,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-04T15:07:11.411Z","updated_at":"2026-04-04T15:07:12.114Z","avatar_url":"https://github.com/upbound.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Crossplane External Name Backup \u0026 Restore Function\n\nA Crossplane composition function designed for **GitOps disaster recovery scenarios** where you need to restore your entire Crossplane infrastructure from Git, but some cloud resources have external names that aren't known upfront.\n\n## The GitOps Gap This Function Fills\n\nIn a typical GitOps setup, your Crossplane XRs and claims are stored in Git. If you lose your Kubernetes cluster, you can restore everything from Git. However, there's a critical gap:\n\n**Cloud resources with auto-generated names** cannot be restored from Git alone because their external names aren't predetermined.\n\n**Common Examples:**\n- **AWS Networking**: VPCs (`vpc-0123456789abcdef0`), subnets (`subnet-0abc123def456789`), security groups (`sg-0987654321fedcba`)\n- **EC2 Instances**: Instances with generated instance-ids\n\n## XR Name Backup (Nested Compositions)\n\nThis function also supports backing up **Composite Resource (XR) names** for nested compositions. When XRs are composed as part of other compositions, their `metadata.name` is generated deterministically from the parent UID:\n\n```\nXR name = SHA256(parentUID + compositionResourceName)[:12]\n```\n\n**The Problem**: If the parent UID changes (e.g., during migration or recreation), all child XR names change, breaking references to existing resources.\n\n**The Solution**: This function backs up `metadata.name` for all resources, allowing XRs to retain their original names even when parent UIDs change.\n\n## Overview\n\n**Primary Use Case**: Full GitOps infrastructure backup and restore for resources with unpredictable external names.\n\nWhen you restore your GitOps configuration from Git without external names explicitly set:\n1. Crossplane recreates the managed resources\n2. Resources with `deletionPolicy: Orphan` still exist in your cloud provider\n3. **Without this function**: Crossplane cannot find the resources → creates duplicate cloud resources\n4. **With this function**: External names are restored from backup → Crossplane adopts existing cloud resources\n\nThis function solves the GitOps gap by:\n- **Backing up** external names and resource names from observed resources during normal operations\n- **Restoring** external names and resource names to desired resources during GitOps recovery\n- **Focusing on orphaned resources** for external names (the ones that survive cluster disasters)\n- **Backing up all resource names** for XRs in nested compositions\n\n## Features\n\n- 🔄 **Automatic Backup \u0026 Restore**: Seamlessly handles external name and resource name persistence\n- 🎯 **Backup Scope**: Choose between processing only orphaned resources or all resources for external names\n- 📦 **XR Name Backup**: Always backs up `metadata.name` for all resources\n- 📊 **Multiple Storage Backends**: AWS DynamoDB or Kubernetes ConfigMaps (ConfigMaps are mainly for short-lived migration use-cases)\n- 🏷️ **Annotation-Based Control**: Fine-grained control through XR annotations\n- 🧹 **Purge Capabilities**: Clean up stored data when needed\n\n## Quick Start\n\n### 1. Deploy the Function\n\n```bash\n# Install the function\nkubectl apply -f package/crossplane.yaml\n```\n\n### 2. Configure Storage Backend\n\nChoose between DynamoDB or Kubernetes ConfigMaps as your storage backend.\n\n#### Option A: DynamoDB (Default)\n\nCreate a DynamoDB table with the following schema:\n\n```yaml\n# DynamoDB Table Schema\nTableName: external-name-backup\nKeySchema:\n  - AttributeName: cluster_id\n    KeyType: HASH\n  - AttributeName: composition_key\n    KeyType: RANGE\nAttributeDefinitions:\n  - AttributeName: cluster_id\n    AttributeType: S\n  - AttributeName: composition_key\n    AttributeType: S\n```\n\n#### Option B: Kubernetes ConfigMaps\n\nNo external infrastructure required. The function stores data in ConfigMaps within your cluster.\n\n**ConfigMap naming convention:** `external-name-backup-{cluster-id}`\n\n**Data structure:**\n- Each ConfigMap contains data for one cluster id\n- Composition keys are base64-encoded as data keys\n- Resource data is stored as JSON\n\n**Required RBAC permissions:**\n\n```yaml\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: function-external-name-backup-restore-configmap\nrules:\n  - apiGroups: [\"\"]\n    resources: [\"configmaps\"]\n    verbs: [\"get\", \"create\", \"update\", \"delete\"]\n  - apiGroups: [\"\"]\n    resources: [\"namespaces\"]\n    verbs: [\"get\"]\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: function-external-name-backup-restore-configmap\nroleRef:\n  apiGroup: rbac.authorization.k8s.io\n  kind: ClusterRole\n  name: function-external-name-backup-restore-configmap\nsubjects:\n  - kind: ServiceAccount\n    name: \u003cservice-account-name\u003e  # See note below\n    namespace: crossplane-system\n```\n\n\u003e **Note:** The function's ServiceAccount name has an auto-generated suffix (e.g., `function-external-name-backup-restore-a1b2c3d4e5f6`). Retrieve it with:\n\u003e ```bash\n\u003e kubectl get serviceaccount -n crossplane-system -l pkg.crossplane.io/function=function-external-name-backup-restore -o name\n\u003e ```\n\nAlternatively use a DeploymentRuntimeConfig to use a predictable serviceaccount.\n\n**Usage in composition:**\n\n```yaml\napiVersion: apiextensions.crossplane.io/v1\nkind: Composition\nmetadata:\n  name: my-composition\nspec:\n  mode: Pipeline\n  pipeline:\n  - step: create-resources\n    functionRef:\n      name: function-patch-and-transform\n  - step: external-name-backup\n    functionRef:\n      name: function-external-name-backup-restore\n    credentials:\n      - name: store-creds\n        source: Secret\n        secretRef:\n          namespace: crossplane-system\n          name: configmap-store-creds  # Can be a dummy secret\n          key: credentials\n```\n\n\u003e **Note:** The `credentials` block is required due to a Crossplane limitation - if a function uses credentials in any composition, the credentials block must be present in all compositions using that function. Create a dummy secret if no actual credentials are needed (e.g., with ConfigMap store).\n\n**XR annotations for ConfigMap store:**\n\n```yaml\napiVersion: example.com/v1alpha1\nkind: MyXR\nmetadata:\n  annotations:\n    fn.crossplane.io/enable-external-store: \"true\"\n    fn.crossplane.io/store-type: \"k8sconfigmap\"\n    fn.crossplane.io/cluster-id: \"my-cluster\"\n    fn.crossplane.io/configmap-namespace: \"crossplane-system\"  # optional, default\n```\n\n### 3. Configure AWS Credentials (DynamoDB only)\n\nCreate a secret with your AWS credentials:\n\n```yaml\napiVersion: v1\nkind: Secret\nmetadata:\n  name: aws-account-creds\n  namespace: crossplane-system\ntype: Opaque\ndata:\n  credentials: \u003cbase64-encoded-json-credentials\u003e\n```\n\nThe credentials can be provided in two formats:\n\n**JSON Format (recommended):**\n```json\n{\n  \"accessKeyId\": \"AKIAIOSFODNN7EXAMPLE\",\n  \"secretAccessKey\": \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\",\n  \"sessionToken\": \"optional-for-temporary-credentials\"\n}\n```\n\n**AWS CLI INI Format:**\n```ini\n[default]\naws_access_key_id = AKIAIOSFODNN7EXAMPLE\naws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\naws_session_token = optional-for-temporary-credentials\n```\n\n### 4. Use in Compositions\n\n```yaml\napiVersion: apiextensions.crossplane.io/v1\nkind: Composition\nmetadata:\n  name: my-composition\nspec:\n  mode: Pipeline\n  pipeline:\n  - step: create-resources\n    functionRef:\n      name: function-patch-and-transform\n    # ... your resource creation logic\n  - step: external-name-backup\n    functionRef:\n      name: function-external-name-backup-restore\n    credentials:\n      - name: aws-creds\n        source: Secret\n        secretRef:\n          namespace: crossplane-system\n          name: aws-account-creds\n          key: credentials\n```\n\n## Configuration\n\nConfiguration is provided through XR annotations. All configuration is specified on your Composite Resource (XR) using annotations:\n\n### Required Annotations\n\n| Annotation | Example | Description |\n|------------|---------|-------------|\n| `fn.crossplane.io/enable-external-store` | `\"true\"` | Enable external store operations |\n| `fn.crossplane.io/cluster-id` | `\"my-cluster\"` | Unique identifier for this cluster |\n| `fn.crossplane.io/store-type` | `\"awsdynamodb\"` | External store type (`awsdynamodb`, `k8sconfigmap`, or `mock`) |\n| `fn.crossplane.io/dynamodb-table` | `\"external-name-backup\"` | DynamoDB table name (only for `awsdynamodb`) |\n| `fn.crossplane.io/dynamodb-region` | `\"us-west-2\"` | AWS region for DynamoDB (only for `awsdynamodb`) |\n| `fn.crossplane.io/configmap-namespace` | `\"crossplane-system\"` | Namespace for ConfigMap store (only for `k8sconfigmap`, default: `crossplane-system`) |\n| `fn.crossplane.io/backup-scope` | `\"orphaned\"` | Backup scope (`orphaned` or `all`) |\n\n### Optional Annotations\n\n| Annotation | Example | Description |\n|------------|---------|-------------|\n| `fn.crossplane.io/override-kind` | `\"XNetwork\"` | Override XR kind in composition key lookup (for migrations) |\n| `fn.crossplane.io/override-namespace` | `\"none\"` | Override namespace in composition key lookup (for migrations from cluster-scoped to namespaced XRs) |\n| `fn.crossplane.io/restore-only` | `\"true\"` | Enable restore-only mode: always restore from store regardless of backup scope, skip backup, fail if any resource is missing from store |\n| `fn.crossplane.io/purge-external-store` | `\"true\"` | Delete all stored external names for this composition |\n\n### AWS Credentials\n\nAWS credentials are provided via Crossplane's credential management system. The function supports:\n- Static credentials (Access Key ID + Secret Access Key)\n\n## Backup Scope\n\n### Only Orphaned Mode (Default - Recommended)\n\n**This is the intended mode of operation for production environments.** This mode is designed for critical production resources that are usually not intended to be deleted, where preserving external names is crucial for maintaining infrastructure consistency.\n\nProcesses only resources that meet orphaned criteria:\n- `deletionPolicy: Orphan`, OR\n- `managementPolicies` that don't contain `\"*\"` or `\"Delete\"`\n\n```yaml\n# This resource WILL be processed\nspec:\n  deletionPolicy: Orphan\n  managementPolicies: [\"*\"]  # Orphan policy takes precedence\n\n# This resource WILL be processed\nspec:\n  deletionPolicy: Delete\n  managementPolicies: [\"Create\", \"Update\", \"Observe\"]  # No Delete policy\n\n# This resource will NOT be processed\nspec:\n  deletionPolicy: Delete\n  managementPolicies: [\"*\"]  # Contains Delete via wildcard\n```\n\n#### Important: Store Cleanup After Resource Deletion\n\nIf a resource protected by this function gets accidentally deleted, you **must purge the store** when creating a new composite or claim that would result in the same composition key.\n\n**Composition keys are formed as:** `{namespace}/{claim-name}/{xr-apiVersion}/{xr-kind}/{xr-name}`\n\nThe namespace component is determined as follows:\n- **For Composites created by Claims**: Uses the claim's namespace from labels\n- **For v2 Namespaced XRs** (without claims): Uses the XR's `metadata.namespace`\n- **For v1 Cluster-scoped XRs** (without claims): Uses `\"none\"`\n\nExamples:\n\n**Claim-based Composite:**\n- Claim: `my-database` in namespace `production`\n- XR: `my-database-xyz` of type `aws.platform.upbound.io/v1alpha1/XRDS`\n- **Key:** `production/my-database/aws.platform.upbound.io/v1alpha1/XRDS/my-database-xyz`\n\n**v2 Namespaced XR (without claim):**\n- XR: `standalone-db` in namespace `team-a` of type `aws.platform.upbound.io/v1alpha1/XRDS`\n- **Key:** `team-a/none/aws.platform.upbound.io/v1alpha1/XRDS/standalone-db`\n\n**v1 Cluster-scoped XR (without claim):**\n- XR: `standalone-db` of type `aws.platform.upbound.io/v1alpha1/XRDS` (cluster-scoped, no namespace)\n- **Key:** `none/none/aws.platform.upbound.io/v1alpha1/XRDS/standalone-db`\n\nIf you need to recreate a deleted composite, purge the store first:\n\n```yaml\napiVersion: aws.platform.upbound.io/v1alpha1\nkind: XRDS\nmetadata:\n  name: my-database-xyz\n  annotations:\n    fn.crossplane.io/purge-external-store: \"true\"\n# Apply this first, then remove the annotation for normal operation\n```\n\n### All Resources Mode (Experimental)\n\n**⚠️ Experimental Feature**: This mode processes all resources regardless of deletion policy. While we don't restrict users from using this mode, it's primarily for testing and experimental use cases.\n\n```yaml\n# Set via environment variable\nBACKUP_SCOPE=all\n\n# Or via ConfigMap\ndata:\n  backup-scope: \"all\"\n```\n\n**Important Considerations for All Resources Mode:**\n- May require store cleanup similar to orphaned scope if resources are deleted\n- Increased DynamoDB usage and costs due to processing more resources\n- Tracking annotations help minimize unnecessary writes, but overhead is still higher\n- Recommended primarily for testing or specific experimental use cases\n\n## XR Annotations\n\nControl function behavior through annotations on your XR:\n\n### Enable External Store Operations\n\nBy default, external store operations are disabled. Enable them with:\n\n```yaml\napiVersion: example.com/v1alpha1\nkind: MyXR\nmetadata:\n  annotations:\n    fn.crossplane.io/enable-external-store: \"true\"\n# Function will perform backup/restore operations for this XR\n```\n\n### Purge Stored Data\n\n```yaml\napiVersion: example.com/v1alpha1\nkind: MyXR\nmetadata:\n  annotations:\n    fn.crossplane.io/purge-external-store: \"true\"\n# Function will delete all stored external names for this composition\n```\n\n### Migration from v1 Cluster-Scoped to v2 Namespaced XRs\n\nWhen migrating from v1 cluster-scoped XRs to v2 namespaced XRs, the composition key format changes. Use override annotations to look up external names stored under the old format:\n\n```yaml\napiVersion: aws.platform.upbound.io/v1alpha1\nkind: Network  # New v2 kind (namespaced)\nmetadata:\n  name: my-network\n  namespace: team-a  # v2 namespaced XR\n  annotations:\n    fn.crossplane.io/enable-external-store: \"true\"\n    fn.crossplane.io/override-kind: \"XNetwork\"      # Look up keys stored under v1 kind\n    fn.crossplane.io/override-namespace: \"none\"      # Look up keys stored under v1 cluster-scoped format\n    fn.crossplane.io/restore-only: \"true\"             # Safety: fail if no data found\n```\n\n**Why these annotations are needed:**\n\n| Scenario | Key Format | Annotations |\n|----------|-----------|-------------|\n| v1 cluster-scoped export | `none/none/.../XNetwork/my-network` | (original backup) |\n| v2 namespaced import | `team-a/none/.../Network/my-network` | (default, wrong key) |\n| v2 with overrides | `none/none/.../XNetwork/my-network` | `override-kind`, `override-namespace` |\n\n**The `require-restore` mode:**\n\nWhen set to `\"true\"`, the function operates in **restore-only mode** with the following behavior:\n\n1. **Bypass backup scope for restore**: External names and resource names are restored from the store regardless of whether resources meet orphan criteria. This is essential for migrations where the new XR may not have matching `deletionPolicy` or `managementPolicies` set yet.\n\n2. **Skip backup operations**: The function will NOT write any new data to the store, preventing accidental overwrites of existing backup data.\n\n3. **Fail if any resource is missing**: The function will fail with a fatal error if:\n   - No data exists in the store for the composition key\n   - Any desired resource doesn't have corresponding data in the store\n\nThis prevents accidental creation of duplicate cloud resources when:\n- Override annotations are misconfigured\n- The store doesn't contain data for the expected key\n- Migration is attempted before backup data exists\n- The composition has more resources than were previously backed up\n\nExample error messages:\n```\nrequire-restore is enabled but no resource data found in store for composition key \"none/none/aws.platform.upbound.io/v1alpha1/XNetwork/my-network\".\nCheck that override-kind and override-namespace annotations are correct, or remove require-restore annotation.\n```\n\n```\nrequire-restore is enabled but no data found in store for resource \"vpc\" (composition key: \"none/none/aws.platform.upbound.io/v1alpha1/XNetwork/my-network\").\nAll resources must have data in the store when require-restore is set.\n```\n\n### Override Kind for Migrations\n\nWhen migrating between composition versions where only the XR kind changes (e.g., `XNetwork` to `Network`), use the `override-kind` annotation to look up external names stored under the old kind:\n\n```yaml\napiVersion: aws.platform.upbound.io/v1alpha1\nkind: Network  # New v2 kind\nmetadata:\n  name: my-network\n  annotations:\n    fn.crossplane.io/enable-external-store: \"true\"\n    fn.crossplane.io/override-kind: \"XNetwork\"  # Look up keys stored under v1 kind\n```\n\nThis is useful for migrations where:\n- The XRD kind changes between versions (e.g., `XNetwork` → `Network`)\n- You want to restore external names backed up from the previous version\n- The composition key format includes the kind: `{namespace}/{claim-name}/{apiVersion}/{kind}/{name}`\n\n## Deletion Behavior\n\nWhen resources are deleted from the external store (e.g., when switching from `deletionPolicy: Orphan` to `deletionPolicy: Delete`), the function:\n\n1. **Removes the external name** from the DynamoDB store\n2. **Removes tracking annotations** (`fn.crossplane.io/stored-external-name`, `fn.crossplane.io/external-name-stored`)\n3. **Adds deletion timestamp** annotation: `fn.crossplane.io/external-name-deleted` with the timestamp when the deletion occurred\n\nThis deletion annotation helps track which resources were processed for deletion and provides an audit trail of when external names were removed from the store.\n\n## How It Works\n\n### Data Storage Structure\n\nResource data is stored in DynamoDB with this structure:\n\n```\nPrimary Key: cluster_id + composition_key\ncomposition_key format: {claim-namespace}/{claim-name}/{xr-apiVersion}/{xr-kind}/{xr-name}\n\nData:\n{\n  \"cluster_id\": \"my-cluster\",\n  \"composition_key\": \"default/my-claim/example.com/v1alpha1/MyXR/my-xr\",\n  \"resources\": {\n    \"my-bucket\": {\n      \"externalName\": \"actual-bucket-name-12345\",\n      \"resourceName\": \"my-bucket-abc123\"\n    },\n    \"my-vpc\": {\n      \"externalName\": \"vpc-0123456789abcdef0\",\n      \"resourceName\": \"my-vpc-def456\"\n    },\n    \"nested-xr\": {\n      \"resourceName\": \"nested-xr-xyz789\"\n    }\n  }\n}\n```\n\n**Note:** The `resourceName` field stores the `metadata.name` of the composed resource. Resources may have only `externalName`, only `resourceName`, or both.\n\n### Function Flow\n\n1. **Load Phase**: Load existing resource data from DynamoDB\n2. **Deletion Check**: Check desired resources for deletion criteria (policy change from Orphan to Delete)\n3. **Desired Resources Processing**:\n   - Check each resource against backup scope criteria (for external names)\n   - Restore external names from store if available and not already set\n   - Restore resource names (`metadata.name`) from store if not already set\n   - Add tracking annotations\n4. **Observed Resources Processing**:\n   - Collect external names from resources (respects backup scope)\n   - Collect resource names from all resources (independent of backup scope)\n   - Use tracking annotations to optimize writes\n5. **Store Phase**: Save collected resource data back to DynamoDB\n\n### Function Annotations\n\nThe function uses several annotations to track state and optimize performance:\n\n**External Name Tracking Annotations** (added during storage):\n```yaml\nmetadata:\n  annotations:\n    fn.crossplane.io/stored-external-name: \"actual-external-name\"\n    fn.crossplane.io/external-name-stored: \"2024-08-06T12:00:00Z\"\n```\n\n**Resource Name Tracking Annotations** (added during storage):\n```yaml\nmetadata:\n  annotations:\n    fn.crossplane.io/stored-resource-name: \"my-resource-abc123\"\n    fn.crossplane.io/resource-name-stored: \"2024-08-06T12:00:00Z\"\n```\n\n**Restoration Annotations** (added during restore):\n```yaml\nmetadata:\n  annotations:\n    fn.crossplane.io/external-name-restored: \"2024-08-06T12:15:00Z\"\n    fn.crossplane.io/resource-name-restored: \"2024-08-06T12:15:00Z\"\n```\n\n**Deletion Annotation** (added during deletion):\n```yaml\nmetadata:\n  annotations:\n    fn.crossplane.io/external-name-deleted: \"2024-08-06T12:30:00Z\"\n```\n\nThese annotations serve multiple purposes:\n- **Performance Optimization**: Tracking annotations prevent unnecessary DynamoDB writes when values haven't changed\n- **Audit Trail**: Timestamps provide visibility into when operations occurred\n- **State Management**: Help the function understand what has been processed\n\n## Examples\n\nSee the [`example/`](./example/) directory for complete examples:\n\n- [`composition.yaml`](./example/composition.yaml) - Test composition with S3 buckets\n- [`xr.yaml`](./example/xr.yaml) - Basic XR example\n- [`xr-skip-store.yaml`](./example/xr-skip-store.yaml) - XR with skip annotation\n- [`configmap.yaml`](./example/configmap.yaml) - ConfigMap configuration\n- [`functions.yaml`](./example/functions.yaml) - Function definitions for local development\n\n## Local Development\n\n### Prerequisites\n\n- Go 1.23+\n- AWS credentials configured\n- DynamoDB table created\n\n### Running Locally\n\n```bash\n# Set environment variables\nexport EXTERNAL_STORE_TYPE=awsdynamodb\nexport DYNAMODB_TABLE_NAME=external-name-backup\nexport DYNAMODB_REGION=us-west-2\n\n# Build and run\ngo build .\n./function-external-name-backup-restore --insecure --debug\n\n# Test with crossplane beta render\nxp render example/xr.yaml example/composition.yaml example/functions.yaml\n```\n\n### Testing Different Backup Scopes\n\n```bash\n# Test orphaned scope (default)\nBACKUP_SCOPE=orphaned xp render example/xr.yaml example/composition.yaml example/functions.yaml\n\n# Test all scope\nBACKUP_SCOPE=all xp render example/xr.yaml example/composition.yaml example/functions.yaml\n```\n\n## AWS Permissions\n\nThe function requires the following AWS IAM permissions:\n\n```json\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"dynamodb:GetItem\",\n        \"dynamodb:PutItem\",\n        \"dynamodb:DeleteItem\",\n        \"dynamodb:DescribeTable\"\n      ],\n      \"Resource\": \"arn:aws:dynamodb:*:*:table/external-name-backup\"\n    }\n  ]\n}\n```\n\n## Troubleshooting\n\n### Common Issues\n\n1. **Function not connecting to DynamoDB**\n   - Verify AWS credentials are configured\n   - Check DynamoDB table exists and is accessible\n   - Verify IAM permissions\n\n2. **External names not being restored**\n   - Check backup scope matches your resource configuration\n   - Verify data exists in DynamoDB for your composition key\n   - Check function logs for processing details\n\n3. **Performance issues**\n   - Ensure tracking annotations are working to prevent unnecessary writes\n   - Consider using `orphaned` scope to reduce processing overhead\n   - Monitor DynamoDB read/write capacity\n\n### Debugging\n\nEnable debug logging:\n\n```bash\ngo run . --insecure --debug\n```\n\nCheck function logs in Kubernetes:\n\n```bash\nkubectl logs -f deployment/function-external-name-backup-restore -n crossplane-system\n```\n\n## Contributing\n\n1. Fork the repository\n2. Create a feature branch\n3. Make your changes\n4. Add tests if applicable\n5. Submit a pull request\n\n## License\n\nThis project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fupbound%2Ffunction-external-name-backup-restore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fupbound%2Ffunction-external-name-backup-restore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fupbound%2Ffunction-external-name-backup-restore/lists"}