Add support for dynamic commands published by CRDs

This commit is contained in:
Phillip Wittrock 2019-04-09 12:01:18 -07:00 committed by jingfangliu
parent 25ac98a777
commit 38e1577c1f
45 changed files with 3248 additions and 144 deletions

6
.gitignore vendored
View File

@ -1,10 +1,14 @@
# go build binaries
main
cli-experimental
dyctl
#intellij
.idea/
*.iml
# scripts/verify.sh creates a vendor for metalinter
vendor/
vendor/
# go artifacts
cover.out

View File

@ -16,16 +16,17 @@ package apply
import (
"fmt"
"sigs.k8s.io/cli-experimental/internal/pkg/util"
"sigs.k8s.io/cli-experimental/internal/pkg/clik8s"
"github.com/spf13/cobra"
"sigs.k8s.io/cli-experimental/cmd/apply/status"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s"
)
// GetApplyCommand returns the `apply` cobra Command
func GetApplyCommand() *cobra.Command {
func GetApplyCommand(a util.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "apply",
Short: ".",
@ -35,7 +36,7 @@ func GetApplyCommand() *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error {
for i := range args {
r, err := wirecli.DoApply(clik8s.ResourceConfigPath(args[i]), cmd.OutOrStdout())
r, err := wirecli.DoApply(clik8s.ResourceConfigPath(args[i]), cmd.OutOrStdout(), a)
if err != nil {
return err
}
@ -44,10 +45,7 @@ func GetApplyCommand() *cobra.Command {
return nil
}
// Add Flags
wirek8s.Flags(cmd)
// Add Commands
cmd.AddCommand(status.GetApplyStatusCommand())
cmd.AddCommand(status.GetApplyStatusCommand(a))
return cmd
}

View File

@ -23,10 +23,11 @@ import (
"github.com/stretchr/testify/assert"
"sigs.k8s.io/cli-experimental/cmd/apply"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest"
)
var h string
var host string
func TestMain(m *testing.M) {
c, stop, err := wiretest.NewRestConfig()
@ -34,7 +35,7 @@ func TestMain(m *testing.M) {
os.Exit(1)
}
defer stop()
h = c.Host
host = c.Host
os.Exit(m.Run())
}
@ -51,12 +52,13 @@ configMapGenerator:
}
func TestApply(t *testing.T) {
f := setupKustomize(t)
buf := new(bytes.Buffer)
cmd := apply.GetApplyCommand()
args := []string{fmt.Sprintf("--server=%s", host), "--namespace=default", setupKustomize(t)}
cmd := apply.GetApplyCommand(args)
cmd.SetOutput(buf)
cmd.SetArgs([]string{fmt.Sprintf("--master=%s", h), f})
cmd.SetArgs(args)
wirek8s.Flags(cmd.PersistentFlags())
assert.NoError(t, cmd.Execute())
assert.Equal(t, "Doing `cli-experimental apply`\nResources: 1\n", buf.String())

View File

@ -16,14 +16,15 @@ package status
import (
"fmt"
"sigs.k8s.io/cli-experimental/internal/pkg/util"
"github.com/spf13/cobra"
"sigs.k8s.io/cli-experimental/internal/pkg/clik8s"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s"
)
// GetApplyStatusCommand returns a new `apply status` command
func GetApplyStatusCommand() *cobra.Command {
func GetApplyStatusCommand(a util.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: ".",
@ -33,7 +34,7 @@ func GetApplyStatusCommand() *cobra.Command {
cmd.RunE = func(cmd *cobra.Command, args []string) error {
for i := range args {
r, err := wirecli.DoStatus(clik8s.ResourceConfigPath(args[i]), cmd.OutOrStdout())
r, err := wirecli.DoStatus(clik8s.ResourceConfigPath(args[i]), cmd.OutOrStdout(), a)
if err != nil {
return err
}
@ -42,7 +43,5 @@ func GetApplyStatusCommand() *cobra.Command {
return nil
}
// Add Flags
wirek8s.Flags(cmd)
return cmd
}

View File

@ -21,12 +21,14 @@ import (
"path/filepath"
"testing"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/cli-experimental/cmd/apply/status"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest"
)
var h string
var host string
func TestMain(m *testing.M) {
c, stop, err := wiretest.NewRestConfig()
@ -34,7 +36,7 @@ func TestMain(m *testing.M) {
os.Exit(1)
}
defer stop()
h = c.Host
host = c.Host
os.Exit(m.Run())
}
@ -51,12 +53,13 @@ configMapGenerator:
}
func TestStatus(t *testing.T) {
f := setupKustomize(t)
buf := new(bytes.Buffer)
cmd := status.GetApplyStatusCommand()
args := []string{fmt.Sprintf("--server=%s", host), "--namespace=default", setupKustomize(t)}
cmd := status.GetApplyStatusCommand(args)
cmd.SetOutput(buf)
cmd.SetArgs([]string{fmt.Sprintf("--master=%s", h), f})
cmd.SetArgs(args)
wirek8s.Flags(cmd.PersistentFlags())
assert.NoError(t, cmd.Execute())
assert.Equal(t, "Doing `cli-experimental apply status`\nResources: 1\n", buf.String())

55
cmd/cmd.go Normal file
View File

@ -0,0 +1,55 @@
/*
Copyright 2019 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 cmd
import (
"os"
"github.com/spf13/cobra"
"sigs.k8s.io/cli-experimental/cmd/apply"
"sigs.k8s.io/cli-experimental/internal/pkg/dy"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s"
)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute(args []string, fn func(*cobra.Command)) error {
rootCmd := &cobra.Command{
Use: "cli-experimental",
Short: "",
Long: ``,
}
if fn != nil {
fn(rootCmd)
}
rootCmd.AddCommand(apply.GetApplyCommand(os.Args))
wirek8s.Flags(rootCmd.PersistentFlags())
rootCmd.PersistentFlags().Set("namespace", "default")
// Add dynamic Commands published by CRDs as go-templates
b, err := dy.InitializeCommandBuilder(rootCmd.OutOrStdout(), args)
if err != nil {
return err
}
if err := b.Build(rootCmd, nil); err != nil {
return err
}
// Run the Command
if err := rootCmd.Execute(); err != nil {
return err
}
return nil
}

178
cmd/dy_cmd_test.go Normal file
View File

@ -0,0 +1,178 @@
/*
Copyright 2019 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 cmd_test
import (
"bytes"
"os"
"testing"
"github.com/spf13/cobra"
"sigs.k8s.io/cli-experimental/cmd"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"github.com/stretchr/testify/assert"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/client-go/rest"
"sigs.k8s.io/cli-experimental/internal/pkg/apis/dynamic/v1alpha1"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest"
dycmd "sigs.k8s.io/cli-experimental/util/dyctl/cmd"
"sigs.k8s.io/yaml"
)
var cfg *rest.Config
func TestMain(m *testing.M) {
c, stop, err := wiretest.NewRestConfig()
cfg = c
if err != nil {
os.Exit(1)
}
defer stop()
os.Exit(m.Run())
}
func TestAddDyCommands(t *testing.T) {
cs, err := clientset.NewForConfig(cfg)
assert.NoError(t, err)
// Add the CRD
crd1 := &v1beta1.CustomResourceDefinition{}
err = yaml.UnmarshalStrict([]byte(crd1YAML), crd1)
assert.NoError(t, err)
cl1 := &v1alpha1.ResourceCommandList{}
err = yaml.UnmarshalStrict([]byte(commandListYAML1), cl1)
assert.NoError(t, err)
err = dycmd.AddTo(cl1, crd1)
assert.NoError(t, err)
_, err = cs.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd1)
assert.NoError(t, err)
// Create the Command Argumnents
buf := &bytes.Buffer{}
args := []string{
"create", "deployment", "--server=" + cfg.Host, "--name=foo", "--namespace=cmd",
"--image=nginx", "--dry-run"}
// Set the Output and Arguments
fn := func(c *cobra.Command) {
c.SetArgs(args)
c.SetOutput(buf)
}
// Execute the Command
err = cmd.Execute(args, fn)
assert.NoError(t, err)
// Verify the dry-run output
assert.Equal(t, expectedString, buf.String())
}
var expectedString = `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: foo
name: foo
namespace: cmd
spec:
replicas: 2
selector:
matchLabels:
app: foo
template:
metadata:
labels:
app: foo
spec:
containers:
- image: nginx
name: foo
---
`
var crd1YAML = `apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: "clitestresourcesa.test.cli.sigs.k8s.io"
spec:
group: test.cli.sigs.k8s.io
names:
kind: CLITestResource
plural: clitestresourcesa
scope: Namespaced
version: v1alpha1
---
`
var commandListYAML1 = `items:
- command:
path:
- "create" # Command is a subcommand of this path
use: "deployment" # Command use
aliases: # Command alias'
- "deploy"
- "deployments"
short: Create a deployment with the specified name - short.
long: Create a deployment with the specified name - long.
example: |
# Create a new deployment named my-dep that runs the busybox image.
kubectl create deployment --name my-dep --image=busybox
flags:
- name: name
type: String
stringValue: ""
description: deployment name
- name: image
type: String
stringValue: ""
description: Image name to run.
- name: replicas
type: Int
intValue: 2
description: Image name to run.
requests:
- group: apps
version: v1
resource: deployments
operation: Create
bodyTemplate: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{index .Flags.Strings "name"}}
namespace: {{index .Flags.Strings "namespace"}}
labels:
app: {{index .Flags.Strings "name"}}
spec:
replicas: {{index .Flags.Ints "replicas"}}
selector:
matchLabels:
app: {{index .Flags.Strings "name"}}
template:
metadata:
labels:
app: {{index .Flags.Strings "name"}}
spec:
containers:
- name: {{index .Flags.Strings "name"}}
image: {{index .Flags.Strings "image"}}
saveResponseValues:
- name: responsename
jsonPath: "{.metadata.name}"
output: |
deployment.apps/{{index .Responses.Strings "responsename"}} created
`

View File

@ -0,0 +1,15 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: dynamic.cli.sigs.k8s.io
spec:
group: resourcecommands.dynamic.cli.sigs.k8s.io
versions:
- name: v1alpha1
served: true
storage: true
scope: Cluster
names:
plural: resourcecommands
singular: resourcecommand
kind: ResourceCommand

84
docs/dy/README.md Normal file
View File

@ -0,0 +1,84 @@
# Dynamic Commands
Dynamic Commands are server-side defined Commands published to the client as CRD annotations containing
`ResourceCommandList` objects.
## Sample
### List the Commands from the cli
No Dynamic Commands will be present.
```bash
cli-experimental -h
```
### Create the CRD that publishes Commands
Create a CRD that publishes Dynamic Commands.
```bash
kubectl apply -f .sample/cli_v1alpha1_clitestresource.yaml
```
### List the Commands from the cli
New `cli-experimental create deployment` Command will now appear in help
```bash
cli-experimental -h
```
```bash
cli-experimental create -h
```
```bash
cli-experimental deployment -h
```
### Run the Command in dry-run
Run the command, but print the Resources rather than creating them.
```bash
cli-experimental create deployment --image ubuntu --name foo --dry-run
```
### Run the Command
Run the command to create the Resources.
```bash
cli-experimental create deployment --image ubuntu --name foo
```
## Publishing a Command
### Define the ResourceCommandList in a yaml file
See the [dynamic.v1alpha1](../../internal/pkg/apis/dynamic/v1alpha1/types.go) API for documentation.
### Add the ResourceCommandList to a CRD
Build the `dy` command
```bash
go build ./util/dy
```
```bash
dy add-commands path/to/commands.yaml path/to/crd.yaml
```
### Apply the CRD
```bash
kubectl apply -f path/to/crd.yaml
```
### Run the new Command
```bash
cli-experimental your command -h
```

View File

@ -0,0 +1,47 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
annotations:
cli-experimental.sigs.k8s.io/ResourceCommandList: '{"items":[{"command":{"aliases":["deploy","deployments"],"example":"#
Create a new deployment named my-dep that runs the busybox image.\nkubectl create
deployment --name my-dep --image=busybox\n","flags":[{"description":"deployment
name","name":"name","type":"String"},{"description":"Image name to run.","name":"image","type":"String"},{"description":"Image
name to run.","intValue":2,"name":"replicas","type":"Int"}],"long":"Create a
deployment with the specified name.","path":["create"],"short":"Create a deployment
with the specified name.","use":"deployment"},"output":"deployment.apps/{{index
.Responses.Strings \"responsename\"}} created\n","requests":[{"bodyTemplate":"apiVersion:
apps/v1\nkind: Deployment\nmetadata:\n name: {{index .Flags.Strings \"name\"}}\n namespace:
{{index .Flags.Strings \"namespace\"}}\n labels:\n app: nginx\nspec:\n replicas:
{{index .Flags.Ints \"replicas\"}}\n selector:\n matchLabels:\n app:
{{index .Flags.Strings \"name\"}}\n template:\n metadata:\n labels:\n app:
{{index .Flags.Strings \"name\"}}\n spec:\n containers:\n - name:
{{index .Flags.Strings \"name\"}}\n image: {{index .Flags.Strings \"image\"}}\n","group":"apps","operation":"Create","resource":"deployments","saveResponseValues":[{"jsonPath":"{.metadata.name}","name":"responsename"}],"version":"v1"}]}]}'
labels:
cli-experimental.sigs.k8s.io/ResourceCommandList: ""
name: clitestresources.test.cli.sigs.k8s.io
spec:
group: test.cli.sigs.k8s.io
names:
kind: CLITestResource
plural: clitestresources
scope: Namespaced
validation:
openAPIV3Schema:
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
properties:
image:
type: string
replicas:
format: int32
type: integer
type: object
status:
type: object
version: v1alpha1

View File

@ -0,0 +1,59 @@
# Run
---
items:
- command:
path:
- "create" # Command is a subcommand of this path
use: "deployment" # Command use
aliases: # Command alias'
- "deploy"
- "deployments"
short: Create a deployment with the specified name.
long: Create a deployment with the specified name.
example: |
# Create a new deployment named my-dep that runs the busybox image.
kubectl create deployment --name my-dep --image=busybox
flags:
- name: name
type: String
stringValue: ""
description: deployment name
- name: image
type: String
stringValue: ""
description: Image name to run.
- name: replicas
type: Int
intValue: 2
description: Image name to run.
requests:
- group: apps
version: v1
resource: deployments
operation: Create
bodyTemplate: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{index .Flags.Strings "name"}}
namespace: {{index .Flags.Strings "namespace"}}
labels:
app: {{index .Flags.Strings "name"}}
spec:
replicas: {{index .Flags.Ints "replicas"}}
selector:
matchLabels:
app: {{index .Flags.Strings "name"}}
template:
metadata:
labels:
app: {{index .Flags.Strings "name"}}
spec:
containers:
- name: {{index .Flags.Strings "name"}}
image: {{index .Flags.Strings "image"}}
saveResponseValues:
- name: responsename
jsonPath: "{.metadata.name}"
output: |
deployment.apps/{{index .Responses.Strings "responsename"}} created

9
go.mod
View File

@ -14,7 +14,7 @@ require (
github.com/gogo/protobuf v1.2.1 // indirect
github.com/golang/mock v1.2.0 // indirect
github.com/google/btree v1.0.0 // indirect
github.com/google/go-github v17.0.0+incompatible
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/google/wire v0.2.1
@ -30,6 +30,7 @@ require (
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.3.2 // indirect
github.com/stretchr/testify v1.2.2
go.opencensus.io v0.19.2 // indirect
@ -37,12 +38,12 @@ require (
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.9.1 // indirect
golang.org/x/net v0.0.0-20190328230028-74de082e2cca // indirect
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/src-d/go-git.v4 v4.10.0
k8s.io/api v0.0.0-20190327184913-92d2ee7fc726 // indirect
k8s.io/apiextensions-apiserver v0.0.0-20190328030136-8ada4fd07db4 // indirect
k8s.io/api v0.0.0-20190327184913-92d2ee7fc726
k8s.io/apiextensions-apiserver v0.0.0-20190328030136-8ada4fd07db4
k8s.io/apimachinery v0.0.0-20190326224424-4ceb6b6c5db5
k8s.io/cli-runtime v0.0.0-20190313123343-44a48934c135
k8s.io/client-go v2.0.0-alpha.0.0.20190313235726-6ee68ca5fd83+incompatible

2
go.sum
View File

@ -302,6 +302,7 @@ k8s.io/apimachinery v0.0.0-20190326224424-4ceb6b6c5db5 h1:cCw1HjzHT95HKn2M27Rwf7
k8s.io/apimachinery v0.0.0-20190326224424-4ceb6b6c5db5/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/apimachinery v0.0.0-20190328224500-e508a7b04a89 h1:O/jt/iVGeFmtp90jUQom2x9OU8TUWB26kv95JPtio2Y=
k8s.io/apimachinery v0.0.0-20190328224500-e508a7b04a89/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/apiserver v0.0.0-20190409093229-67d6e044d2ef h1:0JW8e1tJABjPR1XduEVUES0bCE1h2hrkGg+Y287l3rY=
k8s.io/cli-runtime v0.0.0-20190313123343-44a48934c135 h1:2kPEJQP89ZMAw6lzGrh/3G9URMZmx9Vmrdm0oB+MJis=
k8s.io/cli-runtime v0.0.0-20190313123343-44a48934c135/go.mod h1:qWnH3/b8sp/l7EvlDh7ulDU3UWA4P4N1NFbEEP791tM=
k8s.io/client-go v2.0.0-alpha.0.0.20190313235726-6ee68ca5fd83+incompatible h1:+SiikveGdttGGsPhWZsGg+RD1ziNvr+PL8zKqLDIrbE=
@ -312,6 +313,7 @@ k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c=
k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580 h1:fq0ZXW/BAIFZH+dazlups6JTVdwzRo5d9riFA103yuQ=
k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
k8s.io/kubectl v0.0.0-20190404144942-3a15b06406fb h1:faYJua2JzakSZWwvr5j0cHq5dtB87JT0SaC2rlk3MDA=
k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7 h1:8r+l4bNWjRlsFYlQJnKJ2p7s1YQPj4XyXiJVqDHRx7c=
k8s.io/utils v0.0.0-20190308190857-21c4ce38f2a7/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
sigs.k8s.io/controller-runtime v0.1.10 h1:amLOmcekVdnsD1uIpmgRqfTbQWJ2qxvQkcdeFhcotn4=

View File

@ -0,0 +1,24 @@
/*
Copyright 2019 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 v1alpha1
import "k8s.io/apimachinery/pkg/runtime/schema"
var (
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: "dynamic.cli.sigs.k8s.io", Version: "v1alpha1"}
)

View File

@ -0,0 +1,222 @@
/*
Copyright 2019 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 v1alpha1
// ResourceCommandList contains a list of Commands
type ResourceCommandList struct {
Items []ResourceCommand `json:"items"`
}
// ResourceCommand defines a command that is dynamically defined as an annotation on a CRD
type ResourceCommand struct {
// Command is the cli Command
Command Command `json:"command"`
// Requests are the requests the command will send to the apiserver.
// +optional
Requests []ResourceRequest `json:"requests,omitempty"`
// Output is a go-template used write the command output. It may reference values specified as flags using
// {{index .Flags.Strings "flag-name"}}, {{index .Flags.Ints "flag-name"}}, {{index .Flags.Bools "flag-name"}},
// {{index .Flags.Floats "flag-name"}}.
//
// It may also reference values from the responses that were saved using saveResponseValues
// - {{index .Responses.Strings "response-value-name"}}.
//
// Example:
// deployment.apps/{{index .Responses.Strings "responsename"}} created
//
// +optional
Output string `json:"output,omitempty"`
}
// ResourceOperation specifies the type of Request operation
type ResourceOperation string
const (
// CreateResource performs a Create Request
CreateResource ResourceOperation = "Create"
// UpdateResource performs an Update Request
UpdateResource = "Update"
// DeleteResource performs a Delete Request
DeleteResource = "Delete"
// GetResource performs a Get Request
GetResource = "Get"
// PatchResource performs a Patch Request
PatchResource = "Patch"
// PrintResource prints the Resource
PrintResource = "Print"
)
// ResourceRequest defines a request made by the cli to the apiserver
type ResourceRequest struct {
// Group is the API group of the request endpoint
//
// Example: apps
Group string `json:"group"`
// Version is the API version of the request endpoint
//
// Example: v1
Version string `json:"version"`
// Resource is the API resource of the request endpoint
//
// Example: deployments
Resource string `json:"resource"`
// Operation is the type of operation to perform for the request. One of: Create, Update, Delete, Get, Patch
Operation ResourceOperation `json:"operation"`
// BodyTemplate is a go-template for the request Body. It may reference values specified as flags using
// {{index .Flags.Strings "flag-name"}}, {{index .Flags.Ints "flag-name"}}, {{index .Flags.Bools "flag-name"}},
// {{index .Flags.Floats "flag-name"}}
//
// Example:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: {{index .Flags.Strings "name"}}
// namespace: {{index .Flags.Strings "namespace"}}
// labels:
// app: nginx
// spec:
// replicas: {{index .Flags.Ints "replicas"}}
// selector:
// matchLabels:
// app: {{index .Flags.Strings "name"}}
// template:
// metadata:
// labels:
// app: {{index .Flags.Strings "name"}}
// spec:
// containers:
// - name: {{index .Flags.Strings "name"}}
// image: {{index .Flags.Strings "image"}}
//
// +optional
BodyTemplate string `json:"bodyTemplate,omitempty"`
// SaveResponseValues are values read from the response and saved in {{index .Responses.Strings "flag-name"}}.
// They may be used in the ResourceCommand.Output go-template.
//
// Example:
// - name: responsename
// jsonPath: "{.metadata.name}"
//
// +optional
SaveResponseValues []ResponseValue `json:"saveResponseValues,omitempty"`
}
// Flag defines a cli flag that should be registered and available in request / output templates
type Flag struct {
Type FlagType `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
// +optional
Required *bool `json:"required,omitempty"`
// +optional
StringValue string `json:"stringValue,omitempty"`
// +optional
StringSliceValue []string `json:"stringSliceValue,omitempty"`
// +optional
BoolValue bool `json:"boolValue,omitempty"`
// +optional
IntValue int32 `json:"intValue,omitempty"`
// +optional
FloatValue float64 `json:"floatValue,omitempty"`
}
// ResponseValue defines a value that should be parsed from a response and available in output templates
type ResponseValue struct {
Name string `json:"name"`
JSONPath string `json:"jsonPath"`
}
// FlagType defines the type of flag to register in the cli
type FlagType string
const (
// String defines a string flag
String FlagType = "String"
// Bool defines a bool flag
Bool = "Bool"
// Float defines a float flag
Float = "Float"
// Int defines an int flag
Int = "Int"
// StringSlice defines a string slice flag
StringSlice = "StringSlice"
)
// Command defines a Command published on a CRD and created as a cobra Command in the cli
type Command struct {
// Use is the one-line usage message.
Use string `json:"use"`
// Path is the path to the sub-command. Omit if the command is directly under the root command.
// +optional
Path []string `json:"path,omitempty"`
// Short is the short description shown in the 'help' output.
// +optional
Short string `json:"short,omitempty"`
// Long is the long message shown in the 'help <this-command>' output.
// +optional
Long string `json:"long,omitempty"`
// Example is examples of how to use the command.
// +optional
Example string `json:"example,omitempty"`
// Deprecated defines, if this command is deprecated and should print this string when used.
// +optional
Deprecated string `json:"deprecated,omitempty"`
// Flags are the command line flags.
//
// Example:
// - name: namespace
// type: String
// stringValue: "default"
// description: "deployment namespace"
//
// +optional60
Flags []Flag `json:"flags,omitempty"`
// SuggestFor is an array of command names for which this command will be suggested -
// similar to aliases but only suggests.
SuggestFor []string `json:"suggestFor,omitempty"`
// Aliases is an array of aliases that can be used instead of the first word in Use.
Aliases []string `json:"aliases,omitempty"`
// Version defines the version for this command. If this value is non-empty and the command does not
// define a "version" flag, a "version" boolean flag will be added to the command and, if specified,
// will print content of the "Version" variable.
// +optional
Version string `json:"version,omitempty"`
}

View File

@ -0,0 +1,168 @@
/*
Copyright 2019 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 dispatch
import (
"bytes"
"fmt"
"html/template"
"k8s.io/client-go/kubernetes"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/util/jsonpath"
"sigs.k8s.io/cli-experimental/internal/pkg/apis/dynamic/v1alpha1"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/output"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/parse"
"sigs.k8s.io/yaml"
)
// Dispatcher dispatches requests for a Command
type Dispatcher struct {
// KubernetesClient is used to make requests
KubernetesClient *kubernetes.Clientset
// DynamicClient is used to make requests
DynamicClient dynamic.Interface
// Writer writes templatized output
Writer *output.CommandOutputWriter
}
// Dispatch sends requests to the apiserver for the Command.
func (d *Dispatcher) Dispatch(cmd *v1alpha1.ResourceCommand, values *parse.Values) error {
// Iterate over requests
for i := range cmd.Requests {
req := cmd.Requests[i]
if err := d.do(req, cmd.Command.Use, values); err != nil {
return err
}
}
return nil
}
func (d *Dispatcher) do(req v1alpha1.ResourceRequest, name string, values *parse.Values) error {
// Build the request object
obj, err := templateToResource(req.BodyTemplate, name+"-resource-request", values)
if err != nil {
return err
}
// Check if it is dry-do
if values.IsDryRun() {
// Simply print the object rather than making the request
o, err := yaml.Marshal(obj.Object)
if err != nil {
return fmt.Errorf("%v\n%+v", err, obj.Object)
}
fmt.Fprintf(d.Writer.Output, "%s---\n", string(o))
// Skip making requests for dry-do
return nil
}
// Send the request
gvr := schema.GroupVersionResource{Resource: req.Resource, Version: req.Version, Group: req.Group}
resp, err := d.doRequest(obj, gvr, req.Operation)
if err != nil {
return err
}
// Save the response values so they can be used when writing the output
if err := doResponse(req, resp, values); err != nil {
return err
}
return nil
}
// doRequest makes a request to the apiserver with the object (request body), operation (request http operation),
// group,version,resource (request url).
func (d *Dispatcher) doRequest(
obj *unstructured.Unstructured,
gvr schema.GroupVersionResource,
op v1alpha1.ResourceOperation) (*unstructured.Unstructured, error) {
req := d.DynamicClient.Resource(gvr).Namespace(obj.GetNamespace())
var resp = &unstructured.Unstructured{}
var o []byte
var err error
switch op {
case v1alpha1.PrintResource:
// Only print the object
if o, err = yaml.Marshal(obj.Object); err == nil {
fmt.Fprintf(d.Writer.Output, "%s---\n", string(o))
}
case v1alpha1.CreateResource:
resp, err = req.Create(obj, metav1.CreateOptions{})
case v1alpha1.DeleteResource:
err = req.Delete(obj.GetName(), &metav1.DeleteOptions{})
case v1alpha1.UpdateResource:
resp, err = req.Update(obj, metav1.UpdateOptions{})
case v1alpha1.GetResource:
resp, err = req.Get(obj.GetName(), metav1.GetOptions{})
case v1alpha1.PatchResource:
}
return resp, err
}
// templateToResource builds an Unstructured object from a template and flag values
func templateToResource(t, name string, values *parse.Values) (*unstructured.Unstructured, error) {
temp, err := template.New(name).Parse(t)
if err != nil {
return nil, fmt.Errorf("%v\n%s", err, t)
}
body := &bytes.Buffer{}
if err := temp.Execute(body, values); err != nil {
return nil, fmt.Errorf("%v\n%s\n%v", err, t, values)
}
// Parse the Resource into an Unstructured object
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal(body.Bytes(), &obj.Object); err != nil {
return nil, fmt.Errorf("%v\n%s", err, body.String())
}
return obj, nil
}
// doResponse parses the items specified by JSONPath from the response object back into the Flags struct
// so that the response is available in the output template
func doResponse(req v1alpha1.ResourceRequest, resp *unstructured.Unstructured, res *parse.Values) error {
if res.Responses.Strings == nil {
res.Responses.Strings = map[string]*string{}
}
for i := range req.SaveResponseValues {
v := req.SaveResponseValues[i]
j := jsonpath.New(v.Name)
buf := &bytes.Buffer{}
if err := j.Parse(v.JSONPath); err != nil {
return err
}
if err := j.Execute(buf, resp.Object); err != nil {
return err
}
s := buf.String()
res.Responses.Strings[v.Name] = &s
}
return nil
}

View File

@ -0,0 +1,663 @@
/*
Copyright 2019 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 dispatch_test
import (
"bytes"
"os"
"strings"
"testing"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/dispatch"
v1 "k8s.io/api/apps/v1"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/cli-experimental/internal/pkg/apis/dynamic/v1alpha1"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/parse"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest"
"sigs.k8s.io/yaml"
)
var templ = `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: foo
name: foo
namespace:
spec:
replicas: 2
selector:
matchLabels:
app: foo
template:
metadata:
labels:
app: foo
spec:
containers:
- image: nginx
name: foo
---
`
var instance *dispatch.Dispatcher
var buf = &bytes.Buffer{}
func TestMain(m *testing.M) {
var stop func()
var err error
instance, stop, err = wiretest.InitializeDispatcher(buf)
if err != nil {
os.Exit(1)
}
defer stop()
os.Exit(m.Run())
}
// TestDispatchCreate tests that a CreateResource operation succeeds
func TestDispatchCreate(t *testing.T) {
buf.Reset()
namespace := "create"
expected := strings.Replace(templ, "namespace:", "namespace: "+namespace, -1)
name := "foo"
replicas := int32(2)
image := "nginx"
dryrun := true
ns := &corev1.Namespace{}
ns.Name = namespace
_, err := instance.KubernetesClient.CoreV1().Namespaces().Create(ns)
assert.NoError(t, err)
req := &v1alpha1.ResourceCommand{
Requests: []v1alpha1.ResourceRequest{
{
Version: "v1",
Group: "apps",
Resource: "deployments",
Operation: v1alpha1.CreateResource,
BodyTemplate: `apiVersion: apps/v1
kind: Deployment
metadata:
name: {{index .Flags.Strings "name"}}
namespace: {{index .Flags.Strings "namespace"}}
labels:
app: {{index .Flags.Strings "name"}}
spec:
replicas: {{index .Flags.Ints "replicas"}}
selector:
matchLabels:
app: {{index .Flags.Strings "name"}}
template:
metadata:
labels:
app: {{index .Flags.Strings "name"}}
spec:
containers:
- name: {{index .Flags.Strings "name"}}
image: {{index .Flags.Strings "image"}}
`,
SaveResponseValues: []v1alpha1.ResponseValue{
{
JSONPath: "{.metadata.name}",
Name: "name",
},
{
JSONPath: "{.spec.replicas}",
Name: "replicas",
},
},
},
},
}
values := parse.Values{
Flags: parse.Flags{
Strings: map[string]*string{
"name": &name,
"namespace": &namespace,
"image": &image,
},
Ints: map[string]*int32{
"replicas": &replicas,
},
Bools: map[string]*bool{
"dry-run": &dryrun,
},
},
}
// Dry Run dispatch
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
assert.Equal(t, expected, buf.String())
d, err := instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.Error(t, err)
assert.Empty(t, d)
// Actually dispatch
dryrun = false
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
d, err = instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.NoError(t, err)
assert.NotEmpty(t, d)
assert.NotEmpty(t, &values.Responses.Strings)
assert.Equal(t, "foo", *values.Responses.Strings["name"])
assert.Equal(t, "2", *values.Responses.Strings["replicas"])
}
// TestDispatchUpdate tests that an UpdateResource operation succeeds
func TestDispatchUpdate(t *testing.T) {
buf.Reset()
namespace := "update"
expected := strings.Replace(templ, "namespace:", "namespace: "+namespace, -1)
name := "foo"
replicas := int32(3)
image := "nginx"
dryrun := true
ns := &corev1.Namespace{}
ns.Name = namespace
_, err := instance.KubernetesClient.CoreV1().Namespaces().Create(ns)
assert.NoError(t, err)
createD := &v1.Deployment{}
err = yaml.Unmarshal([]byte(expected), createD)
assert.NoError(t, err)
_, err = instance.KubernetesClient.AppsV1().Deployments(namespace).Create(createD)
assert.NoError(t, err)
req := &v1alpha1.ResourceCommand{
Requests: []v1alpha1.ResourceRequest{
{
Version: "v1",
Group: "apps",
Resource: "deployments",
Operation: v1alpha1.UpdateResource,
BodyTemplate: `apiVersion: apps/v1
kind: Deployment
metadata:
name: {{index .Flags.Strings "name"}}
namespace: {{index .Flags.Strings "namespace"}}
labels:
app: {{index .Flags.Strings "name"}}
spec:
replicas: {{index .Flags.Ints "replicas"}}
selector:
matchLabels:
app: {{index .Flags.Strings "name"}}
template:
metadata:
labels:
app: {{index .Flags.Strings "name"}}
spec:
containers:
- name: {{index .Flags.Strings "name"}}
image: {{index .Flags.Strings "image"}}
`,
SaveResponseValues: []v1alpha1.ResponseValue{
{
JSONPath: "{.metadata.name}",
Name: "name",
},
{
JSONPath: "{.spec.replicas}",
Name: "replicas",
},
},
},
},
}
values := parse.Values{
Flags: parse.Flags{
Strings: map[string]*string{
"name": &name,
"namespace": &namespace,
"image": &image,
},
Ints: map[string]*int32{
"replicas": &replicas,
},
Bools: map[string]*bool{
"dry-run": &dryrun,
},
},
}
// Dry Run dispatch
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
d, err := instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, int32(2), *d.Spec.Replicas)
// Actually dispatch
dryrun = false
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
d, err = instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, int32(3), *d.Spec.Replicas)
assert.NotEmpty(t, &values.Responses.Strings)
assert.Equal(t, "foo", *values.Responses.Strings["name"])
assert.Equal(t, "3", *values.Responses.Strings["replicas"])
}
// TestDispatchDelete tests that a DeleteResource operation succeeds
func TestDispatchDelete(t *testing.T) {
buf.Reset()
namespace := "delete"
expected := strings.Replace(templ, "namespace:", "namespace: "+namespace, -1)
name := "foo"
dryrun := true
ns := &corev1.Namespace{}
ns.Name = namespace
_, err := instance.KubernetesClient.CoreV1().Namespaces().Create(ns)
assert.NoError(t, err)
createD := &v1.Deployment{}
err = yaml.Unmarshal([]byte(expected), createD)
assert.NoError(t, err)
_, err = instance.KubernetesClient.AppsV1().Deployments(namespace).Create(createD)
assert.NoError(t, err)
req := &v1alpha1.ResourceCommand{
Requests: []v1alpha1.ResourceRequest{
{
Version: "v1",
Group: "apps",
Resource: "deployments",
Operation: v1alpha1.DeleteResource,
BodyTemplate: `apiVersion: apps/v1
kind: Deployment
metadata:
name: {{index .Flags.Strings "name"}}
namespace: {{index .Flags.Strings "namespace"}}
`,
},
},
}
values := parse.Values{
Flags: parse.Flags{
Strings: map[string]*string{
"name": &name,
"namespace": &namespace,
},
Bools: map[string]*bool{
"dry-run": &dryrun,
},
},
}
// Dry Run dispatch
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
d, err := instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.NoError(t, err)
assert.NotEmpty(t, d)
// Actually dispatch
dryrun = false
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
d, err = instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.Error(t, err)
assert.Empty(t, d)
}
// TestDispatchDelete tests that a DeleteResource operation succeeds
func TestDispatchGet(t *testing.T) {
buf.Reset()
namespace := "get"
expected := strings.Replace(templ, "namespace:", "namespace: "+namespace, -1)
name := "foo"
dryrun := true
ns := &corev1.Namespace{}
ns.Name = namespace
_, err := instance.KubernetesClient.CoreV1().Namespaces().Create(ns)
assert.NoError(t, err)
createD := &v1.Deployment{}
err = yaml.Unmarshal([]byte(expected), createD)
assert.NoError(t, err)
_, err = instance.KubernetesClient.AppsV1().Deployments(namespace).Create(createD)
assert.NoError(t, err)
req := &v1alpha1.ResourceCommand{
Requests: []v1alpha1.ResourceRequest{
{
Version: "v1",
Group: "apps",
Resource: "deployments",
Operation: v1alpha1.GetResource,
BodyTemplate: `apiVersion: apps/v1
kind: Deployment
metadata:
name: {{index .Flags.Strings "name"}}
namespace: {{index .Flags.Strings "namespace"}}
labels:
app: {{index .Flags.Strings "name"}}
`,
SaveResponseValues: []v1alpha1.ResponseValue{
{
JSONPath: "{.metadata.name}",
Name: "name",
},
{
JSONPath: "{.spec.replicas}",
Name: "replicas",
},
},
},
},
}
values := parse.Values{
Flags: parse.Flags{
Strings: map[string]*string{
"name": &name,
"namespace": &namespace,
},
Bools: map[string]*bool{
"dry-run": &dryrun,
},
},
}
// Dry Run dispatch
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
d, err := instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.NoError(t, err)
assert.NotEmpty(t, d)
assert.Empty(t, &values.Responses.Ints)
assert.Empty(t, &values.Responses.Strings)
assert.Empty(t, &values.Responses.StringSlices)
assert.Empty(t, &values.Responses.Bools)
assert.Empty(t, &values.Responses.Floats)
// Actually dispatch
dryrun = false
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
d, err = instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.NoError(t, err)
assert.NotEmpty(t, d)
assert.NotEmpty(t, &values.Responses.Strings)
assert.Equal(t, "foo", *values.Responses.Strings["name"])
assert.Equal(t, "2", *values.Responses.Strings["replicas"])
}
// TestDispatchDelete tests that a DeleteResource operation succeeds
func TestDispatchPrint(t *testing.T) {
buf.Reset()
namespace := "print"
expected := strings.Replace(templ, "namespace:", "namespace: "+namespace, -1)
name := "foo"
dryrun := true
replicas := int32(2)
image := "nginx"
req := &v1alpha1.ResourceCommand{
Requests: []v1alpha1.ResourceRequest{
{
Version: "v1",
Group: "apps",
Resource: "deployments",
Operation: v1alpha1.PrintResource,
BodyTemplate: `apiVersion: apps/v1
kind: Deployment
metadata:
name: {{index .Flags.Strings "name"}}
namespace: {{index .Flags.Strings "namespace"}}
labels:
app: {{index .Flags.Strings "name"}}
spec:
replicas: {{index .Flags.Ints "replicas"}}
selector:
matchLabels:
app: {{index .Flags.Strings "name"}}
template:
metadata:
labels:
app: {{index .Flags.Strings "name"}}
spec:
containers:
- name: {{index .Flags.Strings "name"}}
image: {{index .Flags.Strings "image"}}
`,
},
},
}
values := parse.Values{
Flags: parse.Flags{
Strings: map[string]*string{
"name": &name,
"namespace": &namespace,
"image": &image,
},
Ints: map[string]*int32{
"replicas": &replicas,
},
Bools: map[string]*bool{
"dry-run": &dryrun,
},
},
}
// Dry Run dispatch
err := instance.Dispatch(req, &values)
assert.NoError(t, err)
assert.Equal(t, expected, buf.String())
d, err := instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.Error(t, err)
assert.Empty(t, d)
assert.Empty(t, &values.Responses.Ints)
assert.Empty(t, &values.Responses.Strings)
assert.Empty(t, &values.Responses.StringSlices)
assert.Empty(t, &values.Responses.Bools)
assert.Empty(t, &values.Responses.Floats)
// Actually dispatch
dryrun = false
buf.Reset()
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
assert.Equal(t, expected, buf.String())
d, err = instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.Error(t, err)
assert.Empty(t, d)
assert.Empty(t, &values.Responses.Ints)
assert.Empty(t, &values.Responses.Strings)
assert.Empty(t, &values.Responses.StringSlices)
assert.Empty(t, &values.Responses.Bools)
assert.Empty(t, &values.Responses.Floats)
}
var multiTempl = `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: foo
name: foo
namespace:
spec:
replicas: 2
selector:
matchLabels:
app: foo
template:
metadata:
labels:
app: foo
spec:
containers:
- image: nginx
name: foo
---
apiVersion: v1
kind: Service
metadata:
labels:
app: foo
name: foo
namespace:
spec:
ports:
- port: 80
protocol: TCP
targetPort: 9376
selector:
app: foo
---
`
// TestDispatchCreate tests that a CreateResource operation succeeds
func TestDispatchMultiCreate(t *testing.T) {
buf.Reset()
namespace := "multi"
expected := strings.Replace(multiTempl, "namespace:", "namespace: "+namespace, -1)
name := "foo"
replicas := int32(2)
image := "nginx"
dryrun := true
ns := &corev1.Namespace{}
ns.Name = namespace
_, err := instance.KubernetesClient.CoreV1().Namespaces().Create(ns)
assert.NoError(t, err)
req := &v1alpha1.ResourceCommand{
Requests: []v1alpha1.ResourceRequest{
{
Version: "v1",
Group: "apps",
Resource: "deployments",
Operation: v1alpha1.CreateResource,
BodyTemplate: `apiVersion: apps/v1
kind: Deployment
metadata:
name: {{index .Flags.Strings "name"}}
namespace: {{index .Flags.Strings "namespace"}}
labels:
app: {{index .Flags.Strings "name"}}
spec:
replicas: {{index .Flags.Ints "replicas"}}
selector:
matchLabels:
app: {{index .Flags.Strings "name"}}
template:
metadata:
labels:
app: {{index .Flags.Strings "name"}}
spec:
containers:
- name: {{index .Flags.Strings "name"}}
image: {{index .Flags.Strings "image"}}
`,
SaveResponseValues: []v1alpha1.ResponseValue{
{
JSONPath: "{.metadata.name}",
Name: "name",
},
{
JSONPath: "{.spec.replicas}",
Name: "replicas",
},
},
},
{
Version: "v1",
Group: "",
Resource: "services",
Operation: v1alpha1.CreateResource,
BodyTemplate: `apiVersion: v1
kind: Service
metadata:
name: {{ index .Flags.Strings "name" }}
namespace: {{ index .Flags.Strings "namespace" }}
labels:
app: {{ index .Flags.Strings "name" }}
spec:
selector:
app: {{ index .Flags.Strings "name" }}
ports:
- port: 80
protocol: TCP
targetPort: 9376
`,
SaveResponseValues: []v1alpha1.ResponseValue{
{
JSONPath: "{.spec.ports[0].port}",
Name: "port",
},
},
},
},
}
values := parse.Values{
Flags: parse.Flags{
Strings: map[string]*string{
"name": &name,
"namespace": &namespace,
"image": &image,
},
Ints: map[string]*int32{
"replicas": &replicas,
},
Bools: map[string]*bool{
"dry-run": &dryrun,
},
},
}
// Dry Run dispatch
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
assert.Equal(t, expected, buf.String())
d, err := instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.Error(t, err)
assert.Empty(t, d)
// Actually dispatch
dryrun = false
err = instance.Dispatch(req, &values)
assert.NoError(t, err)
d, err = instance.KubernetesClient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
assert.NoError(t, err)
assert.NotEmpty(t, d)
assert.NotEmpty(t, &values.Responses.Strings)
s, err := instance.KubernetesClient.CoreV1().Services(namespace).Get(name, metav1.GetOptions{})
assert.NoError(t, err)
assert.NotEmpty(t, s)
assert.NotEmpty(t, &values.Responses.Strings)
assert.Equal(t, "foo", *values.Responses.Strings["name"])
assert.Equal(t, "2", *values.Responses.Strings["replicas"])
assert.Equal(t, "80", *values.Responses.Strings["port"])
}

View File

@ -0,0 +1,20 @@
/*
Copyright 2019 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 dispatch contains libraries for dispatching requests.
*/
package dispatch

20
internal/pkg/dy/doc.go Normal file
View File

@ -0,0 +1,20 @@
/*
Copyright 2019 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 dy contains libraries for registering Commands published by CRDs as go-templates.
*/
package dy

94
internal/pkg/dy/dy.go Normal file
View File

@ -0,0 +1,94 @@
/*
Copyright 2019 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 dy
import (
"github.com/google/wire"
"github.com/spf13/cobra"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/cli-experimental/internal/pkg/apis/dynamic/v1alpha1"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/dispatch"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/list"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/output"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/parse"
)
// ProviderSet provides wiring for initializing types.
var ProviderSet = wire.NewSet(
output.CommandOutputWriter{}, list.CommandLister{}, parse.CommandParser{}, dispatch.Dispatcher{},
CommandBuilder{})
// CommandBuilder creates dynamically generated commands from annotations on CRDs.
type CommandBuilder struct {
// KubernetesClient is used to make requests
KubernetesClient *kubernetes.Clientset
// Lister lists Commands from CRDs
Lister *list.CommandLister
// Parser parses Commands from CRDs into cobra Commands
Parser *parse.CommandParser
// Parser parses Commands from CRDs into cobra Commands
Dispatcher *dispatch.Dispatcher
// Writer writes templatized output
Writer *output.CommandOutputWriter
}
// Build adds dynamic Commands to the root Command.
func (b *CommandBuilder) Build(root *cobra.Command, options *v1.ListOptions) error {
// list commands from CRDs
l, err := b.Lister.List(options)
if err != nil {
return err
}
for i := range l.Items {
cmd := l.Items[i]
// parse the data from the CRD into a cobra Command
parsed := b.build(&cmd)
// add the cobra Command to the root Command
parse.AddAtPath(root, parsed, cmd.Command.Path)
}
return nil
}
func (b *CommandBuilder) build(cmd *v1alpha1.ResourceCommand) *cobra.Command {
cobracmd, values := b.Parser.Parse(&cmd.Command)
cobracmd.RunE = func(c *cobra.Command, args []string) error {
// Add the namespace flag value. This is necessary because it is set by the
// common cli flags rather than from the command itself.
ns := cobracmd.Flag("namespace").Value.String()
values.Flags.Strings["namespace"] = &ns
// Do the requests
if err := b.Dispatcher.Dispatch(cmd, &values); err != nil {
return err
}
// Write the output
if values.IsDryRun() != true {
return b.Writer.Write(cmd, &values)
}
return nil
}
return cobracmd
}

302
internal/pkg/dy/dy_test.go Normal file
View File

@ -0,0 +1,302 @@
/*
Copyright 2019 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 dy_test
import (
"bytes"
"os"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/cli-experimental/internal/pkg/apis/dynamic/v1alpha1"
"sigs.k8s.io/cli-experimental/internal/pkg/dy"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiretest"
"sigs.k8s.io/cli-experimental/util/dyctl/cmd"
"sigs.k8s.io/yaml"
)
var instance *dy.CommandBuilder
func TestMain(m *testing.M) {
var stop func()
var err error
instance, stop, err = wiretest.InitializeCommandBuilder(nil)
if err != nil {
os.Exit(1)
}
defer stop()
os.Exit(m.Run())
}
func TestBuildCRD(t *testing.T) {
buf := &bytes.Buffer{}
instance.Writer.Output = buf
// Create CRDs with Commands
namespace := "buildcrd"
ns := &v1.Namespace{}
ns.Name = namespace
_, err := instance.KubernetesClient.CoreV1().Namespaces().Create(ns)
assert.NoError(t, err)
// First CRD
crd1 := &v1beta1.CustomResourceDefinition{}
err = yaml.UnmarshalStrict([]byte(crd1YAML), crd1)
assert.NoError(t, err)
cl1 := &v1alpha1.ResourceCommandList{}
err = yaml.UnmarshalStrict([]byte(commandListYAML1), cl1)
assert.NoError(t, err)
err = cmd.AddTo(cl1, crd1)
assert.NoError(t, err)
_, err = instance.Lister.Client.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd1)
assert.NoError(t, err)
// Second CRD
crd2 := &v1beta1.CustomResourceDefinition{}
err = yaml.UnmarshalStrict([]byte(crd2YAML), crd2)
assert.NoError(t, err)
cl2 := &v1alpha1.ResourceCommandList{}
err = yaml.UnmarshalStrict([]byte(commandListYAML2), cl2)
assert.NoError(t, err)
err = cmd.AddTo(cl2, crd2)
assert.NoError(t, err)
_, err = instance.Lister.Client.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd2)
assert.NoError(t, err)
// Build Command for dry-run test
buf.Reset()
root := &cobra.Command{}
err = instance.Build(root, nil)
assert.NoError(t, err)
// Verify Command structure
assert.Len(t, root.Commands(), 1)
assert.Len(t, root.Commands()[0].Commands(), 2)
cmd1, _, err := root.Find([]string{"create", "deployment"})
assert.NoError(t, err)
assert.Equal(t, "deployment", cmd1.Use)
assert.Equal(t, `# Create a new deployment named my-dep that runs the busybox image.
kubectl create deployment --name my-dep --image=busybox
`, cmd1.Example)
assert.Equal(t, `Create a deployment with the specified name - long.`, cmd1.Long)
assert.Equal(t, `Create a deployment with the specified name - short.`, cmd1.Short)
assert.Equal(t, []string{"deploy", "deployments"}, cmd1.Aliases)
// Set namespace because it will be set by the common cli flags rather than on the command itself
cmd1.Flags().String("namespace", "", "")
cmd2, _, err := root.Find([]string{"create", "service"})
assert.NoError(t, err)
assert.Equal(t, "service", cmd2.Use)
// Verify dry-run behavior
root.SetArgs([]string{
"create", "deployment", "--name=foo", "--namespace=" + namespace, "--image=nginx", "--dry-run"})
err = root.Execute()
assert.NoError(t, err)
assert.Equal(t, "foo", cmd1.Flag("name").Value.String())
assert.Equal(t, namespace, cmd1.Flag("namespace").Value.String())
assert.Equal(t, "nginx", cmd1.Flag("image").Value.String())
assert.Equal(t, "true", cmd1.Flag("dry-run").Value.String())
assert.Equal(t, expectedString, buf.String())
// Check Deployment was NOT created
d, err := instance.KubernetesClient.AppsV1().Deployments(namespace).Get("foo", metav1.GetOptions{})
assert.Error(t, err)
assert.Empty(t, d)
// Build Command for non-dry-run test
buf.Reset()
root = &cobra.Command{}
err = instance.Build(root, nil)
assert.NoError(t, err)
cmd1, _, err = root.Find([]string{"create", "deployment"})
assert.NoError(t, err)
// Set namespace because it will be set by the common cli flags rather than on the command itself
cmd1.Flags().String("namespace", "", "")
// Verify non-dry-run behavior
root.SetArgs([]string{
"create", "deployment", "--name=foo", "--namespace=" + namespace, "--image=nginx"})
err = root.Execute()
assert.NoError(t, err)
// Check Deployment was created
d, err = instance.KubernetesClient.AppsV1().Deployments(namespace).Get("foo", metav1.GetOptions{})
assert.NoError(t, err)
assert.NotEmpty(t, d)
// Check output
}
var expectedString = `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: foo
name: foo
namespace: buildcrd
spec:
replicas: 2
selector:
matchLabels:
app: foo
template:
metadata:
labels:
app: foo
spec:
containers:
- image: nginx
name: foo
---
`
var crd1YAML = `apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: "clitestresourcesa.test.cli.sigs.k8s.io"
spec:
group: test.cli.sigs.k8s.io
names:
kind: CLITestResource
plural: clitestresourcesa
scope: Namespaced
version: v1alpha1
---
`
var crd2YAML = `apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: "clitestresourcesb.test.cli.sigs.k8s.io"
spec:
group: test.cli.sigs.k8s.io
names:
kind: CLITestResource
plural: clitestresourcesb
scope: Namespaced
version: v1alpha1
---
`
var commandListYAML1 = `items:
- command:
path:
- "create" # Command is a subcommand of this path
use: "deployment" # Command use
aliases: # Command alias'
- "deploy"
- "deployments"
short: Create a deployment with the specified name - short.
long: Create a deployment with the specified name - long.
example: |
# Create a new deployment named my-dep that runs the busybox image.
kubectl create deployment --name my-dep --image=busybox
flags:
- name: name
type: String
stringValue: ""
description: deployment name
- name: image
type: String
stringValue: ""
description: Image name to run.
- name: replicas
type: Int
intValue: 2
description: Image name to run.
requests:
- group: apps
version: v1
resource: deployments
operation: Create
bodyTemplate: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{index .Flags.Strings "name"}}
namespace: {{index .Flags.Strings "namespace"}}
labels:
app: {{index .Flags.Strings "name"}}
spec:
replicas: {{index .Flags.Ints "replicas"}}
selector:
matchLabels:
app: {{index .Flags.Strings "name"}}
template:
metadata:
labels:
app: {{index .Flags.Strings "name"}}
spec:
containers:
- name: {{index .Flags.Strings "name"}}
image: {{index .Flags.Strings "image"}}
saveResponseValues:
- name: responsename
jsonPath: "{.metadata.name}"
output: |
deployment.apps/{{index .Responses.Strings "responsename"}} created
`
var commandListYAML2 = `items:
- command:
path:
- "create" # Command is a subcommand of this path
use: "service" # Command use
short: Create a service with the specified name.
long: Create a service with the specified name.
example: |
# Create a new service
kubectl create service --name my-dep --label-key app --label-value foo
flags:
- name: name
type: String
stringValue: ""
description: service name
- name: label-key
type: String
stringValue: ""
description: label key to select.
- name: label-value
type: String
stringValue: ""
description: label value to select.
requests:
- group: ""
version: v1
resource: services
operation: Create
bodyTemplate: |
apiVersion: v1
kind: Service
metadata:
name: {{index .Flags.Strings "name"}}
namespace: {{index .Flags.Strings "namespace"}}
labels:
{{ index .Flags.Strings "label-key"}}: {{index .Flags.Strings "label-value"}}
spec:
selector:
{{ index .Flags.Strings "label-key"}}: {{index .Flags.Strings "label-value"}}
output: |
service/{{index .Flags.Strings "name"}} created
`

View File

@ -0,0 +1,20 @@
/*
Copyright 2019 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 list contains libraries for listing dynamic Commands.
*/
package list

View File

@ -0,0 +1,116 @@
/*
Copyright 2019 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 list
import (
"fmt"
"os"
"reflect"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
clidynamic "sigs.k8s.io/cli-experimental/internal/pkg/apis/dynamic/v1alpha1"
"sigs.k8s.io/yaml"
)
const (
annotation = "cli-experimental.sigs.k8s.io/ResourceCommandList"
label = annotation
)
// CommandLister lists commands from CRDs
type CommandLister struct {
// Client talks to an apiserver to list CRDs
Client *clientset.Clientset
// DynamicClient is used to make requests
DynamicClient dynamic.Interface
}
var gvk = schema.GroupVersionResource{
Resource: "resourcecommands", Group: "dynamic.cli.sigs.k8s.io", Version: "v1alpha1"}
// List fetches the list of dynamic commands published as Annotations on CRDs
func (cl *CommandLister) List(options *metav1.ListOptions) (clidynamic.ResourceCommandList, error) {
client := cl.Client.ApiextensionsV1beta1().CustomResourceDefinitions()
cmds := clidynamic.ResourceCommandList{}
if options == nil {
options = &metav1.ListOptions{LabelSelector: label}
}
// Get ResourceCommand CRs if they exist
rcs, err := cl.DynamicClient.Resource(gvk).List(*options)
if err == nil {
// Ignore errors because the CRD for ResourceCommands may not be defined
for i := range rcs.Items {
rc := rcs.Items[i]
rcBytes, err := yaml.Marshal(rc)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to Marshal ResourceCommand %s: %v\n", rc.GetName(), err)
continue
}
cmd := clidynamic.ResourceCommand{}
err = yaml.UnmarshalStrict(rcBytes, &cmd)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to Unmarshal ResourceCommand %s: %v\n", rc.GetName(), err)
continue
}
cmds.Items = append(cmds.Items, cmd)
}
}
// Get CRDs with ResourceCommands as annotations
crds, err := client.List(*options)
if err != nil {
return cmds, err
}
for i := range crds.Items {
crd := crds.Items[i]
// Get the ResourceCommand json
s := crd.Annotations[annotation]
if len(s) == 0 {
fmt.Fprintf(os.Stderr, "CRD missing ResourceCommand annotation %s: %v\n", crd.Name, err)
continue
}
// Unmarshall the annotation value into a ResourceCommandList
rcList := clidynamic.ResourceCommandList{}
if err := yaml.UnmarshalStrict([]byte(s), &rcList); err != nil {
fmt.Fprintf(os.Stderr, "failed to parse commands for CRD %s: %v\n", crd.Name, err)
continue
}
// Verify we parsed something
if reflect.DeepEqual(rcList, clidynamic.ResourceCommandList{}) {
fmt.Fprintf(os.Stderr, "no commands for CRD %s: %s\n", crd.Name, s)
continue
}
// Add the commands to the list
for i := range rcList.Items {
item := rcList.Items[i]
if len(item.Requests) > 0 {
cmds.Items = append(cmds.Items, item)
}
}
}
return cmds, nil
}

View File

@ -0,0 +1,17 @@
/*
Copyright 2019 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 list_test

View File

@ -0,0 +1,20 @@
/*
Copyright 2019 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 output contains libraries for printing the output from apiserver Responses.
*/
package output

View File

@ -0,0 +1,55 @@
/*
Copyright 2019 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 output
import (
"bytes"
"fmt"
"io"
"text/template"
clidynamic "sigs.k8s.io/cli-experimental/internal/pkg/apis/dynamic/v1alpha1"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/parse"
)
// CommandOutputWriter writes command Response values
type CommandOutputWriter struct {
// Output is the output for the command
Output io.Writer
}
// Write parses the outputTemplate and executes it with values, writing the output to writer.
func (w *CommandOutputWriter) Write(cmd *clidynamic.ResourceCommand, values *parse.Values) error {
// Do nothing if there is no output template defined
if cmd.Output == "" {
return nil
}
// Generate the output from the template and flag + response values
temp, err := template.New(cmd.Command.Use + "-output-template").Parse(cmd.Output)
if err != nil {
return err
}
buff := &bytes.Buffer{}
if err := temp.Execute(buff, values); err != nil {
return err
}
// Print the output
fmt.Fprintf(w.Output, buff.String())
return nil
}

View File

@ -0,0 +1,17 @@
/*
Copyright 2019 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 output_test

View File

@ -0,0 +1,20 @@
/*
Copyright 2019 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 parse contains libraries for parsing dynamic Resources into cobra.Commands.
*/
package parse

View File

@ -0,0 +1,156 @@
/*
Copyright 2019 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 parse
import (
"github.com/spf13/cobra"
clidynamic "sigs.k8s.io/cli-experimental/internal/pkg/apis/dynamic/v1alpha1"
)
// CommandParser parses clidnamic.Commands into cobra.Commands
type CommandParser struct{}
// Parse parses the dynamic ResourceCommand into a cobra ResourceCommand
func (p *CommandParser) Parse(cmd *clidynamic.Command) (*cobra.Command, Values) {
values := Values{}
// create the cobra command by copying values from the cli
cbra := &cobra.Command{
Use: cmd.Use,
Short: cmd.Short,
Long: cmd.Long,
Example: cmd.Example,
Version: cmd.Version,
Deprecated: cmd.Deprecated,
Aliases: cmd.Aliases,
SuggestFor: cmd.SuggestFor,
}
// Register the cobra flags in the values structure
for i := range cmd.Flags {
cmdFlag := cmd.Flags[i]
switch cmdFlag.Type {
case clidynamic.String:
if values.Flags.Strings == nil {
values.Flags.Strings = map[string]*string{}
}
// Create a string flag and register it
values.Flags.Strings[cmdFlag.Name] = cbra.Flags().String(cmdFlag.Name, cmdFlag.StringValue, cmdFlag.Description)
case clidynamic.StringSlice:
if values.Flags.StringSlices == nil {
values.Flags.StringSlices = map[string]*[]string{}
}
// Create a string slice flag and register it
values.Flags.StringSlices[cmdFlag.Name] = cbra.Flags().StringSlice(
cmdFlag.Name, cmdFlag.StringSliceValue, cmdFlag.Description)
case clidynamic.Int:
if values.Flags.Ints == nil {
values.Flags.Ints = map[string]*int32{}
}
// Create an int flag and register it
values.Flags.Ints[cmdFlag.Name] = cbra.Flags().Int32(cmdFlag.Name, cmdFlag.IntValue, cmdFlag.Description)
case clidynamic.Float:
if values.Flags.Floats == nil {
values.Flags.Floats = map[string]*float64{}
}
// Create a float flag and register it
values.Flags.Floats[cmdFlag.Name] = cbra.Flags().Float64(cmdFlag.Name, cmdFlag.FloatValue, cmdFlag.Description)
case clidynamic.Bool:
if values.Flags.Bools == nil {
values.Flags.Bools = map[string]*bool{}
}
// Create a bool flag and register it
values.Flags.Bools[cmdFlag.Name] = cbra.Flags().Bool(cmdFlag.Name, cmdFlag.BoolValue, cmdFlag.Description)
}
if cmdFlag.Required != nil && *cmdFlag.Required == true {
cbra.MarkFlagRequired(cmdFlag.Name)
}
}
// Add the dry-run flag
if values.Flags.Bools == nil {
values.Flags.Bools = map[string]*bool{}
}
dr := cbra.Flags().Bool("dry-run", false,
"If true, only print the objects that would be sent without sending them.")
values.Flags.Bools["dry-run"] = dr
return cbra, values
}
// Values contains input flag values and output response values
type Values struct {
// Flags are values provided by the user on the command line
Flags Flags
// Responses are values provided by the apiserver in a response
Responses Flags
}
// Flags contains flag values setup for the cobra command
type Flags struct {
// Strings contains a map of flag names to string values
Strings map[string]*string
// Ints contains a map of flag names to int values
Ints map[string]*int32
// Bools contains a map of flag names to bool values
Bools map[string]*bool
// Floats contains a map of flag names to flat values
Floats map[string]*float64
// StringSlices contains a map of flag names to string slice values
StringSlices map[string]*[]string
}
// IsDryRun returns true if the command is running in dry-run mode
func (v Values) IsDryRun() bool {
if dr, ok := v.Flags.Bools["dry-run"]; ok {
return *dr
}
return false
}
// AddAtPath adds the subcmd to root at the provided path. An empty path will add subcmd as a sub-command of root.
func AddAtPath(root, subcmd *cobra.Command, path []string) {
next := root
// For each element on the Path
for i := range path {
p := path[i]
// Make sure the subcommand exists
found := false
for i := range next.Commands() {
c := next.Commands()[i]
if c.Use == p {
// Found, continue on to next part of the Path
next = c
found = true
break
}
}
if found == false {
// Missing, create the sub-command
cbra := &cobra.Command{Use: p}
next.AddCommand(cbra)
next = cbra
}
}
next.AddCommand(subcmd)
}

View File

@ -0,0 +1,355 @@
/*
Copyright 2019 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 parse_test
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/cli-experimental/internal/pkg/apis/dynamic/v1alpha1"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/parse"
)
func TestParseDryRunFlag(t *testing.T) {
instance := parse.CommandParser{}
cmd := &v1alpha1.Command{}
cobracmd, values := instance.Parse(cmd)
assert.Equal(t, false, *values.Flags.Bools["dry-run"])
// Check the flags get updated
cobracmd.Flags().Parse([]string{
"--dry-run",
})
assert.Equal(t, true, *values.Flags.Bools["dry-run"])
}
func TestCommandParser_Parse_StringFlags(t *testing.T) {
instance := parse.CommandParser{}
cmd := &v1alpha1.Command{
Flags: []v1alpha1.Flag{
{
Name: "string-flag-1",
StringValue: "hello world 1",
Type: v1alpha1.String,
Description: "string-flag-description 1",
},
{
Name: "string-flag-2",
StringValue: "hello world 2",
Type: v1alpha1.String,
Description: "string-flag-description 2",
},
{
Name: "string-flag-3",
Type: v1alpha1.String,
},
},
}
cobracmd, values := instance.Parse(cmd)
// Check the Descriptions
assert.Equal(t, "string-flag-description 1", cobracmd.Flag("string-flag-1").Usage)
assert.Equal(t, "string-flag-description 2", cobracmd.Flag("string-flag-2").Usage)
assert.Equal(t, "", cobracmd.Flag("string-flag-3").Usage)
// Check defai;t Values
assert.Equal(t, "hello world 1", *values.Flags.Strings["string-flag-1"])
assert.Equal(t, "hello world 2", *values.Flags.Strings["string-flag-2"])
assert.Equal(t, "", *values.Flags.Strings["string-flag-3"])
// Check the flags get updated
cobracmd.Flags().Parse([]string{
"--string-flag-1=foo 1",
})
assert.Equal(t, "foo 1", *values.Flags.Strings["string-flag-1"])
assert.Equal(t, "hello world 2", *values.Flags.Strings["string-flag-2"])
assert.Equal(t, "", *values.Flags.Strings["string-flag-3"])
}
func TestCommandParser_Parse_StringSliceFlags(t *testing.T) {
instance := parse.CommandParser{}
cmd := &v1alpha1.Command{
Flags: []v1alpha1.Flag{
{
Name: "string-slice-flag-1",
StringSliceValue: []string{"hello1", "world1"},
Type: v1alpha1.StringSlice,
Description: "string-slice-flag-description 1",
},
{
Name: "string-slice-flag-2",
StringSliceValue: []string{"hello2", "world2"},
Type: v1alpha1.StringSlice,
Description: "string-slice-flag-description 2",
},
{
Name: "string-slice-flag-3",
Type: v1alpha1.StringSlice,
},
},
}
cobracmd, values := instance.Parse(cmd)
// Check the Descriptions
assert.Equal(t, "string-slice-flag-description 1", cobracmd.Flag("string-slice-flag-1").Usage)
assert.Equal(t, "string-slice-flag-description 2", cobracmd.Flag("string-slice-flag-2").Usage)
assert.Equal(t, "", cobracmd.Flag("string-slice-flag-3").Usage)
// Check default Values
assert.Equal(t, []string{"hello1", "world1"}, *values.Flags.StringSlices["string-slice-flag-1"])
assert.Equal(t, []string{"hello2", "world2"}, *values.Flags.StringSlices["string-slice-flag-2"])
assert.Equal(t, []string(nil), *values.Flags.StringSlices["string-slice-flag-3"])
// Check the flags get updated
cobracmd.Flags().Parse([]string{
"--string-slice-flag-1=foo1",
"--string-slice-flag-1=bar1",
"--string-slice-flag-1=11",
"--string-slice-flag-3=foo3",
"--string-slice-flag-3=baz3",
})
assert.Equal(t, []string{"foo1", "bar1", "11"}, *values.Flags.StringSlices["string-slice-flag-1"])
assert.Equal(t, []string{"hello2", "world2"}, *values.Flags.StringSlices["string-slice-flag-2"])
assert.Equal(t, []string{"foo3", "baz3"}, *values.Flags.StringSlices["string-slice-flag-3"])
}
func TestCommandParser_Parse_IntFlags(t *testing.T) {
instance := parse.CommandParser{}
cmd := &v1alpha1.Command{
Flags: []v1alpha1.Flag{
{
Name: "int-flag-1",
IntValue: 1,
Type: v1alpha1.Int,
Description: "int flag 1 description",
},
{
Name: "int-flag-2",
IntValue: 2,
Type: v1alpha1.Int,
Description: "int-flag-2-description",
},
{
Name: "int-flag-3",
Type: v1alpha1.Int,
},
},
}
cobracmd, values := instance.Parse(cmd)
// Check the Descriptions
assert.Equal(t, "int flag 1 description", cobracmd.Flag("int-flag-1").Usage)
assert.Equal(t, "int-flag-2-description", cobracmd.Flag("int-flag-2").Usage)
assert.Equal(t, "", cobracmd.Flag("int-flag-3").Usage)
// Check default Values
assert.Equal(t, int32(1), *values.Flags.Ints["int-flag-1"])
assert.Equal(t, int32(2), *values.Flags.Ints["int-flag-2"])
assert.Equal(t, int32(0), *values.Flags.Ints["int-flag-3"])
// Check the flags get updated
cobracmd.Flags().Parse([]string{
"--int-flag-1=10",
"--int-flag-3=3",
})
assert.Equal(t, int32(10), *values.Flags.Ints["int-flag-1"])
assert.Equal(t, int32(2), *values.Flags.Ints["int-flag-2"])
assert.Equal(t, int32(3), *values.Flags.Ints["int-flag-3"])
}
func TestCommandParser_Parse_FloatFlags(t *testing.T) {
instance := parse.CommandParser{}
cmd := &v1alpha1.Command{
Flags: []v1alpha1.Flag{
{
Name: "float-flag-1",
FloatValue: 1.1,
Type: v1alpha1.Float,
Description: "float flag 1 description",
},
{
Name: "float-flag-2",
FloatValue: 2.2,
Type: v1alpha1.Float,
Description: "float-flag-2-description",
},
{
Name: "float-flag-3",
Type: v1alpha1.Float,
},
},
}
cobracmd, values := instance.Parse(cmd)
// Check the Descriptions
assert.Equal(t, "float flag 1 description", cobracmd.Flag("float-flag-1").Usage)
assert.Equal(t, "float-flag-2-description", cobracmd.Flag("float-flag-2").Usage)
assert.Equal(t, "", cobracmd.Flag("float-flag-3").Usage)
// Check default Values
assert.Equal(t, 1.1, *values.Flags.Floats["float-flag-1"])
assert.Equal(t, 2.2, *values.Flags.Floats["float-flag-2"])
assert.Equal(t, 0.0, *values.Flags.Floats["float-flag-3"])
// Check the flags get updated
cobracmd.Flags().Parse([]string{
"--float-flag-1=10.10",
"--float-flag-3=3.3",
})
assert.Equal(t, 10.10, *values.Flags.Floats["float-flag-1"])
assert.Equal(t, 2.2, *values.Flags.Floats["float-flag-2"])
assert.Equal(t, 3.3, *values.Flags.Floats["float-flag-3"])
}
func TestCommandParser_Parse_BoolFlags(t *testing.T) {
instance := parse.CommandParser{}
cmd := &v1alpha1.Command{
Flags: []v1alpha1.Flag{
{
Name: "bool-flag-1",
BoolValue: true,
Type: v1alpha1.Bool,
Description: "bool flag 1 description",
},
{
Name: "bool-flag-2",
BoolValue: false,
Type: v1alpha1.Bool,
Description: "bool-flag-2-description",
},
{
Name: "bool-flag-3",
Type: v1alpha1.Bool,
},
},
}
cobracmd, values := instance.Parse(cmd)
// Check the Descriptions
assert.Equal(t, "bool flag 1 description", cobracmd.Flag("bool-flag-1").Usage)
assert.Equal(t, "bool-flag-2-description", cobracmd.Flag("bool-flag-2").Usage)
assert.Equal(t, "", cobracmd.Flag("bool-flag-3").Usage)
// Check default Values
assert.Equal(t, true, *values.Flags.Bools["bool-flag-1"])
assert.Equal(t, false, *values.Flags.Bools["bool-flag-2"])
assert.Equal(t, false, *values.Flags.Bools["bool-flag-3"])
// Check the flags get updated
cobracmd.Flags().Parse([]string{
"--bool-flag-1=false",
"--bool-flag-3=true",
})
assert.Equal(t, false, *values.Flags.Bools["bool-flag-1"])
assert.Equal(t, false, *values.Flags.Bools["bool-flag-2"])
assert.Equal(t, true, *values.Flags.Bools["bool-flag-3"])
}
func TestCommandParser_Parse(t *testing.T) {
instance := parse.CommandParser{}
cmd := &v1alpha1.Command{
Version: "v1",
Example: "foo example",
Use: "foo",
Aliases: []string{"a1", "a2"},
Long: "long description",
Short: "short description",
SuggestFor: []string{"suggest", "for"},
Deprecated: "deprecated",
}
cobracmd, _ := instance.Parse(cmd)
assert.Equal(t, "v1", cobracmd.Version)
assert.Equal(t, "foo example", cobracmd.Example)
assert.Equal(t, "foo", cobracmd.Use)
assert.Equal(t, []string{"a1", "a2"}, cobracmd.Aliases)
assert.Equal(t, "long description", cobracmd.Long)
assert.Equal(t, "short description", cobracmd.Short)
assert.Equal(t, []string{"suggest", "for"}, cobracmd.SuggestFor)
assert.Equal(t, "deprecated", cobracmd.Deprecated)
}
func TestCommandParser_Parse_RequiredFlags(t *testing.T) {
instance := parse.CommandParser{}
required := true
notrequired := false
cmd := &v1alpha1.Command{
Flags: []v1alpha1.Flag{
{
Name: "float-flag-1",
Type: v1alpha1.Float,
Description: "float flag 1 description",
Required: &required,
},
{
Name: "float-flag-2",
FloatValue: 2.2,
Type: v1alpha1.Float,
Description: "float-flag-2-description",
Required: &notrequired,
},
{
Name: "float-flag-3",
Type: v1alpha1.Float,
},
},
}
cobracmd, _ := instance.Parse(cmd)
cobracmd.Run = func(cmd *cobra.Command, args []string) {}
cobracmd.Flags().Parse([]string{})
err := cobracmd.Execute()
assert.Error(t, err)
cobracmd, _ = instance.Parse(cmd)
cobracmd.Run = func(cmd *cobra.Command, args []string) {}
cobracmd.Flags().Parse([]string{"--float-flag-1=10.10"})
err = cobracmd.Execute()
assert.NoError(t, err)
}
func TestAddAtPath(t *testing.T) {
root := &cobra.Command{Use: "root"}
child1 := &cobra.Command{Use: "child1"}
child2 := &cobra.Command{Use: "child2"}
parse.AddAtPath(root, child1, []string{"path", "to"})
parse.AddAtPath(root, child2, []string{"path", "to"})
assert.Equal(t, 1, len(root.Commands()))
assert.Equal(t, "path", root.Commands()[0].Use)
assert.Equal(t, 1, len(root.Commands()[0].Commands()))
assert.Equal(t, "to", root.Commands()[0].Commands()[0].Use)
assert.Equal(t, 2, len(root.Commands()[0].Commands()[0].Commands()))
assert.Equal(t, "root path to child1", child1.CommandPath())
assert.Equal(t, "root path to child2", child2.CommandPath())
}
func TestValues_IsDryRun(t *testing.T) {
v := &parse.Values{}
assert.Equal(t, false, v.IsDryRun())
v.Flags.Bools = map[string]*bool{}
assert.Equal(t, false, v.IsDryRun())
dr := true
v.Flags.Bools["dry-run"] = &dr
assert.Equal(t, true, v.IsDryRun())
dr = false
assert.Equal(t, false, v.IsDryRun())
}

29
internal/pkg/dy/wire.go Normal file
View File

@ -0,0 +1,29 @@
//+build wireinject
/*
Copyright 2019 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 dy
import (
"io"
"github.com/google/wire"
"sigs.k8s.io/cli-experimental/internal/pkg/util"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli"
)
// InitializeCommandLister creates a new *CommandLister object
func InitializeCommandBuilder(io.Writer, util.Args) (*CommandBuilder, error) {
panic(wire.Build(ProviderSet, wirecli.ProviderSet))
}

View File

@ -0,0 +1,62 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package dy
import (
"io"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/dispatch"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/list"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/output"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/parse"
"sigs.k8s.io/cli-experimental/internal/pkg/util"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s"
)
// Injectors from wire.go:
func InitializeCommandBuilder(writer io.Writer, args util.Args) (*CommandBuilder, error) {
configFlags, err := wirek8s.NewConfigFlags(args)
if err != nil {
return nil, err
}
config, err := wirek8s.NewRestConfig(configFlags)
if err != nil {
return nil, err
}
clientset, err := wirek8s.NewKubernetesClientSet(config)
if err != nil {
return nil, err
}
clientsetClientset, err := wirek8s.NewExtensionsClientSet(config)
if err != nil {
return nil, err
}
dynamicInterface, err := wirek8s.NewDynamicClient(config)
if err != nil {
return nil, err
}
commandLister := &list.CommandLister{
Client: clientsetClientset,
DynamicClient: dynamicInterface,
}
commandParser := &parse.CommandParser{}
commandOutputWriter := &output.CommandOutputWriter{
Output: writer,
}
dispatcher := &dispatch.Dispatcher{
KubernetesClient: clientset,
DynamicClient: dynamicInterface,
Writer: commandOutputWriter,
}
commandBuilder := &CommandBuilder{
KubernetesClient: clientset,
Lister: commandLister,
Parser: commandParser,
Dispatcher: dispatcher,
Writer: commandOutputWriter,
}
return commandBuilder, nil
}

View File

@ -1,57 +0,0 @@
/*
Copyright 2019 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 resourceconfig
import (
"k8s.io/apimachinery/pkg/runtime"
)
// ConfigProvider provides runtime.Objects for a path
type ConfigProvider interface {
// IsSupported returns true if the ConfigProvider supports the given path
IsSupported(path string) bool
// GetConfig returns the Resource Config as runtime.Objects
GetConfig(path string) []runtime.Object
}
type KustomizeProvider struct{}
func (p *KustomizeProvider) IsSupported(path string) bool {
return false
}
func (p *KustomizeProvider) GetConfig(path string) []runtime.Object {
return nil
}
type RawConfigFileProvider struct{}
func (p *RawConfigFileProvider) IsSupported(path string) bool {
return false
}
func (p *RawConfigFileProvider) GetConfig(path string) []runtime.Object {
return nil
}
type RawConfigHTTPProvider struct{}
func (p *RawConfigHTTPProvider) IsSupported(path string) bool {
return false
}
func (p *RawConfigHTTPProvider) GetConfig(path string) []runtime.Object {
return nil
}

View File

@ -24,3 +24,6 @@ func HomeDir() string {
}
return os.Getenv("USERPROFILE") // windows
}
// Args are command line arguments
type Args []string

View File

@ -18,6 +18,8 @@ package wirecli
import (
"io"
"sigs.k8s.io/cli-experimental/internal/pkg/util"
"github.com/google/wire"
"sigs.k8s.io/cli-experimental/internal/pkg/apply"
"sigs.k8s.io/cli-experimental/internal/pkg/clik8s"
@ -25,21 +27,21 @@ import (
)
// InitializeApplyStatus creates a new *status.Status object
func InitializeStatus(clik8s.ResourceConfigPath, io.Writer) (*status.Status, error) {
func InitializeStatus(clik8s.ResourceConfigPath, io.Writer, util.Args) (*status.Status, error) {
panic(wire.Build(ProviderSet))
}
// InitializeApply creates a new *apply.Apply object
func InitializeApply(clik8s.ResourceConfigPath, io.Writer) (*apply.Apply, error) {
func InitializeApply(clik8s.ResourceConfigPath, io.Writer, util.Args) (*apply.Apply, error) {
panic(wire.Build(ProviderSet))
}
// DoStatus creates a new Status object and runs it
func DoStatus(clik8s.ResourceConfigPath, io.Writer) (status.Result, error) {
func DoStatus(clik8s.ResourceConfigPath, io.Writer, util.Args) (status.Result, error) {
panic(wire.Build(ProviderSet))
}
// DoApply creates a new Apply object and runs it
func DoApply(clik8s.ResourceConfigPath, io.Writer) (apply.Result, error) {
func DoApply(clik8s.ResourceConfigPath, io.Writer, util.Args) (apply.Result, error) {
panic(wire.Build(ProviderSet))
}

View File

@ -10,21 +10,24 @@ import (
"sigs.k8s.io/cli-experimental/internal/pkg/apply"
"sigs.k8s.io/cli-experimental/internal/pkg/clik8s"
"sigs.k8s.io/cli-experimental/internal/pkg/status"
"sigs.k8s.io/cli-experimental/internal/pkg/util"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wiregit"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s"
)
// Injectors from wire.go:
func InitializeStatus(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer) (*status.Status, error) {
func InitializeStatus(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer, args util.Args) (*status.Status, error) {
fileSystem := wirek8s.NewFileSystem()
resourceConfigs, err := wirek8s.NewResourceConfig(resourceConfigPath, fileSystem)
if err != nil {
return nil, err
}
masterURL := wirek8s.NewMasterFlag()
kubeConfigPath := wirek8s.NewKubeConfigPathFlag()
config, err := wirek8s.NewRestConfig(masterURL, kubeConfigPath)
configFlags, err := wirek8s.NewConfigFlags(args)
if err != nil {
return nil, err
}
config, err := wirek8s.NewRestConfig(configFlags)
if err != nil {
return nil, err
}
@ -44,10 +47,12 @@ func InitializeStatus(resourceConfigPath clik8s.ResourceConfigPath, writer io.Wr
return statusStatus, nil
}
func InitializeApply(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer) (*apply.Apply, error) {
masterURL := wirek8s.NewMasterFlag()
kubeConfigPath := wirek8s.NewKubeConfigPathFlag()
config, err := wirek8s.NewRestConfig(masterURL, kubeConfigPath)
func InitializeApply(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer, args util.Args) (*apply.Apply, error) {
configFlags, err := wirek8s.NewConfigFlags(args)
if err != nil {
return nil, err
}
config, err := wirek8s.NewRestConfig(configFlags)
if err != nil {
return nil, err
}
@ -72,15 +77,17 @@ func InitializeApply(resourceConfigPath clik8s.ResourceConfigPath, writer io.Wri
return applyApply, nil
}
func DoStatus(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer) (status.Result, error) {
func DoStatus(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer, args util.Args) (status.Result, error) {
fileSystem := wirek8s.NewFileSystem()
resourceConfigs, err := wirek8s.NewResourceConfig(resourceConfigPath, fileSystem)
if err != nil {
return status.Result{}, err
}
masterURL := wirek8s.NewMasterFlag()
kubeConfigPath := wirek8s.NewKubeConfigPathFlag()
config, err := wirek8s.NewRestConfig(masterURL, kubeConfigPath)
configFlags, err := wirek8s.NewConfigFlags(args)
if err != nil {
return status.Result{}, err
}
config, err := wirek8s.NewRestConfig(configFlags)
if err != nil {
return status.Result{}, err
}
@ -104,10 +111,12 @@ func DoStatus(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer) (s
return result, nil
}
func DoApply(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer) (apply.Result, error) {
masterURL := wirek8s.NewMasterFlag()
kubeConfigPath := wirek8s.NewKubeConfigPathFlag()
config, err := wirek8s.NewRestConfig(masterURL, kubeConfigPath)
func DoApply(resourceConfigPath clik8s.ResourceConfigPath, writer io.Writer, args util.Args) (apply.Result, error) {
configFlags, err := wirek8s.NewConfigFlags(args)
if err != nil {
return apply.Result{}, err
}
config, err := wirek8s.NewRestConfig(configFlags)
if err != nil {
return apply.Result{}, err
}

View File

@ -22,12 +22,14 @@ import (
"strings"
"github.com/google/wire"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/kustomize"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/cli-experimental/internal/pkg/clik8s"
"sigs.k8s.io/cli-experimental/internal/pkg/util"
"sigs.k8s.io/kustomize/pkg/fs"
@ -38,39 +40,79 @@ import (
)
// ProviderSet defines dependencies for initializing Kubernetes objects
var ProviderSet = wire.NewSet(NewKubernetesClientSet, NewKubeConfigPathFlag, NewRestConfig,
NewMasterFlag, NewResourceConfig, NewFileSystem)
var kubeConfigPathFlag string
var master string
var ProviderSet = wire.NewSet(NewKubernetesClientSet, NewExtensionsClientSet, NewConfigFlags, NewRestConfig,
NewResourceConfig, NewFileSystem, NewDynamicClient)
// Flags registers flags for talkig to a Kubernetes cluster
func Flags(command *cobra.Command) {
var path string
if len(util.HomeDir()) > 0 {
path = filepath.Join(util.HomeDir(), ".kube", "config")
} else {
path = ""
command.MarkFlagRequired("kubeconfig")
func Flags(fs *pflag.FlagSet) {
kubeConfigFlags := genericclioptions.NewConfigFlags(false)
kubeConfigFlags.AddFlags(fs)
}
// HelpFlags is a list of flags to strips
var HelpFlags = []string{"-h", "--help"}
// WordSepNormalizeFunc normalizes flags
func WordSepNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
if strings.Contains(name, "_") {
return pflag.NormalizedName(strings.Replace(name, "_", "-", -1))
}
command.Flags().StringVar(&kubeConfigPathFlag,
"kubeconfig", path, "absolute path to the kubeconfig file")
command.Flags().StringVar(&master,
"master", "", "address of master")
return pflag.NormalizedName(name)
}
// NewKubeConfigPathFlag provides the path to the kubeconfig file
func NewKubeConfigPathFlag() clik8s.KubeConfigPath {
return clik8s.KubeConfigPath(kubeConfigPathFlag)
// NewConfigFlags parses flags used to generate the *rest.Config
func NewConfigFlags(ar util.Args) (*genericclioptions.ConfigFlags, error) {
a := CopyStrSlice([]string(ar))
// IMPORTANT: If there is an error parsing flags--continue.
kubeConfigFlagSet := pflag.NewFlagSet("dispatcher-kube-config", pflag.ContinueOnError)
kubeConfigFlagSet.ParseErrorsWhitelist.UnknownFlags = true
kubeConfigFlagSet.SetNormalizeFunc(WordSepNormalizeFunc)
kubeConfigFlagSet.Set("namespace", "default")
unusedParameter := true // Could be either true or false
kubeConfigFlags := genericclioptions.NewConfigFlags(unusedParameter)
kubeConfigFlags.AddFlags(kubeConfigFlagSet)
// Remove help flags, since these are special-cased in pflag.Parse,
args := FilterList(a, HelpFlags)
if err := kubeConfigFlagSet.Parse(args); err != nil {
return nil, err
}
return kubeConfigFlags, nil
}
// NewMasterFlag returns the MasterURL parsed from the `--master` flag
func NewMasterFlag() clik8s.MasterURL {
return clik8s.MasterURL(master)
// FilterList returns a copy of "l" with elements from "toRemove" filtered out.
func FilterList(l []string, rl []string) []string {
c := CopyStrSlice(l)
for _, r := range rl {
c = RemoveAllElements(c, r)
}
return c
}
// RemoveAllElements removes all elements from "s" which match the string "r".
func RemoveAllElements(s []string, r string) []string {
for i, rlen := 0, len(s); i < rlen; i++ {
j := i - (rlen - len(s))
if s[j] == r {
s = append(s[:j], s[j+1:]...)
}
}
return s
}
// CopyStrSlice returns a copy of the slice of strings.
func CopyStrSlice(s []string) []string {
c := make([]string, len(s))
copy(c, s)
return c
}
// NewRestConfig returns a new rest.Config parsed from --kubeconfig and --master
func NewRestConfig(master clik8s.MasterURL, path clik8s.KubeConfigPath) (*rest.Config, error) {
return clientcmd.BuildConfigFromFlags(string(master), string(path))
func NewRestConfig(f *genericclioptions.ConfigFlags) (*rest.Config, error) {
return f.ToRESTConfig()
}
// NewKubernetesClientSet provides a clientset for talking to k8s clusters
@ -78,6 +120,16 @@ func NewKubernetesClientSet(c *rest.Config) (*kubernetes.Clientset, error) {
return kubernetes.NewForConfig(c)
}
// NewExtensionsClientSet provides an apiextensions ClientSet
func NewExtensionsClientSet(c *rest.Config) (*clientset.Clientset, error) {
return clientset.NewForConfig(c)
}
// NewDynamicClient provides a dynamic.Interface
func NewDynamicClient(c *rest.Config) (dynamic.Interface, error) {
return dynamic.NewForConfig(c)
}
// NewFileSystem provides a real FileSystem
func NewFileSystem() fs.FileSystem {
return fs.MakeRealFS()

View File

@ -22,6 +22,9 @@ import (
"gopkg.in/src-d/go-git.v4/plumbing/object"
"sigs.k8s.io/cli-experimental/internal/pkg/apply"
"sigs.k8s.io/cli-experimental/internal/pkg/clik8s"
"sigs.k8s.io/cli-experimental/internal/pkg/dy"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/dispatch"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/list"
"sigs.k8s.io/cli-experimental/internal/pkg/status"
)
@ -32,3 +35,15 @@ func InitializeStatus(clik8s.ResourceConfigs, *object.Commit, io.Writer) (*statu
func InitializeApply(clik8s.ResourceConfigs, *object.Commit, io.Writer) (*apply.Apply, func(), error) {
panic(wire.Build(ProviderSet))
}
func InitializeCommandBuilder(io.Writer) (*dy.CommandBuilder, func(), error) {
panic(wire.Build(ProviderSet))
}
func InitializeDispatcher(io.Writer) (*dispatch.Dispatcher, func(), error) {
panic(wire.Build(ProviderSet))
}
func InitializeLister(io.Writer) (*list.CommandLister, func(), error) {
panic(wire.Build(ProviderSet))
}

View File

@ -10,6 +10,11 @@ import (
"io"
"sigs.k8s.io/cli-experimental/internal/pkg/apply"
"sigs.k8s.io/cli-experimental/internal/pkg/clik8s"
"sigs.k8s.io/cli-experimental/internal/pkg/dy"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/dispatch"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/list"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/output"
"sigs.k8s.io/cli-experimental/internal/pkg/dy/parse"
"sigs.k8s.io/cli-experimental/internal/pkg/status"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s"
)
@ -57,3 +62,100 @@ func InitializeApply(resourceConfigs clik8s.ResourceConfigs, commit *object.Comm
cleanup()
}, nil
}
func InitializeCommandBuilder(writer io.Writer) (*dy.CommandBuilder, func(), error) {
config, cleanup, err := NewRestConfig()
if err != nil {
return nil, nil, err
}
clientset, err := wirek8s.NewKubernetesClientSet(config)
if err != nil {
cleanup()
return nil, nil, err
}
clientsetClientset, err := wirek8s.NewExtensionsClientSet(config)
if err != nil {
cleanup()
return nil, nil, err
}
dynamicInterface, err := wirek8s.NewDynamicClient(config)
if err != nil {
cleanup()
return nil, nil, err
}
commandLister := &list.CommandLister{
Client: clientsetClientset,
DynamicClient: dynamicInterface,
}
commandParser := &parse.CommandParser{}
commandOutputWriter := &output.CommandOutputWriter{
Output: writer,
}
dispatcher := &dispatch.Dispatcher{
KubernetesClient: clientset,
DynamicClient: dynamicInterface,
Writer: commandOutputWriter,
}
commandBuilder := &dy.CommandBuilder{
KubernetesClient: clientset,
Lister: commandLister,
Parser: commandParser,
Dispatcher: dispatcher,
Writer: commandOutputWriter,
}
return commandBuilder, func() {
cleanup()
}, nil
}
func InitializeDispatcher(writer io.Writer) (*dispatch.Dispatcher, func(), error) {
config, cleanup, err := NewRestConfig()
if err != nil {
return nil, nil, err
}
clientset, err := wirek8s.NewKubernetesClientSet(config)
if err != nil {
cleanup()
return nil, nil, err
}
dynamicInterface, err := wirek8s.NewDynamicClient(config)
if err != nil {
cleanup()
return nil, nil, err
}
commandOutputWriter := &output.CommandOutputWriter{
Output: writer,
}
dispatcher := &dispatch.Dispatcher{
KubernetesClient: clientset,
DynamicClient: dynamicInterface,
Writer: commandOutputWriter,
}
return dispatcher, func() {
cleanup()
}, nil
}
func InitializeLister(writer io.Writer) (*list.CommandLister, func(), error) {
config, cleanup, err := NewRestConfig()
if err != nil {
return nil, nil, err
}
clientset, err := wirek8s.NewExtensionsClientSet(config)
if err != nil {
cleanup()
return nil, nil, err
}
dynamicInterface, err := wirek8s.NewDynamicClient(config)
if err != nil {
cleanup()
return nil, nil, err
}
commandLister := &list.CommandLister{
Client: clientset,
DynamicClient: dynamicInterface,
}
return commandLister, func() {
cleanup()
}, nil
}

View File

@ -17,6 +17,7 @@ import (
"github.com/google/wire"
"k8s.io/client-go/rest"
"sigs.k8s.io/cli-experimental/internal/pkg/apply"
"sigs.k8s.io/cli-experimental/internal/pkg/dy"
"sigs.k8s.io/cli-experimental/internal/pkg/status"
"sigs.k8s.io/cli-experimental/internal/pkg/wirecli/wirek8s"
"sigs.k8s.io/controller-runtime/pkg/envtest"
@ -24,7 +25,8 @@ import (
// ProviderSet defines dependencies for initializing objects
var ProviderSet = wire.NewSet(
wirek8s.NewKubernetesClientSet, NewRestConfig, status.Status{}, apply.Apply{})
dy.ProviderSet, wirek8s.NewKubernetesClientSet, wirek8s.NewExtensionsClientSet, wirek8s.NewDynamicClient,
NewRestConfig, status.Status{}, apply.Apply{})
// NewRestConfig provides a rest.Config for a testing environment
func NewRestConfig() (*rest.Config, func(), error) {

12
main.go
View File

@ -13,8 +13,16 @@ limitations under the License.
package main
import "sigs.k8s.io/cli-experimental/cmd"
import (
"fmt"
"os"
"sigs.k8s.io/cli-experimental/cmd"
)
func main() {
cmd.Execute()
if err := cmd.Execute(os.Args, nil); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}

View File

@ -20,7 +20,7 @@ source $(dirname ${BASH_SOURCE})/common.sh
header_text "running go vet"
go vet ./internal/... ./pkg/... ./cmd/...
go vet ./internal/... ./pkg/... ./cmd/... ./util/...
header_text "populating vendor for gometalinter.v2"
@ -39,7 +39,7 @@ gometalinter.v2 -e $(go env GOROOT) -e vendor/ -e _gen.go --disable-all \
--enable=goconst \
--enable=goimports \
--enable=gocyclo \
--cyclo-over=7 \
--cyclo-over=20 \
--line-length=120 \
--enable=lll \
--enable=nakedret \
@ -49,5 +49,4 @@ gometalinter.v2 -e $(go env GOROOT) -e vendor/ -e _gen.go --disable-all \
--dupl-threshold=400 \
--enable=dupl \
--enable=misspell \
./pkg/... ./internal/... ./cmd/...
./pkg/... ./internal/... ./cmd/... ./util/...

123
util/dyctl/cmd/add.go Normal file
View File

@ -0,0 +1,123 @@
/*
Copyright 2019 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 cmd
import (
"bytes"
"encoding/json"
"io/ioutil"
"os"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/cli-experimental/internal/pkg/apis/dynamic/v1alpha1"
"sigs.k8s.io/yaml"
)
// addCmd represents the add command
var addCmd = &cobra.Command{
Use: "add-commands",
Short: "Add a Dynamic Command to a CRD Resource.",
Long: `Add a Dynamic Command to a CRD Resource.
Reads a yaml file containing a ResourceCommandList updates a CRD Resource Config yaml file
to publish the Commands.
`,
Example: `# Add the ResourceCommandList from commands.yaml to the CRD definition in crd.yaml
dy add-commands commands.yaml crd.yaml
`,
RunE: runE,
Args: cobra.ExactArgs(2),
}
func runE(cmd *cobra.Command, args []string) error {
// Parse ResourceCommandList
clBytes, err := ioutil.ReadFile(args[0])
if err != nil {
return err
}
commandList := v1alpha1.ResourceCommandList{}
if err := yaml.Unmarshal(clBytes, &commandList); err != nil {
return err
}
// Parse CustomResourceDefinition
crd := unstructured.Unstructured{}
crdBytes, err := ioutil.ReadFile(args[1])
if err != nil {
return err
}
if err := yaml.Unmarshal(crdBytes, &crd); err != nil {
return err
}
crd.SetAPIVersion("apiextensions.k8s.io/v1beta1")
crd.SetKind("CustomResourceDefinition")
if err := AddTo(&commandList, &crd); err != nil {
return err
}
// Write the CRD output
crdOut, err := yaml.Marshal(crd.Object)
if err != nil {
return err
}
info, err := os.Stat(args[1])
if err != nil {
return err
}
return ioutil.WriteFile(args[1], crdOut, info.Mode())
}
// AddTo adds the commandList as an annotation and label to the crd
func AddTo(commandList *v1alpha1.ResourceCommandList, crd v1.Object) error {
// Add the Label
lab := crd.GetLabels()
if lab == nil {
lab = map[string]string{}
}
lab["cli-experimental.sigs.k8s.io/ResourceCommandList"] = ""
crd.SetLabels(lab)
// Add the Annotation
clOut, err := yaml.Marshal(commandList)
if err != nil {
return err
}
clOutJSON, err := yaml.YAMLToJSONStrict(clOut)
if err != nil {
return err
}
clAnn := &bytes.Buffer{}
err = json.Compact(clAnn, clOutJSON)
if err != nil {
return err
}
an := crd.GetAnnotations()
if an == nil {
an = map[string]string{}
}
an["cli-experimental.sigs.k8s.io/ResourceCommandList"] = string(clAnn.String())
crd.SetAnnotations(an)
return nil
}
func init() {
rootCmd.AddCommand(addCmd)
}

View File

@ -18,19 +18,18 @@ import (
"os"
"github.com/spf13/cobra"
"sigs.k8s.io/cli-experimental/cmd/apply"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "dy",
Short: "Utilities for publishing dynamic Commands to CRDs.",
Long: `Utilities for publishing dynamic Commands to CRDs.`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
rootCmd := &cobra.Command{
Use: "cli-experimental",
Short: "",
Long: ``,
}
rootCmd.AddCommand(apply.GetApplyCommand())
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)

20
util/dyctl/main.go Normal file
View File

@ -0,0 +1,20 @@
/*
Copyright 2019 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 main
import "sigs.k8s.io/cli-experimental/util/dyctl/cmd"
func main() {
cmd.Execute()
}