Skip to content

Commit 5c61e87

Browse files
committed
Merge branch '520-add-support-for-opentofu' into 'main'
Resolve "Add support for OpenTofu" Closes #520 See merge request pub/terrareg!407
2 parents 682e372 + 678b4db commit 5c61e87

File tree

9 files changed

+143
-25
lines changed

9 files changed

+143
-25
lines changed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ RUN bash -c 'if [ "$(uname -m)" == "aarch64" ]; \
4242
rm /tmp/infracost.tar.gz'
4343

4444
# Download tfswitch
45-
RUN bash -c 'curl -L https://raw.githubusercontent.com/warrensbox/terraform-switcher/master/install.sh | bash /dev/stdin 1.0.0'
45+
RUN bash -c 'curl -L https://raw.githubusercontent.com/warrensbox/terraform-switcher/master/install.sh | bash /dev/stdin 1.2.2'
4646

4747
# Install go
4848
RUN bash -c 'if [ "$(uname -m)" == "aarch64" ]; \

Dockerfile.tests

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ RUN bash -c 'if [ "$(uname -m)" == "aarch64" ]; \
4242
rm /tmp/infracost.tar.gz'
4343

4444
# Download tfswitch
45-
RUN bash -c 'curl -L https://raw.githubusercontent.com/warrensbox/terraform-switcher/master/install.sh | bash /dev/stdin 1.0.0'
45+
RUN bash -c 'curl -L https://raw.githubusercontent.com/warrensbox/terraform-switcher/master/install.sh | bash /dev/stdin 1.2.2'
4646

4747
# Install go
4848
RUN bash -c 'if [ "$(uname -m)" == "aarch64" ]; \

docs/CONFIG.md

+15-2
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,17 @@ A common configuration may require a 'groups' scope to be added to the list of s
790790
Default: `['openid', 'profile']`
791791

792792

793+
### PRODUCT
794+
795+
796+
Product to use when performing module extraction
797+
798+
Options: terraform, opentofu
799+
800+
801+
Default: `terraform`
802+
803+
793804
### PROVIDER_CATEGORIES
794805

795806

@@ -1073,10 +1084,12 @@ Default: ``
10731084
### TERRAFORM_ARCHIVE_MIRROR
10741085

10751086

1076-
Mirror for obtaining version list and downloading Terraform
1087+
Mirror for obtaining version list and downloading Terraform.
10771088

1089+
Defaults to default mirror used by tfswitch
10781090

1079-
Default: `https://releases.hashicorp.com/terraform`
1091+
1092+
Default: ``
10801093

10811094

10821095
### TERRAFORM_EXAMPLE_VERSION_TEMPLATE

scripts/dictionary.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,5 @@ endpoint
143143
prepended
144144
prepending
145145
FH
146-
netloc
146+
netloc
147+
opentofu

terrareg/config.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ class DefaultUiInputOutputView(Enum):
3333
EXPANDED = "expanded"
3434

3535

36+
class Product(Enum):
37+
"""Type of product"""
38+
TERRAFORM = "terraform"
39+
OPENTOFU = "opentofu"
40+
41+
3642
class Config:
3743

