Skip to content

Commit 06049fc

Browse files
Prevent edit, clone, and upgrade of placeholder package revisions (#487)
* Issue #994 - prevent edit, clone, and upgrade of placeholder package revisions - validate clone, edit, and upgrade operations and reject if they use placeholder/main package revisions as source/upstream - placeholder package revision detection: revision == -1 && workspaceName == repo branch - verify in e2e tests https://github.com/nephio-project/nephio/issues/994 * Add points in relevant guide docs for create-with-placeholder validation failures * lint fixes * Coverage improvements * Incorporate dosubot's documentation suggestions * Address Copilot comments * nitpicky refactor to retrigger tests * address review comments * Additional comment fixes + coverage increases * coverage increases * Add pkgerrors.Wrap'ing according to comment
1 parent ef67165 commit 06049fc

17 files changed

Lines changed: 856 additions & 37 deletions

File tree

controllers/packagevariants/pkg/controllers/packagevariant/packagevariant_controller_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1615,19 +1615,19 @@ func TestIsValidUpstram(t *testing.T) {
16151615
}
16161616

16171617
func TestGetUpstreamPr(t *testing.T) {
1618-
pvReconcier := PackageVariantReconciler{}
1618+
pvReconciler := PackageVariantReconciler{}
16191619
upstream := api.Upstream{}
16201620
prList := porchapi.PackageRevisionList{}
16211621

1622-
_, err := pvReconcier.getUpstreamPR(&upstream, &prList)
1622+
_, err := pvReconciler.getUpstreamPR(&upstream, &prList)
16231623
assert.True(t, strings.HasPrefix(err.Error(), "could not find upstream package revision"))
16241624

16251625
upstream.Repo = "my-repo"
16261626
upstream.Package = "my-package"
16271627
upstream.WorkspaceName = "my-workspace"
16281628

16291629
prList.Items = append(prList.Items, porchapi.PackageRevision{})
1630-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1630+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
16311631
assert.True(t, strings.HasPrefix(err.Error(), "could not find upstream package revision"))
16321632

16331633
prList.Items = append(prList.Items, porchapi.PackageRevision{
@@ -1638,7 +1638,7 @@ func TestGetUpstreamPr(t *testing.T) {
16381638
Revision: 1,
16391639
},
16401640
})
1641-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1641+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
16421642
assert.True(t, strings.HasPrefix(err.Error(), "could not find upstream package revision"))
16431643

16441644
prList.Items = append(prList.Items, porchapi.PackageRevision{
@@ -1649,10 +1649,10 @@ func TestGetUpstreamPr(t *testing.T) {
16491649
Revision: 1,
16501650
},
16511651
})
1652-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1652+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
16531653
assert.True(t, strings.HasPrefix(err.Error(), "could not find upstream package revision"))
16541654

1655-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1655+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
16561656
assert.True(t, strings.HasPrefix(err.Error(), "could not find upstream package revision"))
16571657
prList.Items = append(prList.Items, porchapi.PackageRevision{
16581658
Spec: porchapi.PackageRevisionSpec{
@@ -1662,14 +1662,14 @@ func TestGetUpstreamPr(t *testing.T) {
16621662
Revision: 1,
16631663
},
16641664
})
1665-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1665+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
16661666
assert.True(t, err == nil)
16671667

16681668
upstream.WorkspaceName = ""
1669-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1669+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
16701670
assert.True(t, strings.HasPrefix(err.Error(), "could not find upstream package revision"))
16711671

1672-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1672+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
16731673
assert.True(t, strings.HasPrefix(err.Error(), "could not find upstream package revision"))
16741674

16751675
prList.Items = append(prList.Items, porchapi.PackageRevision{
@@ -1679,11 +1679,11 @@ func TestGetUpstreamPr(t *testing.T) {
16791679
WorkspaceName: "main",
16801680
},
16811681
})
1682-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1682+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
16831683
assert.True(t, err == nil)
16841684

16851685
upstream.Revision = -1
1686-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1686+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
16871687
assert.True(t, err == nil)
16881688

16891689
upstream.Repo = "my-repo2"
@@ -1713,7 +1713,7 @@ func TestGetUpstreamPr(t *testing.T) {
17131713
Revision: -1,
17141714
},
17151715
})
1716-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1716+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
17171717
assert.True(t, err != nil)
17181718

17191719
prList.Items = append(prList.Items, porchapi.PackageRevision{
@@ -1724,7 +1724,7 @@ func TestGetUpstreamPr(t *testing.T) {
17241724
Revision: -1,
17251725
},
17261726
})
1727-
_, err = pvReconcier.getUpstreamPR(&upstream, &prList)
1727+
_, err = pvReconciler.getUpstreamPR(&upstream, &prList)
17281728
assert.True(t, err == nil)
17291729
}
17301730

