Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Jamf Pro API Credentials
JAMF_AUTH_METHOD=oauth2
JAMF_URL=https://your-instance.jamfcloud.com
JAMF_CLIENT_ID=your-client-id
JAMF_CLIENT_SECRET=your-client-secret
53 changes: 46 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
# macOS
# Environment variables
.env
.env.*

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual Environment
venv/
ENV/
env/

# IDE
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Terraform
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there terraform in the gitignore for a Python Library?

*.tfstate
*.tfstate.*
*.tfvars
.terraform/
.terraform.lock.hcl

# VSCode
.vs
Expand All @@ -10,12 +55,6 @@
!.vscode/extensions.json
*.code-workspace

# Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Distribution / packaging
.Python
build/
Expand Down
154 changes: 95 additions & 59 deletions README.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Readme skews the repo toward a single track execution path which is not the intended use. It's a library.

Original file line number Diff line number Diff line change
@@ -1,89 +1,125 @@
# jamftf-python-terraform-importer
# Jamf Pro Terraform Importer

A Python-based utility to automate the import of existing Jamf Pro resources into Terraform state files.
A Python tool to generate Terraform import blocks for existing Jamf Pro resources.

## Overview

Managing Jamf Pro resources with Terraform enhances reproducibility, version control, and automation. However, importing existing Jamf Pro resources into Terraform can be tedious and error-prone. This tool simplifies the process by:
This tool connects to a Jamf Pro instance and generates Terraform import blocks for specified resources. It supports importing various Jamf Pro resources such as:

- Connecting to your Jamf Pro tenant via the Classic API.
- Fetching specified resources (e.g., scripts, policies, configuration profiles).
- Generating Terraform import blocks for each resource.

This facilitates a smoother transition to Infrastructure as Code (IaC) practices with Jamf Pro.

## Features

- Supports multiple Jamf Pro resource types.
- Generates Terraform import blocks compatible with Terraform v1.5 and above.
- Modular design for easy extension to additional resource types.
- Command-line interface for straightforward operation.
- Scripts
- Categories
- Policies
- macOS Configuration Profiles
- Static Computer Groups
- Smart Computer Groups
- Advanced Computer Searches
- Computer Extension Attributes

## Prerequisites

- Python 3.7 or higher
- Access to a Jamf Pro instance with appropriate API credentials
- Terraform v1.5 or higher
- Python 3.8 or higher
- A Jamf Pro instance with API access
- OAuth2 credentials (Client ID and Secret) for Jamf Pro API access

## Installation

1. Clone the repository:

1. Clone this repository:
```bash
git clone https://github.com/deploymenttheory/jamftf-python-terraform-importer.git
git clone https://github.com/yourusername/jamftf-python-terraform-importer.git
cd jamftf-python-terraform-importer
```

2. Install the required Python packages:
2. Create and activate a virtual environment:
```bash
python -m venv env
source env/bin/activate # On Windows: env\Scripts\activate
```

3. Install the package in development mode:
```bash
pip install -r requirements.txt
pip install -e .
```

## Usage
## Configuration

1. Configure your Jamf Pro API credentials and desired resources using environment variables or a JSON config file like `import_config.json`:
1. Create a `.env` file in the project root with your Jamf Pro credentials:
```env
JAMF_AUTH_METHOD=oauth2
JAMF_URL=https://your-instance.jamfcloud.com
JAMF_CLIENT_ID=your-client-id
JAMF_CLIENT_SECRET=your-client-secret
```

2. Create an `import_config.json` file to specify which resources to import:
```json
{
"jamfpro_macos_configuration_profile_plist": true
"scripts": true,
"categories": true,
"policies": true,
"configuration_profiles": true,
"computer_groups_static": true,
"computer_groups_smart": true,
"advanced_computer_searches": true,
"computer_extension_attributes": true
}
```

2. Run the importer script:

```bash
python main.py
```

This will generate Terraform import blocks for the selected resource types.

3. Use the generated import blocks to import resources into Terraform state:

```bash
terraform import <resource_type>.<resource_name> <resource_id>
```

## Supported Resources

- Scripts
- Policies
- Configuration Profiles
- Categories
- Computer Groups (Static and Smart)
- Advanced Computer Searches
- Computer Extension Attributes

Support for additional resource types can be added by extending the `Resource` class and implementing the `_get()` method.

## Contributing
## Usage

Contributions are welcome! Please submit a pull request or open an issue to propose changes or enhancements.
Run the importer with default settings:
```bash
python main.py
```

### Command Line Options

- `-c, --config`: Specify a different configuration file (default: `import_config.json`)
- `-o, --output`: Write output to a file instead of stdout
- `--env-file`: Specify a different `.env` file (default: `.env`)

Example:
```bash
python main.py -c my_config.json -o import_blocks.tf
```

## Output

The tool generates Terraform import blocks in HCL format. Example output:
```hcl
import {
to = jamfpro_computer_extension_attribute.apn_cert_uid1
id = "1"
}
resource "jamfpro_computer_extension_attribute" "apn_cert_uid1" {
name = "APN Cert UID"
}
```

## Development

### Project Structure

