Skip to content

Commit ee763f8

Browse files
thc1006liamfallon
andauthored
Reduce code duplication across rpkg CLI commands (#541)
* Reduce code duplication across rpkg CLI commands 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 #870 Fixes #900 Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * Address @mozesl-nokia post-approval review feedback Two cosmetic items raised after @liamfallon's approval are addressed in this commit. 1. approve/command_test.go used "nephio.org.Specializer.specialize" as the ReadinessGate fixture string, which lingered from the branch state before #981 swept the rest of the repository. The string is pure test data: the test only verifies that the ConditionType on a ReadinessGate matches a Condition.Type with status False. Aligned to "kpt.dev.Specializer.specialize" so it matches what #981 settled on for the rest of the codebase. 2. writeTempKubeconfig was duplicated across pull, push and reject test files. Extracted as the exported rpkgutil.WriteTempKubeconfig in a new pkg/cli/commands/rpkg/util/testhelpers.go, matching the existing testhelpers_v1alpha2.go pattern. The three test files now call the shared helper, and the now-unused "os" import is dropped from pull/command_test.go. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> --------- Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> Co-authored-by: Liam Fallon <35595825+liamfallon@users.noreply.github.com>
1 parent 94ad17d commit ee763f8

17 files changed

Lines changed: 914 additions & 504 deletions

File tree

pkg/cli/commands/rpkg/approve/command.go

Lines changed: 14 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,23 @@ package approve
1717
import (
1818
"context"
1919
"fmt"
20-
"strings"
2120

22-
"github.com/kptdev/kpt/pkg/lib/errors"
2321
porchapi "github.com/kptdev/porch/api/porch/v1alpha1"
2422
cliutils "github.com/kptdev/porch/internal/cliutils"
2523
"github.com/kptdev/porch/pkg/cli/commands/rpkg/docs"
24+
rpkgutil "github.com/kptdev/porch/pkg/cli/commands/rpkg/util"
2625
"github.com/spf13/cobra"
2726
"k8s.io/cli-runtime/pkg/genericclioptions"
28-
"k8s.io/client-go/util/retry"
2927
"sigs.k8s.io/controller-runtime/pkg/client"
3028
)
3129

3230
const (
3331
command = "cmdrpkgapprove"
3432
)
3533

34+
// NewCommand returns the cobra command for `rpkg approve`, which
35+
// transitions a package revision into the Published lifecycle state
36+
// after readiness gates have been satisfied.
3637
func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
3738
v1 := newRunner(ctx, rcg)
3839
v2 := newV1Alpha2Runner(ctx, rcg)
@@ -42,16 +43,15 @@ func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.
4243

4344
func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner {
4445
r := &runner{
45-
ctx: ctx,
46-
cfg: rcg,
46+
Runner: rpkgutil.Runner{Ctx: ctx, Cfg: rcg},
4747
}
4848

4949
c := &cobra.Command{
5050
Use: "approve PACKAGE",
5151
Short: docs.ApproveShort,
5252
Long: docs.ApproveShort + "\n" + docs.ApproveLong,
5353
Example: docs.ApproveExamples,
54-
PreRunE: r.preRunE,
54+
PreRunE: rpkgutil.MakePreRunE(command+".preRunE", rcg, &r.Client),
5555
RunE: r.runE,
5656
Hidden: cliutils.HidePorchCommands,
5757
}
@@ -61,70 +61,18 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
6161
}
6262

6363
type runner struct {
64-
ctx context.Context
65-
cfg *genericclioptions.ConfigFlags
66-
client client.Client
67-
Command *cobra.Command
68-
69-
// Flags
64+
rpkgutil.Runner
7065
}
7166

72-
func (r *runner) preRunE(_ *cobra.Command, _ []string) error {
73-
const op errors.Op = command + ".preRunE"
74-
75-
client, err := cliutils.CreateClientWithFlags(r.cfg)
76-
if err != nil {
77-
return errors.E(op, err)
67+
func approveAction(ctx context.Context, client client.Client, pr *porchapi.PackageRevision) (string, error) {
68+
if err := cliutils.UpdatePackageRevisionApproval(ctx, client, pr, porchapi.PackageRevisionLifecyclePublished); err != nil {
69+
return "", err
7870
}
79-
r.client = client
80-
return nil
71+
return fmt.Sprintf("%s approved", pr.Name), nil
8172
}
8273

8374
func (r *runner) runE(_ *cobra.Command, args []string) error {
84-
const op errors.Op = command + ".runE"
85-
var messages []string
86-
87-
namespace := *r.cfg.Namespace
88-
89-
for _, name := range args {
90-
key := client.ObjectKey{
91-
Namespace: namespace,
92-
Name: name,
93-
}
94-
var lastErr error
95-
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
96-
var pr porchapi.PackageRevision
97-
err := r.client.Get(r.ctx, key, &pr)
98-
if err != nil {
99-
lastErr = err
100-
return err
101-
}
102-
if !porchapi.PackageRevisionIsReady(pr.Spec.ReadinessGates, pr.Status.Conditions) {
103-
lastErr = fmt.Errorf("readiness conditions not met")
104-
return lastErr
105-
}
106-
err = cliutils.UpdatePackageRevisionApproval(r.ctx, r.client, &pr, porchapi.PackageRevisionLifecyclePublished)
107-
if err != nil {
108-
lastErr = err
109-
} else {
110-
lastErr = nil
111-
}
112-
return err
113-
})
114-
// Workaround for k8s retry library bug: OnError/RetryOnConflict sometimes returns nil even when errors occur
115-
if err == nil && lastErr != nil {
116-
err = lastErr
117-
}
118-
if err != nil {
119-
messages = append(messages, err.Error())
120-
fmt.Fprintf(r.Command.ErrOrStderr(), "%s failed (%s)\n", name, err)
121-
} else {
122-
fmt.Fprintf(r.Command.OutOrStdout(), "%s approved\n", name)
123-
}
124-
}
125-
if len(messages) > 0 {
126-
return errors.E(op, fmt.Errorf("errors:\n %s", strings.Join(messages, "\n ")))
127-
}
128-
129-
return nil
75+
return rpkgutil.RunForEachPackage(r.Ctx, r.Client, r.Command, *r.Cfg.Namespace, args,
76+
rpkgutil.RunForEachOpts{CmdName: command, WithRetry: true, CheckReadiness: true},
77+
approveAction)
13078
}

pkg/cli/commands/rpkg/approve/command_test.go

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,21 @@ import (
2222

2323
"github.com/google/go-cmp/cmp"
2424
porchapi "github.com/kptdev/porch/api/porch/v1alpha1"
25+
rpkgutil "github.com/kptdev/porch/pkg/cli/commands/rpkg/util"
2526
"github.com/spf13/cobra"
2627
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27-
"k8s.io/apimachinery/pkg/runtime"
2828
"k8s.io/cli-runtime/pkg/genericclioptions"
2929
"k8s.io/client-go/rest"
3030
"sigs.k8s.io/controller-runtime/pkg/client"
3131
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3232
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
3333
)
3434

35-
func createScheme() (*runtime.Scheme, error) {
36-
scheme := runtime.NewScheme()
37-
for _, api := range (runtime.SchemeBuilder{
38-
porchapi.AddToScheme,
39-
}) {
40-
if err := api(scheme); err != nil {
41-
return nil, err
42-
}
43-
}
44-
scheme.AddKnownTypes(porchapi.SchemeGroupVersion, &porchapi.PackageRevision{})
45-
return scheme, nil
46-
}
47-
4835
func TestCmd(t *testing.T) {
4936
pkgRevName := "test-pr"
5037
repoName := "test-repo"
5138
ns := "ns"
52-
var scheme, err = createScheme()
39+
var scheme, err = rpkgutil.CreateScheme()
5340
if err != nil {
5441
t.Fatalf("error creating scheme: %v", err)
5542
}
@@ -195,14 +182,7 @@ func TestCmd(t *testing.T) {
195182
os.Stdout = write
196183
os.Stderr = write
197184

198-
r := &runner{
199-
ctx: context.Background(),
200-
cfg: &genericclioptions.ConfigFlags{
201-
Namespace: &ns,
202-
},
203-
client: tc.fakeclient,
204-
Command: cmd,
205-
}
185+
r := &runner{Runner: rpkgutil.NewTestRunner(ns, tc.fakeclient, cmd)}
206186
go func() {
207187
defer write.Close()
208188
err := r.runE(cmd, []string{pkgRevName})
@@ -227,21 +207,15 @@ func TestCmd(t *testing.T) {
227207
// issues happen. The easiest way to trigger this in tests is to use an
228208
// unreachable cluster.
229209
func TestLastErrWorkaround(t *testing.T) {
230-
scheme, err := createScheme()
210+
scheme, err := rpkgutil.CreateScheme()
231211
if err != nil {
232212
t.Fatalf("error creating scheme: %v", err)
233213
}
234214
c, err := client.New(&rest.Config{Host: "https://127.0.0.1:1", Timeout: 1, TLSClientConfig: rest.TLSClientConfig{Insecure: true}}, client.Options{Scheme: scheme})
235215
if err != nil {
236216
t.Fatalf("error creating client: %v", err)
237217
}
238-
ns := "ns"
239-
r := &runner{
240-
ctx: context.Background(),
241-
cfg: &genericclioptions.ConfigFlags{Namespace: &ns},
242-
client: c,
243-
Command: &cobra.Command{},
244-
}
218+
r := &runner{Runner: rpkgutil.NewTestRunner("ns", c, &cobra.Command{})}
245219
err = r.runE(r.Command, []string{"test-pkg"})
246220
if err == nil {
247221
t.Fatal("expected error but got nil")

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

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ import (
2323
porchapi "github.com/kptdev/porch/api/porch/v1alpha1"
2424
cliutils "github.com/kptdev/porch/internal/cliutils"
2525
"github.com/kptdev/porch/pkg/cli/commands/rpkg/docs"
26+
rpkgutil "github.com/kptdev/porch/pkg/cli/commands/rpkg/util"
2627
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2728

2829
"github.com/spf13/cobra"
2930
"k8s.io/cli-runtime/pkg/genericclioptions"
30-
"sigs.k8s.io/controller-runtime/pkg/client"
3131
)
3232

3333
const (
@@ -36,8 +36,7 @@ const (
3636

3737
func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner {
3838
r := &runner{
39-
ctx: ctx,
40-
cfg: rcg,
39+
Runner: rpkgutil.Runner{Ctx: ctx, Cfg: rcg},
4140
}
4241
c := &cobra.Command{
4342
Use: "del PACKAGE",
@@ -46,7 +45,7 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
4645
Short: docs.DelShort,
4746
Long: docs.DelShort + "\n" + docs.DelLong,
4847
Example: docs.DelExamples,
49-
PreRunE: r.preRunE,
48+
PreRunE: rpkgutil.MakePreRunE(command+".preRunE", rcg, &r.Client),
5049
RunE: r.runE,
5150
Hidden: cliutils.HidePorchCommands,
5251
}
@@ -65,25 +64,14 @@ func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.
6564
}
6665

6766
type runner struct {
68-
ctx context.Context
69-
cfg *genericclioptions.ConfigFlags
70-
client client.Client
71-
Command *cobra.Command
72-
}
73-
74-
func (r *runner) preRunE(_ *cobra.Command, _ []string) error {
75-
const op errors.Op = command + ".preRunE"
76-
77-
client, err := cliutils.CreateClientWithFlags(r.cfg)
78-
if err != nil {
79-
return errors.E(op, err)
80-
}
81-
r.client = client
82-
return nil
67+
rpkgutil.Runner
8368
}
8469

8570
func (r *runner) runE(_ *cobra.Command, args []string) error {
8671
const op errors.Op = command + ".runE"
72+
if len(args) == 0 {
73+
return errors.E(op, fmt.Errorf("PACKAGE is a required positional argument"))
74+
}
8775
var messages []string
8876

8977
for _, pkg := range args {
@@ -93,12 +81,12 @@ func (r *runner) runE(_ *cobra.Command, args []string) error {
9381
APIVersion: porchapi.SchemeGroupVersion.Identifier(),
9482
},
9583
ObjectMeta: metav1.ObjectMeta{
96-
Namespace: *r.cfg.Namespace,
84+
Namespace: *r.Cfg.Namespace,
9785
Name: pkg,
9886
},
9987
}
10088

101-
if err := r.client.Delete(r.ctx, pr); err != nil {
89+
if err := r.Client.Delete(r.Ctx, pr); err != nil {
10290
messages = append(messages, err.Error())
10391
fmt.Fprintf(r.Command.ErrOrStderr(), "%s failed (%s)\n", pkg, err)
10492
} else {

pkg/cli/commands/rpkg/del/command_test.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/google/go-cmp/cmp"
2424
porchapi "github.com/kptdev/porch/api/porch/v1alpha1"
25+
rpkgutil "github.com/kptdev/porch/pkg/cli/commands/rpkg/util"
2526
"github.com/spf13/cobra"
2627
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2728
"k8s.io/apimachinery/pkg/runtime"
@@ -86,14 +87,7 @@ func TestCmd(t *testing.T) {
8687
os.Stdout = write
8788
os.Stderr = write
8889

89-
r := &runner{
90-
ctx: context.Background(),
91-
cfg: &genericclioptions.ConfigFlags{
92-
Namespace: &tc.ns,
93-
},
94-
client: c,
95-
Command: cmd,
96-
}
90+
r := &runner{Runner: rpkgutil.NewTestRunner(tc.ns, c, cmd)}
9791
go func() {
9892
defer write.Close()
9993
err := r.runE(cmd, []string{pkgRevName})
@@ -121,3 +115,17 @@ func TestNewCommand(t *testing.T) {
121115
t.Fatal("NewCommand returned nil")
122116
}
123117
}
118+
119+
func TestRunE_RequiresPackageArg(t *testing.T) {
120+
ns := "ns"
121+
scheme, err := rpkgutil.CreateScheme()
122+
if err != nil {
123+
t.Fatalf("error creating scheme: %v", err)
124+
}
125+
c := fake.NewClientBuilder().WithScheme(scheme).Build()
126+
r := &runner{Runner: rpkgutil.NewTestRunner(ns, c, &cobra.Command{})}
127+
128+
if err := r.runE(r.Command, nil); err == nil {
129+
t.Fatal("runE with no args must error")
130+
}
131+
}

0 commit comments

Comments
 (0)