Skip to content

Commit a54412e

Browse files
authored
Merge pull request #3539 from xrstf/kubectl-backwards-bc
Implement wrappers in CLI module to allow working with older API versions
2 parents 9482da8 + 7352c88 commit a54412e

File tree

6 files changed

+486
-79
lines changed

6 files changed

+486
-79
lines changed

cli/pkg/bind/plugin/bind.go

Lines changed: 81 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/spf13/cobra"
2828

2929
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/runtime/schema"
3031
"k8s.io/apimachinery/pkg/util/wait"
3132
"k8s.io/cli-runtime/pkg/genericclioptions"
3233
"k8s.io/client-go/rest"
@@ -35,6 +36,9 @@ import (
3536

3637
"github.com/kcp-dev/kcp/cli/pkg/base"
3738
pluginhelpers "github.com/kcp-dev/kcp/cli/pkg/helpers"
39+
apishelpers "github.com/kcp-dev/kcp/cli/pkg/helpers/apis/apis"
40+
"github.com/kcp-dev/kcp/sdk/apis/apis"
41+
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
3842
apisv1alpha2 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha2"
3943
kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster"
4044
)
@@ -143,77 +147,114 @@ func (b *BindOptions) Run(ctx context.Context) error {
143147
return err
144148
}
145149

146-
path, apiExportName := logicalcluster.NewPath(b.APIExportRef).Split()
147-
148-
// if a custom name is not provided, default it to <apiExportname>.
149-
apiBindingName := b.APIBindingName
150-
if apiBindingName == "" {
151-
apiBindingName = apiExportName
152-
}
153-
154150
_, currentClusterName, err := pluginhelpers.ParseClusterURL(config.Host)
155151
if err != nil {
156152
return fmt.Errorf("current URL %q does not point to workspace", config.Host)
157153
}
158154

