diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk index 2e1391f05..4b5b0e3ae 100644 --- a/.bingo/Variables.mk +++ b/.bingo/Variables.mk @@ -41,12 +41,6 @@ $(CRD_REF_DOCS): $(BINGO_DIR)/crd-ref-docs.mod @echo "(re)installing $(GOBIN)/crd-ref-docs-v0.1.0" @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=crd-ref-docs.mod -o=$(GOBIN)/crd-ref-docs-v0.1.0 "github.com/elastic/crd-ref-docs" -GINKGO := $(GOBIN)/ginkgo-v2.22.2 -$(GINKGO): $(BINGO_DIR)/ginkgo.mod - @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. - @echo "(re)installing $(GOBIN)/ginkgo-v2.22.2" - @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=ginkgo.mod -o=$(GOBIN)/ginkgo-v2.22.2 "github.com/onsi/ginkgo/v2/ginkgo" - GOLANGCI_LINT := $(GOBIN)/golangci-lint-v1.63.4 $(GOLANGCI_LINT): $(BINGO_DIR)/golangci-lint.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. diff --git a/.bingo/ginkgo.mod b/.bingo/ginkgo.mod deleted file mode 100644 index 024da10e0..000000000 --- a/.bingo/ginkgo.mod +++ /dev/null @@ -1,7 +0,0 @@ -module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT - -go 1.22.0 - -toolchain go1.23.0 - -require github.com/onsi/ginkgo/v2 v2.22.0 // ginkgo \ No newline at end of file diff --git a/.bingo/ginkgo.sum b/.bingo/ginkgo.sum deleted file mode 100644 index bf4e1c138..000000000 --- a/.bingo/ginkgo.sum +++ /dev/null @@ -1,8 +0,0 @@ -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= -github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= diff --git a/.bingo/variables.env b/.bingo/variables.env index 63a3618d0..b97764b84 100644 --- a/.bingo/variables.env +++ b/.bingo/variables.env @@ -16,8 +16,6 @@ CRD_DIFF="${GOBIN}/crd-diff-v0.1.0" CRD_REF_DOCS="${GOBIN}/crd-ref-docs-v0.1.0" -GINKGO="${GOBIN}/ginkgo-v2.22.2" - GOLANGCI_LINT="${GOBIN}/golangci-lint-v1.63.4" GORELEASER="${GOBIN}/goreleaser-v1.26.2" diff --git a/catalogd/Makefile b/catalogd/Makefile index d58367f42..ce56ed52a 100644 --- a/catalogd/Makefile +++ b/catalogd/Makefile @@ -44,6 +44,8 @@ TESTDATA_DIR := testdata CATALOGD_NAMESPACE := olmv1-system KIND_CLUSTER_IMAGE := kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e +GINKGO := go run github.com/onsi/ginkgo/v2/ginkgo + ##@ General # The help target prints out all targets with their descriptions organized @@ -76,7 +78,7 @@ FOCUS := $(if $(TEST),-v -focus "$(TEST)") ifeq ($(origin E2E_FLAGS), undefined) E2E_FLAGS := endif -test-e2e: $(GINKGO) ## Run the e2e tests on existing cluster +test-e2e: ## Run the e2e tests on existing cluster $(GINKGO) $(E2E_FLAGS) -trace -vv $(FOCUS) test/e2e e2e: KIND_CLUSTER_NAME := catalogd-e2e @@ -105,7 +107,7 @@ run-latest-release: cd ..; curl -L -s https://github.com/operator-framework/operator-controller/releases/latest/download/install.sh | bash -s .PHONY: post-upgrade-checks -post-upgrade-checks: $(GINKGO) +post-upgrade-checks: $(GINKGO) $(E2E_FLAGS) -trace -vv $(FOCUS) test/upgrade ##@ Build diff --git a/catalogd/cmd/catalogd/main.go b/catalogd/cmd/catalogd/main.go index 91d82bedd..8ab76aa32 100644 --- a/catalogd/cmd/catalogd/main.go +++ b/catalogd/cmd/catalogd/main.go @@ -63,7 +63,7 @@ import ( "github.com/operator-framework/operator-controller/catalogd/internal/storage" "github.com/operator-framework/operator-controller/catalogd/internal/version" "github.com/operator-framework/operator-controller/catalogd/internal/webhook" - "github.com/operator-framework/operator-controller/internal/util" + "github.com/operator-framework/operator-controller/internal/fsutil" ) var ( @@ -258,7 +258,7 @@ func main() { systemNamespace = podNamespace() } - if err := util.EnsureEmptyDirectory(cacheDir, 0700); err != nil { + if err := fsutil.EnsureEmptyDirectory(cacheDir, 0700); err != nil { setupLog.Error(err, "unable to ensure empty cache directory") os.Exit(1) } diff --git a/catalogd/internal/source/containers_image.go b/catalogd/internal/source/containers_image.go index 03df10f2f..b57b5b210 100644 --- a/catalogd/internal/source/containers_image.go +++ b/catalogd/internal/source/containers_image.go @@ -30,8 +30,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" catalogdv1 "github.com/operator-framework/operator-controller/catalogd/api/v1" + "github.com/operator-framework/operator-controller/internal/fsutil" "github.com/operator-framework/operator-controller/internal/rukpak/source" - "github.com/operator-framework/operator-controller/internal/util" ) const ConfigDirLabel = "operators.operatorframework.io.index.configs.v1" @@ -297,7 +297,7 @@ func (i *ContainersImageRegistry) unpackImage(ctx context.Context, unpackPath st return wrapTerminal(fmt.Errorf("catalog image is missing the required label %q", ConfigDirLabel), specIsCanonical) } - if err := util.EnsureEmptyDirectory(unpackPath, 0700); err != nil { + if err := fsutil.EnsureEmptyDirectory(unpackPath, 0700); err != nil { return fmt.Errorf("error ensuring empty unpack directory: %w", err) } l := log.FromContext(ctx) diff --git a/catalogd/test/upgrade/unpack_test.go b/catalogd/test/upgrade/unpack_test.go index e13354454..9c42f11f1 100644 --- a/catalogd/test/upgrade/unpack_test.go +++ b/catalogd/test/upgrade/unpack_test.go @@ -54,6 +54,17 @@ var _ = Describe("ClusterCatalog Unpacking", func() { managerPod = managerPods.Items[0] }).Should(Succeed()) + By("Waiting for acquired leader election") + // Average case is under 1 minute but in the worst case: (previous leader crashed) + // we could have LeaseDuration (137s) + RetryPeriod (26s) +/- 163s + leaderCtx, leaderCancel := context.WithTimeout(ctx, 3*time.Minute) + defer leaderCancel() + + leaderSubstrings := []string{"successfully acquired lease"} + leaderElected, err := watchPodLogsForSubstring(leaderCtx, &managerPod, "manager", leaderSubstrings...) + Expect(err).To(Succeed()) + Expect(leaderElected).To(BeTrue()) + By("Reading logs to make sure that ClusterCatalog was reconciled by catalogdv1") logCtx, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 76c0e4af4..16176ddc5 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -63,12 +63,12 @@ import ( "github.com/operator-framework/operator-controller/internal/controllers" "github.com/operator-framework/operator-controller/internal/features" "github.com/operator-framework/operator-controller/internal/finalizers" + "github.com/operator-framework/operator-controller/internal/fsutil" "github.com/operator-framework/operator-controller/internal/httputil" "github.com/operator-framework/operator-controller/internal/resolve" "github.com/operator-framework/operator-controller/internal/rukpak/preflights/crdupgradesafety" "github.com/operator-framework/operator-controller/internal/rukpak/source" "github.com/operator-framework/operator-controller/internal/scheme" - "github.com/operator-framework/operator-controller/internal/util" "github.com/operator-framework/operator-controller/internal/version" ) @@ -300,7 +300,7 @@ func main() { } } - if err := util.EnsureEmptyDirectory(cachePath, 0700); err != nil { + if err := fsutil.EnsureEmptyDirectory(cachePath, 0700); err != nil { setupLog.Error(err, "unable to ensure empty cache directory") os.Exit(1) } diff --git a/internal/util/fs.go b/internal/fsutil/helpers.go similarity index 65% rename from internal/util/fs.go rename to internal/fsutil/helpers.go index 137b0735d..55accac46 100644 --- a/internal/util/fs.go +++ b/internal/fsutil/helpers.go @@ -1,4 +1,4 @@ -package util +package fsutil import ( "io/fs" @@ -8,7 +8,9 @@ import ( // EnsureEmptyDirectory ensures the directory given by `path` is empty. // If the directory does not exist, it will be created with permission bits -// given by `perm`. +// given by `perm`. If the directory exists, it will not simply rm -rf && mkdir -p +// as the calling process may not have permissions to delete the directory. E.g. +// in the case of a pod mount. Rather, it will delete the contents of the directory. func EnsureEmptyDirectory(path string, perm fs.FileMode) error { entries, err := os.ReadDir(path) if err != nil && !os.IsNotExist(err) { diff --git a/internal/fsutil/helpers_test.go b/internal/fsutil/helpers_test.go new file mode 100644 index 000000000..b6fda0b30 --- /dev/null +++ b/internal/fsutil/helpers_test.go @@ -0,0 +1,47 @@ +package fsutil_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-controller/internal/fsutil" +) + +func TestEnsureEmptyDirectory(t *testing.T) { + tempDir := t.TempDir() + dirPath := filepath.Join(tempDir, "testdir") + dirPerms := os.FileMode(0755) + + t.Log("Ensure directory is created with the correct perms if it does not already exist") + require.NoError(t, fsutil.EnsureEmptyDirectory(dirPath, dirPerms)) + + stat, err := os.Stat(dirPath) + require.NoError(t, err) + require.True(t, stat.IsDir()) + require.Equal(t, dirPerms, stat.Mode().Perm()) + + t.Log("Create a file inside directory") + file := filepath.Join(dirPath, "file1") + // nolint:gosec + require.NoError(t, os.WriteFile(file, []byte("test"), 0640)) + + t.Log("Create a sub-directory inside directory") + subDir := filepath.Join(dirPath, "subdir") + require.NoError(t, os.Mkdir(subDir, dirPerms)) + + t.Log("Call EnsureEmptyDirectory against directory with different permissions") + require.NoError(t, fsutil.EnsureEmptyDirectory(dirPath, 0640)) + + t.Log("Ensure directory is now empty") + entries, err := os.ReadDir(dirPath) + require.NoError(t, err) + require.Empty(t, entries) + + t.Log("Ensure original directory permissions are unchanged") + stat, err = os.Stat(dirPath) + require.NoError(t, err) + require.Equal(t, dirPerms, stat.Mode().Perm()) +} diff --git a/internal/rukpak/source/containers_image.go b/internal/rukpak/source/containers_image.go index 67f0f0625..aaf72881d 100644 --- a/internal/rukpak/source/containers_image.go +++ b/internal/rukpak/source/containers_image.go @@ -25,7 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/operator-framework/operator-controller/internal/util" + "github.com/operator-framework/operator-controller/internal/fsutil" ) var insecurePolicy = []byte(`{"default":[{"type":"insecureAcceptAnything"}]}`) @@ -266,7 +266,7 @@ func (i *ContainersImageRegistry) unpackImage(ctx context.Context, unpackPath st } }() - if err := util.EnsureEmptyDirectory(unpackPath, 0700); err != nil { + if err := fsutil.EnsureEmptyDirectory(unpackPath, 0700); err != nil { return fmt.Errorf("error ensuring empty unpack directory: %w", err) } l := log.FromContext(ctx) diff --git a/internal/rukpak/source/containers_image_test.go b/internal/rukpak/source/containers_image_test.go index 29f2788c6..ab1abbb9b 100644 --- a/internal/rukpak/source/containers_image_test.go +++ b/internal/rukpak/source/containers_image_test.go @@ -286,7 +286,7 @@ func TestUnpackUnexpectedFile(t *testing.T) { require.True(t, stat.IsDir()) // Unset read-only to allow cleanup - require.NoError(t, source.UnsetReadOnlyRecursive(unpackPath)) + require.NoError(t, source.SetWritableRecursive(unpackPath)) } func TestUnpackCopySucceedsMountFails(t *testing.T) { diff --git a/internal/rukpak/source/helpers.go b/internal/rukpak/source/helpers.go new file mode 100644 index 000000000..6e87dfb87 --- /dev/null +++ b/internal/rukpak/source/helpers.go @@ -0,0 +1,80 @@ +package source + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "time" +) + +const ( + OwnerWritableFileMode os.FileMode = 0700 + OwnerWritableDirMode os.FileMode = 0700 + OwnerReadOnlyFileMode os.FileMode = 0400 + OwnerReadOnlyDirMode os.FileMode = 0500 +) + +// SetReadOnlyRecursive recursively sets files and directories under the path given by `root` as read-only +func SetReadOnlyRecursive(root string) error { + return setModeRecursive(root, OwnerReadOnlyFileMode, OwnerReadOnlyDirMode) +} + +// SetWritableRecursive recursively sets files and directories under the path given by `root` as writable +func SetWritableRecursive(root string) error { + return setModeRecursive(root, OwnerWritableFileMode, OwnerWritableDirMode) +} + +// DeleteReadOnlyRecursive deletes read-only directory with path given by `root` +func DeleteReadOnlyRecursive(root string) error { + if err := SetWritableRecursive(root); err != nil { + return fmt.Errorf("error making directory writable for deletion: %w", err) + } + return os.RemoveAll(root) +} + +// IsImageUnpacked checks whether an image has been unpacked in `unpackPath`. +// If true, time of unpack will also be returned. If false unpack time is gibberish (zero/epoch time). +// If `unpackPath` is a file, it will be deleted and false will be returned without an error. +func IsImageUnpacked(unpackPath string) (bool, time.Time, error) { + unpackStat, err := os.Stat(unpackPath) + if errors.Is(err, os.ErrNotExist) { + return false, time.Time{}, nil + } + if err != nil { + return false, time.Time{}, err + } + if !unpackStat.IsDir() { + return false, time.Time{}, os.Remove(unpackPath) + } + return true, unpackStat.ModTime(), nil +} + +func setModeRecursive(path string, fileMode os.FileMode, dirMode os.FileMode) error { + return filepath.WalkDir(path, func(path string, d os.DirEntry, err error) error { + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + fi, err := d.Info() + if err != nil { + return err + } + + switch typ := fi.Mode().Type(); typ { + case os.ModeSymlink: + // do not follow symlinks + // 1. if they resolve to other locations in the root, we'll find them anyway + // 2. if they resolve to other locations outside the root, we don't want to change their permissions + return nil + case os.ModeDir: + return os.Chmod(path, dirMode) + case 0: // regular file + return os.Chmod(path, fileMode) + default: + return fmt.Errorf("refusing to change ownership of file %q with type %v", path, typ.String()) + } + }) +} diff --git a/internal/rukpak/source/helpers_test.go b/internal/rukpak/source/helpers_test.go new file mode 100644 index 000000000..a4da1e629 --- /dev/null +++ b/internal/rukpak/source/helpers_test.go @@ -0,0 +1,142 @@ +package source_test + +import ( + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-controller/internal/rukpak/source" +) + +func TestSetReadOnlyRecursive(t *testing.T) { + tempDir := t.TempDir() + targetFilePath := filepath.Join(tempDir, "target") + nestedDir := filepath.Join(tempDir, "nested") + filePath := filepath.Join(nestedDir, "testfile") + symlinkPath := filepath.Join(nestedDir, "symlink") + + t.Log("Create symlink target file outside directory with its own permissions") + // nolint:gosec + require.NoError(t, os.WriteFile(targetFilePath, []byte("something"), 0644)) + + t.Log("Create a nested directory structure that contains a file and sym. link") + require.NoError(t, os.Mkdir(nestedDir, source.OwnerWritableDirMode)) + require.NoError(t, os.WriteFile(filePath, []byte("test"), source.OwnerWritableFileMode)) + require.NoError(t, os.Symlink(targetFilePath, symlinkPath)) + + t.Log("Set directory structure as read-only") + require.NoError(t, source.SetReadOnlyRecursive(nestedDir)) + + t.Log("Check file permissions") + stat, err := os.Stat(filePath) + require.NoError(t, err) + require.Equal(t, source.OwnerReadOnlyFileMode, stat.Mode().Perm()) + + t.Log("Check directory permissions") + nestedStat, err := os.Stat(nestedDir) + require.NoError(t, err) + require.Equal(t, source.OwnerReadOnlyDirMode, nestedStat.Mode().Perm()) + + t.Log("Check symlink target file permissions - should not be affected") + stat, err = os.Stat(targetFilePath) + require.NoError(t, err) + require.Equal(t, fs.FileMode(0644), stat.Mode().Perm()) + + t.Log("Make directory writable to enable test clean-up") + require.NoError(t, source.SetWritableRecursive(tempDir)) +} + +func TestSetWritableRecursive(t *testing.T) { + tempDir := t.TempDir() + targetFilePath := filepath.Join(tempDir, "target") + nestedDir := filepath.Join(tempDir, "nested") + filePath := filepath.Join(nestedDir, "testfile") + symlinkPath := filepath.Join(nestedDir, "symlink") + + t.Log("Create symlink target file outside directory with its own permissions") + // nolint:gosec + require.NoError(t, os.WriteFile(targetFilePath, []byte("something"), 0644)) + + t.Log("Create a nested directory (writable) structure that contains a file (read-only) and sym. link") + require.NoError(t, os.Mkdir(nestedDir, source.OwnerWritableDirMode)) + require.NoError(t, os.WriteFile(filePath, []byte("test"), source.OwnerReadOnlyFileMode)) + require.NoError(t, os.Symlink(targetFilePath, symlinkPath)) + + t.Log("Make directory read-only") + require.NoError(t, os.Chmod(nestedDir, source.OwnerReadOnlyDirMode)) + + t.Log("Call SetWritableRecursive") + require.NoError(t, source.SetWritableRecursive(nestedDir)) + + t.Log("Check file is writable") + stat, err := os.Stat(filePath) + require.NoError(t, err) + require.Equal(t, source.OwnerWritableFileMode, stat.Mode().Perm()) + + t.Log("Check directory is writable") + nestedStat, err := os.Stat(nestedDir) + require.NoError(t, err) + require.Equal(t, source.OwnerWritableDirMode, nestedStat.Mode().Perm()) + + t.Log("Check symlink target file permissions - should not be affected") + stat, err = os.Stat(targetFilePath) + require.NoError(t, err) + require.Equal(t, fs.FileMode(0644), stat.Mode().Perm()) +} + +func TestDeleteReadOnlyRecursive(t *testing.T) { + tempDir := t.TempDir() + nestedDir := filepath.Join(tempDir, "nested") + filePath := filepath.Join(nestedDir, "testfile") + + t.Log("Create a nested read-only directory structure that contains a file and sym. link") + require.NoError(t, os.Mkdir(nestedDir, source.OwnerWritableDirMode)) + require.NoError(t, os.WriteFile(filePath, []byte("test"), source.OwnerReadOnlyFileMode)) + require.NoError(t, os.Chmod(nestedDir, source.OwnerReadOnlyDirMode)) + + t.Log("Set directory structure as read-only") + require.NoError(t, source.DeleteReadOnlyRecursive(nestedDir)) + + t.Log("Ensure directory was deleted") + _, err := os.Stat(nestedDir) + require.ErrorIs(t, err, os.ErrNotExist) +} + +func TestIsImageUnpacked(t *testing.T) { + tempDir := t.TempDir() + unpackPath := filepath.Join(tempDir, "myimage") + + t.Log("Test case: unpack path does not exist") + unpacked, modTime, err := source.IsImageUnpacked(unpackPath) + require.NoError(t, err) + require.False(t, unpacked) + require.True(t, modTime.IsZero()) + + t.Log("Test case: unpack path points to file") + require.NoError(t, os.WriteFile(unpackPath, []byte("test"), source.OwnerWritableFileMode)) + + unpacked, modTime, err = source.IsImageUnpacked(filepath.Join(tempDir, "myimage")) + require.NoError(t, err) + require.False(t, unpacked) + require.True(t, modTime.IsZero()) + + t.Log("Expect file to be deleted") + _, err = os.Stat(unpackPath) + require.ErrorIs(t, err, os.ErrNotExist) + + t.Log("Test case: unpack path points to directory (happy path)") + require.NoError(t, os.Mkdir(unpackPath, source.OwnerWritableDirMode)) + + unpacked, modTime, err = source.IsImageUnpacked(unpackPath) + require.NoError(t, err) + require.True(t, unpacked) + require.False(t, modTime.IsZero()) + + t.Log("Expect unpack time to match directory mod time") + stat, err := os.Stat(unpackPath) + require.NoError(t, err) + require.Equal(t, stat.ModTime(), modTime) +} diff --git a/internal/rukpak/source/util.go b/internal/rukpak/source/util.go deleted file mode 100644 index ca9aa9c2b..000000000 --- a/internal/rukpak/source/util.go +++ /dev/null @@ -1,86 +0,0 @@ -package source - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "time" -) - -// SetReadOnlyRecursive sets directory with path given by `root` as read-only -func SetReadOnlyRecursive(root string) error { - return filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - - fi, err := d.Info() - if err != nil { - return err - } - - if err := func() error { - switch typ := fi.Mode().Type(); typ { - case os.ModeSymlink: - // do not follow symlinks - // 1. if they resolve to other locations in the root, we'll find them anyway - // 2. if they resolve to other locations outside the root, we don't want to change their permissions - return nil - case os.ModeDir: - return os.Chmod(path, 0500) - case 0: // regular file - return os.Chmod(path, 0400) - default: - return fmt.Errorf("refusing to change ownership of file %q with type %v", path, typ.String()) - } - }(); err != nil { - return err - } - return nil - }) -} - -// UnsetReadOnlyRecursive unsets directory with path given by `root` as read-only -func UnsetReadOnlyRecursive(root string) error { - return filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { - if os.IsNotExist(err) { - return nil - } - if err != nil { - return err - } - if !d.IsDir() { - return nil - } - if err := os.Chmod(path, 0700); err != nil { - return err - } - return nil - }) -} - -// DeleteReadOnlyRecursive deletes read-only directory with path given by `root` -func DeleteReadOnlyRecursive(root string) error { - if err := UnsetReadOnlyRecursive(root); err != nil { - return fmt.Errorf("error making directory writable for deletion: %w", err) - } - return os.RemoveAll(root) -} - -// IsImageUnpacked checks whether an image has been unpacked in `unpackPath`. -// If true, time of unpack will also be returned. If false unpack time is gibberish (zero/epoch time). -// If `unpackPath` is a file, it will be deleted and false will be returned without an error. -func IsImageUnpacked(unpackPath string) (bool, time.Time, error) { - unpackStat, err := os.Stat(unpackPath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return false, time.Time{}, nil - } - return false, time.Time{}, err - } - if !unpackStat.IsDir() { - return false, time.Time{}, os.Remove(unpackPath) - } - return true, unpackStat.ModTime(), nil -} diff --git a/requirements.txt b/requirements.txt index 814a741bb..047823056 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Babel==2.16.0 beautifulsoup4==4.12.3 -certifi==2024.12.14 +certifi==2025.1.31 charset-normalizer==3.4.1 click==8.1.8 colorama==0.4.6 @@ -14,7 +14,7 @@ markdown2==2.5.3 MarkupSafe==3.0.2 mergedeep==1.3.4 mkdocs==1.6.1 -mkdocs-material==9.5.50 +mkdocs-material==9.6.1 mkdocs-material-extensions==1.3.1 packaging==24.2 paginate==0.5.7 diff --git a/scripts/install.tpl.sh b/scripts/install.tpl.sh index 0138baade..8088a2515 100644 --- a/scripts/install.tpl.sh +++ b/scripts/install.tpl.sh @@ -76,11 +76,6 @@ kubectl_wait "olmv1-system" "deployment/catalogd-controller-manager" "60s" kubectl_wait "olmv1-system" "deployment/operator-controller-controller-manager" "60s" if [[ "${install_default_catalogs}" != "false" ]]; then - if [[ ! -f "$default_catalogs_manifest" ]]; then - echo "Error: Missing required default catalogs manifest file at $default_catalogs_manifest" - exit 1 - fi - kubectl apply -f "${default_catalogs_manifest}" kubectl wait --for=condition=Serving "clustercatalog/operatorhubio" --timeout="60s" fi diff --git a/test/upgrade-e2e/post_upgrade_test.go b/test/upgrade-e2e/post_upgrade_test.go index 204c79330..0f60210c5 100644 --- a/test/upgrade-e2e/post_upgrade_test.go +++ b/test/upgrade-e2e/post_upgrade_test.go @@ -40,6 +40,17 @@ func TestClusterExtensionAfterOLMUpgrade(t *testing.T) { t.Log("Wait for operator-controller deployment to be ready") managerPod := waitForDeployment(t, ctx, "operator-controller-controller-manager") + t.Log("Wait for acquired leader election") + // Average case is under 1 minute but in the worst case: (previous leader crashed) + // we could have LeaseDuration (137s) + RetryPeriod (26s) +/- 163s + leaderCtx, leaderCancel := context.WithTimeout(ctx, 3*time.Minute) + defer leaderCancel() + + leaderSubstrings := []string{"successfully acquired lease"} + leaderElected, err := watchPodLogsForSubstring(leaderCtx, managerPod, "manager", leaderSubstrings...) + require.NoError(t, err) + require.True(t, leaderElected) + t.Log("Reading logs to make sure that ClusterExtension was reconciled by operator-controller before we update it") // Make sure that after we upgrade OLM itself we can still reconcile old objects without any changes logCtx, cancel := context.WithTimeout(ctx, time.Minute)