Skip to content

Reduce code duplication across rpkg CLI commands#541

Open
thc1006 wants to merge 1 commit into
kptdev:mainfrom
thc1006:fix/issue-1067-shared-helpers
Open

Reduce code duplication across rpkg CLI commands#541
thc1006 wants to merge 1 commit into
kptdev:mainfrom
thc1006:fix/issue-1067-shared-helpers

Conversation

@thc1006
Copy link
Copy Markdown
Contributor

@thc1006 thc1006 commented Apr 16, 2026

Title

Reduce code duplication across rpkg CLI commands


Description

  • What changed:
    Extract three shared helpers to pkg/cli/commands/rpkg/util/common.go:

    1. InitClient -- wraps CreateClientWithFlags with namespace flag validation (ported from get/command.go). Applied to approve, propose, proposedelete, reject, and del.
    2. CreateScheme -- consolidates the identical local createScheme() that was duplicated in both pull and push.
    3. RunForEachPackage -- extracts the retry-with-error-collection loop shared by approve, propose, proposedelete, and reject. Each command now provides only its lifecycle-specific logic as a callback.
  • Why it's needed:
    The five lifecycle commands (approve, propose, proposedelete, reject, del) had near-identical boilerplate: same runner struct, same preRunE with CreateClientWithFlags, same retry loop, same error collection pattern. Any bug fix (like the lastErr workaround from porch#489) had to be applied identically across all of them. Additionally, namespace validation was only present in the get command; the other commands silently accepted --namespace with no value.

  • How it works:
    InitClient checks the namespace flag first, then creates the client. RunForEachPackage takes a PackageAction callback that receives the fetched PackageRevision and returns a success message string. The helper handles retry, error collection, and output formatting. Commands that had side effects in the retry closure (propose and proposedelete printing "already proposed" to stderr) still do so inside the callback; they return ("", nil) to skip the standard success output.


Related Issue(s)


Type of Change

  • Bug fix
  • New feature
  • Enhancement
  • Refactor
  • Documentation
  • Tests
  • Other: ________

Checklist

  • Code follows project style guidelines
  • Self-reviewed changes
  • Tests added/updated
  • Documentation added/updated
  • All tests and gating checks pass

Testing Instructions

  1. go test ./pkg/cli/commands/rpkg/... -count=1 -- all 14 packages should pass
  2. golangci-lint run ./pkg/cli/commands/rpkg/... -- 0 issues
  3. Verify namespace validation: any rpkg subcommand with --namespace (no value) should now return an error

Additional Notes

  • del keeps its own runE (no retry, no Get, constructs object for Delete). Only adopts InitClient.
  • get is unchanged (already has namespace validation, uses resource builder pattern).
  • clone/copy/init/upgrade/pull/push runE are unchanged (unique patterns, minimal duplication).
  • The success-message inconsistency (propose/proposedelete printed inside retry closure; approve/reject printed outside) is resolved: all four now return the message from the callback.
  • kispaljr's NOTE comment in reject about UpdatePackageRevisionApproval has been resolved and removed: both the Proposed -> Draft and DeletionProposed -> Published paths now call cliutils.UpdatePackageRevisionApproval, which answers the question the NOTE raised. The reject test gains a SubResourceUpdate interceptor to cover the new code path.

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 16, 2026

Deploy Preview for porch ready!

Name Link
🔨 Latest commit 78708b3
🔍 Latest deploy log https://app.netlify.com/projects/porch/deploys/6a02bda93510090008d2a0ca
😎 Deploy Preview https://deploy-preview-541--porch.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@thc1006 thc1006 force-pushed the fix/issue-1067-shared-helpers branch 14 times, most recently from c075fbb to c8863a0 Compare April 20, 2026 17:37
Comment thread pkg/cli/commands/rpkg/util/common.go Outdated
Comment thread pkg/cli/commands/rpkg/reject/command.go Outdated
Comment thread pkg/cli/commands/rpkg/reject/command.go Outdated
@thc1006 thc1006 force-pushed the fix/issue-1067-shared-helpers branch 2 times, most recently from f9836dc to 8fad5c3 Compare April 20, 2026 18:55
@thc1006 thc1006 requested a review from efiacor April 20, 2026 18:55
@liamfallon
Copy link
Copy Markdown
Collaborator

I reran the e2e tests because one of the flaky e2e tests failed.

@mozesl-nokia
Copy link
Copy Markdown
Collaborator

This PR goes hand-in-hand with #592, I think we should get this in first and then I will rewrite #592

Copy link
Copy Markdown
Collaborator

@mozesl-nokia mozesl-nokia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work, just added my usual test-related comments

Comment thread pkg/cli/commands/rpkg/util/common.go Outdated
Comment thread pkg/cli/commands/rpkg/util/common.go Outdated
Comment thread pkg/cli/commands/rpkg/util/common_test.go Outdated
Comment thread pkg/cli/commands/rpkg/util/common_test.go Outdated
Comment thread pkg/cli/commands/rpkg/util/common_test.go
Comment thread pkg/cli/commands/rpkg/util/common_test.go Outdated
Comment thread pkg/cli/commands/rpkg/util/common_test.go Outdated
@thc1006 thc1006 force-pushed the fix/issue-1067-shared-helpers branch from 8fad5c3 to 42b2021 Compare April 27, 2026 19:35
@thc1006
Copy link
Copy Markdown
Contributor Author

thc1006 commented Apr 27, 2026

@mozesl-nokia review feedback addressed in the latest push, branch rebased on main. Highlights:

  • CreateScheme now registers corev1, configapi, and metav1 alongside porchapi, matching cliutils.createScheme.
  • RunForEachPackage takes a cmdName argument; callers pass their command const so op tags become cmdrpkg<verb>.runE, consistent with the rest of the package.
  • Test assertions cleaned up per the suggestions.

@liamfallon CI should be green again after the rebase.

@thc1006 thc1006 requested a review from mozesl-nokia April 27, 2026 19:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings May 6, 2026 17:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@thc1006
Copy link
Copy Markdown
Contributor Author

thc1006 commented May 6, 2026

Hi @Catalin-Stratulat-Ericsson, thank you for your patience while I got the rebase and verification in place. Following up here on the backwards compatibility matrix, as promised in my earlier reply.

Now that main has been merged into this branch (so the v1alpha2 work from #514 is incorporated), and the Porch CLI E2E and Porch E2E test suites have completed successfully, I can confirm that the user-visible behavior of the rpkg lifecycle commands remains unchanged from main: the same flag set, the same stdout/stderr message format, and the same error wrapping. SonarCloud's Quality Gate is also passing on this revision, with the new helper code fully covered by tests.

If there is a more specific backwards compatibility matrix that you had in mind beyond the existing E2E coverage, please don't hesitate to let me know — I would be very glad to run it before merge.

Thank you very much again for the thoughtful and careful review.

Copilot AI review requested due to automatic review settings May 11, 2026 06:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Comment thread pkg/cli/commands/rpkg/util/common.go Outdated
Comment thread pkg/cli/commands/rpkg/util/common_test.go Outdated
Comment thread pkg/cli/commands/rpkg/reject/command.go
Comment thread sonar-project.properties Outdated
Copy link
Copy Markdown
Collaborator

@liamfallon liamfallon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the direction you are going with the implementation, it's very nice.

It would be great if you

  • Could apply the "Action" pattern to all the commands
  • Also handle the commands for the new interface as this will be the default interface soon
  • Consider defining the "Action" using an interface, I'm not sure if it's feasible but it might be worth looking at.

Comment thread pkg/cli/commands/rpkg/del/command.go Outdated
Comment thread pkg/cli/commands/rpkg/pull/command.go Outdated
Comment thread pkg/cli/commands/rpkg/push/command.go Outdated
Comment thread pkg/cli/commands/rpkg/del/command.go
Copilot AI review requested due to automatic review settings May 11, 2026 09:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (3)

pkg/cli/commands/rpkg/pull/command.go:88

  • pull's preRunE bypasses rpkgutil.InitClient and therefore does not perform the new namespace-flag empty-value validation. As a result, --namespace= can still slip through for this subcommand even though other lifecycle commands now error early. Consider validating the namespace flag here as well (e.g., call a shared validation helper or switch to rpkgutil.InitClient).
func (r *runner) preRunE(_ *cobra.Command, _ []string) error {
	const op errors.Op = command + ".preRunE"
	config, err := r.Cfg.ToRESTConfig()
	if err != nil {
		return errors.E(op, err)
	}

	scheme, err := rpkgutil.CreateScheme()
	if err != nil {
		return errors.E(op, err)
	}

	c, err := client.New(config, client.Options{Scheme: scheme})
	if err != nil {

pkg/cli/commands/rpkg/push/command.go:93

  • push's preRunE bypasses rpkgutil.InitClient and therefore does not perform the new namespace-flag empty-value validation. This leaves rpkg push inconsistent with the other rpkg subcommands that now fail fast for --namespace=. Consider adding the same validation here (shared helper) or using rpkgutil.InitClient for client creation.
func (r *runner) preRunE(_ *cobra.Command, _ []string) error {
	const op errors.Op = command + ".preRunE"
	config, err := r.Cfg.ToRESTConfig()
	if err != nil {
		return errors.E(op, err)
	}

	scheme, err := rpkgutil.CreateScheme()
	if err != nil {
		return errors.E(op, err)
	}

	c, err := client.New(config, client.Options{Scheme: scheme})
	if err != nil {

pkg/cli/commands/rpkg/del/command.go:75

  • rpkg del's Use string indicates a required PACKAGE argument, but runE does not validate len(args) > 0. Invoking rpkg del with no positional args will currently exit successfully without doing anything. Consider adding an explicit args check (or cobra Args: validator) so misuse fails fast with a helpful error.
	c := &cobra.Command{
		Use:        "del PACKAGE",
		Aliases:    []string{"delete"},
		SuggestFor: []string{},
		Short:      docs.DelShort,
		Long:       docs.DelShort + "\n" + docs.DelLong,
		Example:    docs.DelExamples,
		PreRunE:    rpkgutil.MakePreRunE(command+".preRunE", rcg, &r.Client),
		RunE:       r.runE,
		Hidden:     cliutils.HidePorchCommands,
	}
	r.Command = c

	// Create flags

	return r
}

func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
	v1 := newRunner(ctx, rcg)
	v2 := newV1Alpha2Runner(ctx, rcg)
	cliutils.WrapVersionDispatch(v1.Command, v2.preRunE, v2.runE)
	return v1.Command
}

type runner struct {
	rpkgutil.Runner
}

func (r *runner) runE(_ *cobra.Command, args []string) error {
	const op errors.Op = command + ".runE"
	var messages []string

	for _, pkg := range args {
		pr := &porchapi.PackageRevision{

Comment thread pkg/cli/commands/rpkg/util/common.go Outdated
Comment thread pkg/cli/commands/rpkg/approve/command.go
Comment thread pkg/cli/commands/rpkg/proposedelete/command.go
Comment thread pkg/cli/commands/rpkg/reject/command.go Outdated
Comment thread pkg/cli/commands/rpkg/util/common_test.go Outdated
@thc1006
Copy link
Copy Markdown
Contributor Author

thc1006 commented May 11, 2026

Dear @liamfallon, thank you very much for the thorough review and for taking the time to merge main into the branch.

Your three inline suggestions on del, pull, and push are applied in the latest commit. Each runner now embeds rpkgutil.Runner, with push and pull keeping their printer field on top of the embed.

The 0% preRunE coverage on pull, push, and reject is also addressed. New tests exercise the wiring with a kubeconfig fixture, bringing those files up to 76.9% (pull, push) and 87.5% (reject). Overall new code coverage is now 94.3%, up from 91.4%.

On your three "It would be great" points: the PackageAction signature in this PR (fetch existing PackageRevision, mutate, return a success string) does not fit clone, copy, get, init, or upgrade. clone, copy, and upgrade construct new PackageRevisions, init creates from nothing, and get is a list operation. A more general Action interface would be the right home for them, which dovetails with your third point about defining Action as an interface. May I open a tracking issue to design that, together with mirroring the pattern onto the v1alpha2 commands from #514? Keeping both as a follow up keeps this PR scope clean.

Copilot ran another pass and raised five additional items, all addressed in commit fff2972. For transparency, two of them produce error output where the previous code returned silently: reject now says "PACKAGE" rather than "PACKAGE_REVISION" in its missing argument error, and approve and propose-delete now reject empty argument invocations explicitly, matching reject's existing behaviour. Both paths correspond to invalid CLI usage, so the backwards compatibility matrix is not affected.

SonarCloud currently reports 10.8% Duplication on New Code (36 of 334 lines). The duplicated lines are the rpkgutil import plus the Runner struct embedding pattern, repeating across the seven lifecycle command files by design rather than accidental copy paste. This feels like the situation you described earlier where showing the duplication and considering a waiver might be more appropriate than restructuring further. I would be very grateful for your guidance, whether that is a waiver or a more aggressive structural refactor.

Thank you again for the thoughtful review and the clear direction.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

pkg/cli/commands/rpkg/del/command.go:75

  • rpkg del still succeeds silently when no PACKAGE args are provided (the loop just doesn’t run). Other lifecycle commands now fail fast via RunForEachPackage’s positional-arg check, and the docs describe at least one package name. Consider adding a len(args)==0 validation (either in runE or a small wrapper PreRunE) so porchctl rpkg del returns a clear error instead of success.
func (r *runner) runE(_ *cobra.Command, args []string) error {
	const op errors.Op = command + ".runE"
	var messages []string

	for _, pkg := range args {
		pr := &porchapi.PackageRevision{

Comment thread pkg/cli/commands/rpkg/util/common.go Outdated
Comment thread pkg/cli/commands/rpkg/util/common.go Outdated
Copilot AI review requested due to automatic review settings May 12, 2026 05:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

@liamfallon
Copy link
Copy Markdown
Collaborator

Hi @thc1006,

Would you mind putting a comment after each of the copilot issues above that you have resolved explaining how you have resolved them and then park them resolved.

I think it's OK to take the creation of an "action" interface and also the v1alpha2 work as separate PRs. We'll be discussing the PRs during our community meeting tomorrow and I'll seek the community's opinion on that.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
10.6% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@thc1006
Copy link
Copy Markdown
Contributor Author

thc1006 commented May 12, 2026

Dear @liamfallon, thank you very much for the clear direction and for taking the v1alpha2 and action interface questions to the community meeting.

I have just gone through each unresolved Copilot thread above and added a per-thread comment describing how the issue was resolved, with the commit hash for traceability. The threads are now marked as resolved.

I will not make further code changes while the community meeting is pending. Once direction is set, I will open a tracking issue for the action interface work and another for the v1alpha2 mirroring, both as separate follow-up PRs. The duplication on new code is currently at 10.6%; I am very happy to follow your lead on a waiver once the meeting outcome is clear.

Thank you again for the thoughtful review.

@liamfallon
Copy link
Copy Markdown
Collaborator

The agenda for tomorrow's meeting is here. The meeting is at 13:00 UTC (which I know may not be convenient in your timezone). We have moved Porch meetings across to the kpt CNCF project as Porch is moving to kpt. The link to themeeting is at the bottomo of the kpt project page. The meeting is two hours long in the calendar, the first hour is for general community discussions and the second hour is the technical meeting. If you can, feel free to join both meetings but the second hour is the meeting where your PR will be discussed.

@liamfallon
Copy link
Copy Markdown
Collaborator

Community meeting discussion:

Thanks very much @thc1006 for the work.

  • Using multiple PRs to implement code improvements is fine
  • Once all the PRs are in, we should have consistent use of the same interfaces for all commands
  • v1alpaha2 should also be done at the end

@thc1006
Copy link
Copy Markdown
Contributor Author

thc1006 commented May 14, 2026

Dear @liamfallon, thanks for relaying the meeting outcome and for the kind call-out.

Understood: keep this PR focused on the v1alpha1 refactor, follow up PRs converge the other commands onto a shared interface, and v1alpha2 mirrors the pattern at the end. I'll open the tracking issues once this one is in.

I've just rebased onto the latest kptdev/porch main, so the #981 copyright changes are picked up and CI is back to green.

Thanks again to everyone at the meeting.

Extract shared helpers to pkg/cli/commands/rpkg/util/common.go and apply
them across the seven rpkg lifecycle commands (approve, propose,
proposedelete, reject, del, pull, push):

- InitClient: wraps cliutils.CreateClientWithFlags with namespace flag
  validation. Previously only the get command checked for an empty
  --namespace flag; this now applies to approve, propose,
  proposedelete, reject and del as well. Also rejects a nil cfg up
  front so callers cannot trigger a panic inside ToRESTConfig.

- CreateScheme: consolidates the identical local createScheme()
  function that was duplicated in both pull and push, registering
  porchapi, porchconfig, corev1 and metav1 so callers see the same
  kinds as cliutils.CreateClientWithFlags.

- MakePreRunE: returns a cobra PreRunE closure that validates
  namespace and creates the client. Eliminates duplicate preRunE
  methods from approve, propose, proposedelete and del.

- RunForEachPackage: extracts the retry-with-error-collection loop
  shared by approve, propose, proposedelete and reject. Each command
  now provides only its lifecycle-specific logic as a callback. The
  lastErr workaround for k8s retry library behaviour is included in
  the shared helper. Per-iteration body is further split into
  fetchAndAct and reportResult helpers for clarity. Behavioural
  options are grouped into RunForEachOpts; CmdName is required and
  rejected up front if empty.

- Runner struct + NewTestRunner: shared fields (Ctx, Cfg, Client,
  Command) embedded into each command's runner. NewTestRunner builds
  a Runner pre-wired for table-driven CLI tests. Ctx is intentionally
  stored on the struct, matching the existing per-command runner
  convention (see SonarQube godre:S8242 -- documented rather than
  refactored).

The seven lifecycle command files (approve, propose, proposedelete,
reject, del, pull, push) all embed rpkgutil.Runner. pull and push
retain their printer field on top of the embed.

Behavioural improvements that surfaced during the refactor:
- approve and propose-delete now reject empty argument invocations
  with "PACKAGE is a required positional argument", matching reject's
  existing behaviour. The reject error message itself is aligned to
  say PACKAGE rather than PACKAGE_REVISION, matching the Use string.
- reject's DeletionProposed -> Published path now uses
  UpdatePackageRevisionApproval like the Proposed -> Draft path,
  resolving the inconsistency kispaljr's NOTE comment had pointed at.
- pull, push and reject preRunE gain explicit unit tests that
  exercise the cfg.ToRESTConfig + CreateScheme + client.New wiring
  through a kubeconfig fixture, raising coverage on those files from
  0% to 76.9% (pull/push) and 87.5% (reject).

Fixes kptdev#870
Fixes kptdev#900

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006 thc1006 force-pushed the fix/issue-1067-shared-helpers branch from 28ba81b to 7a61c9a Compare May 14, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reduce code duplication across rpkg CLI commands [BUG]: Porchctl does not report an error when namespace flag specified without a value

6 participants