docs/content/en/docs/4_tutorials_and_how-tos/working_with_package_revisions/_index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ Common issues when working with PackageRevisions and their solutions:
163163
- Verify service account has proper roles for PackageRevision operations
164164
- Ensure repository authentication is configured correctly
165165

166+
**Errors about "placeholder package revision"?**
167+
168+
- Clone, edit/copy, and upgrade operations cannot be performed on or using placeholder PackageRevisions
169+
- (identified by revision number -1 and workspace name matching the repository's Git branch, typically 'main')
170+
- Use published PackageRevisions with specific version numbers in these operations
171+
- For more information on placeholder PackageRevisions, see [Core Concepts]({{% relref "/docs/2_concepts#core-concepts" %}})
172+
166173
**Pipeline functions failing?**
167174

168175
- Check function image availability and version

docs/content/en/docs/4_tutorials_and_how-tos/working_with_package_revisions/cloning-packages.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,12 @@ Common issues when cloning PackageRevisions and how to resolve them.
269269
- Check the exact name including repository, package, and workspace
270270
- Ensure you have permission to read the source PackageRevision
271271

272+
**Clone fails with "upstream revision may not be the placeholder package revision":**
273+
274+
- Using the upstream package's placeholder PackageRevision as the upstream PackageRevision is not supported
275+
- See [Core Concepts]({{% relref "/docs/2_concepts#core-concepts" %}}) for the rules which identify a placeholder PackageRevision
276+
- Choose a different PackageRevision from the same package to use as the upstream PackageRevision
277+
272278
**Clone fails with "workspace already exists"?**
273279

274280
- The workspace name must be unique within the package in the target repository

docs/content/en/docs/4_tutorials_and_how-tos/working_with_package_revisions/copying-packages.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,12 @@ Common issues when copying PackageRevisions and how to resolve them.
272272
- Ensure you have permission to read the source PackageRevision
273273
- Ensure the source is in the same repository (copy only works within the same repository)
274274

275+
**Copy fails with "source revision may not be the placeholder package revision":**
276+
277+
- Using the package's placeholder PackageRevision as the source PackageRevision is not supported
278+
- See [Core Concepts]({{% relref "/docs/2_concepts#core-concepts" %}}) for the rules which identify a placeholder PackageRevision
279+
- Choose a different PackageRevision from the same package to use as the source PackageRevision
280+
275281
**Copied PackageRevision has unexpected content:**
276282

277283
- The copy includes all resources from the source at the time of copying

docs/content/en/docs/4_tutorials_and_how-tos/working_with_package_revisions/upgrading-packages.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ For more details, run `porchctl rpkg upgrade --help`.
329329

330330
* **Separate Repositories:** For better organization and access control, keep blueprint packages and deployment packages in separate Git repositories.
331331
* **Understand Your Strategy:** Before upgrading, be certain which merge strategy fits your use case to avoid accidentally losing important local customizations. When in doubt, the default `resource-merge` is the safest and most intelligent option.
332+
* **Use Fixed Revisions:** Do not upgrade packages' placeholder/main package revisions or attempt to use them as upstream for an upgrade - this is not supported. Select defined package revisions instead - see [Core Concepts]({{% relref "/docs/2_concepts#core-concepts" %}}) for the rules which identify a placeholder package revision
332333

333334
### Cleanup
334335

docs/content/en/docs/5_architecture_and_components/engine/functionality/validation-business-rules.md

Lines changed: 134 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,7 @@ CreatePackageRevision
209209

210210
## Clone Task Validation
211211

212-
The Engine validates clone tasks to prevent creating duplicate packages:
213-
214-
### Clone Constraint Check
212+
The Engine validates clone tasks to prevent creating invalid packages:
215213

216214
```
217215
Clone Task Validation
@@ -226,9 +224,19 @@ Clone Task Validation
226224
227225
Continue
228226
227+
Resolve Repository Containing Proposed Upstream
228+
229+
Upstream is Placeholder? ──Yes──> Reject
230+
231+
No
232+
229233
Allow Clone
230234
```
231235

236+
### Clone Constraint Check: Package Uniqueness In Repository
237+
238+
The package name must be unique in the repository to avoid creating duplicate packages.
239+
232240
**Validation process:**
233241
1. **List all revisions** in the repository
234242
2. **Check for existing package** with same name
@@ -243,6 +251,24 @@ Clone Task Validation
243251
- Existing packages should use `edit` or `copy` for new revisions
244252
- Prevents accidental overwriting of existing packages
245253

254+
### Clone Constraint Check: Exclude Placeholder Package Revision
255+
256+
The upstream package revision cannot be a placeholder package revision (identified by `Revision == -1` and `WorkspaceName` matching the Git repository's branch)
257+
258+
**Validation process:**
259+
1. **Resolve repository details** for the upstream package revision
260+
2. **Check if placeholder** (revision == -1 and WorkspaceName matches repo Git branch)
261+
3. **Reject if placeholder** package revision
262+
4. **Allow if not placeholder**
263+
264+
**Error message:**
265+
- "upstream revision may not be the placeholder package revision {repo}/{name}"
266+
267+
**Rationale:**
268+
- Placeholder package revisions represent the main/branch-HEAD state
269+
- they are not fixed revisions, making them unsuitable for cloning
270+
- Operations would fail in later lifecycle stages
271+
246272
### Clone vs Edit/Copy
247273

248274
**When to use clone:**
@@ -255,9 +281,72 @@ Clone Task Validation
255281
- Package already exists in repository
256282
- Iterating on existing package
257283

284+
## Edit Task Validation
285+
286+
The Engine validates edit tasks to ensure source revisions meet requirements:
287+
288+
```
289+
Edit Task Validation
290+
291+
Fetch Source Revision
292+
293+
Check Same Package? ──No──> Reject
294+
295+
Yes
296+
297+
Check if Placeholder? ──Yes──> Reject
298+
299+
No
300+
301+
Check Published? ──No──> Reject
302+
303+
Yes
304+
305+
Allow Edit
306+
```
307+
308+
### Edit Constraint Check: Source Package Revision Validation
309+
310+
The new package revision must be from the same package as the source package revision since we are iterating on the existing package.
311+
312+
**Validation process:**
313+
1. **Fetch source revision** from specified reference
314+
2. **Verify same package** (same repository and package name)
315+
3. **Check if published** (only published revisions can be edited)
316+
4. **Allow if all checks pass**
317+
318+
**Error messages:**
319+
- "source revision must be from same package {repo}/{package}"
320+
- "source revision must be published"
321+
322+
**Rationale:**
323+
- Edit creates new revisions from existing packages in the same repository
324+
- Placeholder package revisions represent unstable main branch state
325+
- Only published revisions provide stable source for editing
326+
327+
### Edit Constraint Check: Exclude Placeholder Package Revision
328+
329+
The source package revision cannot be a placeholder package revision (identified by `Revision == -1` and `WorkspaceName` matching the Git repository's branch)
330+
331+
332+
**Validation process:**
333+
1. **Fetch source revision** from specified reference
334+
2. **Resolve repository details** for the source package revision
335+
3. **Check if placeholder** (Revision == -1 and WorkspaceName matches repo Git branch)
336+
4. **Reject if placeholder** package revision
337+
5. **Allow if not placeholder**
338+
339+
**Error message:**
340+
- "source revision may not be the placeholder package revision {repo}/{name}"
341+
342+
**Rationale:**
343+
- Placeholder package revisions are not fixed revisions
344+
- Editing from unstable main/branch-HEAD state is not supported
345+
- Prevents creating revisions from non-deterministic sources
346+
258347
## Upgrade Task Validation
259348

260-
The Engine validates upgrade tasks to ensure source revisions are published:
349+
The Engine validates upgrade tasks to ensure the three source revisions meet requirements:
261350

262351
### Upgrade Source Validation
263352

@@ -280,6 +369,18 @@ Upgrade Task Validation
280369
281370
Continue
282371
372+
Resolve Repository Containing Proposed New Upstream
373+
374+
Upstream is Placeholder? ──Yes──> Reject
375+
376+
No
377+
378+
Resolve Repository Containing Local Revision
379+
380+
Local Revision is Placeholder? ──Yes──> Reject
381+
382+
No
383+
283384
Allow Upgrade
284385
```
285386

@@ -304,15 +405,42 @@ Upgrade Task Validation
304405
### Upgrade Source Requirements
305406

306407
**Required sources:**
307-
- **OldUpstream**: The upstream version currently used
308-
- **NewUpstream**: The upstream version to upgrade to
408+
- **OldUpstream**: The package's current upstream version
409+
- **NewUpstream**: The upstream version to which to upgrade
309410
- **LocalPackageRevision**: The local package revision to upgrade
310411

311412
**All sources must be:**
312413
- Published lifecycle state
313414
- Accessible in repository
314415
- Valid package revisions
315416

417+
### Placeholder Package Revision Check
418+
419+
Neither the target upstream package revision (the new revision being upgraded to) nor the package revision specified for upgrade can be a placeholder package revision (identified by `Revision == -1` and `WorkspaceName` matching the repository's Git branch)
420+
421+
**Validation process:**
422+
1. **Fetch target upstream revision** (NewUpstream)
423+
2. **Resolve repository details** for the target upstream package revision
424+
3. **Check if placeholder** (revision == -1 and workspaceName matches repo Git branch)
425+
4. **Reject if target upstream is placeholder**
426+
5. **Fetch local package revision** to be upgraded
427+
6. **Resolve repository details** for the local package revision
428+
7. **Check if placeholder** (revision == -1 and workspaceName matches repo Git branch)
429+
8. **Reject if local revision is placeholder**
430+
9. **Allow if neither is placeholder**
431+
432+
**Error messages:**
433+
- "target upstream revision may not be the placeholder package revision {repo}/{name}"
434+
- "the placeholder package revision {repo}/{name} may not be upgraded"
435+
436+
**Rationale:**
437+
- Upgrade performs three-way merge requiring stable source revisions
438+
- Placeholder package revisions represent unstable main branch state
439+
- Using placeholder revisions would produce non-deterministic upgrade results
440+
- Prevents upgrading to non-fixed revision states
441+
- Validation on clone and edit operations precludes possibility of old upstream being a placeholder
442+
443+
316444
## Package Path Overlap Validation
317445

318446
The Engine validates package paths to prevent nested packages:

pkg/repository/util.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
package repository
1616

1717
import (
18+
"context"
1819
"fmt"
1920
"strconv"
2021
"strings"
2122

2223
kptfilev1 "github.com/kptdev/kpt/pkg/api/kptfile/v1"
2324
porchapi "github.com/nephio-project/porch/api/porch/v1alpha1"
25+
configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
2426
"github.com/nephio-project/porch/pkg/util"
27+
pkgerrors "github.com/pkg/errors"
2528
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2629
)
2730

@@ -188,3 +191,21 @@ func PathsOverlap(path1, path2 string) bool {
188191
}
189192
return false
190193
}
194+
195+
func PackageRevisionIsPlaceholder(ctx context.Context, namespace string, referenceResolver ReferenceResolver, packageRevision PackageRevision) (bool, error) {
196+
if packageRevision.Key().Revision != -1 {
197+
return false, nil
198+
}
199+
200+
var upstreamRepo configapi.Repository
201+
err := referenceResolver.ResolveReference(ctx, namespace, packageRevision.Key().RKey().Name, &upstreamRepo)
202+
if err != nil {
203+
return false, pkgerrors.Wrapf(err, "failed to resolve repository reference for %q when checking placeholder revision: %v", packageRevision.Key().RKey().Name, err)
204+
}
205+
206+
if upstreamRepo.Spec.Git != nil && packageRevision.Key().WorkspaceName == upstreamRepo.Spec.Git.Branch {
207+
return true, nil
208+
}
209+
210+
return false, nil
211+
}

0 commit comments

Comments
 (0)