Skip to content
This repository has been archived by the owner on May 6, 2022. It is now read-only.

Add svcat command to create user-defined cluster-scoped classes #2190

Merged
merged 16 commits into from
Jul 30, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions cmd/svcat/class/create_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2018 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 class

import (
"fmt"

"github.com/kubernetes-incubator/service-catalog/cmd/svcat/command"
"github.com/kubernetes-incubator/service-catalog/cmd/svcat/output"
"github.com/kubernetes-incubator/service-catalog/pkg/apis/servicecatalog/v1beta1"
"github.com/spf13/cobra"
)

type createCmd struct {
*command.Context
name string
from string
}

// NewCreateCmd builds a "svcat create class" command
func NewCreateCmd(cxt *command.Context) *cobra.Command {
createCmd := &createCmd{Context: cxt}
cmd := &cobra.Command{
Use: "class [NAME] --from [EXISTING_NAME]",
Short: "Copies an existing class into a new user-defined cluster-scoped class",
Example: command.NormalizeExamples(`
svcat create class newclass --from mysqldb
`),
PreRunE: command.PreRunE(createCmd),
RunE: command.RunE(createCmd),
}
cmd.Flags().StringVarP(&createCmd.from, "from", "f", "",
"Name from an existing class that will be copied (Required)",
)
cmd.MarkFlagRequired("from")

return cmd
}

func (c *createCmd) Validate(args []string) error {
if len(args) <= 0 {
return fmt.Errorf("new class name should be provided")
}

c.name = args[0]

return nil
}

