Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: opentofu support #99

Merged
merged 31 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b43bbbc
feat: open tofu flag
chlohilt Aug 1, 2024
7a1015c
feat: update install command and alter dir obj to hold opentofu dir
chlohilt Aug 1, 2024
1eb2155
fix: url set up
chlohilt Aug 1, 2024
de7522d
chore: open tofu versions dir and zip path
chlohilt Aug 1, 2024
2dc155a
chore: spelling and syntax fix
chlohilt Aug 2, 2024
ff52957
feat: get latest function supports opentofu
tylerablackham Aug 2, 2024
e6af177
chore: improve some logging
tylerablackham Aug 2, 2024
46bd062
fix: install from web and download for redirection
chlohilt Aug 2, 2024
ccf7751
feat: support current with OpenTofu and update README
chlohilt Aug 2, 2024
135f7f0
fix: regex bug
chlohilt Aug 2, 2024
77a8e1e
fix: await for settings
chlohilt Aug 2, 2024
4157565
chore: get rid of extra line
chlohilt Aug 2, 2024
8a7060c
chore: merge with beta
chlohilt Aug 2, 2024
de1f8ba
fix: lint and delete file
chlohilt Aug 2, 2024
184eb42
fix: scripts for opentofu and working with beta branch changes
chlohilt Aug 5, 2024
8f37a7b
fix: install bug
chlohilt Aug 5, 2024
a04f279
fix: semver stuff and other bugs
chlohilt Aug 5, 2024
6d042f0
feat: detect works with opentofu
tylerablackham Aug 6, 2024
21a97ea
fix: lint
chlohilt Aug 6, 2024
9c094ea
Merge branch 'feat/opentofu-support' of https://github.com/byu-oit/tf…
chlohilt Aug 6, 2024
848790c
fix: more lint
chlohilt Aug 6, 2024
4a0169d
feat: type on list feature
chlohilt Aug 8, 2024
fc8fbc4
clean: openTofuCheck
chlohilt Aug 8, 2024
6ff29a6
feat: note for downloading versions less than 1.6.0
chlohilt Aug 8, 2024
bb90e12
chore: update README
chlohilt Aug 8, 2024
b4994c8
feat: delete executable when useOpenTofu flag is set
chlohilt Aug 8, 2024
7e50137
wip: switch types message
chlohilt Aug 9, 2024
93bc9c1
fix: annoying carriage return chars
chlohilt Aug 9, 2024
28ac6c3
chore: clean up
chlohilt Aug 9, 2024
96b9388
fix: current version starred
chlohilt Aug 9, 2024
710523e
fix: lint
chlohilt Aug 9, 2024
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
8 changes: 5 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Run `tfvm` in any command line, followed by one of these commands:
- `tfvm config disableErrors=true` - disables configuration warnings.
- `tfvm config disableAWSWarnings=true` - disables AWS warnings that appear when using older terraform versions.
- `tfvm config disableSettingPrompts=true` - disables prompts that show how to hide some error messages.
- `tfvm config useOpenTofu=true` - uses the open source version of Terraform, OpenTofu (experimental flag)
- `help`: prints usage information. Run `tfvm help <command>` to see information about the other tfvm commands.

## FAQ
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/lib/commands/current.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import getTerraformVersion from '../util/tfVersion.js'
import getErrorMessage from '../util/errorChecker.js'
import { logger } from '../util/logger.js'
import { TfvmFS } from '../util/getDirectoriesObj.js'
import getSettings from '../util/getSettings.js'

