mirror of https://github.com/fluxcd/cli-utils.git
Add support for dynamic commands published by CRDs
This commit is contained in:
parent
25ac98a777
commit
38e1577c1f
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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
|
|
@ -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
|
||||
```
|
|
@ -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
|
|
@ -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
9
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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"}
|
||||
)
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"])
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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: ¬required,
|
||||
},
|
||||
{
|
||||
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())
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -24,3 +24,6 @@ func HomeDir() string {
|
|||
}
|
||||
return os.Getenv("USERPROFILE") // windows
|
||||
}
|
||||
|
||||
// Args are command line arguments
|
||||
type Args []string
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
12
main.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/...
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue