Skip to content

Commit c1b5d70

Browse files
authored
Require Registered Branch To Exist (#3024)
When working with empty repository, Porch may push the first branch to the remote. GitHub makes the first branch the default which cannot be easily deleted. For more predictable user experience, require the branch the client provided at registration (defaulting to `main`) to exist. Improve the archive command to set consistent file timestamps and delete more unnecessary content.
1 parent 49d817e commit c1b5d70

File tree

15 files changed

+248
-17
lines changed

15 files changed

+248
-17
lines changed

porch/engine/pkg/engine/clone.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ func (m *clonePackageMutation) cloneFromGit(ctx context.Context, gitPackage *api
128128

129129
spec := configapi.GitRepository{
130130
Repo: gitPackage.Repo,
131-
Branch: gitPackage.Ref,
132131
Directory: gitPackage.Directory,
133132
SecretRef: configapi.SecretRef{
134133
Name: gitPackage.SecretRef.Name,
@@ -142,7 +141,8 @@ func (m *clonePackageMutation) cloneFromGit(ctx context.Context, gitPackage *api
142141
defer os.RemoveAll(dir)
143142

144143
r, err := git.OpenRepository(ctx, "", "", &spec, dir, git.GitRepositoryOptions{
145-
CredentialResolver: m.credentialResolver,
144+
CredentialResolver: m.credentialResolver,
145+
SkipMainBranchVerification: true, // We are only reading so we don't need the main branch to exist.
146146
})
147147
if err != nil {
148148
return repository.PackageResources{}, fmt.Errorf("cannot clone Git repository: %w", err)

porch/hack/tar-test-repo.sh

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,19 @@ if [[ $# -ne 2 ]]; then
2222
error "Invalid # of arguments; ${#}. 2 expected: GIT_DIRECTORY OUTPUT_TAR_FILE"
2323
fi
2424

25-
tar -c -f "${2}" -C "${1}" --owner=porch:0 --group=porch:0 \
26-
--exclude '.git/logs' \
27-
--exclude '.git/COMMIT_EDITMSG' \
28-
--exclude '.git/ORIG_HEAD' \
29-
.git
25+
GIT_DIRECTORY="${1}"
26+
OUTPUT_TAR_FILE="${2}"
27+
28+
if [ -d "${GIT_DIRECTORY}" ]; then
29+
30+
tar -c -v -f "${OUTPUT_TAR_FILE}" -C "${GIT_DIRECTORY}" --owner=porch:0 --group=porch:0 \
31+
--sort=name --mtime='PST 2022-02-02' \
32+
--exclude '.git/logs' \
33+
--exclude '.git/COMMIT_EDITMSG' \
34+
--exclude '.git/ORIG_HEAD' \
35+
--exclude '.git/info/exclude' \
36+
.git
37+
38+
else
39+
error "${GIT_DIRECTORY} doesn't exist"
40+
fi

porch/repository/pkg/git/git.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,23 @@ type GitRepository interface {
5050
}
5151

5252
type GitRepositoryOptions struct {
53-
CredentialResolver repository.CredentialResolver
54-
UserInfoProvider repository.UserInfoProvider
53+
CredentialResolver repository.CredentialResolver
54+
UserInfoProvider repository.UserInfoProvider
55+
SkipMainBranchVerification bool
5556
}
5657

5758
func OpenRepository(ctx context.Context, name, namespace string, spec *configapi.GitRepository, root string, opts GitRepositoryOptions) (GitRepository, error) {
5859
replace := strings.NewReplacer("/", "-", ":", "-")
5960
dir := filepath.Join(root, replace.Replace(spec.Repo))
6061

62+
// Cleanup the cache directory in case initialization fails.
63+
cleanup := dir
64+
defer func() {
65+
if cleanup != "" {
66+
os.RemoveAll(cleanup)
67+
}
68+
}()
69+
6170
var repo *git.Repository
6271

6372
if fi, err := os.Stat(dir); err != nil {
@@ -72,9 +81,11 @@ func OpenRepository(ctx context.Context, name, namespace string, spec *configapi
7281

7382
repo = r
7483
} else if !fi.IsDir() {
75-
// Internal error - corrupted cache.
84+
// Internal error - corrupted cache. We will cleanup on the way out.
7685
return nil, fmt.Errorf("cannot clone git repository %q: %w", spec.Repo, err)
7786
} else {
87+
cleanup = "" // Existing directory; do not delete it.
88+
7889
r, err := openRepository(dir)
7990
if err != nil {
8091
return nil, err
@@ -90,8 +101,6 @@ func OpenRepository(ctx context.Context, name, namespace string, spec *configapi
90101

91102
branch := MainBranch
92103
if spec.Branch != "" {
93-
// TODO: Validate branch name syntax (we can't check whether the branch exists;
94-
// the repository may be empty).
95104
branch = BranchName(spec.Branch)
96105
}
97106

@@ -110,6 +119,12 @@ func OpenRepository(ctx context.Context, name, namespace string, spec *configapi
110119
return nil, err
111120
}
112121

122+
if err := repository.verifyRepository(&opts); err != nil {
123+
return nil, err
124+
}
125+
126+
cleanup = "" // Success. Keep the git directory.
127+
113128
return repository, nil
114129
}
115130

@@ -705,6 +720,22 @@ func (r *gitRepository) fetchRemoteRepository(ctx context.Context) error {
705720
return nil
706721
}
707722

723+
// Verifies repository. Repository must be fetched already.
724+
func (r *gitRepository) verifyRepository(opts *GitRepositoryOptions) error {
725+
// When opening a temporary repository, such as for cloning a package
726+
// from unregistered upstream, we won't be pushing into the remote so
727+
// we don't need to verify presence of the main branch.
728+
if !opts.SkipMainBranchVerification {
729+
// Check existence of main branch. If it doesn't exist, fail.
730+
// If the main branch doesn't exist, we could create draft or proposal branch
731+
// which could become the default branch and those are hard to delete.
732+
if _, err := r.repo.Reference(r.branch.RefInLocal(), false); err != nil {
733+
return fmt.Errorf("branch %q doesn't exist: %v", r.branch, err)
734+
}
735+
}
736+
return nil
737+
}
738+
708739
// Creates a commit which deletes the package from the branch, and returns its commit hash.
709740
// If the branch doesn't exist, will return zero hash and no error.
710741
func (r *gitRepository) createPackageDeleteCommit(ctx context.Context, branch plumbing.ReferenceName, pkg *gitPackageRevision) (plumbing.Hash, error) {

porch/repository/pkg/git/git_test.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ func TestMain(m *testing.M) {
4444
os.Exit(m.Run())
4545
}
4646

47+
func TestOpenEmptyRepository(t *testing.T) {
48+
tempdir := t.TempDir()
49+
tarfile := filepath.Join("testdata", "empty-repository.tar")
50+
_, address := ServeGitRepository(t, tarfile, tempdir)
51+
52+
ctx := context.Background()
53+
const (
54+
name = "empty"
55+
namespace = "default"
56+
)
57+
58+
repository := &configapi.GitRepository{
59+
Repo: address,
60+
Branch: "main",
61+
Directory: "/",
62+
}
63+
64+
if _, err := OpenRepository(ctx, name, namespace, repository, tempdir, GitRepositoryOptions{}); err == nil {
65+
t.Errorf("Unexpectedly succeeded opening empty repository with main branch validation enabled.")
66+
}
67+
68+
if _, err := OpenRepository(ctx, name, namespace, repository, tempdir, GitRepositoryOptions{SkipMainBranchVerification: true}); err != nil {
69+
t.Errorf("Failed to open empty git repository with main branch validation disabled: %v", err)
70+
}
71+
}
72+
4773
// TestGitPackageRoundTrip creates a package in git and verifies we can read the contents back.
4874
func TestGitPackageRoundTrip(t *testing.T) {
4975
tempdir := t.TempDir()
@@ -283,9 +309,9 @@ info:
283309
description: Empty Package
284310
`
285311

286-
func TestListPackagesEmpty(t *testing.T) {
312+
func TestListPackagesTrivial(t *testing.T) {
287313
tempdir := t.TempDir()
288-
tarfile := filepath.Join("testdata", "empty-repository.tar")
314+
tarfile := filepath.Join("testdata", "trivial-repository.tar")
289315
_, address := ServeGitRepository(t, tarfile, tempdir)
290316

291317
ctx := context.Background()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
empty-repository/
2+
trivial-repository/
3+
simple-repository/
4+
drafts-repository/
5+
nested-repository/
Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,53 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
REPOSITORIES = \
16+
empty-repository.tar \
17+
trivial-repository.tar \
18+
simple-repository.tar \
19+
drafts-repository.tar \
20+
nested-repository.tar
21+
22+
123
.PHONY: expand
224
expand:
3-
rm -rf drafts-repository; mkdir drafts-repository
4-
cd drafts-repository; tar xfv ../drafts-repository.tar; git reset --hard main
25+
@for f in $(REPOSITORIES); do \
26+
dir=$${f%.*}; \
27+
rm -rf $${dir}; mkdir $${dir}; \
28+
tar xf $${f} -C $${dir}; \
29+
echo "$${f} --> $${dir}/"; \
30+
done
31+
32+
.PHONY: checkout
33+
checkout: expand
34+
@for f in $(REPOSITORIES); do \
35+
dir=$${f%.*}; \
36+
(cd $${dir}; git reset --hard refs/heads/main -- 2>/dev/null ); \
37+
done
538

639
.PHONY: tars
740
tars:
8-
../../../../hack/tar-test-repo.sh drafts-repository/ drafts-repository.tar
41+
@for f in $(REPOSITORIES); do \
42+
dir=$${f%.*}; \
43+
../../../../hack/tar-test-repo.sh $${dir}/ $${f}; \
44+
rm -rf $${dir}; \
45+
echo "$${dir}/ --> $${f}"; \
46+
done
47+
48+
.PHONY: clean
49+
clean:
50+
@for f in $(REPOSITORIES); do \
51+
dir=$${f%.*}; \
52+
rm -rf $${dir}; \
53+
done
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Drafts Repository
2+
3+
For easier orientation, an overview of the contents of the `drafts-repository.tar`.
4+
5+
## Contents
6+
7+
The repository in `drafts-repository.tar` has the following contents in `main` branch:
8+
9+
```
10+
.
11+
├── basens
12+
│   ├── Kptfile
13+
│   ├── namespace.yaml
14+
│   ├── package-context.yaml
15+
│   └── README.md
16+
├── empty
17+
│   ├── Kptfile
18+
│   └── README.md
19+
└── istions
20+
├── istions.yaml
21+
├── Kptfile
22+
├── package-context.yaml
23+
└── README.md
24+
```
25+
26+
## Commits
27+
28+
The commit graph of the repository is:
29+
30+
```
31+
* 487eaa0fe7652a313dcdb05790aa32034398593a (drafts/none/v1) Add none package with Kptfile marker
32+
* c7edca419782f88646f9572b0a829d686b2d91bd (HEAD -> main, tag: istions/v2) Add Istio Namespace Resource
33+
* c93d417f1393ae5d7def978da70c42b62e645cda (tag: basens/v2) Add Base Namespace Resource
34+
| * 032c503a3921f322850e9bd49319346e0e0b129d (drafts/bucket/v1) Add Bucket Resource
35+
| * 1950b803f552e4c89ac17c528ec466c1a7375083 Add Bucket Package Context
36+
| * 5ea104c29951f3c3995ae15c4a367823794bd47d Empty Bucket Package
37+
|/
38+
* f8fb59f626182319ec78dd542afcce35f98811e2 (tag: istions/v1) Add Istio Namespace Package Context
39+
* 740397bf8f594b785f6802755945bd58a5e94192 (tag: basens/v1) Add Base Namespace Package Context
40+
* 3d21cf677b3afcca132e0b415144e83f1f2ca2a9 Empty Istio Namespace
41+
* ca725b9cd90d5a3ee22101f0ea66f83e40217a6f Empty Base Namespace
42+
* 7a6308e79524221d74f549bac53bf1ed771958f7 (tag: empty/v1) Empty Package
43+
44+
```
45+
46+
In addition to several published packages there are two drafts (`none` and `bucket`).
0 Bytes
Binary file not shown.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Empty Repository
2+
3+
For easier orientation, an overview of the contents of the `empty-repository.tar`.
4+
5+
## Contents
6+
7+
The repository in `empty-repository.tar` is truly empty. It has no commits, no refs.
8+
Because `HEAD` is required for a valid git repository, `HEAD` is a symbolic ref to
9+
(non-existent) `refs/heads/main`.
10+
11+
## Commits
12+
13+
There are no commits in this repository.
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)