Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package opentofu for openSUSE:Factory checked in at 2026-05-11 17:02:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/opentofu (Old) and /work/SRC/openSUSE:Factory/.opentofu.new.1966 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "opentofu" Mon May 11 17:02:48 2026 rev:46 rq:1352470 version:1.11.7 Changes: -------- --- /work/SRC/openSUSE:Factory/opentofu/opentofu.changes 2026-04-18 21:37:25.964184710 +0200 +++ /work/SRC/openSUSE:Factory/.opentofu.new.1966/opentofu.changes 2026-05-11 17:10:01.540103551 +0200 @@ -1,0 +2,12 @@ +Mon May 11 12:21:08 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 1.11.7: + * BUG FIXES: + - When installing provider packages into a local cache + directory, the installer will now return an error if a + conflicting entry is already present in the cache that + doesn't match the expected checksum. Previously OpenTofu + would just silently write over the existing entry in that + case. (#4082) + +------------------------------------------------------------------- Old: ---- opentofu-1.11.6.obscpio New: ---- opentofu-1.11.7.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ opentofu.spec ++++++ --- /var/tmp/diff_new_pack.EmbkPv/_old 2026-05-11 17:10:04.104209099 +0200 +++ /var/tmp/diff_new_pack.EmbkPv/_new 2026-05-11 17:10:04.108209263 +0200 @@ -19,7 +19,7 @@ %define executable_name tofu Name: opentofu -Version: 1.11.6 +Version: 1.11.7 Release: 0 Summary: Declaratively manage your cloud infrastructure License: MPL-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.EmbkPv/_old 2026-05-11 17:10:04.140210581 +0200 +++ /var/tmp/diff_new_pack.EmbkPv/_new 2026-05-11 17:10:04.144210745 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/opentofu/opentofu/</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v1.11.6</param> + <param name="revision">v1.11.7</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.EmbkPv/_old 2026-05-11 17:10:04.172211898 +0200 +++ /var/tmp/diff_new_pack.EmbkPv/_new 2026-05-11 17:10:04.176212063 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/opentofu/opentofu/</param> - <param name="changesrevision">698e5e5204756963ffb28107fb61d77970e47f66</param></service></servicedata> + <param name="changesrevision">398c818dcf617837724e3137e4682062574d1019</param></service></servicedata> (No newline at EOF) ++++++ opentofu-1.11.6.obscpio -> opentofu-1.11.7.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.6/CHANGELOG.md new/opentofu-1.11.7/CHANGELOG.md --- old/opentofu-1.11.6/CHANGELOG.md 2026-04-08 12:54:56.000000000 +0200 +++ new/opentofu-1.11.7/CHANGELOG.md 2026-05-11 12:46:32.000000000 +0200 @@ -1,6 +1,11 @@ The v1.11.x release series is supported until **August 1 2026**. -## 1.11.7 (Unreleased) +## 1.11.8 (Unreleased) +## 1.11.7 + +BUG FIXES: + +* When installing provider packages into a local cache directory, the installer will now return an error if a conflicting entry is already present in the cache that doesn't match the expected checksum. Previously OpenTofu would just silently write over the existing entry in that case. ([#4082](https://github.com/opentofu/opentofu/pull/4082)) ## 1.11.6 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.6/internal/command/e2etest/providers_tamper_test.go new/opentofu-1.11.7/internal/command/e2etest/providers_tamper_test.go --- old/opentofu-1.11.6/internal/command/e2etest/providers_tamper_test.go 2026-04-08 12:54:56.000000000 +0200 +++ new/opentofu-1.11.7/internal/command/e2etest/providers_tamper_test.go 2026-05-11 12:46:32.000000000 +0200 @@ -9,6 +9,7 @@ "fmt" "os" "path/filepath" + "runtime" "slices" "strings" "testing" @@ -21,6 +22,11 @@ "github.com/opentofu/opentofu/internal/getproviders" ) +// providerTamperingFixtureNullProviderVersion must contain a string +// representation of the same version number used for "hashicorp/null" in the +// "provider-tampering-base" fixture. +const providerTamperingFixtureNullProviderVersion = "3.1.0" + // TestProviderTampering tests various ways that the provider plugins in the // local cache directory might be modified after an initial "tofu init", // which other OpenTofu commands which use those plugins should catch and @@ -49,9 +55,16 @@ } seedDir := tf.WorkDir() - const providerVersion = "3.1.0" // must match the version in the fixture config - pluginDir := filepath.Join(".terraform", "providers", "registry.opentofu.org", "hashicorp", "null", providerVersion, getproviders.CurrentPlatform.String()) - pluginExe := filepath.Join(pluginDir, "terraform-provider-null_v"+providerVersion+"_x5") + pluginDir := filepath.Join( + ".terraform", + "providers", + "registry.opentofu.org", + "hashicorp", + "null", + providerTamperingFixtureNullProviderVersion, + getproviders.CurrentPlatform.String(), + ) + pluginExe := filepath.Join(pluginDir, "terraform-provider-null_v"+providerTamperingFixtureNullProviderVersion+"_x5") if getproviders.CurrentPlatform.OS == "windows" { pluginExe += ".exe" // ugh } @@ -271,6 +284,201 @@ }) } +// TestProviderCacheJunkDirectory verifies our behavior for if there's already +// a directory present where we'd want to create a provider cache entry, for +// whatever reason: in that case, we want to fail with a clear error so that +// the operator can decide how to fix the problem, rather than either totally +// clobbering whatever was already there or merging the new package contents +// into an preexisting directory. +// +// One way we could get into this case is if someone has intentionally modified +// something in the provider cache directory to quickly test something and then +// ran "tofu init" again. We prefer to fail in that case to avoid silently +// clobbering whatever modification was made; operator should either delete +// their modified directory or restore it to match the official package before +// continuing. +func TestProviderCacheJunkDirectory(t *testing.T) { + t.Parallel() + + // This test reaches out to registry.opentofu.org to download the + // null provider, so it can only run if network access is allowed. + skipIfCannotAccessNetwork(t) + + // We have a special case to allow installing into a precreated empty + // directory, since someone reading our documentation about directory + // layout might possibly try to create the empty directory manually first + // and there's no real harm in accepting that case because an empty + // directory is easy enough to recreate if that's what the operator really + // wanted, for some reason. (An empty provider package is never actually + // valid though, so that would be strange.) + for _, emptyDir := range []bool{true, false} { + t.Run(fmt.Sprintf("emptyDir=%#v", emptyDir), func(t *testing.T) { + t.Parallel() + + // We reuse the "tampering" test fixture here even though this is a slightly + // different situation where the potential problem exists before init, + // rather than being introduced after init already ran. + fixturePath := filepath.Join("testdata", "provider-tampering-base") + tofu := e2e.NewBinary(t, tofuBin, fixturePath) + + // This test starts with a strange mismatching directory at the location + // where the null provider package needs to be extracted, meaning that we + // won't be able to install the provider without clobbering it. + unexpectedDir := tofu.Path( + ".terraform", + "providers", + addrs.DefaultProviderRegistryHost.String(), + "hashicorp", + "null", + providerTamperingFixtureNullProviderVersion, + getproviders.CurrentPlatform.String(), + ) + err := os.MkdirAll(unexpectedDir, os.ModePerm) + if err != nil { + t.Fatalf("failed to make 'unexpected' directory: %s", err) + } + if !emptyDir { + // We'll make a file in the directory just to make it clear that this + // directory can't possibly match the expected content of the provider + // package, and so the provider installer definitely can't just try to + // use this directory as-is without clobbering it. + err = os.WriteFile( + filepath.Join(unexpectedDir, "extra.txt"), + []byte("this file is not part of the hashicorp/null package"), + os.ModePerm, + ) + if err != nil { + t.Fatalf("failed to make file in the 'unexpected' directory: %s", err) + } + } + + // Now we run "tofu init" with the expectation that it should try to + // install "hashicorp/null" into the same location where we created + // the directory above. There's no dependency lock file to tell us + // that the existing directory might be enough to skip installation + // completely, so this should always attempt installation. + stdout, stderr, err := tofu.Run("init") + if emptyDir { + if err != nil { + t.Fatalf("unexpected failure: %s\n(installing into a preexisting empty directory should be allowed)\n%s\n%s", err, stderr, stdout) + } + } else { + if err == nil { + t.Fatalf("unexpected success; want error about conflicting cache entry\n%s\n%s", stderr, stdout) + } + if want := "does not match the content of the downloaded package"; !strings.Contains(stderr, want) { + t.Fatalf("stderr missing expected substring %q\n%s", want, stderr) + } + } + }) + } +} + +// TestProviderCacheJunkSymlink verifies our behavior for if there's already +// a symlink present where we'd want to create a provider cache entry, for +// whatever reason: in that case, we want to fail with a clear error so that +// the operator can decide how to fix the problem, rather than either totally +// clobbering their symlink or merging the package contents into whereever +// the symlink points. +// +// One way we could get into this case is if someone has intentionally created +// a symlink in their cache directory for provider development or testing +// purposes, and then later ran "tofu init" again. We prefer to fail in that +// case to avoid silently clobbering whatever modification was made; operator +// should either delete their symlink or make sure it refers to a directory +// that matches the expected package contents before continuing. +func TestProviderCacheJunkSymlink(t *testing.T) { + t.Parallel() + + // This test reaches out to registry.opentofu.org to download the + // null provider, so it can only run if network access is allowed. + skipIfCannotAccessNetwork(t) + + // There is a special case to allow installing into a precreated empty + // directory which we test in [TestProviderCacheJunkDirectory] above, + // but that exception does not apply when what we find is a symlink _to_ + // an empty directory: a symlink is only acceptable if it refers to a + // directory that was already correctly populated, because otherwise we + // might be writing into an existing empty directory somewhere else in + // the filesystem that is shared by other processes that expect it to stay + // empty. + for _, emptyDir := range []bool{true, false} { + t.Run(fmt.Sprintf("emptyDir=%#v", emptyDir), func(t *testing.T) { + t.Parallel() + + // We reuse the "tampering" test fixture here even though this is a slightly + // different situation where the potential problem exists before init, + // rather than being introduced after init already ran. + fixturePath := filepath.Join("testdata", "provider-tampering-base") + tofu := e2e.NewBinary(t, tofuBin, fixturePath) + + // This test starts with an unexpected symlink at the location where the + // null provider package needs to be extracted, meaning that we won't + // be able to install the provider without clobbering it. + targetDir := tofu.Path("symlink-target") + err := os.Mkdir(targetDir, os.ModePerm) + if err != nil { + t.Fatalf("failed to make symlink target directory: %s", err) + } + if !emptyDir { + // We'll make a file in the directory just to make it clear that this + // directory can't possibly match the expected content of the provider + // package, and so the provider installer definitely can't just try to + // use this directory as-is without clobbering the symlink. + err = os.WriteFile( + filepath.Join(targetDir, "extra.txt"), + []byte("this file is not part of the hashicorp/null package"), + os.ModePerm, + ) + if err != nil { + t.Fatalf("failed to make file in the symlink target directory: %s", err) + } + } + + unexpectedSymlink := tofu.Path( + ".terraform", + "providers", + addrs.DefaultProviderRegistryHost.String(), + "hashicorp", + "null", + providerTamperingFixtureNullProviderVersion, + getproviders.CurrentPlatform.String(), + ) + symlinkParent := filepath.Dir(unexpectedSymlink) + err = os.MkdirAll(symlinkParent, os.ModePerm) + if err != nil { + t.Fatalf("failed to create parent directory of symlink: %s", err) + } + err = os.Symlink(targetDir, unexpectedSymlink) + if err != nil { + if runtime.GOOS == "windows" { + // By default Windows does not allow creation of symlinks, so + // we'll skip this test to avoid creating false-negative noise + // for anyone developing OpenTofu on Windows without their + // administrator having allowed symlink creation. + t.Skipf("can't create symlink on this Windows system: %s", err) + } + t.Fatalf("failed to make 'unexpected' symlink: %s", err) + } + + // Now we run "tofu init" with the expectation that it should try to + // install "hashicorp/null" into the same location where we created + // the directory above. There's no dependency lock file to tell us + // that the existing directory might be enough to skip installation + // completely, so this should always attempt installation. + stdout, stderr, err := tofu.Run("init") + // Note that unlike [TestProviderCacheJunkDirectory] we expect this + // one to fail regardless of whether emptyDir is set. + if err == nil { + t.Fatalf("unexpected success; want error about conflicting cache entry\n%s\n%s", stderr, stdout) + } + if want := "does not match the content of the downloaded package"; !strings.Contains(stderr, want) { + t.Errorf("stderr missing expected substring %q\n%s", want, stderr) + } + }) + } +} + // TestProviderLocksFromPredecessorProject is an end-to-end test of our // special treatment of lock files that were originally created by the // project that OpenTofu was forked from, and so refer to providers from diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.6/internal/getproviders/hash.go new/opentofu-1.11.7/internal/getproviders/hash.go --- old/opentofu-1.11.6/internal/getproviders/hash.go 2026-04-08 12:54:56.000000000 +0200 +++ new/opentofu-1.11.7/internal/getproviders/hash.go 2026-05-11 12:46:32.000000000 +0200 @@ -366,6 +366,10 @@ return HashSchemeZip.New(fmt.Sprintf("%x", sum[:])) } +// emptyPackageHashV1 is the representation of a completely empty package using +// the V1 hashing scheme. +const emptyPackageHashV1 = Hash("h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") + // PackageHashV1 computes a hash of the contents of the package at the given // location using hash algorithm 1. The resulting Hash is guaranteed to have // the scheme HashScheme1. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.6/internal/getproviders/hash_test.go new/opentofu-1.11.7/internal/getproviders/hash_test.go --- old/opentofu-1.11.6/internal/getproviders/hash_test.go 2026-04-08 12:54:56.000000000 +0200 +++ new/opentofu-1.11.7/internal/getproviders/hash_test.go 2026-05-11 12:46:32.000000000 +0200 @@ -324,3 +324,14 @@ }) } } + +func TestEmptyPackageHashV1(t *testing.T) { + emptyDir := t.TempDir() + realHash, err := PackageHashV1(PackageLocalDir(emptyDir)) + if err != nil { + t.Fatalf("failed to calculate hash of empty package: %s", err) + } + if realHash != emptyPackageHashV1 { + t.Errorf("emptyPackageHashV1 does not match freshly-calculated hash of empty package\nemptyPackageHashV1: %s\ncalculated result: %s", emptyPackageHashV1, realHash) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.6/internal/getproviders/package_location_local_archive.go new/opentofu-1.11.7/internal/getproviders/package_location_local_archive.go --- old/opentofu-1.11.6/internal/getproviders/package_location_local_archive.go 2026-04-08 12:54:56.000000000 +0200 +++ new/opentofu-1.11.7/internal/getproviders/package_location_local_archive.go 2026-05-11 12:46:32.000000000 +0200 @@ -69,22 +69,44 @@ filename := meta.Location.String() span.SetAttributes(semconv.FilePath(filename)) - // NOTE: Packages are immutable, but we may want to skip overwriting the existing - // files in due to specific scenarios defined below. - - if _, err := os.Stat(targetDir); err == nil { - // If the package might already be installed, we should try to skip overwriting the contents. - // When run with TF_PLUGIN_CACHE_DIR or similar, a given provider might already be executing - // and therefore locking the provider binary in the target directory (preventing the overwrite below) - // - // This does incur the overhead of two additional hash computations and could be - // skipped with smarter checks around re-use scenarios in the future. + // If there is already a package at the location we would've been installing + // to then that's okay if the content already matches what we would've + // installed, but we reject it otherwise so the operator can investigate. + if info, err := os.Lstat(targetDir); err == nil { + log.Printf("[TRACE] There's already a directory entry at %s, so we'll check if it matches our expectations", targetDir) targetHash, targetErr := PackageHashV1(PackageLocalDir(targetDir)) - fileHash, fileErr := PackageHashV1(meta.Location) - - if targetHash == fileHash && fileErr == nil && targetErr == nil { - // Package is properly installed, bad or missing lock file will be caught elsewhere + // If the existing entry is an empty directory then we permit that just + // because sometimes a caller might want to create the target directory + // themselves before installing into it, such as if the target directory + // is a temporary directory created with [os.MkdirTemp]. Only a direct + // empty directory is allowed here, not a symlink to an empty directory. + isEmptyDir := info.IsDir() && targetHash == emptyPackageHashV1 + if !isEmptyDir { + fileHash, fileErr := PackageHashV1(meta.Location) + var err error + if fileErr != nil { + err = fmt.Errorf("failed to calculate checksum for temporary copy of provider package at %s: %s", meta.Location.String(), fileErr) + } else if targetErr != nil { + err = fmt.Errorf("failed to calculate checksum for existing cached provider package at %s: %s", targetDir, targetErr) + } else if targetHash != fileHash { + // This means that there's already something at the cache location + // where we'd need to install to but the existing content doesn't + // match what we're trying to install. In this case we don't want + // to just clobber the existing directory because the operator + // might have modified it for a reason and want to keep something + // they changed in there, and so we'll report an error so they can + // investigate and delete this directory themselves when they are + // ready. + err = fmt.Errorf("existing cached package at %s does not match the content of the downloaded package; does it contain local modifications?", targetDir) + } + if err != nil { + tracing.SetSpanError(span, err) + return authResult, err + } + // If the stat succeeded and we've confirmed that the contents of + // targetDir match the package we were about to install anyway then + // we don't have any more work to do here. log.Printf("[INFO] Skipping local installation of provider %s %s as the existing contents already match the new contents", meta.Provider, meta.Version) return authResult, nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.6/version/VERSION new/opentofu-1.11.7/version/VERSION --- old/opentofu-1.11.6/version/VERSION 2026-04-08 12:54:56.000000000 +0200 +++ new/opentofu-1.11.7/version/VERSION 2026-05-11 12:46:32.000000000 +0200 @@ -1 +1 @@ -1.11.6 +1.11.7 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/opentofu-1.11.6/website/docs/language/settings/backends/s3.mdx new/opentofu-1.11.7/website/docs/language/settings/backends/s3.mdx --- old/opentofu-1.11.6/website/docs/language/settings/backends/s3.mdx 2026-04-08 12:54:56.000000000 +0200 +++ new/opentofu-1.11.7/website/docs/language/settings/backends/s3.mdx 2026-05-11 12:46:32.000000000 +0200 @@ -345,7 +345,7 @@ The following configuration is required: * `bucket` - (Required) Name of the S3 Bucket. -* `key` - (Required) Path to the state file inside the S3 Bucket. When using a non-default [workspace](../../../language/state/workspaces.mdx), the state path will be `/workspace_key_prefix/workspace_name/key` (see also the `workspace_key_prefix` configuration). +* `key` - (Required) Path to the state file inside the S3 Bucket. When using a non-default [workspace](../../../language/state/workspaces.mdx), the state path will be `workspace_key_prefix/workspace_name/key` (see also the `workspace_key_prefix` configuration). The following configuration is optional: ++++++ opentofu.obsinfo ++++++ --- /var/tmp/diff_new_pack.EmbkPv/_old 2026-05-11 17:10:10.732481941 +0200 +++ /var/tmp/diff_new_pack.EmbkPv/_new 2026-05-11 17:10:10.736482106 +0200 @@ -1,5 +1,5 @@ name: opentofu -version: 1.11.6 -mtime: 1775645696 -commit: 698e5e5204756963ffb28107fb61d77970e47f66 +version: 1.11.7 +mtime: 1778496392 +commit: 398c818dcf617837724e3137e4682062574d1019 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/opentofu/vendor.tar.gz /work/SRC/openSUSE:Factory/.opentofu.new.1966/vendor.tar.gz differ: char 13, line 1