```
jamftf/
├── __init__.py
├── config_ingest.py # Configuration file handling
├── constants.py # Constants and mappings
├── dataclasses.py # Data structures
├── enums.py # Enumerations
├── exceptions.py # Custom exceptions
├── hcl.py # HCL generation
├── importer.py # Main importer logic
├── models.py # Base resource models
└── resources.py # Resource-specific implementations
```

### Adding New Resource Types

1. Add the resource type to `ProviderResourceTags` in `enums.py`
2. Add the response key to `ResourceResponseKeys` in `enums.py`
3. Create a new resource class in `resources.py`
4. Add the resource to `RESOURCE_KEY_MAP` and `RESOURCE_TYPE_OBJECT_MAP` in `constants.py`

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

---

For more information and updates, visit the [GitHub repository](https://github.com/deploymenttheory/jamftf-python-terraform-importer).
This project is licensed under the MIT License - see the LICENSE file for details.
10 changes: 10 additions & 0 deletions import_config.json
Copy link
Collaborator

@thejoeker12 thejoeker12 Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be in a library or a CLI tool?

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"scripts": true,
"policies": true,
"configuration_profiles": true,
"categories": true,
"computer_groups_static": true,
"computer_groups_smart": true,
"advanced_computer_searches": true,
"computer_extension_attributes": true
}
4 changes: 3 additions & 1 deletion jamftf/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""python importer magic"""
"""Jamf Pro Terraform importer package."""

__version__ = "0.1.0"

from .importer import Importer
from .resources import *
Expand Down
21 changes: 13 additions & 8 deletions jamftf/config_ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@
from pathlib import Path
from typing import List
import json
from .constants import valid_resource_key, RESOURCE_TYPE_OBJECT_MAP
from .constants import valid_resource_key, RESOURCE_TYPE_OBJECT_MAP, RESOURCE_KEY_MAP
from .exceptions import InvalidResourceTypeError
from .models import Resource

class ConfigIngest:
"""Configuration ingestion class for Jamf Pro resources."""

def __init__(self, config_path: str):
"""Initialize with path to config file."""
self.resources = parse_config_file(config_path)

def get_resources(self) -> List[Resource]:
"""Get the list of active resources from the config."""
return self.resources

def parse_config_file(path: str) -> list[Resource]:
"""
Expand All @@ -15,7 +25,6 @@ def parse_config_file(path: str) -> list[Resource]:
The path is sanitized, expanded, and validated before reading.
Raises FileNotFoundError if the file does not exist.
"""

safe_path = Path(path).expanduser().resolve(strict=False)

if not safe_path.is_file():
Expand All @@ -26,26 +35,22 @@ def parse_config_file(path: str) -> list[Resource]:

return parse_config_dict(json_data)


def parse_config_dict(config_json: dict) -> List[Resource]:
"""
Parses a config dictionary into a list of active Resource instances.

Skips inactive entries and raises InvalidResourceTypeError for unknown resource types.
"""

out = []

for res_key, active in config_json.items():

if not valid_resource_key(res_key):
raise InvalidResourceTypeError(f"invalid resource type: {res_key}")

if not active:
continue

out.append(
RESOURCE_TYPE_OBJECT_MAP[res_key]()
)
resource_type = RESOURCE_KEY_MAP[res_key]
out.append(RESOURCE_TYPE_OBJECT_MAP[resource_type]())

return out
18 changes: 13 additions & 5 deletions jamftf/constants.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Storage for all constant values for easier configuration."""

from .enums import ProviderResourceTags

from .resources import (
Scripts,
Categories,
Expand All @@ -19,7 +18,17 @@
"valid_resource_key",
]


# Map from config file keys to resource types
RESOURCE_KEY_MAP = {
"scripts": ProviderResourceTags.SCRIPT,
"categories": ProviderResourceTags.CATEGORY,
"policies": ProviderResourceTags.POLICY,
"configuration_profiles": ProviderResourceTags.MACOS_CONFIG_PROFILE,
"computer_groups_static": ProviderResourceTags.COMPUTER_GROUP_STATIC,
"computer_groups_smart": ProviderResourceTags.COMPUTER_GROUP_SMART,
"advanced_computer_searches": ProviderResourceTags.ADVANCED_COMPUTER_SEARCH,
"computer_extension_attributes": ProviderResourceTags.COMPUTER_EXT_ATTR,
}

RESOURCE_TYPE_OBJECT_MAP = {
ProviderResourceTags.SCRIPT: Scripts,
Expand All @@ -32,7 +41,6 @@
ProviderResourceTags.COMPUTER_EXT_ATTR: ComputerExtensionAttributes,
}


def valid_resource_key(key: str) -> bool:
"""Check if the key is a valid provider resource tag."""
return ProviderResourceTags.valid_resource_check(key)
"""Check if the key is a valid resource type in the config file."""
return key in RESOURCE_KEY_MAP
5 changes: 3 additions & 2 deletions jamftf/dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Storage for dataclases"""
"""Storage for dataclasses."""

from dataclasses import dataclass

@dataclass
class SingleItem:
"""Represents a single Jamf resource item."""
def __init__(self, resource_type, jpro_id):
def __init__(self, resource_type, resource_name, jpro_id):
self.resource_type = resource_type
self.resource_name = resource_name
self.jpro_id = jpro_id
Loading