From 905e66eda271cdb459b442b1f391c048c8d57fe2 Mon Sep 17 00:00:00 2001 From: Tom Straub Date: Sun, 21 Aug 2022 13:08:22 -0500 Subject: [PATCH] TFx 0.1.0 release prep (#26) * tfx workspace cv list, "workspaceName" to "workspace-name" * tfx release list (tfe and replicated), added `--all` flag * tfx registry list (module and provider), added `--max-items` and `--all` flags * fixed bug with registry module version create when the module did not exist --- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 19 +++++++++++----- README.md | 14 +++++++++++- cmd/apply.go | 6 ++--- cmd/helper_rest.go | 1 + cmd/plan.go | 18 +++++++-------- cmd/registry_module.go | 32 ++++++++++++++++++++------- cmd/registry_module_version.go | 17 +++++++------- cmd/registry_provider.go | 32 ++++++++++++++++++++------- cmd/release_replicated.go | 11 +++++++-- cmd/release_tfe.go | 11 +++++++-- cmd/workspace_configurationversion.go | 19 ++++++++-------- cmd/workspace_run.go | 2 +- cmd/workspace_stateversion.go | 2 +- site/docs/about/contributing.md | 2 +- site/docs/about/style_guide.md | 2 +- site/mkdocs.yml | 10 ++------- version/version.go | 2 +- 19 files changed, 134 insertions(+), 70 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 581a785..a4139f4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: main +name: Build `tfx` - Cross Compile on: push: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e736468..99b90d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: release +name: Release `tfx` on: release: diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e468b..5663432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.0.5] - 2022.08.20 +## [Unreleased] + +**Added** + +**Changed** + +**Removed** + +## [0.1.0] - 2022.08.21 + +First official release! **Added** @@ -14,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **Changed** * Moved and updated docs to the `site/` folder and published to a custom domain [tfx.rocks](tfx.rocks). -* Created a styleguide and updated Commands, more information can be found at [https://tfx.rocks/about/style_guide/]() +* Created a style guide and updated Commands, more information can be found at [https://tfx.rocks/about/style_guide/]() * Some of these changes are **BREAKING** changes * Mainly moving away from Command Flags that used camel case (example: "workspaceName" to "workspace-name") * Updated all of the `tfx registry module` commands to support JSON (`--json`) output. @@ -191,6 +201,5 @@ New Commands: **Removed** -[Unreleased]: https://github.com/straubt1/tfx/compare/v1.0.0...HEAD -[0.0.1]: https://github.com/ostraubt1/tfx/compare/v0.0.0...v0.0.1 -[0.0.0]: https://github.com/straubt1/tfx/releases/tag/v0.0.1 +[Unreleased]: https://github.com/straubt1/tfx/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/straubt1/tfx/releases/tag/v0.1.0 diff --git a/README.md b/README.md index aeaaae5..c649cce 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Terraform Cloud/Enterprise CLI +# Terraform Cloud/Terraform Enterprise CLI [![main](https://github.com/straubt1/tfx/actions/workflows/main.yml/badge.svg)](https://github.com/straubt1/tfx/actions/workflows/main.yml) ![Go Version](https://img.shields.io/badge/go%20version-%3E=1.18-61CFDD.svg?style=flat-square) @@ -15,3 +15,15 @@ The initial focus of _tfx_ was to execute the API-Driven workflow for a Workspac Looking for more information? Check out our docs site [tfx.rocks](https://tfx.rocks) + +## General Roadmap + +Future implementation items: + +- [ ] Create testing setup +- [ ] Refactor `plan` and `apply` workflow +- [ ] Support Variable Sets +- [ ] Support Sentinel Publishing +- [ ] Support Cloud Agents + +Priority may changes, please submit an issue to voice changes you would like to see. diff --git a/cmd/apply.go b/cmd/apply.go index a4f4398..c8f9fed 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -43,9 +43,9 @@ var ( func init() { // All `tfx apply` commands - applyCmd.PersistentFlags().StringP("runId", "i", "", "Run Id to apply") + applyCmd.PersistentFlags().StringP("run-id", "i", "", "Run Id to apply") - applyCmd.MarkPersistentFlagRequired("runId") + applyCmd.MarkPersistentFlagRequired("run-id") rootCmd.AddCommand(applyCmd) } @@ -57,7 +57,7 @@ func runApply() error { hostname := *viperString("tfeHostname") orgName := *viperString("tfeOrganization") wsName := *viperString("workspaceName") - runID := *viperString("runId") + runID := *viperString("run-id") client, ctx := getClientContext() diff --git a/cmd/helper_rest.go b/cmd/helper_rest.go index 7c1be52..6516483 100644 --- a/cmd/helper_rest.go +++ b/cmd/helper_rest.go @@ -148,6 +148,7 @@ type GPGList struct { } func ListGPGKeys(c TfxClientContext) (*GPGList, error) { + // TODO: No pagination, waiting on go-tfe to provide a List function since it is unlikely an org would have more than 100 keys // create url "https://${HOST}/api/registry/private/v2/gpg-keys?filter%5Bnamespace%5D=${provider_namespace}" url := fmt.Sprintf( "https://%s/api/registry/private/v2/gpg-keys?filter[namespace]=%s", diff --git a/cmd/plan.go b/cmd/plan.go index 32982aa..4dd2239 100644 --- a/cmd/plan.go +++ b/cmd/plan.go @@ -57,18 +57,18 @@ var ( func init() { // All `tfx plan` commands - planCmd.Flags().StringP("workspaceName", "w", "", "Workspace name") + planCmd.Flags().StringP("workspace-name", "w", "", "Workspace name") planCmd.Flags().StringP("directory", "d", "./", "Directory of Terraform (optional, defaults to current directory)") - planCmd.Flags().StringP("configurationId", "i", "", "Configuration Version Id (optional, i.e. cv-*)") + planCmd.Flags().StringP("configuration-id", "i", "", "Configuration Version Id (optional, i.e. cv-*)") planCmd.Flags().StringP("message", "m", "", "Run Message (optional)") planCmd.Flags().Bool("speculative", false, "Perform a Speculative Plan (optional)") planCmd.Flags().Bool("destroy", false, "Perform a Destroy Plan (optional)") - planCmd.Flags().StringSlice("env", []string{}, "Environment variables to write to the Workspace. Can be suplied multiple times. (optional, i.e. '--env='AWS_REGION=us-east1')") - planCmd.MarkFlagRequired("workspaceName") + planCmd.Flags().StringSlice("env", []string{}, "Environment variables to write to the Workspace. Can be supplied multiple times. (optional, i.e. '--env='AWS_REGION=us-east1')") + planCmd.MarkFlagRequired("workspace-name") - planExportCmd.Flags().StringP("planId", "i", "", "Plan Id (i.e. plan-*)") + planExportCmd.Flags().StringP("plan-id", "i", "", "Plan Id (i.e. plan-*)") planExportCmd.Flags().StringP("directory", "d", "", "Directory to download export to (optional, defaults to a temp directory)") - planExportCmd.MarkFlagRequired("planId") + planExportCmd.MarkFlagRequired("plan-id") planExportCmd.MarkFlagRequired("directory") rootCmd.AddCommand(planCmd) @@ -79,9 +79,9 @@ func runPlan() error { // Validate flags hostname := *viperString("tfeHostname") orgName := *viperString("tfeOrganization") - wsName := *viperString("workspaceName") + wsName := *viperString("workspace-name") dir := *viperString("directory") - configID := *viperString("configurationId") + configID := *viperString("configuration-id") message := *viperString("message") isSpeculative := *viperBool("speculative") isDestroy := *viperBool("destroy") @@ -156,7 +156,7 @@ func runPlan() error { } func runPlanExport() error { - planID := *viperString("planId") + planID := *viperString("plan-id") directory := *viperString("directory") client, ctx := getClientContext() diff --git a/cmd/registry_module.go b/cmd/registry_module.go index e9e42aa..f6facfd 100644 --- a/cmd/registry_module.go +++ b/cmd/registry_module.go @@ -21,6 +21,8 @@ package cmd import ( + "math" + tfe "github.com/hashicorp/go-tfe" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -40,8 +42,14 @@ var ( Short: "List modules", Long: "List modules in the Private Module Registry of a TFx Organization.", RunE: func(cmd *cobra.Command, args []string) error { + m := *viperInt("max-items") + if *viperBool("all") { + m = math.MaxInt + } + return registryModuleList( - getTfxClientContext()) + getTfxClientContext(), + m) }, } @@ -87,6 +95,8 @@ var ( func init() { // `tfx registry module list` arguments + registryModuleListCmd.Flags().IntP("max-items", "m", 10, "Max number of results (optional)") + registryModuleListCmd.Flags().BoolP("all", "a", false, "Retrieve all results regardless of maxItems flag (optional)") // `tfx registry module create` arguments registryModuleCreateCmd.Flags().StringP("name", "n", "", "Name of the Module (no spaces)") @@ -113,13 +123,15 @@ func init() { registryModuleCmd.AddCommand(registryModuleDeleteCmd) } -func registryModuleListAll(c TfxClientContext, orgName string) ([]*tfe.RegistryModule, error) { +func registryModuleListAll(c TfxClientContext, orgName string, maxItems int) ([]*tfe.RegistryModule, error) { + pageSize := 100 + if maxItems < 100 { + pageSize = maxItems // Only get what we need in one page + } + allItems := []*tfe.RegistryModule{} opts := tfe.RegistryModuleListOptions{ - ListOptions: tfe.ListOptions{ - PageNumber: 1, - PageSize: 100, - }, + ListOptions: tfe.ListOptions{PageNumber: 1, PageSize: pageSize}, } for { @@ -129,6 +141,10 @@ func registryModuleListAll(c TfxClientContext, orgName string) ([]*tfe.RegistryM } allItems = append(allItems, items.Items...) + if len(allItems) >= maxItems { + break // Hit the max, break. For maxItems > 100 it is possible to return more than max in this approach + } + if items.CurrentPage >= items.TotalPages { break } @@ -138,9 +154,9 @@ func registryModuleListAll(c TfxClientContext, orgName string) ([]*tfe.RegistryM return allItems, nil } -func registryModuleList(c TfxClientContext) error { +func registryModuleList(c TfxClientContext, maxItems int) error { o.AddMessageUserProvided("List Modules for Organization:", c.OrganizationName) - items, err := registryModuleListAll(c, c.OrganizationName) + items, err := registryModuleListAll(c, c.OrganizationName, maxItems) if err != nil { return errors.Wrap(err, "failed to list modules") } diff --git a/cmd/registry_module_version.go b/cmd/registry_module_version.go index 0efe478..051302b 100644 --- a/cmd/registry_module_version.go +++ b/cmd/registry_module_version.go @@ -116,6 +116,12 @@ var ( ) func init() { + // `tfx registry module version list` arguments + registryModuleVersionListCmd.Flags().StringP("name", "n", "", "Name of the Module (no spaces)") + registryModuleVersionListCmd.Flags().StringP("provider", "p", "", "Name of the provider (no spaces) (i.e. aws, azure, google)") + registryModuleVersionListCmd.MarkFlagRequired("name") + registryModuleVersionListCmd.MarkFlagRequired("provider") + // `tfx registry module version create` arguments registryModuleVersionCreateCmd.Flags().StringP("name", "n", "", "Name of the Module (no spaces)") registryModuleVersionCreateCmd.Flags().StringP("provider", "p", "", "Name of the provider (no spaces) (i.e. aws, azure, google)") @@ -125,12 +131,6 @@ func init() { registryModuleVersionCreateCmd.MarkFlagRequired("provider") registryModuleVersionCreateCmd.MarkFlagRequired("version") - // `tfx registry module version list` arguments - registryModuleVersionListCmd.Flags().StringP("name", "n", "", "Name of the Module (no spaces)") - registryModuleVersionListCmd.Flags().StringP("provider", "p", "", "Name of the provider (no spaces) (i.e. aws, azure, google)") - registryModuleVersionListCmd.MarkFlagRequired("name") - registryModuleVersionListCmd.MarkFlagRequired("provider") - // `tfx registry module version delete` arguments registryModuleVersionDeleteCmd.Flags().StringP("name", "n", "", "Name of the Module (no spaces)") registryModuleVersionDeleteCmd.Flags().StringP("provider", "p", "", "Name of the provider (no spaces) (i.e. aws, azure, google)") @@ -189,9 +189,10 @@ func registryModuleVersionCreate(c TfxClientContext, moduleName string, provider Version: &moduleVersion, }) if err != nil { - errors.Wrap(err, "failed to create module version") + return errors.Wrap(err, "failed to create module version") } - o.AddMessageUserProvided("Module Created, Uploading...", "") + + o.AddMessageUserProvided("Module Version Created, Uploading...", "") err = c.Client.RegistryModules.Upload(c.Context, *module, directory) if err != nil { errors.Wrap(err, "failed to upload module version") diff --git a/cmd/registry_provider.go b/cmd/registry_provider.go index 11e76ba..b3ba38f 100644 --- a/cmd/registry_provider.go +++ b/cmd/registry_provider.go @@ -20,6 +20,8 @@ package cmd import ( + "math" + tfe "github.com/hashicorp/go-tfe" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -39,8 +41,14 @@ var ( Short: "List Providers in a Private Registry", Long: "List Providers in a Private Registry of a TFx Organization.", RunE: func(cmd *cobra.Command, args []string) error { + m := *viperInt("max-items") + if *viperBool("all") { + m = math.MaxInt + } + return registryProviderList( - getTfxClientContext()) + getTfxClientContext(), + m) }, } @@ -83,6 +91,8 @@ var ( func init() { // `tfx registry provider list` arguments + registryProviderListCmd.Flags().IntP("max-items", "m", 10, "Max number of results (optional)") + registryProviderListCmd.Flags().BoolP("all", "a", false, "Retrieve all results regardless of maxItems flag (optional)") // `tfx registry provider create` arguments registryProviderCreateCmd.Flags().StringP("name", "n", "", "Name of the Provider") @@ -103,14 +113,16 @@ func init() { registryProviderCmd.AddCommand(registryProviderDeleteCmd) } -func registryProviderListAll(c TfxClientContext) ([]*tfe.RegistryProvider, error) { +func registryProviderListAll(c TfxClientContext, maxItems int) ([]*tfe.RegistryProvider, error) { + pageSize := 100 + if maxItems < 100 { + pageSize = maxItems // Only get what we need in one page + } + allItems := []*tfe.RegistryProvider{} opts := tfe.RegistryProviderListOptions{ + ListOptions: tfe.ListOptions{PageNumber: 1, PageSize: pageSize}, // RegistryName: tfe.PrivateRegistry, // Can restrict to just private - ListOptions: tfe.ListOptions{ - PageNumber: 1, - PageSize: 100, - }, // Include: &[]tfe.RegistryProviderIncludeOps{"provider-versions"}, does not work, cant get provider versions from this call? } for { @@ -120,6 +132,10 @@ func registryProviderListAll(c TfxClientContext) ([]*tfe.RegistryProvider, error } allItems = append(allItems, items.Items...) + if len(allItems) >= maxItems { + break // Hit the max, break. For maxItems > 100 it is possible to return more than max in this approach + } + if items.CurrentPage >= items.TotalPages { break } @@ -129,9 +145,9 @@ func registryProviderListAll(c TfxClientContext) ([]*tfe.RegistryProvider, error return allItems, nil } -func registryProviderList(c TfxClientContext) error { +func registryProviderList(c TfxClientContext, maxItems int) error { o.AddMessageUserProvided("List Providers in Registry for Organization:", c.OrganizationName) - items, err := registryProviderListAll(c) + items, err := registryProviderListAll(c, maxItems) if err != nil { return errors.Wrap(err, "failed to list providers") } diff --git a/cmd/release_replicated.go b/cmd/release_replicated.go index 1bd5ea2..3dfa58f 100644 --- a/cmd/release_replicated.go +++ b/cmd/release_replicated.go @@ -21,6 +21,7 @@ package cmd import ( "fmt" + "math" "strings" "github.com/mmcdole/gofeed" @@ -42,8 +43,13 @@ var ( Short: "List Replicated binaries", Long: "List available Replicated releases.", RunE: func(cmd *cobra.Command, args []string) error { + m := *viperInt("max-items") + if *viperBool("all") { + m = math.MaxInt + } + return releaseReplicatedList( - *viperInt("max-items")) + m) }, } @@ -70,7 +76,8 @@ var ( func init() { // `tfx release replicated list` - releaseReplicatedListCmd.Flags().IntP("max-items", "r", 10, "The number of results to print") + releaseReplicatedListCmd.Flags().IntP("max-items", "m", 10, "The number of results to print") + releaseReplicatedListCmd.Flags().BoolP("all", "a", false, "Retrieve all results regardless of maxItems flag (optional)") // `tfx release replicated download` releaseReplicatedDownloadCmd.Flags().StringP("directory", "d", "./", "Directory to save binary") diff --git a/cmd/release_tfe.go b/cmd/release_tfe.go index 75b8e73..aa92161 100644 --- a/cmd/release_tfe.go +++ b/cmd/release_tfe.go @@ -21,6 +21,7 @@ package cmd import ( "fmt" + "math" "strconv" "strings" @@ -43,10 +44,15 @@ var ( Short: "List TFE Releases", Long: "List available Terraform Enterprise releases.", RunE: func(cmd *cobra.Command, args []string) error { + m := *viperInt("max-items") + if *viperBool("all") { + m = math.MaxInt + } + return releaseTfeList( *viperString("license-id"), *viperString("password"), - *viperInt("max-items")) + m) }, } @@ -86,7 +92,8 @@ func init() { // `tfx release tfe list` releaseTfeListCmd.Flags().StringP("license-id", "l", "", "License Id for TFE/Replicated") releaseTfeListCmd.Flags().StringP("password", "p", "", "Password to authenticate") - releaseTfeListCmd.Flags().IntP("max-items", "r", 10, "The number of results to print") + releaseTfeListCmd.Flags().IntP("max-items", "m", 10, "The number of results to print") + releaseTfeListCmd.Flags().BoolP("all", "a", false, "Retrieve all results regardless of maxItems flag (optional)") releaseTfeListCmd.MarkFlagRequired("license-id") releaseTfeListCmd.MarkFlagRequired("password") diff --git a/cmd/workspace_configurationversion.go b/cmd/workspace_configurationversion.go index da09dcd..3df6a96 100644 --- a/cmd/workspace_configurationversion.go +++ b/cmd/workspace_configurationversion.go @@ -45,13 +45,14 @@ var ( Short: "List Configuration Versions", Long: "List Configuration Versions of a TFx Workspace.", RunE: func(cmd *cobra.Command, args []string) error { - m := *viperInt("maxItems") + m := *viperInt("max-items") if *viperBool("all") { m = math.MaxInt } + return cvList( getTfxClientContext(), - *viperString("workspaceName"), + *viperString("workspace-name"), m) }, } @@ -68,7 +69,7 @@ var ( return cvCreate( getTfxClientContext(), - *viperString("workspaceName"), + *viperString("workspace-name"), *viperString("directory"), *viperBool("speculative")) }, @@ -109,16 +110,16 @@ func init() { // `tfx workspace configuration-version` commands // `tfx workspace configuration-version list` command - cvListCmd.Flags().StringP("workspaceName", "w", "", "Workspace name") - cvListCmd.Flags().IntP("maxItems", "", 10, "Max number of results (optional)") + cvListCmd.Flags().StringP("workspace-name", "w", "", "Workspace name") + cvListCmd.Flags().IntP("max-items", "m", 10, "Max number of results (optional)") cvListCmd.Flags().BoolP("all", "a", false, "Retrieve all results regardless of maxItems flag (optional)") - cvListCmd.MarkFlagRequired("workspaceName") + cvListCmd.MarkFlagRequired("workspace-name") // `tfx cv create` - cvCreateCmd.Flags().StringP("workspaceName", "w", "", "Workspace name") + cvCreateCmd.Flags().StringP("workspace-name", "w", "", "Workspace name") cvCreateCmd.Flags().StringP("directory", "d", "./", "Directory of Terraform (optional, defaults to current directory)") cvCreateCmd.Flags().BoolP("speculative", "s", false, "Perform a Speculative Plan (optional, defaults to false)") - cvCreateCmd.MarkFlagRequired("workspaceName") + cvCreateCmd.MarkFlagRequired("workspace-name") // `tfx cv show` cvShowCmd.Flags().StringP("id", "i", "", "Configuration Version Id (i.e. cv-*)") @@ -138,10 +139,10 @@ func init() { func cvListAll(c TfxClientContext, workspaceId string, maxItems int) ([]*tfe.ConfigurationVersion, error) { pageSize := 100 - if maxItems < 100 { pageSize = maxItems // Only get what we need in one page } + allItems := []*tfe.ConfigurationVersion{} opts := tfe.ConfigurationVersionListOptions{ ListOptions: tfe.ListOptions{PageNumber: 1, PageSize: pageSize}, diff --git a/cmd/workspace_run.go b/cmd/workspace_run.go index ca32427..ad17e11 100644 --- a/cmd/workspace_run.go +++ b/cmd/workspace_run.go @@ -81,7 +81,7 @@ func init() { // `tfx workspace run list` command runListCmd.Flags().StringP("workspace-name", "w", "", "Workspace name") - runListCmd.Flags().IntP("max-items", "", 10, "Max number of results (optional)") + runListCmd.Flags().IntP("max-items", "m", 10, "Max number of results (optional)") runListCmd.MarkFlagRequired("workspace-name") // `tfx workspace run create` command diff --git a/cmd/workspace_stateversion.go b/cmd/workspace_stateversion.go index abdf8f2..6ca6589 100644 --- a/cmd/workspace_stateversion.go +++ b/cmd/workspace_stateversion.go @@ -113,7 +113,7 @@ var ( func init() { // `tfx workspace state list` command stateListCmd.Flags().StringP("workspace-name", "w", "", "Workspace name") - stateListCmd.Flags().IntP("max-items", "", 10, "Max number of results (optional)") + stateListCmd.Flags().IntP("max-items", "m", 10, "Max number of results (optional)") stateListCmd.Flags().BoolP("all", "a", false, "Retrieve all results regardless of maxItems flag (optional)") stateListCmd.MarkFlagRequired("workspace-name") diff --git a/site/docs/about/contributing.md b/site/docs/about/contributing.md index 0ad3fd9..ee3b68a 100644 --- a/site/docs/about/contributing.md +++ b/site/docs/about/contributing.md @@ -9,7 +9,7 @@ Please note we have a code of conduct, please follow it in all your interactions 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. -2. Update the README.md with details of changes to the interface, this includes new environment +2. Update the README.md and CHANGELOG.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. 3. Request review from a repo maintainer and work to resolve any feedback/suggestions. diff --git a/site/docs/about/style_guide.md b/site/docs/about/style_guide.md index 9a5e685..95ec4f0 100644 --- a/site/docs/about/style_guide.md +++ b/site/docs/about/style_guide.md @@ -7,7 +7,7 @@ The goal of this document is to outline the general user experience when dealing ## Colors !!! Note "" - When using the `--json` flag on a command these colors will not apply. + When using the `--json` flag on a command these colors will not apply. - User Provided: Green - These values are provided by the user, either through a configuration file or a command flag. diff --git a/site/mkdocs.yml b/site/mkdocs.yml index 3d6221b..7390aee 100644 --- a/site/mkdocs.yml +++ b/site/mkdocs.yml @@ -9,7 +9,8 @@ copyright: "TFx is licensed under the