async function current () {
try {
const settings = await getSettings()
const currentTFVersion = await getTerraformVersion()
if (currentTFVersion !== null) {
console.log(chalk.white.bold('Current Terraform version:\n' +
console.log(chalk.white.bold(`Current ${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} version:\n` +
currentTFVersion + ` (Currently using ${TfvmFS.bitWidth}-bit executable)`))
} else {
console.log(chalk.cyan.bold('It appears there is no terraform version running on your computer, or ' +
console.log(chalk.cyan.bold(`It appears there is no ${settings.useOpenTofu ? 'opentofu' : 'terraform'} version running on your computer, or ` +
'there was an error extracting the version.\n'))
console.log(chalk.green.bold('Run tfvm use <version> to set your terraform version, ' +
'or `terraform -v` to manually check the current version.'))
console.log(chalk.green.bold(`Run tfvm use <version> to set your ${settings.useOpenTofu ? 'opentofu' : 'terraform'} version, ` +
`or ${settings.useOpenTofu ? 'tofu' : 'terraform'} -v to manually check the current version.`))
}
} catch (error) {
logger.fatal(error, 'Fatal error when running "current" command: ')
Expand Down
33 changes: 26 additions & 7 deletions packages/cli/lib/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,34 @@ import getErrorMessage from '../util/errorChecker.js'
import getTerraformVersion from '../util/tfVersion.js'
import getLatest from '../util/getLatest.js'
import { logger } from '../util/logger.js'
import getSettings from '../util/getSettings.js'

async function install (versionNum) {
try {
const settings = await getSettings()
const installVersion = 'v' + versionNum
if (!versionRegEx.test(installVersion) && versionNum !== 'latest') {
logger.warn(`invalid version attempted to install with version ${installVersion}`)
console.log(chalk.red.bold('Invalid version syntax.'))
console.log(chalk.white.bold('Version should be formatted as \'vX.X.X\'\nGet a list of all current ' +
if (settings.useOpenTofu) {
console.log(chalk.white.bold('Version should be formatted as \'vX.X.X\'\nGet a list of all current ' +
'opentofu versions here: https://github.com/opentofu/opentofu/releases'))
} else {
console.log(chalk.white.bold('Version should be formatted as \'vX.X.X\'\nGet a list of all current ' +
'terraform versions here: https://releases.hashicorp.com/terraform/'))
}
} else if (versionNum === 'latest') {
const installedVersions = await getInstalledVersions()
const latest = await getLatest()
const currentVersion = await getTerraformVersion()
if (latest) {
const versionLatest = 'v' + latest
if (installedVersions.includes(versionLatest) && currentVersion !== versionLatest) {
console.log(chalk.bold.cyan(`The latest terraform version is ${latest} and is ` +
console.log(chalk.bold.cyan(`The latest ${settings.useOpenTofu ? 'opentofu' : 'terraform'} version is ${latest} and is ` +
`already installed on your computer. Run 'tfvm use ${latest}' to use.`))
} else if (installedVersions.includes(versionLatest) && currentVersion === versionLatest) {
const currentVersion = await getTerraformVersion()
console.log(chalk.bold.cyan(`The latest terraform version is ${currentVersion} and ` +
console.log(chalk.bold.cyan(`The latest ${settings.useOpenTofu ? 'opentofu' : 'terraform'} version is ${currentVersion} and ` +
'is already installed and in use on your computer.'))
} else {
await installFromWeb(latest)
Expand All @@ -39,7 +46,7 @@ async function install (versionNum) {
} else {
const installedVersions = await getInstalledVersions()
if (installedVersions.includes(installVersion)) {
console.log(chalk.white.bold(`Terraform version ${installVersion} is already installed.`))
console.log(chalk.white.bold(`${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} version ${installVersion} is already installed.`))
} else {
await installFromWeb(versionNum)
}
Expand All @@ -53,9 +60,21 @@ async function install (versionNum) {
export default install

export async function installFromWeb (versionNum, printMessage = true) {
const zipPath = TfvmFS.getPath(TfvmFS.tfVersionsDir, `v${versionNum}.zip`)
const newVersionDir = TfvmFS.getPath(TfvmFS.tfVersionsDir, 'v' + versionNum)
const url = `https://releases.hashicorp.com/terraform/${versionNum}/terraform_${versionNum}_${TfvmFS.architecture}.zip`
const settingsObj = await getSettings()

let url
let zipPath
let newVersionDir

if (settingsObj.useOpenTofu) {
zipPath = TfvmFS.getPath(TfvmFS.otfVersionsDir, `v${versionNum}.zip`)
newVersionDir = TfvmFS.getPath(TfvmFS.otfVersionsDir, 'v' + versionNum)
url = `https://github.com/opentofu/opentofu/releases/download/v${versionNum}/tofu_${versionNum}_${TfvmFS.architecture}.zip`
} else {
zipPath = TfvmFS.getPath(TfvmFS.tfVersionsDir, `v${versionNum}.zip`)
newVersionDir = TfvmFS.getPath(TfvmFS.tfVersionsDir, 'v' + versionNum)
url = `https://releases.hashicorp.com/terraform/${versionNum}/terraform_${versionNum}_${TfvmFS.architecture}.zip`
}
await download(url, zipPath, versionNum)
await fs.mkdir(newVersionDir)
await unzipFile(zipPath, newVersionDir)
Expand Down
1 change: 0 additions & 1 deletion packages/cli/lib/commands/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ async function list () {

if (tfList.length > 0) {
const currentTFVersion = await getTerraformVersion()
console.log('\n')
tfList.sort(compareVersions).reverse()
for (const versionDir of tfList) {
if (versionDir === currentTFVersion) {
Expand Down
14 changes: 10 additions & 4 deletions packages/cli/lib/commands/uninstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@ import getInstalledVersions from '../util/getInstalledVersions.js'
import { TfvmFS } from '../util/getDirectoriesObj.js'
import getErrorMessage from '../util/errorChecker.js'
import { logger } from '../util/logger.js'
import getSettings from '../util/getSettings.js'

async function uninstall (uninstallVersion) {
try {
uninstallVersion = 'v' + uninstallVersion
if (!versionRegEx.test(uninstallVersion)) {
console.log(chalk.red.bold('Invalid version syntax'))
} else {
const settings = await getSettings()
const installedVersions = await getInstalledVersions()
if (!installedVersions.includes(uninstallVersion)) {
console.log(chalk.white.bold(`terraform ${uninstallVersion} is not installed. Type "tfvm list" to see what is installed.`))
console.log(chalk.white.bold(`${settings.useOpenTofu ? 'opentofu' : 'terraform'} ${uninstallVersion} is not installed. Type "tfvm list" to see what is installed.`))
} else {
console.log(chalk.white.bold(`Uninstalling terraform ${uninstallVersion}...`))
await TfvmFS.deleteDirectory(TfvmFS.tfVersionsDir, uninstallVersion)
console.log(chalk.cyan.bold(`Successfully uninstalled terraform ${uninstallVersion}`))
console.log(chalk.white.bold(`Uninstalling ${settings.useOpenTofu ? 'opentofu' : 'terraform'} ${uninstallVersion}...`))
if (settings.useOpenTofu) {
await TfvmFS.deleteDirectory(TfvmFS.otfVersionsDir, uninstallVersion)
} else {
await TfvmFS.deleteDirectory(TfvmFS.tfVersionsDir, uninstallVersion)
}
console.log(chalk.cyan.bold(`Successfully uninstalled ${settings.useOpenTofu ? 'opentofu' : 'terraform'} ${uninstallVersion}`))
}
}
} catch (error) {
Expand Down
24 changes: 16 additions & 8 deletions packages/cli/lib/commands/use.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ export async function useVersion (version) {
* @returns {Promise<boolean>} true if the user opted to install the version, false if they did not
*/
export async function installNewVersion (version) {
console.log(chalk.white.bold(`Terraform v${version} is not installed. Would you like to install it?`))
const settings = await getSettings()
console.log(chalk.white.bold(`${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} v${version} is not installed. Would you like to install it?`))
const installToggle = new enquirer.Toggle({
disabled: 'Yes',
enabled: 'No'
})
if (await installToggle.run()) {
console.log(chalk.white.bold(`No action taken. Use 'tfvm install ${version}' to install terraform v${version}`))
console.log(chalk.white.bold(`No action taken. Use 'tfvm install ${version}' to install ${settings.useOpenTofu ? 'opentofu' : 'terraform'} v${version}`))
return false
} else {
await installFromWeb(version, false)
Expand All @@ -67,17 +68,24 @@ export async function installNewVersion (version) {
* @returns {Promise<void>}
*/
export async function switchVersionTo (version) {
const settings = await getSettings()
if (version[0] === 'v') version = version.substring(1)

await TfvmFS.createTfAppDataDir()
await TfvmFS.deleteCurrentTfExe()

await fs.copyFile(
TfvmFS.getPath(TfvmFS.tfVersionsDir, 'v' + version, 'terraform.exe'), // source file
TfvmFS.getPath(TfvmFS.terraformDir, 'terraform.exe') // destination file
)
console.log(chalk.cyan.bold(`Now using terraform v${version} (${TfvmFS.bitWidth}-bit)`))
const settings = await getSettings()
if (settings.useOpenTofu) {
await fs.copyFile(
TfvmFS.getPath(TfvmFS.otfVersionsDir, 'v' + version, 'tofu.exe'), // source file
TfvmFS.getPath(TfvmFS.openTofuDir, 'tofu.exe') // destination file
)
} else {
await fs.copyFile(
TfvmFS.getPath(TfvmFS.tfVersionsDir, 'v' + version, 'terraform.exe'), // source file
TfvmFS.getPath(TfvmFS.terraformDir, 'terraform.exe') // destination file
)
}
console.log(chalk.cyan.bold(`Now using ${settings.useOpenTofu ? 'opentofu' : 'terraform'} v${version} (${TfvmFS.bitWidth}-bit)`))
if (requiresOldAWSAuth(version) && !settings.disableAWSWarnings) {
console.log(chalk.yellow.bold('Warning: This tf version is not compatible with the newest ' +
'AWS CLI authentication methods (e.g. aws sso login). Use short-term credentials instead.'))
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ program
.action(config)
.addHelpText('after', '\nAll settings are either true or false (default is false), and set like this:\n' +
chalk.cyan('\n tfvm config <setting>=<true/false>\n\n') +
'Herer are all the available settings:\n' +
'Here are all the available settings:\n' +
'disableErrors - Disables some recurrent warning messages\n' +
'disableAWSWarnings - Disables warnings about needing old AWS authentication with tf versions older than 0.14.6\n' +
'disableSettingsPrompts - Disables prompts to turn off warnings by enabling these settings')
'disableSettingsPrompts - Disables prompts to turn off warnings by enabling these settings\n' +
'useOpenTofu - Uses OpenTofu instead of Terraform')

program
.command('install <version>')
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/lib/scripts/addToPathOpenTofu.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# This script attempts to add opentofu to the system and local paths.
# It will not add it if it already exists.
# It will not add it to the system path if the shell it is being run from does not have admin rights

$userName = $env:UserName
$path2addOpenTofu = ";C:\Users\$userName\AppData\Roaming\opentofu"
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User');

# attempts to add to local path
If (!$userPath.contains($path2addOpenTofu)) {
$userPath += $path2addOpenTofu
$userPath = $userPath -join ';'
[Environment]::SetEnvironmentVariable('Path', $userPath, 'User');
}
3 changes: 2 additions & 1 deletion packages/cli/lib/util/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const versionRegEx = /^v[0-9]+.{1}[0-9]+.{1}[0-9]+/
export const versionRegEx = /^v[0-9]+\.[0-9]+\.[0-9]+/

// Uses positive-lookbehind to only match versions preceded by 'Terraform ' but without extracting 'Terraform '
export const tfCurrVersionRegEx = /(?<=Terraform )v[0-9]+.{1}[0-9]+.{1}[0-9]+/gm
export const openTofuCurrVersionRegEx = /(?<=OpenTofu )v[0-9]+.{1}[0-9]+.{1}[0-9]+/gm
8 changes: 5 additions & 3 deletions packages/cli/lib/util/download.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as https from 'https'
import * as http from 'http'
import chalk from 'chalk'
import fs from 'node:fs'
import { logger } from './logger.js'
import getSettings from './getSettings.js'
import followRedirects from 'follow-redirects'
const { http, https } = followRedirects

async function download (url, filePath, version) {
const proto = !url.charAt(4).localeCompare('s') ? https : http
const settings = await getSettings()

return new Promise((resolve, reject) => {
const file = fs.createWriteStream(filePath)
Expand All @@ -14,7 +16,7 @@ async function download (url, filePath, version) {
const request = proto.get(url, response => {
if (response.statusCode !== 200) {
fs.unlink(filePath, () => {
console.log(chalk.red.bold(`Terraform ${version} is not yet released or available.`))
console.log(chalk.red.bold(`${settings.useOpenTofu ? 'OpenTofu' : 'Terraform'} ${version} is not yet released or available.`))
})
return
}
Expand Down
13 changes: 11 additions & 2 deletions packages/cli/lib/util/getDirectoriesObj.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const settingsFileName = 'settings.json'
const logFolderName = 'logs'
const tfVersionsFolderName = 'versions'
const tfvmAppDataFolderName = 'tfvm'
const otfvmAppDataFolderName = 'otfvm'
const dirSeparator = '\\'

/**
Expand All @@ -13,21 +14,25 @@ const dirSeparator = '\\'
export class TfvmFS {
static appDataDir = process.env.APPDATA || (process.platform === 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + '/.local/share')
static tfvmDir = this.appDataDir.concat(dirSeparator + tfvmAppDataFolderName) // where tfvms own files are in AppData
static otfvmDir = this.appDataDir.concat(dirSeparator + otfvmAppDataFolderName) // where open tofu version manager (still tfvm) are in AppData
static tfVersionsDir = this.tfvmDir.concat(dirSeparator + tfVersionsFolderName) // where all the versions of terraform are stored
static otfVersionsDir = this.otfvmDir.concat(dirSeparator + tfVersionsFolderName) // where all the versions of terraform are stored
static logsDir = this.tfvmDir.concat(dirSeparator + logFolderName) // where tfvm logs are stored (in appdata)=
static terraformDir = this.appDataDir.concat(dirSeparator + 'terraform') // where the path is looking for terraform.exe to be found
static openTofuDir = this.appDataDir.concat(dirSeparator + 'opentofu') // where the path is looking for OpenTofu to be found
static settingsDir = this.tfvmDir.concat(dirSeparator + settingsFileName) // where the tfvm settings file can be located
static architecture = process.env.PROCESSOR_ARCHITECTURE === 'AMD64' ? 'windows_amd64' : 'windows_386'
static bitWidth = process.env.PROCESSOR_ARCHITECTURE === 'AMD64' ? '64' : '32'

static getDirectoriesObj () {
return {
appDataDir: this.appDataDir,
tfvmDir: this.tfvmDir,
logsDir: this.logsDir,
tfVersionsDir: this.tfVersionsDir,
terraformDir: this.terraformDir,
settingsDir: this.settingsDir
settingsDir: this.settingsDir,
otfvmDir: this.otfvmDir
}
}

Expand All @@ -37,6 +42,7 @@ export class TfvmFS {
*/
static async createTfAppDataDir () {
if (!fs.existsSync(this.terraformDir)) fs.mkdirSync(TfvmFS.terraformDir)
if (!fs.existsSync(this.openTofuDir)) fs.mkdirSync(TfvmFS.openTofuDir)
}

/**
Expand All @@ -48,6 +54,9 @@ export class TfvmFS {
if ((await fsp.readdir(this.terraformDir)).includes('terraform.exe')) {
await fsp.unlink(this.terraformDir + dirSeparator + 'terraform.exe')
}
if ((await fsp.readdir(this.openTofuDir)).includes('tofu.exe')) {
await fsp.unlink(this.openTofuDir + dirSeparator + 'tofu.exe')
}
}

/**
Expand Down
21 changes: 16 additions & 5 deletions packages/cli/lib/util/getInstalledVersions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'node:fs/promises'
import { versionRegEx } from './constants.js'
import { TfvmFS } from './getDirectoriesObj.js'
import { logger } from './logger.js'
import getSettings from './getSettings.js'

let installedVersions

Expand All @@ -10,20 +11,30 @@ let installedVersions
* @returns {Promise<string[]>}
*/
async function getInstalledVersions () {
const settings = await getSettings()
// return the list of installed versions if that is already cached
if (!installedVersions) {
const tfList = []
let versionsList = []
let files
if (settings.useOpenTofu) {
files = await fs.readdir(TfvmFS.otfVersionsDir)
} else {
files = await fs.readdir(TfvmFS.tfVersionsDir)
}

const files = await fs.readdir(TfvmFS.tfVersionsDir)
if (files && files.length) {
files.forEach(file => {
if (versionRegEx.test(file)) {
tfList.push(file)
versionsList.push(file)
}
})
installedVersions = tfList
installedVersions = versionsList
} else {
logger.debug(`Unable to find installed versions of terraform with directoriesObj=${JSON.stringify(TfvmFS.getDirectoriesObj())} and files=${JSON.stringify(files)}`)
if (settings.useOpenTofu) {
logger.debug(`Unable to find installed versions of OpenTofu with directoriesObj=${JSON.stringify(TfvmFS.getDirectoriesObj())} and files=${JSON.stringify(files)}`)
} else {
logger.debug(`Unable to find installed versions of Terraform with directoriesObj=${JSON.stringify(TfvmFS.getDirectoriesObj())} and files=${JSON.stringify(files)}`)
}
return []
}
}
Expand Down
Loading