diff --git a/contrib/demos-unmaintained/demo/prototype3-script/apibinding/cert-manager-apiexport.yaml b/contrib/demos-unmaintained/demo/prototype3-script/apibinding/cert-manager-apiexport.yaml index d1e806e7ba1..aa22f702922 100644 --- a/contrib/demos-unmaintained/demo/prototype3-script/apibinding/cert-manager-apiexport.yaml +++ b/contrib/demos-unmaintained/demo/prototype3-script/apibinding/cert-manager-apiexport.yaml @@ -11,3 +11,6 @@ spec: - today.clusterissuers.cert-manager.io - today.issuers.cert-manager.io - today.orders.acme.cert-manager.io + permissionClaims: + - resource: secrets + all: true diff --git a/pkg/cliplugins/claims/cmd/cmd.go b/pkg/cliplugins/claims/cmd/cmd.go index 6eb3a697a96..b2adb97c723 100644 --- a/pkg/cliplugins/claims/cmd/cmd.go +++ b/pkg/cliplugins/claims/cmd/cmd.go @@ -35,6 +35,12 @@ var ( # List permission claims and their respective status for all APIBindings in current workspace. %[1]s claims get apibinding + + # Edit the permission claims' status related to a specific APIBinding with an interactive prompt. + %[1]s claims get apibinding cert-manager + + # Edit the permission claims' status for all APIBindings in current workspace with an interactive prompt. + %[1]s claims get apibinding ` ) diff --git a/pkg/cliplugins/claims/plugin/edit.go b/pkg/cliplugins/claims/plugin/edit.go index 36bcdf395a6..267700a6bd7 100644 --- a/pkg/cliplugins/claims/plugin/edit.go +++ b/pkg/cliplugins/claims/plugin/edit.go @@ -20,15 +20,17 @@ import ( "context" "fmt" - apiv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" - kcpclient "github.com/kcp-dev/kcp/pkg/client/clientset/versioned" - "github.com/kcp-dev/kcp/pkg/cliplugins/base" - pluginhelpers "github.com/kcp-dev/kcp/pkg/cliplugins/helpers" "github.com/kcp-dev/logicalcluster/v2" "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" + + apiv1alpha1 "github.com/kcp-dev/kcp/pkg/apis/apis/v1alpha1" + kcpclient "github.com/kcp-dev/kcp/pkg/client/clientset/versioned" + "github.com/kcp-dev/kcp/pkg/cliplugins/base" + pluginhelpers "github.com/kcp-dev/kcp/pkg/cliplugins/helpers" ) // EditAPIBindingOptions contains the options for fetching claims @@ -88,12 +90,7 @@ func (e *EditAPIBindingOptions) Run(ctx context.Context) error { return fmt.Errorf("error while creating kcp client %w", err) } - _, err = fmt.Fprintf(e.Out, "Running interactive promt. Enter (%s/%s/%s)", "Yes", "No", "Skip") - if err != nil { - return err - } - - // The status of an apibinding has a set of `exportClaims`. The subset which is present in exportClaims but not in spec.AcceptableClaims, are the open claims. + // The status of an apibinding has a set of `exportClaims`. The subset which is present in exportClaims but not in spec.AcceptableClaims, are the open claims. // The command will list those open claims and update `spec.AcceptableClaims` with the status based on the input of the user. apibindings := []apiv1alpha1.APIBinding{} @@ -112,15 +109,39 @@ func (e *EditAPIBindingOptions) Run(ctx context.Context) error { apibindings = append(apibindings, *binding) } + if len(apibindings) == 0 { + _, err = fmt.Fprintf(e.Out, "No apibindings available in %s.", currentClusterName) + if err != nil { + return err + } + return nil + } + + _, err = fmt.Fprintf(e.Out, "Running interactive prompt. Enter (%s/%s/%s)\n", "Yes", "No", "Skip") + if err != nil { + return err + } + allErrors := []error{} for _, apibinding := range apibindings { openClaims := getOpenClaims(apibinding.Status.ExportPermissionClaims, apibinding.Spec.PermissionClaims) - // The status of an apibinding has a set of `exportClaims`. The subset which is present in exportClaims but not in spec.AcceptableClaims, are the open claims. - // The command will list those open claims and update `spec.AcceptableClaims` with the status based on the input of the user. + if len(openClaims) == 0 { + _, err = fmt.Fprintf(e.Out, "No open claims found for apibinding: %q\n", apibinding.Name) + if err != nil { + allErrors = append(allErrors, err) + } + } + + // List open claims and update `spec.AcceptableClaims` with the status based on the input of the user. for _, openClaim := range openClaims { // print the prompt and infer the user input. - action := getRequiredInput(e.In, apibinding.Name, openClaim.Group, openClaim.Resource) - err := updateAPIBinding(ctx, action, openClaim, &apibinding, currentClusterName, kcpClusterClient) + action, err := getRequiredInput(e.In, e.Out, apibinding.Name, openClaim.Group, openClaim.Resource) + if err != nil { + allErrors = append(allErrors, err) + } + + // Update apibinding based on the user input. + err = updateAPIBinding(ctx, action, openClaim, &apibinding, currentClusterName, kcpClusterClient) if err != nil { allErrors = append(allErrors, err) } @@ -133,41 +154,40 @@ func (e *EditAPIBindingOptions) Run(ctx context.Context) error { func getOpenClaims(exportClaims []apiv1alpha1.PermissionClaim, aceptableClaims []apiv1alpha1.AcceptablePermissionClaim) []apiv1alpha1.PermissionClaim { openPermissionClaims := []apiv1alpha1.PermissionClaim{} - // Convert both the lists into a map, with IdentityHash being the key so that - // the complexity of finding a resource which in in both export and acceptable claim is not n^2. - exportClaimsMap := map[string]apiv1alpha1.PermissionClaim{} + // Find the subset which is in exportClaims but not in AcceptableClaims. + exportClaimsMap := map[apiv1alpha1.GroupResource]apiv1alpha1.PermissionClaim{} for _, e := range exportClaims { - exportClaimsMap[e.IdentityHash] = e + exportClaimsMap[e.GroupResource] = e } - acceptableClaimsMap := map[string]apiv1alpha1.PermissionClaim{} + acceptableClaimsMap := map[apiv1alpha1.GroupResource]apiv1alpha1.PermissionClaim{} for _, a := range aceptableClaims { - acceptableClaimsMap[a.IdentityHash] = a.PermissionClaim + acceptableClaimsMap[a.GroupResource] = a.PermissionClaim } - for identity, _ := range exportClaimsMap { - if val, ok := acceptableClaimsMap[identity]; !ok { - openPermissionClaims = append(openPermissionClaims, val) + for gr, cl := range exportClaimsMap { + if _, ok := acceptableClaimsMap[gr]; !ok { + openPermissionClaims = append(openPermissionClaims, cl) } } return openPermissionClaims } +// updateAPIBinding send a request to API server to update the APIBinding with the updated status of the claim. func updateAPIBinding(ctx context.Context, action ClaimAction, permissionClaim apiv1alpha1.PermissionClaim, apibinding *apiv1alpha1.APIBinding, clusterName logicalcluster.Name, kcpclusterclient kcpclient.ClusterInterface) error { if action == SkipClaim { return nil } - for _, pc := range apibinding.Spec.PermissionClaims { - if pc.IdentityHash == permissionClaim.IdentityHash { - if action == AcceptClaim { - pc.State = apiv1alpha1.ClaimAccepted - } else if action == RejectClaim { - pc.State = apiv1alpha1.ClaimRejected - } - } + var state apiv1alpha1.AcceptablePermissionClaimState + if action == AcceptClaim { + state = apiv1alpha1.ClaimAccepted + } else if action == RejectClaim { + state = apiv1alpha1.ClaimRejected } + apibinding.Spec.PermissionClaims = append(apibinding.Spec.PermissionClaims, apiv1alpha1.AcceptablePermissionClaim{PermissionClaim: permissionClaim, State: state}) + _, err := kcpclusterclient.Cluster(clusterName).ApisV1alpha1().APIBindings().Update(ctx, apibinding, metav1.UpdateOptions{}) return err } diff --git a/pkg/cliplugins/claims/plugin/util.go b/pkg/cliplugins/claims/plugin/util.go index f529eaee5e3..e4e4c460fde 100644 --- a/pkg/cliplugins/claims/plugin/util.go +++ b/pkg/cliplugins/claims/plugin/util.go @@ -24,8 +24,9 @@ import ( "strings" ) -// ClaimAction captures the user preference on action with permission claim. -// commands. +// ClaimAction captures the user's preference of a specific action +// on permission claim. They can either accept, reject or ignore/ skip +// ay action on an open permission claim. type ClaimAction int const ( @@ -37,20 +38,32 @@ const ( SkipClaim ) +// Print the message for the user in the prompt. The resource name in a claim is a required field, +// but the group can be empty. func printMessage(bindingName, claimGroup, claimResource string) string { - return fmt.Sprintf("Accept %s-%s (APIBinding: %s) >\n", claimGroup, claimResource, bindingName) + if claimGroup != "" { + return fmt.Sprintf("Accept permission claim for Group: %s, Resource: %s (APIBinding: %s) > ", claimGroup, claimResource, bindingName) + } + return fmt.Sprintf("Accept permission claim for Resource: %s (APIBinding: %s) > ", claimResource, bindingName) } -func getRequiredInput(rd io.Reader, bindingName, claimGroup, claimResource string) ClaimAction { +func getRequiredInput(rd io.Reader, wr io.Writer, bindingName, claimGroup, claimResource string) (ClaimAction, error) { reader := bufio.NewReader(rd) for { - printMessage(bindingName, claimGroup, claimResource) + _, err := fmt.Fprint(wr, printMessage(bindingName, claimGroup, claimResource)) + if err != nil { + return -1, err + } + value := readInput(reader) if value != "" { - return inferText(value) + return inferText(value, wr) + } + _, err = fmt.Fprintf(wr, "Input is required. Enter `skip/s` instead. ") + if err != nil { + return -1, err } - fmt.Printf("Input is required. ") } } @@ -70,15 +83,18 @@ func readLine(reader *bufio.Reader) string { return strings.ToLower(strings.Trim(strings.TrimSpace(text), "`'\"")) } -func inferText(input string) ClaimAction { +func inferText(input string, wr io.Writer) (ClaimAction, error) { if input == "y" || input == "yes" { - return AcceptClaim + return AcceptClaim, nil } else if input == "n" || input == "no" { - return RejectClaim + return RejectClaim, nil } else if input == "skip" || input == "s" { - return SkipClaim + return SkipClaim, nil } else { - fmt.Println("Unknown input, skipping any action") - return SkipClaim + _, err := fmt.Fprintf(wr, "Unknown input, skipping any action.\n") + if err != nil { + return -1, err + } + return SkipClaim, nil } }