159-
binding := &apisv1alpha2.APIBinding{
160-
ObjectMeta: metav1.ObjectMeta{
161-
Name: apiBindingName,
162-
},
163-
Spec: apisv1alpha2.APIBindingSpec{
164-
Reference: apisv1alpha2.BindingReference{
165-
Export: &apisv1alpha2.ExportBindingReference{
166-
Path: path.String(),
167-
Name: apiExportName,
168-
},
169-
},
170-
},
155+
preferredAPIBindingVersion, err := pluginhelpers.PreferredVersion(config, schema.GroupResource{
156+
Group: apis.GroupName,
157+
Resource: "apibindings",
158+
})
159+
if err != nil {
160+
return fmt.Errorf("service discovery failed: %w", err)
171161
}
172162

173-
if len(b.acceptedPermissionClaims) > 0 {
174-
binding.Spec.PermissionClaims = b.acceptedPermissionClaims
175-
}
176-
if len(b.rejectedPermissionClaims) > 0 {
177-
binding.Spec.PermissionClaims = append(binding.Spec.PermissionClaims, b.rejectedPermissionClaims...)
163+
apiBinding, err := b.newAPIBinding(preferredAPIBindingVersion)
164+
if err != nil {
165+
return fmt.Errorf("failed to create APIBinding: %w", err)
178166
}
179167

180-
kcpclient, err := newKCPClusterClient(config)
168+
kcpClusterClient, err := newKCPClusterClient(config)
181169
if err != nil {
182170
return err
183171
}
184172

185-
createdBinding, err := kcpclient.Cluster(currentClusterName).ApisV1alpha2().APIBindings().Create(ctx, binding, metav1.CreateOptions{})
186-
if err != nil {
187-
return err
173+
if err := apiBinding.Create(ctx, kcpClusterClient.Cluster(currentClusterName)); err != nil {
174+
return fmt.Errorf("failed to create APIBinding: %w", err)
188175
}
189176

190-
if _, err := fmt.Fprintf(b.Out, "apibinding %s created. Waiting to successfully bind ...\n", binding.Name); err != nil {
177+
if _, err := fmt.Fprintf(b.Out, "apibinding %s created. Waiting to successfully bind ...\n", apiBinding.Name()); err != nil {
191178
return err
192179
}
193180

194181
// wait for phase to be bound
195-
if createdBinding.Status.Phase != apisv1alpha2.APIBindingPhaseBound {
196-
if err := wait.PollUntilContextTimeout(ctx, time.Millisecond*500, b.BindWaitTimeout, true, func(ctx context.Context) (done bool, err error) {
197-
createdBinding, err := kcpclient.Cluster(currentClusterName).ApisV1alpha2().APIBindings().Get(ctx, binding.Name, metav1.GetOptions{})
198-
if err != nil {
182+
if !apiBinding.IsBound() {
183+
if err := wait.PollUntilContextTimeout(ctx, time.Millisecond*500, b.BindWaitTimeout, true, func(ctx context.Context) (bool, error) {
184+
if err := apiBinding.Refresh(ctx, kcpClusterClient.Cluster(currentClusterName)); err != nil {
199185
return false, err
200186
}
201-
if createdBinding.Status.Phase == apisv1alpha2.APIBindingPhaseBound {
202-
return true, nil
203-
}
204-
return false, nil
187+
188+
return apiBinding.IsBound(), nil
205189
}); err != nil {
206-
return fmt.Errorf("could not bind %s: %w", binding.Name, err)
190+
return fmt.Errorf("could not bind %s: %w", apiBinding.Name(), err)
207191
}
208192
}
209193

210-
if _, err := fmt.Fprintf(b.Out, "%s created and bound.\n", binding.Name); err != nil {
194+
if _, err := fmt.Fprintf(b.Out, "%s created and bound.\n", apiBinding.Name()); err != nil {
211195
return err
212196
}
213197

214198
return nil
215199
}
216200

201+
func (b *BindOptions) newAPIBinding(preferredAPIBindingVersion string) (apishelpers.APIBinding, error) {
202+
path, apiExportName := logicalcluster.NewPath(b.APIExportRef).Split()
203+
204+
// if a custom name is not provided, default it to <apiExportname>.
205+
apiBindingName := b.APIBindingName
206+
if apiBindingName == "" {
207+
apiBindingName = apiExportName
208+
}
209+
210+
var binding apishelpers.APIBinding
211+
212+
switch preferredAPIBindingVersion {
213+
case "v1alpha2":
214+
binding = apishelpers.NewAPIBinding(&apisv1alpha2.APIBinding{
215+
ObjectMeta: metav1.ObjectMeta{
216+
Name: apiBindingName,
217+
},
218+
Spec: apisv1alpha2.APIBindingSpec{
219+
Reference: apisv1alpha2.BindingReference{
220+
Export: &apisv1alpha2.ExportBindingReference{
221+
Path: path.String(),
222+
Name: apiExportName,
223+
},
224+
},
225+
},
226+
})
227+
228+
case "v1alpha1":
229+
binding = apishelpers.NewAPIBinding(&apisv1alpha1.APIBinding{
230+
ObjectMeta: metav1.ObjectMeta{
231+
Name: apiBindingName,
232+
},
233+
Spec: apisv1alpha1.APIBindingSpec{
234+
Reference: apisv1alpha1.BindingReference{
235+
Export: &apisv1alpha1.ExportBindingReference{
236+
Path: path.String(),
237+
Name: apiExportName,
238+
},
239+
},
240+
},
241+
})
242+
243+
default:
244+
return nil, fmt.Errorf("%s is not supported by this plugin", preferredAPIBindingVersion)
245+
}
246+
247+
claims := []apisv1alpha2.AcceptablePermissionClaim{}
248+
claims = append(claims, b.acceptedPermissionClaims...)
249+
claims = append(claims, b.rejectedPermissionClaims...)
250+
251+
if err := binding.SetPermissionClaims(claims); err != nil {
252+
return nil, fmt.Errorf("invalid permission claims: %w", err)
253+
}
254+
255+
return binding, nil
256+
}
257+
217258
func (b *BindOptions) parsePermissionClaim(claim string, accepted bool) error {
218259
claimParts := strings.SplitN(claim, ".", 2)
219260
if len(claimParts) != 2 {

cli/pkg/claims/plugin/claims.go

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,20 @@ package plugin
1919
import (
2020
"context"
2121
"fmt"
22-
"io"
2322
"net/url"
24-
"strings"
2523

2624
"github.com/spf13/cobra"
2725

28-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29-
utilerrors "k8s.io/apimachinery/pkg/util/errors"
26+
"k8s.io/apimachinery/pkg/runtime/schema"
3027
"k8s.io/cli-runtime/pkg/genericclioptions"
3128
"k8s.io/cli-runtime/pkg/printers"
3229
"k8s.io/client-go/rest"
3330
"k8s.io/client-go/tools/clientcmd"
3431

3532
"github.com/kcp-dev/kcp/cli/pkg/base"
3633
pluginhelpers "github.com/kcp-dev/kcp/cli/pkg/helpers"
37-
apiv1alpha2 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha2"
34+
apishelpers "github.com/kcp-dev/kcp/cli/pkg/helpers/apis/apis"
35+
"github.com/kcp-dev/kcp/sdk/apis/apis"
3836
kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned/cluster"
3937
)
4038

@@ -90,6 +88,14 @@ func (g *GetAPIBindingOptions) Run(ctx context.Context) error {
9088
return fmt.Errorf("current URL %q does not point to workspace", cfg.Host)
9189
}
9290

91+
preferredAPIBindingVersion, err := pluginhelpers.PreferredVersion(cfg, schema.GroupResource{
92+
Group: apis.GroupName,
93+
Resource: "apibindings",
94+
})
95+
if err != nil {
96+
return fmt.Errorf("service discovery failed: %w", err)
97+
}
98+
9399
kcpClusterClient, err := newKCPClusterClient(g.ClientConfig)
94100
if err != nil {
95101
return fmt.Errorf("error while creating kcp client %w", err)
@@ -98,38 +104,24 @@ func (g *GetAPIBindingOptions) Run(ctx context.Context) error {
98104
out := printers.GetNewTabWriter(g.Out)
99105
defer out.Flush()
100106

101-
err = printHeaders(out)
102-
if err != nil {
103-
return fmt.Errorf("error: %w", err)
104-
}
105-
106-
allErrors := []error{}
107-
apibindings := []apiv1alpha2.APIBinding{}
108107
// List permission claims for all bindings in current workspace.
108+
109+
var bindingsList apishelpers.APIBindingList
109110
if g.allBindings {
110-
bindings, err := kcpClusterClient.Cluster(currentClusterName).ApisV1alpha2().APIBindings().List(ctx, metav1.ListOptions{})
111+
bindings, err := apishelpers.ListAPIBindings(ctx, kcpClusterClient.Cluster(currentClusterName), preferredAPIBindingVersion)
111112
if err != nil {
112-
return fmt.Errorf("error listing apibindings in %q workspace: %w", currentClusterName, err)
113+
return fmt.Errorf("error listing APIBindings in %q workspace: %w", currentClusterName, err)
113114
}
114-
apibindings = append(apibindings, bindings.Items...)
115+
bindingsList = bindings
115116
} else {
116-
binding, err := kcpClusterClient.Cluster(currentClusterName).ApisV1alpha2().APIBindings().Get(ctx, g.APIBindingName, metav1.GetOptions{})
117+
binding, err := apishelpers.GetAPIBinding(ctx, kcpClusterClient.Cluster(currentClusterName), preferredAPIBindingVersion, g.APIBindingName)
117118
if err != nil {
118-
return fmt.Errorf("error finding apibinding: %w", err)
119+
return fmt.Errorf("error finding APIBinding: %w", err)
119120
}
120-
apibindings = append(apibindings, *binding)
121+
bindingsList = apishelpers.NewAPIBindingList(binding)
121122
}
122123

123-
for _, b := range apibindings {
124-
for _, claim := range b.Spec.PermissionClaims {
125-
err := printDetails(out, b.Name, claim.Group+"-"+claim.Resource, string(claim.State))
126-
if err != nil {
127-
allErrors = append(allErrors, err)
128-
}
129-
}
130-
}
131-
132-
return utilerrors.NewAggregate(allErrors)
124+
return bindingsList.PrintPermissionClaims(out)
133125
}
134126

135127
func newKCPClusterClient(clientConfig clientcmd.ClientConfig) (kcpclientset.ClusterInterface, error) {
@@ -147,14 +139,3 @@ func newKCPClusterClient(clientConfig clientcmd.ClientConfig) (kcpclientset.Clus
147139
clusterConfig.UserAgent = rest.DefaultKubernetesUserAgent()
148140
return kcpclientset.NewForConfig(clusterConfig)
149141
}
150-
151-
func printHeaders(out io.Writer) error {
152-
columnNames := []string{"APIBINDING", "RESOURCE GROUP-VERSION", "STATUS"}
153-
_, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t"))
154-
return err
155-
}
156-
157-
func printDetails(w io.Writer, name, binding, status string) error {
158-
_, err := fmt.Fprintf(w, "%s\t%s\t%s\n", name, binding, status)
159-
return err
160-
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Copyright 2025 The KCP Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package apis
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"io"
23+
24+
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
25+
apisv1alpha2 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha2"
26+
kcpclientset "github.com/kcp-dev/kcp/sdk/client/clientset/versioned"
27+
)
28+
29+
type APIBinding interface {
30+
Name() string
31+
Refresh(ctx context.Context, client kcpclientset.Interface) error
32+
Create(ctx context.Context, client kcpclientset.Interface) error
33+
SetPermissionClaims(claims []apisv1alpha2.AcceptablePermissionClaim) error
34+
IsBound() bool
35+
}
36+
37+
type APIBindingList interface {
38+
PrintPermissionClaims(out io.Writer) error
39+
}
40+
41+
func GetAPIBinding(ctx context.Context, client kcpclientset.Interface, preferredVersion string, name string) (APIBinding, error) {
42+
switch preferredVersion {
43+
case "v1alpha1":
44+
return getAPIBindingV1alpha1(ctx, client, name)
45+
46+
case "v1alpha2":
47+
return getAPIBindingV1alpha2(ctx, client, name)
48+
49+
default:
50+
return nil, fmt.Errorf("version %q is not supported by this plugin", preferredVersion)
51+
}
52+
}
53+
54+
func ListAPIBindings(ctx context.Context, client kcpclientset.Interface, preferredVersion string) (APIBindingList, error) {
55+
switch preferredVersion {
56+
case "v1alpha1":
57+
return listAPIBindingsV1alpha1(ctx, client)
58+
59+
case "v1alpha2":
60+
return listAPIBindingsV1alpha2(ctx, client)
61+
62+
default:
63+
return nil, fmt.Errorf("version %q is not supported by this plugin", preferredVersion)
64+
}
65+
}
66+
67+
func NewAPIBinding(nativeBinding any) APIBinding {
68+
switch asserted := nativeBinding.(type) {
69+
case *apisv1alpha1.APIBinding:
70+
return &apiBindingV1alpha1{binding: asserted}
71+
case *apisv1alpha2.APIBinding:
72+
return &apiBindingV1alpha2{binding: asserted}
73+
}
74+
75+
panic("Unsupported APIBinding version provided.")
76+
}
77+
78+
func NewAPIBindingList(nativeBinding any) APIBindingList {
79+
switch asserted := nativeBinding.(type) {
80+
case *apisv1alpha1.APIBinding:
81+
return &apiBindingListV1alpha1{bindings: []*apisv1alpha1.APIBinding{asserted}}
82+
case *apisv1alpha2.APIBinding:
83+
return &apiBindingListV1alpha2{bindings: []*apisv1alpha2.APIBinding{asserted}}
84+
}
85+
86+
panic("Unsupported APIBinding version provided.")
87+
}

0 commit comments

Comments
 (0)