diff --git a/cmd/commands/cobra.go b/cmd/commands/cobra.go index 8361467e1..a9394a304 100644 --- a/cmd/commands/cobra.go +++ b/cmd/commands/cobra.go @@ -70,6 +70,15 @@ func AtPosition(i int, validator PositionalArg) cobra.PositionalArgs { } } +func OptionalAtPosition(i int, validator PositionalArg) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) > i { + return validator(cmd, args[i]) + } + return nil + } +} + // KubernetesValidation turns a kubernetes-style validation function into a PositionalArg func KubernetesValidation(k8s func(string) []string) PositionalArg { return func(cmd *cobra.Command, arg string) error { diff --git a/cmd/commands/cobra_test.go b/cmd/commands/cobra_test.go index 9c7443330..c8c1d47ae 100644 --- a/cmd/commands/cobra_test.go +++ b/cmd/commands/cobra_test.go @@ -17,9 +17,11 @@ package commands_test import ( + "errors" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/projectriff/riff/cmd/commands" + . "github.com/spf13/cobra" ) var _ = Describe("The cobra extensions", func() { @@ -53,5 +55,22 @@ var _ = Describe("The cobra extensions", func() { }) + It("should not fail if an optional argument to validate is not provided", func() { + command := &Command{ + Use: "some-command", + Args: commands.ArgValidationConjunction( + MaximumNArgs(1), + commands.OptionalAtPosition(1, func(_ *Command, _ string) error { + return errors.New("should not be called") + }), + ), + RunE: func(cmd *Command, args []string) error { + return nil + }, + } + + Expect(command.Execute()).NotTo(HaveOccurred()) + }) + }) }) diff --git a/cmd/commands/function.go b/cmd/commands/function.go index 4e14fca05..6d9a30702 100644 --- a/cmd/commands/function.go +++ b/cmd/commands/function.go @@ -19,7 +19,6 @@ package commands import ( "fmt" - "github.com/knative/eventing/pkg/apis/channels/v1alpha1" "github.com/projectriff/riff/pkg/core" "github.com/spf13/cobra" ) @@ -43,11 +42,7 @@ func Function() *cobra.Command { } func FunctionCreate(fcTool *core.Client) *cobra.Command { - - createInputChannelOptions := core.CreateChannelOptions{} - createOutputChannelOptions := core.CreateChannelOptions{} createFunctionOptions := core.CreateFunctionOptions{} - createSubscriptionOptions := core.CreateSubscriptionOptions{} invokers := map[string]string{ "command": "https://github.com/projectriff/command-function-invoker/raw/v0.0.7/command-invoker.yaml", @@ -57,7 +52,7 @@ func FunctionCreate(fcTool *core.Client) *cobra.Command { command := &cobra.Command{ Use: "create", - Short: "Create a new function resource, with optional input and output channels", + Short: "Create a new function resource", Long: "Create a new function resource from the content of the provided Git repo/revision.\n" + "\nThe INVOKER arg defines the language invoker that is added to the function code in the build step. The resulting image is then used to create a Knative Service (`service.serving.knative.dev`) instance of the name specified for the function." + "\nFrom then on you can use the sub-commands for the `service` command to interact with the service created for the function.\n\n" + @@ -66,21 +61,13 @@ func FunctionCreate(fcTool *core.Client) *cobra.Command { ` + envFromLongDesc + ` `, Example: ` riff function create node square --git-repo https://github.com/acme/square --image acme/square --namespace joseph-ns - riff function create java tweets-logger --git-repo https://github.com/acme/tweets --image acme/tweets-logger:1.0.0 --input tweets --bus kafka`, + riff function create java tweets-logger --git-repo https://github.com/acme/tweets --image acme/tweets-logger:1.0.0`, Args: ArgValidationConjunction( cobra.ExactArgs(functionCreateNumberOfArgs), AtPosition(functionCreateInvokerIndex, ValidName()), AtPosition(functionCreateFunctionNameIndex, ValidName()), ), - PreRunE: FlagsValidatorAsCobraRunE( - FlagsValidationConjunction( - FlagsDependency(Set("input"), exactlyOneOfBusOrClusterBus), - FlagsDependency(NotSet("input"), NoneOf("bus", "cluster-bus")), - FlagsDependency(NotSet("input"), NoneOf("output")), - ), - ), RunE: func(cmd *cobra.Command, args []string) error { - fnName := args[functionCreateFunctionNameIndex] invoker := args[functionCreateInvokerIndex] invokerURL, exists := invokers[invoker] @@ -95,43 +82,11 @@ func FunctionCreate(fcTool *core.Client) *cobra.Command { return err } - var c *v1alpha1.Channel - var subscr *v1alpha1.Subscription - if createInputChannelOptions.Name != "" { - c, err = (*fcTool).CreateChannel(createInputChannelOptions) - if err != nil { - return err - } - - if createOutputChannelOptions.Name != "" { - c, err = (*fcTool).CreateChannel(createOutputChannelOptions) - if err != nil { - return err - } - } - createSubscriptionOptions.Name = subscriptionNameFromService(fnName) - createSubscriptionOptions.Subscriber = subscriberNameFromService(fnName) - subscr, err = (*fcTool).CreateSubscription(createSubscriptionOptions) - if err != nil { - return err - } - } - if createFunctionOptions.DryRun { marshaller := NewMarshaller(cmd.OutOrStdout()) if err = marshaller.Marshal(f); err != nil { return err } - if c != nil { - if err = marshaller.Marshal(c); err != nil { - return err - } - } - if subscr != nil { - if err = marshaller.Marshal(subscr); err != nil { - return err - } - } } else { printSuccessfulCompletion(cmd) if !createFunctionOptions.Verbose && !createFunctionOptions.Wait { @@ -152,55 +107,17 @@ func FunctionCreate(fcTool *core.Client) *cobra.Command { command.Flags().VarP( BroadcastStringValue("", &createFunctionOptions.Namespace, - &createInputChannelOptions.Namespace, - &createOutputChannelOptions.Namespace, - &createSubscriptionOptions.Namespace, ), "namespace", "n", "the `namespace` of the subscription, channel, and function", ) - command.Flags().VarP( - BroadcastStringValue("", - &createInputChannelOptions.Name, - &createSubscriptionOptions.Channel, - ), - "input", "i", "name of the function's input `channel`, if any", - ) - - command.Flags().VarP( - BroadcastStringValue("", - &createOutputChannelOptions.Name, - &createSubscriptionOptions.ReplyTo, - ), - "output", "o", "name of the function's output `channel`, if any", - ) - command.Flags().VarPF( BroadcastBoolValue(false, &createFunctionOptions.DryRun, - &createInputChannelOptions.DryRun, - &createOutputChannelOptions.DryRun, - &createSubscriptionOptions.DryRun, ), "dry-run", "", dryRunUsage, ).NoOptDefVal = "true" - command.Flags().Var( - BroadcastStringValue("", - &createInputChannelOptions.Bus, - &createOutputChannelOptions.Bus, - ), - "bus", busUsage, - ) - - command.Flags().Var( - BroadcastStringValue("", - &createInputChannelOptions.ClusterBus, - &createOutputChannelOptions.ClusterBus, - ), - "cluster-bus", clusterBusUsage, - ) - command.Flags().StringVar(&createFunctionOptions.Image, "image", "", "the name of the image to build; must be a writable `repository/image[:tag]` with credentials configured") command.MarkFlagRequired("image") command.Flags().StringVar(&createFunctionOptions.GitRepo, "git-repo", "", "the `URL` for a git repository hosting the function code") diff --git a/cmd/commands/function_test.go b/cmd/commands/function_test.go index 6815f74fa..ee0e1d0f1 100644 --- a/cmd/commands/function_test.go +++ b/cmd/commands/function_test.go @@ -22,7 +22,6 @@ import ( "strings" - v1alpha12 "github.com/knative/eventing/pkg/apis/channels/v1alpha1" "github.com/knative/serving/pkg/apis/serving/v1alpha1" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -65,12 +64,6 @@ var _ = Describe("The riff function create command", func() { Expect(err).To(MatchError(ContainSubstring("git-repo"))) Expect(err).To(MatchError(ContainSubstring("image"))) }) - It("should fail when input is set w/o bus or cluster-bus", func() { - fc.SetArgs([]string{"node", "square", "--image", "foo/bar", "--git-repo", "https://github.com/repo", - "--input", "i"}) - err := fc.Execute() - Expect(err).To(MatchError("when --input is set, at least one of --bus, --cluster-bus must be set")) - }) }) Context("when given suitable args and flags", func() { @@ -132,39 +125,8 @@ var _ = Describe("The riff function create command", func() { err := fc.Execute() Expect(err).NotTo(HaveOccurred()) }) - It("should create channel/subscription when asked to", func() { - fc.SetArgs([]string{"node", "square", "--image", "foo/bar", "--git-repo", "https://github.com/repo", - "--input", "my-channel", "--bus", "kafka"}) - - functionOptions := core.CreateFunctionOptions{ - GitRepo: "https://github.com/repo", - GitRevision: "master", - InvokerURL: "https://github.com/projectriff/node-function-invoker/raw/v0.0.8/node-invoker.yaml", - } - functionOptions.Name = "square" - functionOptions.Image = "foo/bar" - functionOptions.Env = []string{} - functionOptions.EnvFrom = []string{} - - channelOptions := core.CreateChannelOptions{ - Name: "my-channel", - Bus: "kafka", - } - subscriptionOptions := core.CreateSubscriptionOptions{ - Name: "square", - Channel: "my-channel", - Subscriber: "square", - } - - asMock.On("CreateFunction", functionOptions, mock.Anything).Return(nil, nil) - asMock.On("CreateChannel", channelOptions).Return(nil, nil) - asMock.On("CreateSubscription", subscriptionOptions).Return(nil, nil) - err := fc.Execute() - Expect(err).NotTo(HaveOccurred()) - }) It("should print when --dry-run is set", func() { - fc.SetArgs([]string{"node", "square", "--image", "foo/bar", "--git-repo", "https://github.com/repo", - "--input", "my-channel", "--bus", "kafka", "--dry-run"}) + fc.SetArgs([]string{"node", "square", "--image", "foo/bar", "--git-repo", "https://github.com/repo", "--dry-run"}) functionOptions := core.CreateFunctionOptions{ GitRepo: "https://github.com/repo", @@ -177,27 +139,9 @@ var _ = Describe("The riff function create command", func() { functionOptions.EnvFrom = []string{} functionOptions.DryRun = true - channelOptions := core.CreateChannelOptions{ - Name: "my-channel", - Bus: "kafka", - DryRun: true, - } - subscriptionOptions := core.CreateSubscriptionOptions{ - Name: "square", - Channel: "my-channel", - Subscriber: "square", - DryRun: true, - } - f := v1alpha1.Service{} f.Name = "square" - c := v1alpha12.Channel{} - c.Name = "my-channel" - s := v1alpha12.Subscription{} - s.Name = "square" asMock.On("CreateFunction", functionOptions, mock.Anything).Return(&f, nil) - asMock.On("CreateChannel", channelOptions).Return(&c, nil) - asMock.On("CreateSubscription", subscriptionOptions).Return(&s, nil) stdout := &strings.Builder{} fc.SetOutput(stdout) @@ -268,20 +212,6 @@ const fnCreateDryRun = `metadata: spec: {} status: {} --- -metadata: - creationTimestamp: null - name: my-channel -spec: {} -status: {} ---- -metadata: - creationTimestamp: null - name: square -spec: - channel: "" - subscriber: "" -status: {} ---- ` var _ = Describe("The riff function build command", func() { diff --git a/cmd/commands/service.go b/cmd/commands/service.go index cfea018de..b2ab654e6 100644 --- a/cmd/commands/service.go +++ b/cmd/commands/service.go @@ -23,7 +23,6 @@ import ( "time" "github.com/frioux/shellquote" - "github.com/knative/eventing/pkg/apis/channels/v1alpha1" v1alpha12 "github.com/knative/serving/pkg/apis/serving/v1alpha1" "github.com/projectriff/riff/pkg/core" "github.com/spf13/cobra" @@ -55,11 +54,6 @@ const ( serviceInvokeMaxNumberOfArgs ) -const ( - serviceSubscribeServiceNameIndex = iota - serviceSubscribeNumberOfArgs -) - const ( serviceDeleteServiceNameIndex = iota serviceDeleteNumberOfArgs @@ -75,34 +69,22 @@ func Service() *cobra.Command { func ServiceCreate(fcTool *core.Client) *cobra.Command { - createInputChannelOptions := core.CreateChannelOptions{} - createOutputChannelOptions := core.CreateChannelOptions{} createServiceOptions := core.CreateOrReviseServiceOptions{} - createSubscriptionOptions := core.CreateSubscriptionOptions{} command := &cobra.Command{ Use: "create", - Short: "Create a new service resource, with optional input binding", + Short: "Create a new service resource", Long: `Create a new service resource from a given image. -` + channelLongDesc + ` - ` + envFromLongDesc + ` `, Example: ` riff service create square --image acme/square:1.0 --namespace joseph-ns riff service create greeter --image acme/greeter:1.0 --env FOO=bar --env MESSAGE=Hello - riff service create tweets-logger --image acme/tweets-logger:1.0.0 --input tweets --bus kafka`, + riff service create tweets-logger --image acme/tweets-logger:1.0.0`, Args: ArgValidationConjunction( cobra.ExactArgs(serviceCreateNumberOfArgs), AtPosition(serviceCreateServiceNameIndex, ValidName()), ), - PreRunE: FlagsValidatorAsCobraRunE( - FlagsValidationConjunction( - FlagsDependency(Set("input"), exactlyOneOfBusOrClusterBus), - FlagsDependency(NotSet("input"), NoneOf("bus", "cluster-bus")), - FlagsDependency(NotSet("input"), NoneOf("output")), - ), - ), RunE: func(cmd *cobra.Command, args []string) error { fnName := args[serviceCreateServiceNameIndex] @@ -112,43 +94,11 @@ func ServiceCreate(fcTool *core.Client) *cobra.Command { return err } - var c *v1alpha1.Channel - var subscr *v1alpha1.Subscription - if createInputChannelOptions.Name != "" { - c, err = (*fcTool).CreateChannel(createInputChannelOptions) - if err != nil { - return err - } - - if createOutputChannelOptions.Name != "" { - c, err = (*fcTool).CreateChannel(createOutputChannelOptions) - if err != nil { - return err - } - } - createSubscriptionOptions.Name = subscriptionNameFromService(fnName) - createSubscriptionOptions.Subscriber = subscriberNameFromService(fnName) // TODO - subscr, err = (*fcTool).CreateSubscription(createSubscriptionOptions) - if err != nil { - return err - } - } - if createServiceOptions.DryRun { marshaller := NewMarshaller(cmd.OutOrStdout()) if err = marshaller.Marshal(f); err != nil { return err } - if c != nil { - if err = marshaller.Marshal(c); err != nil { - return err - } - } - if subscr != nil { - if err = marshaller.Marshal(subscr); err != nil { - return err - } - } } else { printSuccessfulCompletion(cmd) } @@ -162,55 +112,17 @@ func ServiceCreate(fcTool *core.Client) *cobra.Command { command.Flags().VarP( BroadcastStringValue("", &createServiceOptions.Namespace, - &createInputChannelOptions.Namespace, - &createOutputChannelOptions.Namespace, - &createSubscriptionOptions.Namespace, ), "namespace", "n", "the `namespace` of the service and any namespaced resources specified", ) - command.Flags().VarP( - BroadcastStringValue("", - &createInputChannelOptions.Name, - &createSubscriptionOptions.Channel, - ), - "input", "i", "name of the service's input `channel`, if any", - ) - - command.Flags().VarP( - BroadcastStringValue("", - &createOutputChannelOptions.Name, - &createSubscriptionOptions.ReplyTo, - ), - "output", "o", "name of the service's output `channel`, if any", - ) - command.Flags().VarPF( BroadcastBoolValue(false, &createServiceOptions.DryRun, - &createInputChannelOptions.DryRun, - &createOutputChannelOptions.DryRun, - &createSubscriptionOptions.DryRun, ), "dry-run", "", dryRunUsage, ).NoOptDefVal = "true" - command.Flags().Var( - BroadcastStringValue("", - &createInputChannelOptions.Bus, - &createOutputChannelOptions.Bus, - ), - "bus", busUsage, - ) - - command.Flags().Var( - BroadcastStringValue("", - &createInputChannelOptions.ClusterBus, - &createOutputChannelOptions.ClusterBus, - ), - "cluster-bus", clusterBusUsage, - ) - command.Flags().StringVar(&createServiceOptions.Image, "image", "", "the `name[:tag]` reference of an image containing the application/function") command.MarkFlagRequired("image") @@ -422,54 +334,6 @@ Additional curl arguments and flags may be specified after a double dash (--).`, return command } -func ServiceSubscribe(fcClient *core.Client) *cobra.Command { - - createSubscriptionOptions := core.CreateSubscriptionOptions{} - - command := &cobra.Command{ - Use: "subscribe", - Short: "Subscribe a service to an existing input channel", - Example: ` riff service subscribe square --input numbers --namespace joseph-ns`, - Args: ArgValidationConjunction( - cobra.ExactArgs(serviceSubscribeNumberOfArgs), - AtPosition(serviceSubscribeServiceNameIndex, ValidName()), - ), - RunE: func(cmd *cobra.Command, args []string) error { - - fnName := args[serviceSubscribeServiceNameIndex] - - if createSubscriptionOptions.Name == "" { - createSubscriptionOptions.Name = subscriptionNameFromService(fnName) - } - createSubscriptionOptions.Subscriber = subscriberNameFromService(fnName) - s, err := (*fcClient).CreateSubscription(createSubscriptionOptions) - if err != nil { - return err - } - if createSubscriptionOptions.DryRun { - marshaller := NewMarshaller(cmd.OutOrStdout()) - if err = marshaller.Marshal(s); err != nil { - return err - } - } else { - printSuccessfulCompletion(cmd) - } - return nil - }, - } - - LabelArgs(command, "SERVICE_NAME") - - command.Flags().StringVar(&createSubscriptionOptions.Name, "subscription", "", "`name` of the subscription (default SERVICE_NAME)") - command.Flags().StringVarP(&createSubscriptionOptions.Channel, "input", "i", "", "the name of an input `channel` for the service") - command.MarkFlagRequired("input") - command.Flags().StringVarP(&createSubscriptionOptions.ReplyTo, "output", "o", "", "the name of an output `channel` for the service") - command.Flags().StringVarP(&createSubscriptionOptions.Namespace, "namespace", "n", "", "the `namespace` of the subscription, channel, and service") - command.Flags().BoolVar(&createSubscriptionOptions.DryRun, "dry-run", false, dryRunUsage) - - return command -} - func ServiceDelete(fcClient *core.Client) *cobra.Command { deleteServiceOptions := core.DeleteServiceOptions{} @@ -501,15 +365,3 @@ func ServiceDelete(fcClient *core.Client) *cobra.Command { return command } - -// subscriptionNameFromService returns the name to use for the subscription being created alongside -// a service/function. By convention, this is chosen to be the name of the service. -func subscriptionNameFromService(fnName string) string { - return fnName -} - -// subscriberNameFromService returns the name to use for the `subscriber` field of a subscription, -// given a service/function that is being created/subscribed. This has to be the name of the service itself. -func subscriberNameFromService(fnName string) string { - return fnName -} diff --git a/cmd/commands/service_test.go b/cmd/commands/service_test.go index 6efccf598..fab8933f7 100644 --- a/cmd/commands/service_test.go +++ b/cmd/commands/service_test.go @@ -64,16 +64,6 @@ var _ = Describe("The riff service create command", func() { err := cc.Execute() Expect(err).To(MatchError(`required flag(s) "image" not set`)) }) - It("should fail when both bus and cluster-bus are set", func() { - cc.SetArgs([]string{"my-service", "--bus", "b", "--cluster-bus", "cb", "--input", "c"}) - err := cc.Execute() - Expect(err).To(MatchError("when --input is set, at most one of --bus, --cluster-bus must be set")) - }) - It("should fail when neither bus or cluster-bus is set", func() { - cc.SetArgs([]string{"my-service", "--input", "c"}) - err := cc.Execute() - Expect(err).To(MatchError("when --input is set, at least one of --bus, --cluster-bus must be set")) - }) }) Context("when given suitable args and flags", func() { @@ -132,8 +122,7 @@ var _ = Describe("The riff service create command", func() { Expect(err).NotTo(HaveOccurred()) }) It("should print when --dry-run is set", func() { - sc.SetArgs([]string{"square", "--image", "foo/bar", - "--input", "my-channel", "--bus", "kafka", "--dry-run"}) + sc.SetArgs([]string{"square", "--image", "foo/bar", "--dry-run"}) serviceOptions := core.CreateOrReviseServiceOptions{ Name: "square", @@ -142,17 +131,6 @@ var _ = Describe("The riff service create command", func() { EnvFrom: []string{}, DryRun: true, } - channelOptions := core.CreateChannelOptions{ - Name: "my-channel", - Bus: "kafka", - DryRun: true, - } - subscriptionOptions := core.CreateSubscriptionOptions{ - Name: "square", - Channel: "my-channel", - Subscriber: "square", - DryRun: true, - } svc := v1alpha1.Service{} svc.Name = "square" @@ -161,8 +139,6 @@ var _ = Describe("The riff service create command", func() { s := v1alpha12.Subscription{} s.Name = "square" asMock.On("CreateService", serviceOptions).Return(&svc, nil) - asMock.On("CreateChannel", channelOptions).Return(&c, nil) - asMock.On("CreateSubscription", subscriptionOptions).Return(&s, nil) stdout := &strings.Builder{} sc.SetOutput(stdout) @@ -182,20 +158,6 @@ const serviceCreateDryRun = `metadata: spec: {} status: {} --- -metadata: - creationTimestamp: null - name: my-channel -spec: {} -status: {} ---- -metadata: - creationTimestamp: null - name: square -spec: - channel: "" - subscriber: "" -status: {} ---- ` var _ = Describe("The riff service revise command", func() { @@ -503,108 +465,6 @@ foo Failed: It's dead, Jim wizz Running ` -var _ = Describe("The riff service subscribe command", func() { - Context("when given wrong args or flags", func() { - var ( - mockClient core.Client - ss *cobra.Command - ) - BeforeEach(func() { - mockClient = nil - ss = commands.ServiceSubscribe(&mockClient) - }) - It("should fail with no args", func() { - ss.SetArgs([]string{}) - err := ss.Execute() - Expect(err).To(MatchError("accepts 1 arg(s), received 0")) - }) - It("should fail with invalid service name", func() { - ss.SetArgs([]string{".invalid"}) - err := ss.Execute() - Expect(err).To(MatchError(ContainSubstring("must start and end with an alphanumeric character"))) - }) - It("should fail without required flags", func() { - ss.SetArgs([]string{"my-service"}) - err := ss.Execute() - Expect(err).To(MatchError(`required flag(s) "input" not set`)) - }) - }) - - Context("when given suitable args and flags", func() { - var ( - client core.Client - asMock *mocks.Client - ss *cobra.Command - ) - BeforeEach(func() { - client = new(mocks.Client) - asMock = client.(*mocks.Client) - - ss = commands.ServiceSubscribe(&client) - }) - AfterEach(func() { - asMock.AssertExpectations(GinkgoT()) - }) - It("should involve the core.Client", func() { - ss.SetArgs([]string{"my-service", "--input", "my-channel", "--namespace", "ns"}) - - o := core.CreateSubscriptionOptions{ - Name: "my-service", - Channel: "my-channel", - Subscriber: "my-service", - } - o.Namespace = "ns" - - asMock.On("CreateSubscription", o).Return(nil, nil) - err := ss.Execute() - Expect(err).NotTo(HaveOccurred()) - }) - It("should propagate core.Client errors", func() { - ss.SetArgs([]string{"my-service", "--input", "my-channel"}) - - e := fmt.Errorf("some error") - asMock.On("CreateSubscription", mock.Anything).Return(nil, e) - err := ss.Execute() - Expect(err).To(MatchError(e)) - }) - It("should print when --dry-run is set", func() { - ss.SetArgs([]string{"square", "--input", "my-channel", "--dry-run"}) - - subscriptionOptions := core.CreateSubscriptionOptions{ - Name: "square", - Channel: "my-channel", - Subscriber: "square", - DryRun: true, - } - - s := v1alpha12.Subscription{} - s.Name = "square" - s.Spec.Channel = "my-channel" - s.Spec.Subscriber = "square" - asMock.On("CreateSubscription", subscriptionOptions).Return(&s, nil) - - stdout := &strings.Builder{} - ss.SetOutput(stdout) - - err := ss.Execute() - Expect(err).NotTo(HaveOccurred()) - - Expect(stdout.String()).To(Equal(serviceSubscribeDryRun)) - }) - - }) -}) - -const serviceSubscribeDryRun = `metadata: - creationTimestamp: null - name: square -spec: - channel: my-channel - subscriber: square -status: {} ---- -` - var _ = Describe("The riff service delete command", func() { Context("when given wrong args or flags", func() { var ( diff --git a/cmd/commands/subscription.go b/cmd/commands/subscription.go new file mode 100644 index 000000000..b313ca650 --- /dev/null +++ b/cmd/commands/subscription.go @@ -0,0 +1,67 @@ +package commands + +import ( + "github.com/projectriff/riff/pkg/core" + . "github.com/spf13/cobra" +) + +const ( + subscriptionCreateNameIndex = iota + subscriptionCreateMaxNumberOfArgs +) + +func Subscription() *Command { + return &Command{ + Use: "subscription", + Short: "Interact with subscription-related resources", + } +} + +func SubscriptionCreate(client *core.Client) *Command { + options := core.CreateSubscriptionOptions{} + + command := &Command{ + Use: "create", + Short: "Create a new subscription, binding a service to an input channel", + Long: `Create a new, optionally named subscription, binding a service to an input channel. +The default name of the subscription is the provided service name. +The service can optionally be bound to an output channel.`, + Example: ` riff subscription create --from tweets --processor tweets-logger + riff subscription create my-subscription --from tweets --processor tweets-logger + riff subscription create --from tweets --processor tweets-logger --to logged-tweets`, + Args: ArgValidationConjunction( + MaximumNArgs(subscriptionCreateMaxNumberOfArgs), + OptionalAtPosition(subscriptionCreateNameIndex, ValidName()), + ), + RunE: func(cmd *Command, args []string) error { + options.Name = computeSubscriptionName(args, options) + _, err := (*client).CreateSubscription(options) + if err != nil { + return err + } + printSuccessfulCompletion(cmd) + return nil + }, + } + + LabelArgs(command, "SUBSCRIPTION_NAME") + defineFlags(command, &options) + return command +} + +func defineFlags(command *Command, options *core.CreateSubscriptionOptions) { + flags := command.Flags() + flags.StringVarP(&options.Subscriber, "processor", "s", "", "the subscriber registered in the subscription") + flags.StringVarP(&options.Channel, "from", "i", "", "the input channel the service binds to") + flags.StringVarP(&options.ReplyTo, "to", "o", "", "the optional output channel the service binds to") + flags.StringVarP(&options.Namespace, "namespace", "n", "", "the namespace of the subscription") + command.MarkFlagRequired("processor") + command.MarkFlagRequired("from") +} + +func computeSubscriptionName(args []string, options core.CreateSubscriptionOptions) string { + if len(args) == subscriptionCreateMaxNumberOfArgs { + return args[subscriptionCreateNameIndex] + } + return options.Subscriber +} diff --git a/cmd/commands/subscription_test.go b/cmd/commands/subscription_test.go new file mode 100644 index 000000000..f9bb8b2d1 --- /dev/null +++ b/cmd/commands/subscription_test.go @@ -0,0 +1,171 @@ +package commands_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pkg/errors" + "github.com/projectriff/riff/cmd/commands" + "github.com/projectriff/riff/pkg/core" + "github.com/projectriff/riff/pkg/core/mocks" + "github.com/spf13/cobra" + "strings" +) + +var _ = Describe("The riff subscription create command", func() { + + var ( + client core.Client + clientMock *mocks.Client + createCommand *cobra.Command + ) + + BeforeEach(func() { + client = new(mocks.Client) + clientMock = client.(*mocks.Client) + createCommand = commands.SubscriptionCreate(&client) + }) + + AfterEach(func() { + clientMock.AssertExpectations(GinkgoT()) + }) + + It("should be documented", func() { + Expect(createCommand.Name()).To(Equal("create")) + Expect(createCommand.Short).NotTo(BeEmpty(), "missing short description") + Expect(createCommand.Long).NotTo(BeEmpty(), "missing long description") + Expect(createCommand.Example).NotTo(BeEmpty(), "missing example") + }) + + It("should define flags", func() { + Expect(createCommand.Flag("processor")).NotTo(BeNil()) + Expect(createCommand.Flag("from")).NotTo(BeNil()) + Expect(createCommand.Flag("to")).NotTo(BeNil()) + Expect(createCommand.Flag("namespace")).NotTo(BeNil()) + }) + + Context("when given wrong args or flags", func() { + + It("should fail with missing required flags", func() { + createCommand.SetArgs([]string{}) + + err := createCommand.Execute() + + Expect(err).To(MatchError(`required flag(s) "from", "processor" not set`)) + }) + + It("should fail with too many args", func() { + createCommand.SetArgs([]string{ + "too", "much", "--processor", "service", "--from", "input"}) + + err := createCommand.Execute() + + Expect(err).To(MatchError(`accepts at most 1 arg(s), received 2`)) + }) + + It("should fail with an invalid subscription name", func() { + createCommand.SetArgs([]string{ + "@@invalid@@", "--processor", "service", "--from", "input"}) + + err := createCommand.Execute() + + Expect(err.Error()).To(HavePrefix("a DNS-1123 subdomain must consist")) + }) + }) + + Context("when given valid args and flags", func() { + It("should create the subscription with the provided name", func() { + stdout := &strings.Builder{} + createCommand.SetOutput(stdout) + createCommand.SetArgs([]string{ + "subscription-name", "--from", "coco-chanel", "--processor", "my-service"}) + clientMock.On("CreateSubscription", core.CreateSubscriptionOptions{ + Name: "subscription-name", + Subscriber: "my-service", + Channel: "coco-chanel", + }).Return(nil, nil) + + err := createCommand.Execute() + + Expect(err).NotTo(HaveOccurred()) + Expect(stdout.String()).To(Equal("create completed successfully\n")) + }) + + It("should create the subscription with the service name by default", func() { + stdout := &strings.Builder{} + createCommand.SetOutput(stdout) + createCommand.SetArgs([]string{ + "--from", "coco-chanel", "--processor", "my-service"}) + clientMock.On("CreateSubscription", core.CreateSubscriptionOptions{ + Name: "my-service", + Subscriber: "my-service", + Channel: "coco-chanel", + }).Return(nil, nil) + + err := createCommand.Execute() + + Expect(err).NotTo(HaveOccurred()) + Expect(stdout.String()).To(Equal("create completed successfully\n")) + }) + + It("should create the subscription with the output channel binding", func() { + stdout := &strings.Builder{} + createCommand.SetOutput(stdout) + createCommand.SetArgs([]string{ + "subscription-name", "--from", "coco-chanel", "--processor", "my-service", + "--to", "chanel-number-five"}) + clientMock.On("CreateSubscription", core.CreateSubscriptionOptions{ + Name: "subscription-name", + Subscriber: "my-service", + Channel: "coco-chanel", + ReplyTo: "chanel-number-five", + }).Return(nil, nil) + + err := createCommand.Execute() + + Expect(err).NotTo(HaveOccurred()) + Expect(stdout.String()).To(Equal("create completed successfully\n")) + }) + + It("should create the subscription in the output provided namespace", func() { + stdout := &strings.Builder{} + createCommand.SetOutput(stdout) + createCommand.SetArgs([]string{ + "subscription-name", + "--from", "coco-chanel", + "--processor", "my-service", + "--to", "chanel-number-five", + "--namespace", "myspace"}) + expectedOptions := core.CreateSubscriptionOptions{ + Name: "subscription-name", + Subscriber: "my-service", + Channel: "coco-chanel", + ReplyTo: "chanel-number-five", + } + expectedOptions.Namespace = "myspace" + clientMock.On("CreateSubscription", expectedOptions).Return(nil, nil) + + err := createCommand.Execute() + + Expect(err).NotTo(HaveOccurred()) + Expect(stdout.String()).To(Equal("create completed successfully\n")) + }) + + It("should propagate the client error", func() { + stdout := &strings.Builder{} + createCommand.SetOutput(stdout) + createCommand.SetArgs([]string{ + "--from", "coco-chanel", "--processor", "my-service"}) + expectedError := errors.New("client failure") + clientMock.On("CreateSubscription", core.CreateSubscriptionOptions{ + Name: "my-service", + Subscriber: "my-service", + Channel: "coco-chanel", + }).Return(nil, expectedError) + + err := createCommand.Execute() + + Expect(err).To(MatchError(expectedError)) + }) + }) + +}) diff --git a/cmd/commands/wiring.go b/cmd/commands/wiring.go index 72d797d1a..e4ff55a44 100644 --- a/cmd/commands/wiring.go +++ b/cmd/commands/wiring.go @@ -123,7 +123,6 @@ See https://projectriff.io and https://github.com/knative/docs`, ServiceRevise(&client), ServiceStatus(&client), ServiceInvoke(&client), - ServiceSubscribe(&client), ServiceDelete(&client), ) @@ -150,6 +149,11 @@ See https://projectriff.io and https://github.com/knative/docs`, SystemUninstall(&kc), ) + subscription := Subscription() + subscription.AddCommand( + SubscriptionCreate(&client), + ) + rootCmd.AddCommand( function, service, @@ -157,6 +161,7 @@ See https://projectriff.io and https://github.com/knative/docs`, image, namespace, system, + subscription, Docs(rootCmd), Version(), Completion(rootCmd), diff --git a/cmd/commands/wiring_test.go b/cmd/commands/wiring_test.go new file mode 100644 index 000000000..9dea10080 --- /dev/null +++ b/cmd/commands/wiring_test.go @@ -0,0 +1,46 @@ +package commands_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/projectriff/riff/cmd/commands" + "github.com/spf13/cobra" +) + +var _ = Describe("`riff` root command", func() { + Context("subscription", func() { + var rootCommand *cobra.Command + + BeforeEach(func() { + rootCommand = commands.CreateAndWireRootCommand() + }) + + It("should be included in riff subcommands", func() { + Expect(commandNamesOf(rootCommand.Commands())).To(ContainElement("subscription")) + }) + + It("should define a create subcommand", func() { + serviceCmd := matchSubcommandByName(rootCommand, "subscription") + + Expect(commandNamesOf(serviceCmd.Commands())).To(ContainElement("create")) + }) + }) + +}) + +func commandNamesOf(commands []*cobra.Command) []string { + result := make([]string, len(commands)) + for _, e := range commands { + result = append(result, e.Name()) + } + return result +} + +func matchSubcommandByName(command *cobra.Command, name string) *cobra.Command { + for _, e := range command.Commands() { + if e.Name() == name { + return e + } + } + return nil +} diff --git a/docs/riff.md b/docs/riff.md index f9468f2f3..69b621976 100644 --- a/docs/riff.md +++ b/docs/riff.md @@ -25,6 +25,7 @@ See https://projectriff.io and https://github.com/knative/docs * [riff image](riff_image.md) - Interact with docker images * [riff namespace](riff_namespace.md) - Manage namespaces used for riff resources * [riff service](riff_service.md) - Interact with service related resources +* [riff subscription](riff_subscription.md) - Interact with subscription-related resources * [riff system](riff_system.md) - Manage system related resources * [riff version](riff_version.md) - Print version information about riff diff --git a/docs/riff_function.md b/docs/riff_function.md index 001119cb8..5c6760671 100644 --- a/docs/riff_function.md +++ b/docs/riff_function.md @@ -23,5 +23,5 @@ Interact with function related resources * [riff](riff.md) - Commands for creating and managing function resources * [riff function build](riff_function_build.md) - Trigger a revision build for a function resource -* [riff function create](riff_function_create.md) - Create a new function resource, with optional input and output channels +* [riff function create](riff_function_create.md) - Create a new function resource diff --git a/docs/riff_function_create.md b/docs/riff_function_create.md index 763109cae..ac6011732 100644 --- a/docs/riff_function_create.md +++ b/docs/riff_function_create.md @@ -1,6 +1,6 @@ ## riff function create -Create a new function resource, with optional input and output channels +Create a new function resource ### Synopsis @@ -25,15 +25,13 @@ riff function create [flags] ``` riff function create node square --git-repo https://github.com/acme/square --image acme/square --namespace joseph-ns - riff function create java tweets-logger --git-repo https://github.com/acme/tweets --image acme/tweets-logger:1.0.0 --input tweets --bus kafka + riff function create java tweets-logger --git-repo https://github.com/acme/tweets --image acme/tweets-logger:1.0.0 ``` ### Options ``` --artifact path path to the function source code or jar file; auto-detected if not specified - --bus name the name of the bus to create the channel in. - --cluster-bus name the name of the cluster bus to create the channel in. --dry-run don't create resources but print yaml representation on stdout --env stringArray environment variable expressed in a 'key=value' format --env-from stringArray environment variable created from a source reference; see command help for supported formats @@ -42,9 +40,7 @@ riff function create [flags] --handler method or class the name of the method or class to invoke, depending on the invoker used -h, --help help for create --image repository/image[:tag] the name of the image to build; must be a writable repository/image[:tag] with credentials configured - -i, --input channel name of the function's input channel, if any -n, --namespace namespace the namespace of the subscription, channel, and function - -o, --output channel name of the function's output channel, if any -v, --verbose print details of command progress -w, --wait wait until the created resource reaches either a successful or an error state (automatic with --verbose) ``` diff --git a/docs/riff_service.md b/docs/riff_service.md index 007c152ff..e85f8ab27 100644 --- a/docs/riff_service.md +++ b/docs/riff_service.md @@ -22,11 +22,10 @@ Interact with service (as in `service.serving.knative.dev`) related resources. ### SEE ALSO * [riff](riff.md) - Commands for creating and managing function resources -* [riff service create](riff_service_create.md) - Create a new service resource, with optional input binding +* [riff service create](riff_service_create.md) - Create a new service resource * [riff service delete](riff_service_delete.md) - Delete an existing service * [riff service invoke](riff_service_invoke.md) - Invoke a service * [riff service list](riff_service_list.md) - List service resources * [riff service revise](riff_service_revise.md) - Create a new revision for a service, with updated attributes * [riff service status](riff_service_status.md) - Display the status of a service -* [riff service subscribe](riff_service_subscribe.md) - Subscribe a service to an existing input channel diff --git a/docs/riff_service_create.md b/docs/riff_service_create.md index 2edd517e1..0620cccfe 100644 --- a/docs/riff_service_create.md +++ b/docs/riff_service_create.md @@ -1,13 +1,11 @@ ## riff service create -Create a new service resource, with optional input binding +Create a new service resource ### Synopsis Create a new service resource from a given image. -If an input channel and bus are specified, create the channel in the bus and subscribe the service to the channel. - If `--env-from` is specified the source reference can be `configMapKeyRef` to select a key from a ConfigMap or `secretKeyRef` to select a key from a Secret. The following formats are supported: --env-from configMapKeyRef:{config-map-name}:{key-to-select} @@ -23,22 +21,18 @@ riff service create [flags] ``` riff service create square --image acme/square:1.0 --namespace joseph-ns riff service create greeter --image acme/greeter:1.0 --env FOO=bar --env MESSAGE=Hello - riff service create tweets-logger --image acme/tweets-logger:1.0.0 --input tweets --bus kafka + riff service create tweets-logger --image acme/tweets-logger:1.0.0 ``` ### Options ``` - --bus name the name of the bus to create the channel in. - --cluster-bus name the name of the cluster bus to create the channel in. --dry-run don't create resources but print yaml representation on stdout --env stringArray environment variable expressed in a 'key=value' format --env-from stringArray environment variable created from a source reference; see command help for supported formats -h, --help help for create --image name[:tag] the name[:tag] reference of an image containing the application/function - -i, --input channel name of the service's input channel, if any -n, --namespace namespace the namespace of the service and any namespaced resources specified - -o, --output channel name of the service's output channel, if any ``` ### Options inherited from parent commands diff --git a/docs/riff_service_subscribe.md b/docs/riff_service_subscribe.md deleted file mode 100644 index 94c444aaa..000000000 --- a/docs/riff_service_subscribe.md +++ /dev/null @@ -1,40 +0,0 @@ -## riff service subscribe - -Subscribe a service to an existing input channel - -### Synopsis - -Subscribe a service to an existing input channel - -``` -riff service subscribe [flags] -``` - -### Examples - -``` - riff service subscribe square --input numbers --namespace joseph-ns -``` - -### Options - -``` - --dry-run don't create resources but print yaml representation on stdout - -h, --help help for subscribe - -i, --input channel the name of an input channel for the service - -n, --namespace namespace the namespace of the subscription, channel, and service - -o, --output channel the name of an output channel for the service - --subscription name name of the subscription (default SERVICE_NAME) -``` - -### Options inherited from parent commands - -``` - --kubeconfig path the path of a kubeconfig (default "~/.kube/config") - --master address the address of the Kubernetes API server; overrides any value in kubeconfig -``` - -### SEE ALSO - -* [riff service](riff_service.md) - Interact with service related resources - diff --git a/docs/riff_subscription.md b/docs/riff_subscription.md new file mode 100644 index 000000000..29db4b54d --- /dev/null +++ b/docs/riff_subscription.md @@ -0,0 +1,26 @@ +## riff subscription + +Interact with subscription-related resources + +### Synopsis + +Interact with subscription-related resources + +### Options + +``` + -h, --help help for subscription +``` + +### Options inherited from parent commands + +``` + --kubeconfig path the path of a kubeconfig (default "~/.kube/config") + --master address the address of the Kubernetes API server; overrides any value in kubeconfig +``` + +### SEE ALSO + +* [riff](riff.md) - Commands for creating and managing function resources +* [riff subscription create](riff_subscription_create.md) - Create a new subscription, binding a service to an input channel + diff --git a/docs/riff_subscription_create.md b/docs/riff_subscription_create.md new file mode 100644 index 000000000..d497318e9 --- /dev/null +++ b/docs/riff_subscription_create.md @@ -0,0 +1,43 @@ +## riff subscription create + +Create a new subscription, binding a service to an input channel + +### Synopsis + +Create a new, optionally named subscription, binding a service to an input channel. +The default name of the subscription is the provided service name. +The service can optionally be bound to an output channel. + +``` +riff subscription create [flags] +``` + +### Examples + +``` + riff subscription create --from tweets --processor tweets-logger + riff subscription create my-subscription --from tweets --processor tweets-logger + riff subscription create --from tweets --processor tweets-logger --to logged-tweets +``` + +### Options + +``` + -i, --from string the input channel the service binds to + -h, --help help for create + -n, --namespace string the namespace of the subscription + -s, --processor string the subscriber registered in the subscription + -o, --to string the optional output channel the service binds to +``` + +### Options inherited from parent commands + +``` + --kubeconfig path the path of a kubeconfig (default "~/.kube/config") + --master address the address of the Kubernetes API server; overrides any value in kubeconfig +``` + +### SEE ALSO + +* [riff subscription](riff_subscription.md) - Interact with subscription-related resources +