diff --git a/internal/util/temp.go b/internal/util/temp.go
new file mode 100644
index 00000000..054b1280
--- /dev/null
+++ b/internal/util/temp.go
@@ -0,0 +1,52 @@
+/*
+Copyright 2021 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package util
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// TempDirForObj creates a new temporary directory in the directory dir
+// in the format of 'Kind-Namespace-Name-*', and returns the
+// pathname of the new directory.
+func TempDirForObj(dir string, obj client.Object) (string, error) {
+ return os.MkdirTemp(dir, pattern(obj))
+}
+
+// TempPathForObj creates a temporary file path in the format of
+// '
/Kind-Namespace-Name-'.
+// If the given dir is empty, os.TempDir is used as a default.
+func TempPathForObj(dir, suffix string, obj client.Object) string {
+ if dir == "" {
+ dir = os.TempDir()
+ }
+ randBytes := make([]byte, 16)
+ rand.Read(randBytes)
+ return filepath.Join(dir, pattern(obj)+hex.EncodeToString(randBytes)+suffix)
+}
+
+func pattern(obj client.Object) (p string) {
+ kind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind)
+ return fmt.Sprintf("%s-%s-%s-", kind, obj.GetNamespace(), obj.GetName())
+}
diff --git a/internal/util/temp_test.go b/internal/util/temp_test.go
new file mode 100644
index 00000000..7db873e2
--- /dev/null
+++ b/internal/util/temp_test.go
@@ -0,0 +1,85 @@
+/*
+Copyright 2021 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package util
+
+import (
+ "os"
+ "testing"
+
+ . "github.com/onsi/gomega"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+func TestTempDirForObj(t *testing.T) {
+ g := NewWithT(t)
+
+ got, err := TempDirForObj("", mockObj())
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got).To(BeADirectory())
+ defer os.RemoveAll(got)
+
+ got2, err := TempDirForObj(got, mockObj())
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(got2).To(BeADirectory())
+ defer os.RemoveAll(got2)
+ g.Expect(got2).To(ContainSubstring(got))
+}
+
+func TestTempPathForObj(t *testing.T) {
+ tests := []struct {
+ name string
+ dir string
+ suffix string
+ want string
+ }{
+ {
+ name: "default",
+ want: os.TempDir() + "/secret-default-foo-",
+ },
+ {
+ name: "with directory",
+ dir: "/foo",
+ want: "/foo/secret-default-foo-",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewWithT(t)
+ got := TempPathForObj(tt.dir, tt.suffix, mockObj())
+ g.Expect(got[:len(got)-32]).To(Equal(tt.want))
+ })
+ }
+}
+
+func Test_pattern(t *testing.T) {
+ g := NewWithT(t)
+ g.Expect(pattern(mockObj())).To(Equal("secret-default-foo-"))
+}
+
+func mockObj() client.Object {
+ return &corev1.Secret{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "Secret",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "foo",
+ Namespace: "default",
+ },
+ }
+}