func (c *createCmd) Run() error {
class, err := c.App.RetrieveClassByName(c.from)
if err != nil {
return err
}

newClass := &v1beta1.ClusterServiceClass{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused about why you changed this part? Previously, it would copy the spec of the original class, and save it with a new name which was the desired behavior. With this change, the new class is missing key information from the original and it wouldn't be usable.

My vote is for going back to what you had and making sure through tests that it does what we need without unwanted side effects (if that was the concern that made you change it?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized that this was in response to one of Jonathan's comments above. I replied there but I believe we can go back to what you had before (I spoke with him in person about this last week) and just make sure that we have enough tests to verify that it works as we hoped, without odd side-effects.

Spec: v1beta1.ClusterServiceClassSpec{
ClusterServiceBrokerName: class.Spec.ClusterServiceBrokerName,
CommonServiceClassSpec: v1beta1.CommonServiceClassSpec{
ExternalName: c.name,
Description: class.Spec.Description,
Tags: class.Spec.Tags,
},
},
}

createdClass, err := c.App.CreateClass(newClass)
if err != nil {
return err
}

output.WriteCreatedResourceName(c.Output, createdClass.Spec.ExternalName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return nil
}
11 changes: 11 additions & 0 deletions cmd/svcat/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func buildRootCommand(cxt *command.Context) *cobra.Command {
cmd.PersistentFlags().StringVar(&opts.KubeContext, "context", "", "name of the kubeconfig context to use.")
cmd.PersistentFlags().StringVar(&opts.KubeConfig, "kubeconfig", "", "path to kubeconfig file. Overrides $KUBECONFIG")

cmd.AddCommand(newCreateCmd(cxt))
cmd.AddCommand(newGetCmd(cxt))
cmd.AddCommand(newDescribeCmd(cxt))
cmd.AddCommand(instance.NewProvisionCmd(cxt))
Expand Down Expand Up @@ -143,6 +144,16 @@ func newSyncCmd(cxt *command.Context) *cobra.Command {
return cmd
}

func newCreateCmd(cxt *command.Context) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create an user-defined resource",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: change "an" to "a"

}
cmd.AddCommand(class.NewCreateCmd(cxt))

return cmd
}

func newGetCmd(cxt *command.Context) *cobra.Command {
cmd := &cobra.Command{
Use: "get",
Expand Down
5 changes: 5 additions & 0 deletions cmd/svcat/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ func formatStatusFull(condition string, conditionStatus v1beta1.ConditionStatus,
return fmt.Sprintf("%s - %s @ %s", status, message, timestamp.UTC())
}

// WriteCreatedResourceName prints the name of a created resource
func WriteCreatedResourceName(w io.Writer, resourceName string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we don't have anything suitable for this in output/class.go?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the question/suggestion?

For other resources when we create them (like a binding), we have just printed out the details (basically the output of describe). We haven't had svcat create class resources yet so this is net new. Once we start allowing creating plans too, this function will be resued.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No? We couldn't use something like WriteClass or WriteClassDetails?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! I didn't get that you are were suggesting that we output WriteClassDetails instead of just the name. 😊 Yeah that would be fine with me either way.

fmt.Fprintf(w, "created %s\n", resourceName)
}

// WriteDeletedResourceName prints the name of a deleted resource
func WriteDeletedResourceName(w io.Writer, resourceName string) {
fmt.Fprintf(w, "deleted %s\n", resourceName)
Expand Down
1 change: 1 addition & 0 deletions cmd/svcat/svcat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func TestCommandOutput(t *testing.T) {
{name: "get class by uuid", cmd: "get class --uuid 4f6e6cf6-ffdd-425f-a2c7-3c9258ad2468", golden: "output/get-class.txt"},
{name: "describe class by name", cmd: "describe class user-provided-service", golden: "output/describe-class.txt"},
{name: "describe class uuid", cmd: "describe class --uuid 4f6e6cf6-ffdd-425f-a2c7-3c9258ad2468", golden: "output/describe-class.txt"},
{name: "create class", cmd: "create class new-class --from user-provided-service", golden: "output/create-class.txt"},

{name: "list all plans", cmd: "get plans", golden: "output/get-plans.txt"},
{name: "list all plans (json)", cmd: "get plans -o json", golden: "output/get-plans.json"},
Expand Down
51 changes: 51 additions & 0 deletions cmd/svcat/testdata/output/completion-bash.txt
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,56 @@ _svcat_completion()
noun_aliases=()
}

_svcat_create_class()
{
last_command="svcat_create_class"
commands=()

flags=()
two_word_flags=()
local_nonpersistent_flags=()
flags_with_completion=()
flags_completion=()

flags+=("--from=")
two_word_flags+=("-f")
local_nonpersistent_flags+=("--from=")
flags+=("--context=")
flags+=("--kubeconfig=")
flags+=("--logtostderr")
flags+=("--v=")
two_word_flags+=("-v")

must_have_one_flag=()
must_have_one_flag+=("--from=")
must_have_one_flag+=("-f")
must_have_one_noun=()
noun_aliases=()
}

_svcat_create()
{
last_command="svcat_create"
commands=()
commands+=("class")

flags=()
two_word_flags=()
local_nonpersistent_flags=()
flags_with_completion=()
flags_completion=()

flags+=("--context=")
flags+=("--kubeconfig=")
flags+=("--logtostderr")
flags+=("--v=")
two_word_flags+=("-v")

must_have_one_flag=()
must_have_one_noun=()
noun_aliases=()
}

_svcat_deprovision()
{
last_command="svcat_deprovision"
Expand Down Expand Up @@ -920,6 +970,7 @@ _svcat_root_command()
commands=()
commands+=("bind")
commands+=("completion")
commands+=("create")
commands+=("deprovision")
commands+=("describe")
commands+=("get")
Expand Down
51 changes: 51 additions & 0 deletions cmd/svcat/testdata/output/completion-zsh.txt
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,56 @@ _svcat_completion()
noun_aliases=()
}

_svcat_create_class()
{
last_command="svcat_create_class"
commands=()

flags=()
two_word_flags=()
local_nonpersistent_flags=()
flags_with_completion=()
flags_completion=()

flags+=("--from=")
two_word_flags+=("-f")
local_nonpersistent_flags+=("--from=")
flags+=("--context=")
flags+=("--kubeconfig=")
flags+=("--logtostderr")
flags+=("--v=")
two_word_flags+=("-v")

must_have_one_flag=()
must_have_one_flag+=("--from=")
must_have_one_flag+=("-f")
must_have_one_noun=()
noun_aliases=()
}

_svcat_create()
{
last_command="svcat_create"
commands=()
commands+=("class")

flags=()
two_word_flags=()
local_nonpersistent_flags=()
flags_with_completion=()
flags_completion=()

flags+=("--context=")
flags+=("--kubeconfig=")
flags+=("--logtostderr")
flags+=("--v=")
two_word_flags+=("-v")

must_have_one_flag=()
must_have_one_noun=()
noun_aliases=()
}

_svcat_deprovision()
{
last_command="svcat_deprovision"
Expand Down Expand Up @@ -1054,6 +1104,7 @@ _svcat_root_command()
commands=()
commands+=("bind")
commands+=("completion")
commands+=("create")
commands+=("deprovision")
commands+=("describe")
commands+=("get")
Expand Down
1 change: 1 addition & 0 deletions cmd/svcat/testdata/output/create-class.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
created new-class
14 changes: 14 additions & 0 deletions cmd/svcat/testdata/plugin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ tree:
Svcat shell completion\\nsource '$HOME/.svcat/svcat_completion.bash.inc'\\n\"
>> $HOME/.bash_profile\n source $HOME/.bash_profile"
command: ./svcat completion
- name: create
use: create
shortDesc: Create an user-defined resource
command: ./svcat create
tree:
- name: class
use: class [NAME] --from [EXISTING_NAME]
shortDesc: Copies an existing class into a new user-defined cluster-scoped class
example: ' svcat create class newclass --from mysqldb'
command: ./svcat create class
flags:
- name: from
shorthand: f
desc: Name from an existing class that will be copied (Required)
- name: deprovision
use: deprovision NAME
shortDesc: Deletes an instance of a service
Expand Down
10 changes: 10 additions & 0 deletions pkg/svcat/service-catalog/class.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,13 @@ func (sdk *SDK) RetrieveClassByPlan(plan *v1beta1.ClusterServicePlan,

return class, nil
}

// CreateClass returns new created class
func (sdk *SDK) CreateClass(class *v1beta1.ClusterServiceClass) (*v1beta1.ClusterServiceClass, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to come up with a unique verb for creating a class like register/provision/bind for brokers/instances/bindings?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't change this one on latest commit. Since I don't have any strong opinion about it, I will wait for a decision :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't planning on it, no.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create class seems fine, then.

created, err := sdk.ServiceCatalog().ClusterServiceClasses().Create(class)
if err != nil {
return nil, fmt.Errorf("unable to create class (%s)", err)
}

return created, nil
}
24 changes: 24 additions & 0 deletions pkg/svcat/service-catalog/class_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,28 @@ var _ = Describe("Class", func() {
Expect(actions[0].(testing.GetActionImpl).Name).To(Equal(fakeClassName))
})
})
Describe("CreateClass", func() {
It("Calls the generated v1beta1 create method with the passed in class", func() {
className := "newclass"
nc := &v1beta1.ClusterServiceClass{ObjectMeta: metav1.ObjectMeta{Name: className}}

class, err := sdk.CreateClass(nc)

Expect(err).NotTo(HaveOccurred())
Expect(class).To(Equal(nc))
actions := svcCatClient.Actions()
Expect(actions[0].Matches("create", "clusterserviceclasses")).To(BeTrue())
objectFromRequest := actions[0].(testing.CreateActionImpl).Object.(*v1beta1.ClusterServiceClass)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above, this is a change in behavior and I'm not sure what's driving it?

Expect(objectFromRequest.ObjectMeta.Name).To(Equal(className))
})
It("Bubbles up errors", func() {
class, err := sdk.CreateClass(sc)

Expect(class).To(BeNil())
Expect(err).To(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring("unable to create class"))
actions := svcCatClient.Actions()
Expect(actions[0].Matches("create", "clusterserviceclasses")).To(BeTrue())
})
})
})