3844
@property
@@ -912,12 +918,23 @@ def DEFAULT_TERRAFORM_VERSION(self):
912918
"""
913919
return os.environ.get("DEFAULT_TERRAFORM_VERSION", "1.3.6")
914920

921+
@property
922+
def PRODUCT(self):
923+
"""
924+
Product to use when performing module extraction
925+
926+
Options: terraform, opentofu
927+
"""
928+
return Product(os.environ.get('PRODUCT', Product.TERRAFORM.value).lower())
929+
915930
@property
916931
def TERRAFORM_ARCHIVE_MIRROR(self):
917932
"""
918-
Mirror for obtaining version list and downloading Terraform
933+
Mirror for obtaining version list and downloading Terraform.
934+
935+
Defaults to default mirror used by tfswitch
919936
"""
920-
return os.environ.get("TERRAFORM_ARCHIVE_MIRROR", "https://releases.hashicorp.com/terraform")
937+
return os.environ.get("TERRAFORM_ARCHIVE_MIRROR", "")
921938

922939
@property
923940
def MANAGE_TERRAFORM_RC_FILE(self):

terrareg/module_extractor.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
UnableToGetGlobalTerraformLockError,
3333
TerraformVersionSwitchError
3434
)
35+
import terrareg.terraform_product
3536
from terrareg.utils import PathDoesNotExistError, get_public_url_details, safe_iglob, safe_join_paths
3637
from terrareg.config import Config
3738
from terrareg.constants import EXTRACTION_VERSION
@@ -51,9 +52,10 @@ def __init__(self, module_version: 'terrareg.models.ModuleVersion'):
5152
self._upload_directory = tempfile.TemporaryDirectory() # noqa: R1732
5253

5354
@staticmethod
54-
def terraform_binary():
55+
def terraform_binary() -> str:
5556
"""Return path of terraform binary"""
56-
return os.path.join(os.getcwd(), "bin", "terraform")
57+
product = terrareg.terraform_product.ProductFactory.get_product()
58+
return os.path.join(os.getcwd(), "bin", product.get_executable_name())
5759

5860
@property
5961
def terraform_rc_file(self):
@@ -130,16 +132,25 @@ def _switch_terraform_versions(cls, module_path):
130132
"Unable to obtain global Terraform lock in 60 seconds"
131133
)
132134
try:
133-
default_terraform_version = Config().DEFAULT_TERRAFORM_VERSION
135+
config = Config()
136+
137+
default_terraform_version = config.DEFAULT_TERRAFORM_VERSION
134138
tfswitch_env = os.environ.copy()
135139

136140
if default_terraform_version:
137-
tfswitch_env["TF_VERSION"] = default_terraform_version
141+
tfswitch_env["TF_DEFAULT_VERSION"] = default_terraform_version
142+
143+
product = terrareg.terraform_product.ProductFactory.get_product()
144+
tfswitch_env["TF_PRODUCT"] = product.get_tfswitch_product_arg()
145+
146+
tfswitch_args = []
147+
if config.TERRAFORM_ARCHIVE_MIRROR:
148+
tfswitch_args += ["--mirror", config.TERRAFORM_ARCHIVE_MIRROR]
138149

139150
# Run tfswitch
140151
try:
141152
subprocess.check_output(
142-
["tfswitch", "--mirror", Config().TERRAFORM_ARCHIVE_MIRROR, "--bin", cls.terraform_binary()],
153+
["tfswitch", "--bin", cls.terraform_binary(), *tfswitch_args],
143154
env=tfswitch_env,
144155
cwd=module_path
145156
)

terrareg/terraform_product.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import abc
2+
3+
import terrareg.config
4+
5+
6+
class BaseProduct(abc.ABC):
7+
8+
@abc.abstractmethod
9+
def get_tfswitch_product_arg(self) -> str:
10+
"""Return name of tfswitch product argument value"""
11+
...
12+
13+
@abc.abstractmethod
14+
def get_executable_name(self) -> str:
15+
"""Return executable name for product"""
16+
...
17+
18+
class Terraform(BaseProduct):
19+
"""Terraform product"""
20+
21+
def get_tfswitch_product_arg(self) -> str:
22+
"""Return name of tfswitch product argument value"""
23+
return "terraform"
24+
25+
def get_executable_name(self) -> str:
26+
"""Return executable name for product"""
27+
return "terraform"
28+
29+
30+
class OpenTofu(BaseProduct):
31+
"""OpenTofu product"""
32+
33+
def get_tfswitch_product_arg(self) -> str:
34+
"""Return name of tfswitch product argument value"""
35+
return "opentofu"
36+
37+
def get_executable_name(self) -> str:
38+
"""Return executable name for product"""
39+
return "tofu"
40+
41+
42+
class ProductFactory:
43+
44+
@staticmethod
45+
def get_product():
46+
"""Obtain current product"""
47+
product_enum = terrareg.config.Config().PRODUCT
48+
if product_enum is terrareg.config.Product.TERRAFORM:
49+
return Terraform()
50+
elif product_enum is terrareg.config.Product.OPENTOFU:
51+
return OpenTofu()
52+
raise Exception("Could not determine product class")

test/unit/terrareg/test_config.py

+1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ def test_list_configs(self, config_name, test_value, expected_value):
167167
('SERVER', terrareg.config.ServerType, terrareg.config.ServerType.BUILTIN),
168168
('ALLOW_MODULE_HOSTING', terrareg.config.ModuleHostingMode, terrareg.config.ModuleHostingMode.ALLOW),
169169
('DEFAULT_UI_DETAILS_VIEW', terrareg.config.DefaultUiInputOutputView, terrareg.config.DefaultUiInputOutputView.TABLE),
170+
('PRODUCT', terrareg.config.Product, terrareg.config.Product.TERRAFORM),
170171
])
171172
def test_enum_configs(self, config_name, enum, expected_default):
172173
"""Test enum configs to ensure they are overridden with environment variables."""

test/unit/terrareg/test_module_extractor.py

+36-13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
)
1717
from terrareg.module_extractor import GitModuleExtractor, ModuleExtractor
1818
import terrareg.models
19+
import terrareg.config
1920

2021

2122
class TestGitModuleExtractor(TerraregUnitTest):
@@ -253,36 +254,46 @@ def test_description_extraction_when_disabled(self, mock_models):
253254
with unittest.mock.patch('terrareg.config.Config.AUTOGENERATE_MODULE_PROVIDER_DESCRIPTION', False):
254255
assert module_extractor._extract_description(test_text) == None
255256

256-
def test_run_tf_init(self):
257+
@pytest.mark.parametrize('config_product, expected_binary', [
258+
(terrareg.config.Product.TERRAFORM, 'terraform'),
259+
(terrareg.config.Product.OPENTOFU, 'tofu'),
260+
])
261+
def test_run_tf_init(self, config_product, expected_binary):
257262
"""Test running terraform init"""
258263
with unittest.mock.patch('terrareg.module_extractor.subprocess.check_call', unittest.mock.MagicMock()) as check_output_mock, \
259-
unittest.mock.patch("terrareg.module_extractor.ModuleExtractor._create_terraform_rc_file", unittest.mock.MagicMock()) as mock_create_terraform_rc_file:
264+
unittest.mock.patch("terrareg.module_extractor.ModuleExtractor._create_terraform_rc_file", unittest.mock.MagicMock()) as mock_create_terraform_rc_file, \
265+
unittest.mock.patch('terrareg.config.Config.PRODUCT', config_product):
260266

261267
module_extractor = GitModuleExtractor(module_version=None)
262268

263269
assert module_extractor._run_tf_init(module_path='/tmp/mock-patch/to/module') is True
264270

265271
check_output_mock.assert_called_once_with(
266-
[os.path.join(os.getcwd(), 'bin', 'terraform'), 'init'],
272+
[os.path.join(os.getcwd(), 'bin', expected_binary), 'init'],
267273
cwd='/tmp/mock-patch/to/module'
268274
)
269275
mock_create_terraform_rc_file.assert_called_once_with()
270276

271-
def test_run_tf_init_error(self):
277+
@pytest.mark.parametrize('config_product, expected_binary', [
278+
(terrareg.config.Product.TERRAFORM, 'terraform'),
279+
(terrareg.config.Product.OPENTOFU, 'tofu'),
280+
])
281+
def test_run_tf_init_error(self, config_product, expected_binary):
272282
"""Test running terraform init with error returned"""
273283

274284
def raise_exception(*args, **kwargs):
275285
raise subprocess.CalledProcessError(cmd="test", returncode=2)
276286

277287
with unittest.mock.patch('terrareg.module_extractor.subprocess.check_call', unittest.mock.MagicMock(side_effect=raise_exception)) as mock_check_call, \
278-
unittest.mock.patch("terrareg.module_extractor.ModuleExtractor._create_terraform_rc_file", unittest.mock.MagicMock()) as mock_create_terraform_rc_file:
288+
unittest.mock.patch("terrareg.module_extractor.ModuleExtractor._create_terraform_rc_file", unittest.mock.MagicMock()) as mock_create_terraform_rc_file, \
289+
unittest.mock.patch('terrareg.config.Config.PRODUCT', config_product):
279290

280291
module_extractor = GitModuleExtractor(module_version=None)
281292

282293
assert module_extractor._run_tf_init(module_path='/tmp/mock-patch/to/module') is False
283294

284295
mock_check_call.assert_called_once_with(
285-
[os.path.join(os.getcwd(), 'bin', 'terraform'), 'init'],
296+
[os.path.join(os.getcwd(), 'bin', expected_binary), 'init'],
286297
cwd='/tmp/mock-patch/to/module'
287298
)
288299
mock_create_terraform_rc_file.assert_called_once_with()
@@ -391,7 +402,11 @@ def test_override_tf_backend(self, file_contents, expected_backend_file):
391402
shutil.rmtree(temp_dir)
392403

393404

394-
def test_switch_terraform_versions(self):
405+
@pytest.mark.parametrize('config_product', [
406+
terrareg.config.Product.TERRAFORM,
407+
terrareg.config.Product.OPENTOFU,
408+
])
409+
def test_switch_terraform_versions(self, config_product):
395410
"""Test switching terraform versions."""
396411
module_extractor = GitModuleExtractor(module_version=None)
397412
mock_lock = unittest.mock.MagicMock()
@@ -400,16 +415,19 @@ def test_switch_terraform_versions(self):
400415
with unittest.mock.patch('terrareg.module_extractor.ModuleExtractor.TERRAFORM_LOCK', mock_lock), \
401416
unittest.mock.patch('terrareg.module_extractor.subprocess.check_output', unittest.mock.MagicMock()) as check_output_mock, \
402417
unittest.mock.patch('terrareg.config.Config.DEFAULT_TERRAFORM_VERSION', 'unittest-tf-version'), \
403-
unittest.mock.patch('terrareg.config.Config.TERRAFORM_ARCHIVE_MIRROR', 'https://localhost-archive/mirror/terraform'):
418+
unittest.mock.patch('terrareg.config.Config.TERRAFORM_ARCHIVE_MIRROR', 'https://localhost-archive/mirror/terraform'), \
419+
unittest.mock.patch('terrareg.config.Config.PRODUCT', config_product):
404420

405421
with module_extractor._switch_terraform_versions(module_path='/tmp/mock-patch/to/module'):
406422
pass
407423

408424
mock_lock.acquire.assert_called_once_with(blocking=True, timeout=60)
425+
expected_bin = f"{os.getcwd()}/bin/" + ('terraform' if config_product is terrareg.config.Product.TERRAFORM else 'tofu')
409426
expected_env = os.environ.copy()
410-
expected_env['TF_VERSION'] = "unittest-tf-version"
427+
expected_env['TF_DEFAULT_VERSION'] = "unittest-tf-version"
428+
expected_env['TF_PRODUCT'] = 'terraform' if config_product is terrareg.config.Product.TERRAFORM else 'opentofu'
411429
check_output_mock.assert_called_once_with(
412-
["tfswitch", "--mirror", "https://localhost-archive/mirror/terraform", "--bin", f"{os.getcwd()}/bin/terraform"],
430+
["tfswitch", "--bin", expected_bin, "--mirror", "https://localhost-archive/mirror/terraform"],
413431
env=expected_env,
414432
cwd="/tmp/mock-patch/to/module"
415433
)
@@ -446,17 +464,22 @@ def test_switch_terraform_versions_with_lock(self):
446464
mock_lock.acquire.assert_called_once_with(blocking=True, timeout=60)
447465
check_output_mock.assert_not_called()
448466

449-
def test_get_graph_data(self):
467+
@pytest.mark.parametrize('config_product, expected_binary', [
468+
(terrareg.config.Product.TERRAFORM, 'terraform'),
469+
(terrareg.config.Product.OPENTOFU, 'tofu'),
470+
])
471+
def test_get_graph_data(self, config_product, expected_binary):
450472
"""Test call to terraform graph to generate graph output"""
451473
module_extractor = GitModuleExtractor(module_version=None)
452474

453475
with unittest.mock.patch('terrareg.module_extractor.subprocess.check_output',
454-
unittest.mock.MagicMock(return_value="Output graph data".encode("utf-8"))) as mock_check_output:
476+
unittest.mock.MagicMock(return_value="Output graph data".encode("utf-8"))) as mock_check_output, \
477+
unittest.mock.patch('terrareg.config.Config.PRODUCT', config_product):
455478

456479
module_extractor._get_graph_data(module_path='/tmp/mock-patch/to/module')
457480

458481
mock_check_output.assert_called_once_with(
459-
[os.getcwd() + '/bin/terraform', 'graph'],
482+
[f'{os.getcwd()}/bin/{expected_binary}', 'graph'],
460483
cwd='/tmp/mock-patch/to/module'
461484
)
462485

0 commit comments

Comments
 (0)