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

Add Warning for Deprecated Modules in Init #34943

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

Maed223
Copy link
Contributor

@Maed223 Maed223 commented Apr 3, 2024

Jira Ticket

Target Release

1.8.x

Draft CHANGELOG entry

ENHANCEMENTS

  • terraform init: Displays a warning when deprecated modules are in use.

Description

All module deprecation info is consolidated into and displayed as a single warning upon running terraform init. This includes both cases of where the module does, or does not, need installation. Furthermore, the warning can be raised for both modules that are directly used in configuration, as well as any external dependencies they depend on.

Screenshot

Note that this screenshot includes both the root module as well as it's external dependencies

Screenshot 2024-04-29 at 12 28 13 PM

internal/command/get.go Outdated Show resolved Hide resolved
internal/command/init.go Outdated Show resolved Hide resolved
@Maed223 Maed223 force-pushed the TF-12601/deprecated-module-warnings branch 2 times, most recently from c809c74 to c8d198a Compare May 7, 2024 17:00
Copy link
Contributor

@sebasslash sebasslash left a comment

Choose a reason for hiding this comment

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

Things are looking great so far! Some items down below

var deprecationList []*ModuleDeprecationDiagnosticExtraDeprecationItem
for _, modDeprecationInfo := range i.ModuleDeprecationInfos {
if modDeprecationInfo != nil && modDeprecationInfo.RegistryDeprecation != nil {
modDeprecation := fmt.Sprintf("\x1b[1mVersion %s of %s\x1b[0m", modDeprecationInfo.RegistryDeprecation.Version, modDeprecationInfo.SourceName)
Copy link
Contributor

Choose a reason for hiding this comment

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

Haven't tried this yet, but maybe we can leverage mitchellh/colorstring to format the colors rather than hardcoding the ANSI escape codes.

Suggested change
modDeprecation := fmt.Sprintf("\x1b[1mVersion %s of %s\x1b[0m", modDeprecationInfo.RegistryDeprecation.Version, modDeprecationInfo.SourceName)
msg := colorize.Color("[reset][bold]Version %s of %s[reset]")
modDeprecation := fmt.Sprintf(msg, modDeprecationInfo.RegistryDeprecation.Version, modDeprecationInfo.SourceName)

// as well as placed in the Diagnostic Extra for parsing for the SRO view in HCP Terraform
func (i *WorkspaceDeprecationInfo) BuildDeprecationWarning() *hcl.Diagnostic {
modDeprecationStrings := []string{}
var deprecationList []*ModuleDeprecationDiagnosticExtraDeprecationItem
Copy link
Contributor

Choose a reason for hiding this comment

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

(optional 💅) I'm wondering if it's beneficial to pre-allocate this slice to have a capacity of len(i.ModuleDeprecationInfos), since we know the underlying array size won't exceed that. So it becomes:

deprecationList := make([]*ModuleDeprecationDiagnosticExtraDeprecationItem, 0, len(i.ModuleDeprecationInfo))

This may prevent extra memory allocations on append() for scenarios where we anticipate many deprecation diagnostics. Read about append() growth rates. See if you can also apply this to modDeprecationStrings

Comment on lines +276 to +289
found := false
for _, modProviderVersions := range resp.Modules {
for _, modVersions := range modProviderVersions.Versions {
vm, _ := version.NewVersion(modVersions.Version)
if vm.Equal(record.Version) {
moduleVersion = modVersions
found = true
break
}
}
if found {
break
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
found := false
for _, modProviderVersions := range resp.Modules {
for _, modVersions := range modProviderVersions.Versions {
vm, _ := version.NewVersion(modVersions.Version)
if vm.Equal(record.Version) {
moduleVersion = modVersions
found = true
break
}
}
if found {
break
}
}
found:
for _, modProviderVersions := range resp.Modules {
for _, modVersions := range modProviderVersions.Versions {
vm, _ := version.NewVersion(modVersions.Version)
if vm.Equal(record.Version) {
moduleVersion = modVersions
break found
}
}
}

Use label breaks instead 👍

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package configs
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't forget to add tests for module_deprecations_test.go 👍

Copy link
Collaborator

@nfagerlund nfagerlund left a comment

Choose a reason for hiding this comment

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

Several change requests inline; feel free to ask for more detail on anything.

I also have a couple big-picture questions for ya.

  1. Have y'all started work on the TFC version of this warning? And where will TFC be receiving the details from? I ask because, terraform init doesn't have a json log format, and we suppress its output entirely unless it errors out with a non-zero exit code (Cf. core/components/terraform/step_terraform_init.go in tfc-agent), so TFC won't be able to learn about this from CLI. You likely already have plans to work around this, but I wanted to make sure I was raising the issue just in case
  2. I think the current design of passing the deprecations info around as a return value from the module walk is viable, but for posterity's sake and to make sure you've considered the options — can you say a bit more about why this belongs in a separate return, rather than in a field on the configs.Config struct? Config is already a nested structure with child Configs for each module, so it sort of mirrors the separate structure you're building here.
  3. We need tests for this behavior!! 😄

Also some follow-up smaller notes:

  • I'm not sure how we plan to support this in the stacks runtime — I think it might require changes in go-slug/sourcebundle. Any other tf-core stacks peeps want to chime in on that?

@@ -111,7 +115,7 @@ func buildTestModules(root *Config, walker ModuleWalker) hcl.Diagnostics {
CallRange: run.Module.DeclRange,
}

cfg, modDiags := loadModule(root, &req, walker)
cfg, modDiags, _ := loadModule(root, &req, walker)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh right 🤔 I guess we have no way to know if the root module is deprecated, because we didn't fetch it from its source.

@@ -253,6 +263,8 @@ func rebaseChildModule(cfg *Config, root *Config) {
cfg.Root = root
}

// refactor this into it's own file, doesn't make sense to place this here
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks like detritus from a previous iteration; please remove.

for _, modProviderVersions := range resp.Modules {
for _, modVersions := range modProviderVersions.Versions {
vm, _ := version.NewVersion(modVersions.Version)
if vm.Equal(record.Version) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Haha, this freaked me out for a minute, but turns out that .Equal explicitly expects to maybe be called on a nil receiver. Ok!

@@ -149,6 +150,7 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost
Summary: desc.Summary,
Detail: desc.Detail,
Address: desc.Address,
Extra: diag.ExtraInfo(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

🚨 This makes me nervous!

As you saw in the tests that this broke, the existing ExtraInfo() types do not expect that they will be exposed to the outside world, and might create nonsense if they do get exposed. ("extra": true? huh???)

I think you might want to

  • Create a new tfdiags.PublicExtraInfo interface type (with one do-nothing function that types must implement).
  • Grab the value of diag.ExtraInfo() and do a type-check on it, and conditionally add it to the output value only if it implements PublicExtraInfo.
  • Implement PublicExtraInfo for ModuleDeprecationDiagnosticExtra.

That way any existing extra types stay unexposed.

@@ -42,7 +42,6 @@ func (c *GetCommand) Run(args []string) int {
}

path = c.normalizePath(path)

Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks like detritus from a previous iteration; plz remove.

var deprecationList []*ModuleDeprecationDiagnosticExtraDeprecationItem
for _, modDeprecationInfo := range i.ModuleDeprecationInfos {
if modDeprecationInfo != nil && modDeprecationInfo.RegistryDeprecation != nil {
modDeprecation := fmt.Sprintf("\x1b[1mVersion %s of %s\x1b[0m", modDeprecationInfo.RegistryDeprecation.Version, modDeprecationInfo.SourceName)
Copy link
Collaborator

Choose a reason for hiding this comment

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

👀 Mmmmmm, this can't fly — unconditionally embedding ansi control codes into the text means this can never be a good citizen when -no-color is specified.

On the CLI, diagnostics end up going through View.Diagnostics. So, you can use colorize codes in the text that will get stripped out when it's in no-color mode. In this case, you'd want to replace the ansi codes with [bold]something[reset].

Comment on lines +92 to +94
for _, deprecationString := range modDeprecationStrings {
deprecationsMessage += deprecationString + "\n\n"
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you want strings.Join here.

@@ -38,6 +38,7 @@ type Diagnostic struct {
Address string `json:"address,omitempty"`
Range *DiagnosticRange `json:"range,omitempty"`
Snippet *DiagnosticSnippet `json:"snippet,omitempty"`
Extra interface{} `json:"extra,omitempty"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

A change to this externally-facing json format will require an update to its docs page (https://developer.hashicorp.com/terraform/cli/commands/validate#json), and possibly a bump to its minor version number.

(But, I asked around about how seriously we take the minor version guarantees in these json formats, and the answer was inconclusive. We do at least want it documented, though.)

deprecationsMessage += deprecationString + "\n\n"
}

return &hcl.Diagnostic{
Copy link
Collaborator

Choose a reason for hiding this comment

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

I've got a modest architectural suggestion for you here — instead of re-using the hcl.Diagnostic type here, what do you think about making your own DeprecationsDiagnostic struct, shoving the flattened deprecationList (and a final boolean hasDeprecations result) into it, and implementing tfdiags.Diagnostic on it?

I think it could yield some cleaner-feeling code, but it's not something I'd necessarily insist on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants