Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[kueuectl] Pass-through commands. #2181

Merged
merged 3 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile-test.mk
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ test: gotestsum ## Run tests.
$(GOTESTSUM) --junitfile $(ARTIFACTS)/junit.xml -- $(GO_TEST_FLAGS) $(shell $(GO_CMD) list ./... | grep -v '/test/') -coverpkg=./... -coverprofile $(ARTIFACTS)/cover.out

.PHONY: test-integration
test-integration: gomod-download envtest ginkgo mpi-operator-crd ray-operator-crd jobset-operator-crd kf-training-operator-crd cluster-autoscaler-crd ## Run tests.
test-integration: gomod-download envtest ginkgo mpi-operator-crd ray-operator-crd jobset-operator-crd kf-training-operator-crd cluster-autoscaler-crd kueuectl ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" \
KUEUE_BIN=$(PROJECT_DIR)/bin \
$(GINKGO) $(GINKGO_ARGS) -procs=$(INTEGRATION_NPROCS) --junit-report=junit.xml --output-dir=$(ARTIFACTS) -v $(INTEGRATION_TARGET)

CREATE_KIND_CLUSTER ?= true
Expand Down
2 changes: 1 addition & 1 deletion apis/kueue/v1beta1/clusterqueue_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ type FairSharing struct {
// +genclient:nonNamespaced
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:resource:scope=Cluster,shortName={cq}
trasc marked this conversation as resolved.
Show resolved Hide resolved
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Cohort",JSONPath=".spec.cohort",type=string,description="Cohort that this ClusterQueue belongs to"
// +kubebuilder:printcolumn:name="Strategy",JSONPath=".spec.queueingStrategy",type=string,description="The queueing strategy used to prioritize workloads",priority=1
Expand Down
2 changes: 1 addition & 1 deletion apis/kueue/v1beta1/localqueue_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ type LocalQueueResourceUsage struct {
// +kubebuilder:printcolumn:name="ClusterQueue",JSONPath=".spec.clusterQueue",type=string,description="Backing ClusterQueue"
// +kubebuilder:printcolumn:name="Pending Workloads",JSONPath=".status.pendingWorkloads",type=integer,description="Number of pending workloads"
// +kubebuilder:printcolumn:name="Admitted Workloads",JSONPath=".status.admittedWorkloads",type=integer,description="Number of admitted workloads that haven't finished yet."
// +kubebuilder:resource:shortName={queue,queues}
// +kubebuilder:resource:shortName={queue,queues,lq}

// LocalQueue is the Schema for the localQueues API
type LocalQueue struct {
Expand Down
2 changes: 2 additions & 0 deletions charts/kueue/templates/crd/kueue.x-k8s.io_clusterqueues.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ spec:
kind: ClusterQueue
listKind: ClusterQueueList
plural: clusterqueues
shortNames:
- cq
singular: clusterqueue
scope: Cluster
versions:
Expand Down
1 change: 1 addition & 0 deletions charts/kueue/templates/crd/kueue.x-k8s.io_localqueues.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ spec:
shortNames:
- queue
- queues
- lq
singular: localqueue
scope: Namespaced
versions:
Expand Down
9 changes: 9 additions & 0 deletions cmd/kueuectl/app/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package app

import (
"fmt"
"os"

"github.com/spf13/cobra"
Expand All @@ -25,6 +26,7 @@ import (

"sigs.k8s.io/kueue/cmd/kueuectl/app/create"
"sigs.k8s.io/kueue/cmd/kueuectl/app/list"
"sigs.k8s.io/kueue/cmd/kueuectl/app/passthrough"
"sigs.k8s.io/kueue/cmd/kueuectl/app/resume"
"sigs.k8s.io/kueue/cmd/kueuectl/app/stop"
"sigs.k8s.io/kueue/cmd/kueuectl/app/util"
Expand Down Expand Up @@ -68,6 +70,13 @@ func NewKueuectlCmd(o KueuectlOptions) *cobra.Command {
cmd.AddCommand(resume.NewResumeCmd(clientGetter, o.IOStreams))
cmd.AddCommand(stop.NewStopCmd(clientGetter, o.IOStreams))
cmd.AddCommand(list.NewListCmd(clientGetter, o.IOStreams))
pCommands, err := passthrough.NewCommands()
if err != nil {
// we can still use the other commands, jut push an warning
fmt.Println(err)
} else {
cmd.AddCommand(pCommands...)
}

return cmd
}
83 changes: 83 additions & 0 deletions cmd/kueuectl/app/passthrough/passthrough.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
Copyright 2024 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package passthrough

import (
"fmt"
"os"
"os/exec"
"syscall"

"github.com/spf13/cobra"
)

type passThroughType struct {
name string
aliases []string
}

var (
passThroughCmds = []string{"get", "delete", "edit", "describe", "patch"}
passThroughTypes = []passThroughType{
{name: "workload", aliases: []string{"wl"}},
{name: "clusterqueue", aliases: []string{"cq"}},
{name: "localqueue", aliases: []string{"lq"}}}
)

func NewCommands() ([]*cobra.Command, error) {
kubectlPath, err := exec.LookPath("kubectl")
if err != nil {
return nil, fmt.Errorf("pass-through commands are not available: %w, PATH=%q", err, os.Getenv("PATH"))
}

commands := make([]*cobra.Command, len(passThroughCmds))
for i, pCmd := range passThroughCmds {
commands[i] = newCmd(kubectlPath, pCmd, passThroughTypes)
}
return commands, nil
}

func newCmd(kubectlPath string, command string, ptTypes []passThroughType) *cobra.Command {
cmd := &cobra.Command{
Use: command,
Short: fmt.Sprintf("Pass-through %q to kubectl", command),
}

for _, ptType := range ptTypes {
cmd.AddCommand(newSubcommand(kubectlPath, command, ptType))
}

return cmd
}

func newSubcommand(kubectlPath string, command string, ptType passThroughType) *cobra.Command {
cmd := &cobra.Command{
Use: ptType.name,
Aliases: ptType.aliases,
Short: fmt.Sprintf("Pass-through \"%s %s\" to kubectl", command, ptType),
FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
RunE: func(_ *cobra.Command, _ []string) error {
// prepare the args
args := os.Args
args[0] = kubectlPath

// go in kubectl
return syscall.Exec(kubectlPath, args, os.Environ())
},
}
return cmd
}
2 changes: 2 additions & 0 deletions config/components/crd/bases/kueue.x-k8s.io_clusterqueues.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ spec:
kind: ClusterQueue
listKind: ClusterQueueList
plural: clusterqueues
shortNames:
- cq
singular: clusterqueue
scope: Cluster
versions:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ spec:
shortNames:
- queue
- queues
- lq
singular: localqueue
scope: Namespaced
versions:
Expand Down
9 changes: 5 additions & 4 deletions keps/2076-kueuectl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,14 @@ None.

### Pass-through

For completeness there will be 4 additional commands that will simply execute regular kubectl
For completeness there will be 5 additional commands that will simply execute regular kubectl
so that the users won't have to remember to switch the command to kubectl.

* `delete workload|clusterqueue|cq|localqueue|lq`
* `get workload|clusterqueue|cq|localqueue|lq`
* `edit workload|clusterqueue|cq|localqueue|lq`
* `describe workload|clusterqueue|cq|localqueue|lq`
* `edit workload|clusterqueue|cq|localqueue|lq`
* `patch workload|clusterqueue|cq|localqueue|lq`
* `delete workload|clusterqueue|cq|localqueue|lq`

### Test Plan

Expand Down Expand Up @@ -344,4 +345,4 @@ KEP: 2023-04-27.

* Use existing kubectl functionality and perform management operations via
API manipulations.
* Don't use kubectl plugins but write CLI from scratch.
* Don't use kubectl plugins but write CLI from scratch.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: "Pass-through commands"
linkTitle: "Pass-through"
date: 2024-05-14
weight: 100
description: >
Commands delegated to `kubectl`
---

### Usage:

```
kubectl kueue <command> <type> [FLAGS]
```

### Examples:

```bash
# Edit a local queue
kubectl kueue edit localqueue my-local-queue

# Delete a cluster queue
kubectl kueue delete cq my-cluster-queue
```

## Supported commands

The following table includes a list of commands that are passed through to `kubectl`.

| Name | Short |
|------------|--------------------------------------------------------------------------------------------------|
| get | Display one or many resources
| describe | Show details of a specific resource or group of resources
| edit | Edit a resource on the server
| patch | Update fields of a resource
| delete | Delete resources by file names, stdin, resources and names, or by resources and label selector

## Resource types

The following table includes a list of all the supported resource types and their abbreviated aliases:

| Name | Short | API version | Namespaced | Kind |
|--------------|-------|------------------------|------------|--------------|
| workload | wl | kueue.x-k8s.io/v1beta1 | true | Workload |
| clusterqueue | cq | kueue.x-k8s.io/v1beta1 | false | ClusterQueue |
| localqueue | lq | kueue.x-k8s.io/v1beta1 | true | LocalQueue |
134 changes: 134 additions & 0 deletions test/integration/kueuectl/passthrough_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
Copyright 2024 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package kueuectl

import (
"fmt"
"os"
"os/exec"
"path/filepath"

"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/set"
"sigs.k8s.io/controller-runtime/pkg/client"

"sigs.k8s.io/kueue/pkg/util/testing"
"sigs.k8s.io/kueue/test/util"
)

func makePassThroughWorkload(ns string) client.Object {
return testing.MakeWorkload("pass-through-wl", ns).Obj()
}

func makePassThroughCQ(_ string) client.Object {
return testing.MakeClusterQueue("pass-through-cq").Obj()
}

func makePassThroughLQ(ns string) client.Object {
return testing.MakeLocalQueue("pass-through-lq", ns).ClusterQueue("queue").Obj()
}

func setupEnv(c *exec.Cmd, kassetsPath string, kubeconfigPath string) {
c.Env = os.Environ()
cmdPath := os.Getenv("PATH")
if cmdPath == "" {
cmdPath = kassetsPath
} else if !set.New(filepath.SplitList(cmdPath)...).Has(kassetsPath) {
cmdPath = fmt.Sprintf("%s%c%s", kassetsPath, os.PathListSeparator, cmdPath)
}

c.Env = append(c.Env, "PATH="+cmdPath)
c.Env = append(c.Env, "KUBECONFIG="+kubeconfigPath)
}

var _ = ginkgo.Describe("Kueuectl Pass-through", ginkgo.Ordered, ginkgo.ContinueOnFailure, func() {
var (
ns *corev1.Namespace
)

ginkgo.BeforeEach(func() {
ns = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{GenerateName: "ns-"}}
gomega.Expect(k8sClient.Create(ctx, ns)).To(gomega.Succeed())
})

ginkgo.AfterEach(func() {
gomega.Expect(util.DeleteNamespace(ctx, k8sClient, ns)).To(gomega.Succeed())
})

ginkgo.DescribeTable("Pass-through commands",
func(oType string, namespaced bool, makeObject func(ns string) client.Object, getPath string, expectGet string, patch string, expectGetAfterPatch string) {
obj := makeObject(ns.Name)
gomega.Expect(k8sClient.Create(ctx, obj)).To(gomega.Succeed())
key := client.ObjectKeyFromObject(obj)

identityArgs := []string{oType, key.Name}
if namespaced {
identityArgs = append(identityArgs, "-n", key.Namespace)
}

// get it
args := append([]string{"get"}, identityArgs...)
args = append(args, fmt.Sprintf("-o=jsonpath='%s'", getPath))
cmd := exec.Command(kueuectlPath, args...)
setupEnv(cmd, kassetsPath, kubeconfigPath)
out, err := cmd.CombinedOutput()
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "%q", string(out))
gomega.Expect(string(out)).To(gomega.BeComparableTo(expectGet))

// patch it
if patch != "" {
args = append([]string{"patch"}, identityArgs...)
args = append(args, "--type=merge", "-p", patch)
cmd = exec.Command(kueuectlPath, args...)
setupEnv(cmd, kassetsPath, kubeconfigPath)
_, err = cmd.CombinedOutput()
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "%q", string(out))

args = append([]string{"get"}, identityArgs...)
args = append(args, fmt.Sprintf("-o=jsonpath='%s'", getPath))
cmd = exec.Command(kueuectlPath, args...)
setupEnv(cmd, kassetsPath, kubeconfigPath)
out, err = cmd.CombinedOutput()
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "%q", string(out))
gomega.Expect(string(out)).To(gomega.BeComparableTo(expectGetAfterPatch))
}
// delete it
args = []string{"delete", oType}
if namespaced {
args = append(args, "-n", key.Namespace)
}
args = append(args, key.Name)
cmd = exec.Command(kueuectlPath, args...)
setupEnv(cmd, kassetsPath, kubeconfigPath)
_, err = cmd.CombinedOutput()
gomega.Expect(err).NotTo(gomega.HaveOccurred(), "%q", string(out))

gomega.Eventually(func() error {
return k8sClient.Get(ctx, key, obj)
}, util.Timeout, util.Interval).Should(testing.BeNotFoundError())
},
ginkgo.Entry("Workload", "workload", true, makePassThroughWorkload, "{.spec.active}", "'true'", `{"spec":{"active":false}}`, "'false'"),
ginkgo.Entry("Workload(short)", "wl", true, makePassThroughWorkload, "{.spec.active}", "'true'", `{"spec":{"active":false}}`, "'false'"),
ginkgo.Entry("Cluster Queue", "clusterqueue", false, makePassThroughCQ, "{.spec.stopPolicy}", "'None'", `{"spec":{"stopPolicy":"Hold"}}`, "'Hold'"),
ginkgo.Entry("Cluster Queue(short)", "cq", false, makePassThroughCQ, "{.spec.stopPolicy}", "'None'", `{"spec":{"stopPolicy":"Hold"}}`, "'Hold'"),
ginkgo.Entry("Local Queue", "localqueue", true, makePassThroughLQ, "{.spec.clusterQueue}", "'queue'", ``, ""),
ginkgo.Entry("Local Queue (short)", "lq", true, makePassThroughLQ, "{.spec.clusterQueue}", "'queue'", ``, ""),
trasc marked this conversation as resolved.
Show resolved Hide resolved
)
})
Loading