diff --git a/go.mod b/go.mod index f8b9cda6d..2f21e32c9 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/streamnative/pulsarctl go 1.25.8 require ( - github.com/apache/pulsar-client-go v0.18.0-candidate-1.0.20251222030102-3bb7d4eff361 + github.com/apache/pulsar-client-go v0.19.0-candidate-1 github.com/docker/go-connections v0.5.0 github.com/fatih/color v1.7.0 github.com/ghodss/yaml v1.0.0 @@ -24,6 +24,7 @@ require ( require ( dario.cat/mergo v1.0.0 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/AthenZ/athenz v1.12.31 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/DataDog/zstd v1.5.7 // indirect diff --git a/go.sum b/go.sum index 91c0b6e48..e825ecf34 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AthenZ/athenz v1.12.31 h1:GQnRDLgivPlVvklSpH9gp+t/dho9DJTtt+hlLYo5TX8= github.com/AthenZ/athenz v1.12.31/go.mod h1:6Siq4JOA4OjgYVgtTVIeHrb4HB2hEL8i4fx7aOFrgfY= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= @@ -14,8 +14,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw= github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY= -github.com/apache/pulsar-client-go v0.18.0-candidate-1.0.20251222030102-3bb7d4eff361 h1:Fb4j4v85TPq64FRp+QMLWaW3/Hg1Jg7TBWaZwPcSO9Y= -github.com/apache/pulsar-client-go v0.18.0-candidate-1.0.20251222030102-3bb7d4eff361/go.mod h1:/Zf8Q8bSSc6ndEJ8V1muIHf6ZWsMrHoQU+98Ww9pOeI= +github.com/apache/pulsar-client-go v0.19.0-candidate-1 h1:FtOmcndcFzmleMZmkQEO1OEZ0HroCchqPMpIflKC1lY= +github.com/apache/pulsar-client-go v0.19.0-candidate-1/go.mod h1:/Zf8Q8bSSc6ndEJ8V1muIHf6ZWsMrHoQU+98Ww9pOeI= github.com/ardielle/ardielle-go v1.5.2 h1:TilHTpHIQJ27R1Tl/iITBzMwiUGSlVfiVhwDNGM3Zj4= github.com/ardielle/ardielle-go v1.5.2/go.mod h1:I4hy1n795cUhaVt/ojz83SNVCYIGsAFAONtv2Dr7HUI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/pkg/ctl/brokers/broker.go b/pkg/ctl/brokers/broker.go index 0985a175f..e2a43f721 100644 --- a/pkg/ctl/brokers/broker.go +++ b/pkg/ctl/brokers/broker.go @@ -30,6 +30,7 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { "broker") cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getBrokerListCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, leaderBrokerCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getDynamicConfigListNameCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getOwnedNamespacesCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, updateDynamicConfig) diff --git a/pkg/ctl/brokers/leader_broker.go b/pkg/ctl/brokers/leader_broker.go new file mode 100644 index 000000000..470eda413 --- /dev/null +++ b/pkg/ctl/brokers/leader_broker.go @@ -0,0 +1,67 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 brokers + +import "github.com/streamnative/pulsarctl/pkg/cmdutils" + +func leaderBrokerCmd(vc *cmdutils.VerbCmd) { + desc := cmdutils.LongDescription{} + desc.CommandUsedFor = "Get the information of the leader broker" + desc.CommandPermission = "This command requires super-user permissions." + + var examples []cmdutils.Example + get := cmdutils.Example{ + Desc: desc.CommandUsedFor, + Command: "pulsarctl brokers leader-broker", + } + examples = append(examples, get) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "{\n" + + " \"brokerId\": \"broker-1\",\n" + + " \"serviceUrl\": \"http://127.0.0.1:8080\"\n" + + "}", + } + out = append(out, successOut) + desc.CommandOutput = out + + vc.SetDescription( + "leader-broker", + desc.CommandUsedFor, + desc.ToString(), + desc.ExampleToString(), + "leader-broker") + + vc.SetRunFunc(func() error { + return doGetLeaderBroker(vc) + }) + vc.EnableOutputFlagSet() +} + +func doGetLeaderBroker(vc *cmdutils.VerbCmd) error { + admin := cmdutils.NewPulsarClient() + info, err := admin.Brokers().GetLeaderBroker() + if err == nil { + oc := cmdutils.NewOutputContent().WithObject(info) + err = vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), oc) + } + return err +} diff --git a/pkg/ctl/brokers/leader_broker_test.go b/pkg/ctl/brokers/leader_broker_test.go new file mode 100644 index 000000000..db5ff743b --- /dev/null +++ b/pkg/ctl/brokers/leader_broker_test.go @@ -0,0 +1,57 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 brokers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/stretchr/testify/assert" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func TestLeaderBroker(t *testing.T) { + oldURL := cmdutils.PulsarCtlConfig.WebServiceURL + defer func() { + cmdutils.PulsarCtlConfig.WebServiceURL = oldURL + }() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/admin/v2/brokers/leaderBroker", r.URL.Path) + _, _ = w.Write([]byte(`{"brokerId":"broker-1","serviceUrl":"http://127.0.0.1:8080"}`)) + })) + defer srv.Close() + + cmdutils.PulsarCtlConfig.WebServiceURL = srv.URL + + args := []string{"leader-broker"} + out, execErr, _, err := TestBrokersCommands(leaderBrokerCmd, args) + assert.Nil(t, err) + assert.Nil(t, execErr) + + var info utils.BrokerInfo + err = json.Unmarshal(out.Bytes(), &info) + assert.Nil(t, err) + assert.Equal(t, "broker-1", info.BrokerID) + assert.Equal(t, "http://127.0.0.1:8080", info.ServiceURL) +} diff --git a/pkg/ctl/namespace/bookie_affinity_group.go b/pkg/ctl/namespace/bookie_affinity_group.go new file mode 100644 index 000000000..ea7a403ee --- /dev/null +++ b/pkg/ctl/namespace/bookie_affinity_group.go @@ -0,0 +1,166 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetBookieAffinityGroupCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "Get bookie affinity group configured for a namespace" + desc.CommandPermission = "This command requires super-user permissions." + + var examples []cmdutils.Example + get := cmdutils.Example{ + Desc: "Get bookie affinity group configured for a namespace", + Command: "pulsarctl namespaces get-bookie-affinity-group tenant/namespace", + } + examples = append(examples, get) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "{\n \"bookkeeperAffinityGroupPrimary\": \"primary\",\n \"bookkeeperAffinityGroupSecondary\": \"secondary\"\n}", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "get-bookie-affinity-group", + "Get bookie affinity group configured for a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + return doGetBookieAffinityGroup(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doGetBookieAffinityGroup(vc *cmdutils.VerbCmd) error { + admin := cmdutils.NewPulsarClient() + group, err := admin.Namespaces().GetBookieAffinityGroup(vc.NameArg) + if err == nil { + oc := cmdutils.NewOutputContent().WithObject(group) + err = vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), oc) + } + return err +} + +func SetBookieAffinityGroupCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "Set bookie affinity group configured for a namespace" + desc.CommandPermission = "This command requires super-user permissions." + + var examples []cmdutils.Example + set := cmdutils.Example{ + Desc: "Set bookie affinity group configured for a namespace", + Command: "pulsarctl namespaces set-bookie-affinity-group tenant/namespace \n" + + "\t--primary-group primary-group \n" + + "\t--secondary-group secondary-group", + } + examples = append(examples, set) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "Set bookie affinity group successfully for [tenant/namespace]", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "set-bookie-affinity-group", + "Set bookie affinity group configured for a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + data := utils.BookieAffinityGroupData{} + vc.FlagSetGroup.InFlagSet("Bookie Affinity Group", func(set *pflag.FlagSet) { + set.StringVar(&data.BookkeeperAffinityGroupPrimary, "primary-group", "", "primary affinity group") + set.StringVar(&data.BookkeeperAffinityGroupSecondary, "secondary-group", "", "secondary affinity group") + _ = cobra.MarkFlagRequired(set, "primary-group") + }) + + vc.SetRunFuncWithNameArg(func() error { + return doSetBookieAffinityGroup(vc, data) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doSetBookieAffinityGroup(vc *cmdutils.VerbCmd, data utils.BookieAffinityGroupData) error { + admin := cmdutils.NewPulsarClient() + err := admin.Namespaces().SetBookieAffinityGroup(vc.NameArg, data) + if err == nil { + vc.Command.Printf("Set bookie affinity group successfully for [%s]\n", vc.NameArg) + } + return err +} + +func DeleteBookieAffinityGroupCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "Delete bookie affinity group configured for a namespace" + desc.CommandPermission = "This command requires super-user permissions." + + var examples []cmdutils.Example + del := cmdutils.Example{ + Desc: "Delete bookie affinity group configured for a namespace", + Command: "pulsarctl namespaces delete-bookie-affinity-group tenant/namespace", + } + examples = append(examples, del) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "Deleted bookie affinity group successfully for [tenant/namespace]", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "delete-bookie-affinity-group", + "Delete bookie affinity group configured for a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + vc.SetRunFuncWithNameArg(func() error { + return doDeleteBookieAffinityGroup(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doDeleteBookieAffinityGroup(vc *cmdutils.VerbCmd) error { + admin := cmdutils.NewPulsarClient() + err := admin.Namespaces().DeleteBookieAffinityGroup(vc.NameArg) + if err == nil { + vc.Command.Printf("Deleted bookie affinity group successfully for [%s]\n", vc.NameArg) + } + return err +} diff --git a/pkg/ctl/namespace/bookie_affinity_group_test.go b/pkg/ctl/namespace/bookie_affinity_group_test.go new file mode 100644 index 000000000..37baa20cb --- /dev/null +++ b/pkg/ctl/namespace/bookie_affinity_group_test.go @@ -0,0 +1,71 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestBookieAffinityGroupCmd(t *testing.T) { + ns := "public/test-bookie-affinity-group" + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"get-bookie-affinity-group", ns} + getOut, execErr, _, _ := TestNamespaceCommands(GetBookieAffinityGroupCmd, args) + assert.Nil(t, execErr) + var initialGroup *utils.BookieAffinityGroupData + err := json.Unmarshal(getOut.Bytes(), &initialGroup) + assert.Nil(t, err) + + args = []string{"set-bookie-affinity-group", ns, "--primary-group", "primary", "--secondary-group", "secondary"} + setOut, execErr, _, _ := TestNamespaceCommands(SetBookieAffinityGroupCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, fmt.Sprintf("Set bookie affinity group successfully for [%s]\n", ns), setOut.String()) + + args = []string{"get-bookie-affinity-group", ns} + getOut, execErr, _, _ = TestNamespaceCommands(GetBookieAffinityGroupCmd, args) + assert.Nil(t, execErr) + var currentGroup *utils.BookieAffinityGroupData + err = json.Unmarshal(getOut.Bytes(), ¤tGroup) + assert.Nil(t, err) + if !assert.NotNil(t, currentGroup) { + return + } + assert.Equal(t, "primary", currentGroup.BookkeeperAffinityGroupPrimary) + assert.Equal(t, "secondary", currentGroup.BookkeeperAffinityGroupSecondary) + + args = []string{"delete-bookie-affinity-group", ns} + delOut, execErr, _, _ := TestNamespaceCommands(DeleteBookieAffinityGroupCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, fmt.Sprintf("Deleted bookie affinity group successfully for [%s]\n", ns), delOut.String()) + + args = []string{"get-bookie-affinity-group", ns} + getOut, execErr, _, _ = TestNamespaceCommands(GetBookieAffinityGroupCmd, args) + assert.Nil(t, execErr) + var afterDeleteGroup *utils.BookieAffinityGroupData + err = json.Unmarshal(getOut.Bytes(), &afterDeleteGroup) + assert.Nil(t, err) + assert.Equal(t, initialGroup, afterDeleteGroup) +} diff --git a/pkg/ctl/namespace/get_topic_auto_creation.go b/pkg/ctl/namespace/get_topic_auto_creation.go new file mode 100644 index 000000000..c2ec76a31 --- /dev/null +++ b/pkg/ctl/namespace/get_topic_auto_creation.go @@ -0,0 +1,63 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func getTopicAutoCreation(vc *cmdutils.VerbCmd) { + desc := cmdutils.LongDescription{} + desc.CommandUsedFor = "Get topic auto-creation config for a namespace" + desc.CommandPermission = "This command requires tenant admin permissions." + + desc.CommandExamples = []cmdutils.Example{ + { + Desc: "Get topic auto-creation config for a namespace", + Command: "pulsarctl namespaces get-auto-topic-creation tenant/namespace", + }, + } + + vc.SetDescription( + "get-auto-topic-creation", + "Get topic auto-creation config for a namespace", + desc.ToString(), + desc.ExampleToString(), + "get-auto-topic-creation", + ) + + vc.SetRunFuncWithNameArg(func() error { + return doGetTopicAutoCreation(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") + vc.EnableOutputFlagSet() +} + +func doGetTopicAutoCreation(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + config, err := admin.Namespaces().GetTopicAutoCreation(*ns) + if err == nil { + err = vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), cmdutils.NewOutputContent().WithObject(config)) + } + return err +} diff --git a/pkg/ctl/namespace/get_topic_auto_creation_test.go b/pkg/ctl/namespace/get_topic_auto_creation_test.go new file mode 100644 index 000000000..7605c3e79 --- /dev/null +++ b/pkg/ctl/namespace/get_topic_auto_creation_test.go @@ -0,0 +1,30 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetTopicAutoCreationNameError(t *testing.T) { + _, _, nameErr, _ := TestNamespaceCommands(getTopicAutoCreation, []string{"get-auto-topic-creation"}) + assert.NotNil(t, nameErr) + assert.Equal(t, "the namespace name is not specified or the namespace name is specified more than one", nameErr.Error()) +} diff --git a/pkg/ctl/namespace/max_topics_per_namespace.go b/pkg/ctl/namespace/max_topics_per_namespace.go new file mode 100644 index 000000000..f82849aaa --- /dev/null +++ b/pkg/ctl/namespace/max_topics_per_namespace.go @@ -0,0 +1,188 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetMaxTopicsPerNamespaceCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "This command is used for getting the max topics per namespace of a namespace." + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []cmdutils.Example + get := cmdutils.Example{ + Desc: "Get the max topics per namespace of the namespace (namespace-name)", + Command: "pulsarctl namespaces get-max-topics-per-namespace (namespace-name)", + } + examples = append(examples, get) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "The max topics per namespace of the namespace (namespace-name) is (size)", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "get-max-topics-per-namespace", + "Get the max topics per namespace of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + vc.SetRunFuncWithNameArg(func() error { + return doGetMaxTopicsPerNamespace(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doGetMaxTopicsPerNamespace(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + max, err := admin.Namespaces().GetMaxTopicsPerNamespace(*ns) + if err == nil { + if max == -1 { + vc.Command.Printf("The max topics per namespace of the namespace %s is not set\n", ns.String()) + } else { + vc.Command.Printf("The max topics per namespace of the namespace %s is %d\n", ns.String(), max) + } + } + return err +} + +func SetMaxTopicsPerNamespaceCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "This command is used for setting the max topics per namespace of a namespace." + desc.CommandPermission = "This command requires super-user permissions and broker has write policies permission." + + var examples []cmdutils.Example + set := cmdutils.Example{ + Desc: "Set the max topics per namespace of the namespace (namespace-name) to (size)", + Command: "pulsarctl namespaces set-max-topics-per-namespace --max-topics-per-namespace (size) (namespace-name)", + } + examples = append(examples, set) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "Successfully set the max topics per namespace of the namespace (namespace-name) to (size)", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "set-max-topics-per-namespace", + "Set the max topics per namespace of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + var max int + vc.FlagSetGroup.InFlagSet("Max Topics Per Namespace", func(set *pflag.FlagSet) { + set.IntVarP(&max, "max-topics-per-namespace", "t", -1, "max topics per namespace") + _ = cobra.MarkFlagRequired(set, "max-topics-per-namespace") + }) + + vc.SetRunFuncWithNameArg(func() error { + return doSetMaxTopicsPerNamespace(vc, max) + }, "the namespace name is not specified or the namespace name is specified more than one") + + vc.EnableOutputFlagSet() +} + +func doSetMaxTopicsPerNamespace(vc *cmdutils.VerbCmd, max int) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + if max < 0 { + return errors.New("the specified max topics value must bigger than 0") + } + + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().SetMaxTopicsPerNamespace(*ns, max) + if err == nil { + vc.Command.Printf("Successfully set the max topics per namespace of the namespace %s to %d\n", + ns.String(), max) + } + return err +} + +func RemoveMaxTopicsPerNamespaceCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "This command is used for removing the max topics per namespace of a namespace." + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []cmdutils.Example + remove := cmdutils.Example{ + Desc: "Remove the max topics per namespace of the namespace (namespace-name)", + Command: "pulsarctl namespaces remove-max-topics-per-namespace (namespace-name)", + } + examples = append(examples, remove) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "Successfully removed the max topics per namespace of the namespace (namespace-name)", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "remove-max-topics-per-namespace", + "Remove the max topics per namespace of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + vc.SetRunFuncWithNameArg(func() error { + return doRemoveMaxTopicsPerNamespace(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doRemoveMaxTopicsPerNamespace(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().RemoveMaxTopicsPerNamespace(*ns) + if err == nil { + vc.Command.Printf("Successfully removed the max topics per namespace of the namespace %s\n", ns.String()) + } + return err +} diff --git a/pkg/ctl/namespace/max_topics_per_namespace_test.go b/pkg/ctl/namespace/max_topics_per_namespace_test.go new file mode 100644 index 000000000..bc66231ac --- /dev/null +++ b/pkg/ctl/namespace/max_topics_per_namespace_test.go @@ -0,0 +1,69 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMaxTopicsPerNamespaceCmd(t *testing.T) { + ns := "public/test-max-topics-per-namespace" + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"get-max-topics-per-namespace", ns} + initialOut, execErr, _, _ := TestNamespaceCommands(GetMaxTopicsPerNamespaceCmd, args) + assert.Nil(t, execErr) + + args = []string{"set-max-topics-per-namespace", "--max-topics-per-namespace", "11", ns} + setOut, execErr, _, _ := TestNamespaceCommands(SetMaxTopicsPerNamespaceCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, + fmt.Sprintf("Successfully set the max topics per namespace of the namespace %s to %d\n", ns, 11), + setOut.String()) + + args = []string{"get-max-topics-per-namespace", ns} + getOut, execErr, _, _ := TestNamespaceCommands(GetMaxTopicsPerNamespaceCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, + fmt.Sprintf("The max topics per namespace of the namespace %s is %d\n", ns, 11), + getOut.String()) + + args = []string{"remove-max-topics-per-namespace", ns} + removeOut, execErr, _, _ := TestNamespaceCommands(RemoveMaxTopicsPerNamespaceCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, + fmt.Sprintf("Successfully removed the max topics per namespace of the namespace %s\n", ns), + removeOut.String()) + + args = []string{"get-max-topics-per-namespace", ns} + getOut, execErr, _, _ = TestNamespaceCommands(GetMaxTopicsPerNamespaceCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, initialOut.String(), getOut.String()) +} + +func TestSetMaxTopicsPerNamespaceWithInvalidSize(t *testing.T) { + args := []string{"set-max-topics-per-namespace", "--max-topics-per-namespace", "-1", "public/invalid-max-topics"} + _, execErr, _, _ := TestNamespaceCommands(SetMaxTopicsPerNamespaceCmd, args) + assert.NotNil(t, execErr) + assert.Equal(t, "the specified max topics value must bigger than 0", execErr.Error()) +} diff --git a/pkg/ctl/namespace/namespace.go b/pkg/ctl/namespace/namespace.go index 116f260f3..c8a175eaa 100644 --- a/pkg/ctl/namespace/namespace.go +++ b/pkg/ctl/namespace/namespace.go @@ -38,12 +38,15 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddVerbCmd(flagGrouping, resourceCmd, deleteNs) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, setMessageTTL) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getMessageTTL) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, removeMessageTTL) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getRetention) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, setRetention) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, removeRetention) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getBacklogQuota) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, setBacklogQuota) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, removeBacklogQuota) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, setTopicAutoCreation) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getTopicAutoCreation) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, removeTopicAutoCreation) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetSchemaValidationEnforcedCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetSchemaValidationEnforcedCmd) @@ -60,14 +63,18 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetMaxConsumersPerSubscriptionCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetMaxConsumersPerTopicCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetMaxConsumersPerTopicCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RemoveMaxConsumersPerSubscriptionCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RemoveMaxConsumersPerTopicCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetMaxProducersPerTopicCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetMaxProducersPerTopicCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RemoveMaxProducersPerTopicCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getAntiAffinityGroup) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, setAntiAffinityGroup) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, deleteAntiAffinityGroup) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getAntiAffinityNamespaces) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getPersistence) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, setPersistence) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, removePersistence) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, setDeduplication) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, setReplicationClusters) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getReplicationClusters) @@ -78,6 +85,7 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RevokePermissionsCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GrantSubPermissionsCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RevokeSubPermissionsCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetSubPermissionsCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, ClearBacklogCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetDispatchRateCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetDispatchRateCmd) @@ -97,5 +105,22 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetInactiveTopicCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetInactiveTopicCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RemoveInactiveTopicCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetMaxTopicsPerNamespaceCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetMaxTopicsPerNamespaceCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RemoveMaxTopicsPerNamespaceCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetSubscriptionExpirationTimeCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetSubscriptionExpirationTimeCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RemoveSubscriptionExpirationTimeCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetBookieAffinityGroupCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetBookieAffinityGroupCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, DeleteBookieAffinityGroupCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetSchemaCompatibilityStrategyCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetSchemaCompatibilityStrategyCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetPropertiesCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetPropertiesCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, ClearPropertiesCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, GetPropertyCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, SetPropertyCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, RemovePropertyCmd) return resourceCmd } diff --git a/pkg/ctl/namespace/properties.go b/pkg/ctl/namespace/properties.go new file mode 100644 index 000000000..48674c222 --- /dev/null +++ b/pkg/ctl/namespace/properties.go @@ -0,0 +1,339 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "fmt" + "io" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func getNamespaceProperties(namespace string) (utils.NameSpaceName, map[string]string, error) { + ns, err := utils.GetNamespaceName(namespace) + if err != nil { + return utils.NameSpaceName{}, nil, err + } + admin := cmdutils.NewPulsarClient() + properties, err := admin.Namespaces().GetProperties(*ns) + if err != nil { + return utils.NameSpaceName{}, nil, err + } + if properties == nil { + properties = map[string]string{} + } + return *ns, properties, nil +} + +func updateNamespaceProperties(ns utils.NameSpaceName, properties map[string]string) error { + admin := cmdutils.NewPulsarClient() + return admin.Namespaces().UpdateProperties(ns, properties) +} + +func removeNamespaceProperties(ns utils.NameSpaceName) error { + admin := cmdutils.NewPulsarClient() + return admin.Namespaces().RemoveProperties(ns) +} + +func GetPropertiesCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "Get properties of a namespace" + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []cmdutils.Example + examples = append(examples, cmdutils.Example{ + Desc: "Get properties of a namespace", + Command: "pulsarctl namespaces get-properties tenant/namespace", + }) + desc.CommandExamples = examples + desc.CommandOutput = append(desc.CommandOutput, ArgError, NsNotExistError) + desc.CommandOutput = append(desc.CommandOutput, NsErrors...) + + vc.SetDescription( + "get-properties", + "Get properties of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + return doGetProperties(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doGetProperties(vc *cmdutils.VerbCmd) error { + _, properties, err := getNamespaceProperties(vc.NameArg) + if err != nil { + return err + } + oc := cmdutils.NewOutputContent().WithObject(properties) + return vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), oc) +} + +func SetPropertiesCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "Set properties of a namespace" + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []cmdutils.Example + examples = append(examples, cmdutils.Example{ + Desc: "Set properties of a namespace", + Command: "pulsarctl namespaces set-properties tenant/namespace -p k1=v1,k2=v2", + }) + desc.CommandExamples = examples + desc.CommandOutput = append(desc.CommandOutput, ArgError, NsNotExistError) + desc.CommandOutput = append(desc.CommandOutput, NsErrors...) + + vc.SetDescription( + "set-properties", + "Set properties of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + properties := map[string]string{} + vc.FlagSetGroup.InFlagSet("Properties", func(set *pflag.FlagSet) { + set.StringToStringVarP(&properties, "properties", "p", nil, "comma separated key=value pairs") + _ = cobra.MarkFlagRequired(set, "properties") + }) + + vc.SetRunFuncWithNameArg(func() error { + return doSetProperties(vc, properties) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doSetProperties(vc *cmdutils.VerbCmd, properties map[string]string) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + err = updateNamespaceProperties(*ns, properties) + if err == nil { + vc.Command.Printf("Updated properties successfully for [%s]\n", ns.String()) + } + return err +} + +func ClearPropertiesCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "Clear properties of a namespace" + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []cmdutils.Example + examples = append(examples, cmdutils.Example{ + Desc: "Clear properties of a namespace", + Command: "pulsarctl namespaces clear-properties tenant/namespace", + }) + desc.CommandExamples = examples + desc.CommandOutput = append(desc.CommandOutput, ArgError, NsNotExistError) + desc.CommandOutput = append(desc.CommandOutput, NsErrors...) + + vc.SetDescription( + "clear-properties", + "Clear properties of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + vc.SetRunFuncWithNameArg(func() error { + return doClearProperties(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doClearProperties(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + err = removeNamespaceProperties(*ns) + if err == nil { + vc.Command.Printf("Cleared properties successfully for [%s]\n", ns.String()) + } + return err +} + +func GetPropertyCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "Get a single property of a namespace" + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []cmdutils.Example + examples = append(examples, cmdutils.Example{ + Desc: "Get a single property of a namespace", + Command: "pulsarctl namespaces get-property tenant/namespace -k key", + }) + desc.CommandExamples = examples + desc.CommandOutput = append(desc.CommandOutput, ArgError, NsNotExistError) + desc.CommandOutput = append(desc.CommandOutput, NsErrors...) + + vc.SetDescription( + "get-property", + "Get a single property of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + var key string + vc.FlagSetGroup.InFlagSet("Properties", func(set *pflag.FlagSet) { + set.StringVarP(&key, "key", "k", "", "property key") + _ = cobra.MarkFlagRequired(set, "key") + }) + vc.EnableOutputFlagSet() + + vc.SetRunFuncWithNameArg(func() error { + return doGetProperty(vc, key) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doGetProperty(vc *cmdutils.VerbCmd, key string) error { + _, properties, err := getNamespaceProperties(vc.NameArg) + if err != nil { + return err + } + value, ok := properties[key] + if !ok { + return writeNullablePropertyValue(vc, key, nil) + } + return writeNullablePropertyValue(vc, key, &value) +} + +func SetPropertyCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "Set a single property of a namespace" + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []cmdutils.Example + examples = append(examples, cmdutils.Example{ + Desc: "Set a single property of a namespace", + Command: "pulsarctl namespaces set-property tenant/namespace -k key --value value", + }) + desc.CommandExamples = examples + desc.CommandOutput = append(desc.CommandOutput, ArgError, NsNotExistError) + desc.CommandOutput = append(desc.CommandOutput, NsErrors...) + + vc.SetDescription( + "set-property", + "Set a single property of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + var key string + var value string + vc.FlagSetGroup.InFlagSet("Properties", func(set *pflag.FlagSet) { + set.StringVarP(&key, "key", "k", "", "property key") + set.StringVar(&value, "value", "", "property value") + _ = cobra.MarkFlagRequired(set, "key") + _ = cobra.MarkFlagRequired(set, "value") + }) + + vc.SetRunFuncWithNameArg(func() error { + return doSetProperty(vc, key, value) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doSetProperty(vc *cmdutils.VerbCmd, key, value string) error { + ns, properties, err := getNamespaceProperties(vc.NameArg) + if err != nil { + return err + } + properties[key] = value + err = updateNamespaceProperties(ns, properties) + if err == nil { + vc.Command.Printf("Set property %q successfully for [%s]\n", key, ns.String()) + } + return err +} + +func RemovePropertyCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "Remove a single property of a namespace" + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []cmdutils.Example + examples = append(examples, cmdutils.Example{ + Desc: "Remove a single property of a namespace", + Command: "pulsarctl namespaces remove-property tenant/namespace -k key", + }) + desc.CommandExamples = examples + desc.CommandOutput = append(desc.CommandOutput, ArgError, NsNotExistError) + desc.CommandOutput = append(desc.CommandOutput, NsErrors...) + + vc.SetDescription( + "remove-property", + "Remove a single property of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + var key string + vc.FlagSetGroup.InFlagSet("Properties", func(set *pflag.FlagSet) { + set.StringVarP(&key, "key", "k", "", "property key") + _ = cobra.MarkFlagRequired(set, "key") + }) + vc.EnableOutputFlagSet() + + vc.SetRunFuncWithNameArg(func() error { + return doRemoveProperty(vc, key) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doRemoveProperty(vc *cmdutils.VerbCmd, key string) error { + ns, properties, err := getNamespaceProperties(vc.NameArg) + if err != nil { + return err + } + value, ok := properties[key] + if !ok { + return writeNullablePropertyValue(vc, key, nil) + } + delete(properties, key) + + if len(properties) == 0 { + err = removeNamespaceProperties(ns) + } else { + err = updateNamespaceProperties(ns, properties) + } + if err == nil { + return writeNullablePropertyValue(vc, key, &value) + } + return err +} + +func writeNullablePropertyValue(vc *cmdutils.VerbCmd, key string, value *string) error { + var obj interface{} + if value != nil { + obj = map[string]string{key: *value} + } + + oc := cmdutils.NewOutputContent(). + WithObject(obj). + WithTextFunc(func(w io.Writer) error { + if value == nil { + _, err := io.WriteString(w, "null\n") + return err + } + _, err := fmt.Fprintln(w, *value) + return err + }) + + return vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), oc) +} diff --git a/pkg/ctl/namespace/properties_test.go b/pkg/ctl/namespace/properties_test.go new file mode 100644 index 000000000..e43d42228 --- /dev/null +++ b/pkg/ctl/namespace/properties_test.go @@ -0,0 +1,105 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNamespacePropertiesCmd(t *testing.T) { + ns := "public/test-namespace-properties" + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"clear-properties", ns} + _, execErr, _, _ = TestNamespaceCommands(ClearPropertiesCmd, args) + assert.Nil(t, execErr) + + args = []string{"set-properties", ns, "-p", "k1=v1,k2=v2"} + setPropertiesOut, execErr, _, _ := TestNamespaceCommands(SetPropertiesCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, fmt.Sprintf("Updated properties successfully for [%s]\n", ns), setPropertiesOut.String()) + + args = []string{"get-properties", ns} + getPropertiesOut, execErr, _, _ := TestNamespaceCommands(GetPropertiesCmd, args) + assert.Nil(t, execErr) + properties := map[string]string{} + err := json.Unmarshal(getPropertiesOut.Bytes(), &properties) + assert.Nil(t, err) + assert.Equal(t, map[string]string{"k1": "v1", "k2": "v2"}, properties) + + args = []string{"get-property", ns, "-k", "k1"} + getPropertyOut, execErr, _, _ := TestNamespaceCommands(GetPropertyCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, "v1\n", getPropertyOut.String()) + + args = []string{"set-property", ns, "-k", "k3", "--value", "v3"} + setPropertyOut, execErr, _, _ := TestNamespaceCommands(SetPropertyCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, fmt.Sprintf("Set property %q successfully for [%s]\n", "k3", ns), setPropertyOut.String()) + + args = []string{"get-properties", ns} + getPropertiesOut, execErr, _, _ = TestNamespaceCommands(GetPropertiesCmd, args) + assert.Nil(t, execErr) + properties = map[string]string{} + err = json.Unmarshal(getPropertiesOut.Bytes(), &properties) + assert.Nil(t, err) + assert.Equal(t, map[string]string{"k1": "v1", "k2": "v2", "k3": "v3"}, properties) + + args = []string{"remove-property", ns, "-k", "k2"} + removePropertyOut, execErr, _, _ := TestNamespaceCommands(RemovePropertyCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, "v2\n", removePropertyOut.String()) + + args = []string{"remove-property", ns, "-k", "k1"} + _, execErr, _, _ = TestNamespaceCommands(RemovePropertyCmd, args) + assert.Nil(t, execErr) + + args = []string{"remove-property", ns, "-k", "k3"} + _, execErr, _, _ = TestNamespaceCommands(RemovePropertyCmd, args) + assert.Nil(t, execErr) + + args = []string{"get-properties", ns} + getPropertiesOut, execErr, _, _ = TestNamespaceCommands(GetPropertiesCmd, args) + assert.Nil(t, execErr) + properties = map[string]string{} + err = json.Unmarshal(getPropertiesOut.Bytes(), &properties) + assert.Nil(t, err) + assert.Equal(t, map[string]string{}, properties) +} + +func TestGetMissingPropertyCmd(t *testing.T) { + ns := "public/test-missing-namespace-property" + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"clear-properties", ns} + _, execErr, _, _ = TestNamespaceCommands(ClearPropertiesCmd, args) + assert.Nil(t, execErr) + + args = []string{"get-property", ns, "-k", "missing"} + out, execErr, _, _ := TestNamespaceCommands(GetPropertyCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, "null\n", out.String()) +} diff --git a/pkg/ctl/namespace/remove_settings.go b/pkg/ctl/namespace/remove_settings.go new file mode 100644 index 000000000..289bc4861 --- /dev/null +++ b/pkg/ctl/namespace/remove_settings.go @@ -0,0 +1,136 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func newNamespaceRemoveCmd(use, short string, run func(*cmdutils.VerbCmd) error) func(*cmdutils.VerbCmd) { + return func(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = short + desc.CommandPermission = "This command requires tenant admin permissions." + desc.CommandOutput = append(desc.CommandOutput, ArgError) + desc.CommandOutput = append(desc.CommandOutput, NsErrors...) + desc.CommandOutput = append(desc.CommandOutput, NsNotExistError) + + vc.SetDescription(use, short, desc.ToString(), desc.ExampleToString()) + vc.SetRunFuncWithNameArg(func() error { + return run(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") + } +} + +var removeMessageTTL = newNamespaceRemoveCmd( + "remove-message-ttl", + "Remove message TTL for a namespace", + func(vc *cmdutils.VerbCmd) error { + admin := cmdutils.NewPulsarClient() + err := admin.Namespaces().RemoveNamespaceMessageTTL(vc.NameArg) + if err == nil { + vc.Command.Printf("Removed message TTL successfully for [%s]\n", vc.NameArg) + } + return err + }, +) + +var removeRetention = newNamespaceRemoveCmd( + "remove-retention", + "Remove retention for a namespace", + func(vc *cmdutils.VerbCmd) error { + admin := cmdutils.NewPulsarClient() + err := admin.Namespaces().RemoveRetention(vc.NameArg) + if err == nil { + vc.Command.Printf("Removed retention successfully for [%s]\n", vc.NameArg) + } + return err + }, +) + +var removePersistence = newNamespaceRemoveCmd( + "remove-persistence", + "Remove persistence for a namespace", + func(vc *cmdutils.VerbCmd) error { + admin := cmdutils.NewPulsarClient() + err := admin.Namespaces().RemovePersistence(vc.NameArg) + if err == nil { + vc.Command.Printf("Removed persistence successfully for [%s]\n", vc.NameArg) + } + return err + }, +) + +func removeMaxConsumersPerSubscription(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().RemoveMaxConsumersPerSubscription(*ns) + if err == nil { + vc.Command.Printf("Removed max consumers per subscription successfully for [%s]\n", ns.String()) + } + return err +} + +var RemoveMaxConsumersPerSubscriptionCmd = newNamespaceRemoveCmd( + "remove-max-consumers-per-subscription", + "Remove the max consumers per subscription of a namespace", + removeMaxConsumersPerSubscription, +) + +func removeMaxConsumersPerTopic(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().RemoveMaxConsumersPerTopic(*ns) + if err == nil { + vc.Command.Printf("Removed max consumers per topic successfully for [%s]\n", ns.String()) + } + return err +} + +var RemoveMaxConsumersPerTopicCmd = newNamespaceRemoveCmd( + "remove-max-consumers-per-topic", + "Remove the max consumers per topic of a namespace", + removeMaxConsumersPerTopic, +) + +func removeMaxProducersPerTopic(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().RemoveMaxProducersPerTopic(*ns) + if err == nil { + vc.Command.Printf("Removed max producers per topic successfully for [%s]\n", ns.String()) + } + return err +} + +var RemoveMaxProducersPerTopicCmd = newNamespaceRemoveCmd( + "remove-max-producers-per-topic", + "Remove the max producers per topic of a namespace", + removeMaxProducersPerTopic, +) diff --git a/pkg/ctl/namespace/remove_settings_test.go b/pkg/ctl/namespace/remove_settings_test.go new file mode 100644 index 000000000..eeaada43e --- /dev/null +++ b/pkg/ctl/namespace/remove_settings_test.go @@ -0,0 +1,169 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRemoveMessageTTLCmd(t *testing.T) { + ns := "public/test-remove-message-ttl" + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"get-message-ttl", ns} + initialOut, execErr, _, _ := TestNamespaceCommands(getMessageTTL, args) + assert.Nil(t, execErr) + + args = []string{"set-message-ttl", ns, "-t", "123"} + _, execErr, _, _ = TestNamespaceCommands(setMessageTTL, args) + assert.Nil(t, execErr) + + args = []string{"remove-message-ttl", ns} + removeOut, execErr, _, _ := TestNamespaceCommands(removeMessageTTL, args) + assert.Nil(t, execErr) + assert.Equal(t, fmt.Sprintf("Removed message TTL successfully for [%s]\n", ns), removeOut.String()) + + args = []string{"get-message-ttl", ns} + currentOut, execErr, _, _ := TestNamespaceCommands(getMessageTTL, args) + assert.Nil(t, execErr) + assert.Equal(t, initialOut.String(), currentOut.String()) +} + +func TestRemoveRetentionCmd(t *testing.T) { + ns := "public/test-remove-retention" + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"get-retention", ns} + initialOut, execErr, _, _ := TestNamespaceCommands(getRetention, args) + assert.Nil(t, execErr) + + args = []string{"set-retention", ns, "--time", "10m", "--size", "10M"} + _, execErr, _, _ = TestNamespaceCommands(setRetention, args) + assert.Nil(t, execErr) + + args = []string{"remove-retention", ns} + removeOut, execErr, _, _ := TestNamespaceCommands(removeRetention, args) + assert.Nil(t, execErr) + assert.Equal(t, fmt.Sprintf("Removed retention successfully for [%s]\n", ns), removeOut.String()) + + args = []string{"get-retention", ns} + currentOut, execErr, _, _ := TestNamespaceCommands(getRetention, args) + assert.Nil(t, execErr) + assert.Equal(t, initialOut.String(), currentOut.String()) +} + +func TestRemovePersistenceCmd(t *testing.T) { + ns := "public/test-remove-persistence" + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"get-persistence", ns} + initialOut, execErr, _, _ := TestNamespaceCommands(getPersistence, args) + assert.Nil(t, execErr) + + args = []string{"set-persistence", ns, + "--ensemble-size", "2", + "--write-quorum-size", "2", + "--ack-quorum-size", "2", + "--ml-mark-delete-max-rate", "1.5", + } + _, execErr, _, _ = TestNamespaceCommands(setPersistence, args) + assert.Nil(t, execErr) + + args = []string{"remove-persistence", ns} + removeOut, execErr, _, _ := TestNamespaceCommands(removePersistence, args) + assert.Nil(t, execErr) + assert.Equal(t, fmt.Sprintf("Removed persistence successfully for [%s]\n", ns), removeOut.String()) + + args = []string{"get-persistence", ns} + currentOut, execErr, _, _ := TestNamespaceCommands(getPersistence, args) + assert.Nil(t, execErr) + assert.Equal(t, initialOut.String(), currentOut.String()) +} + +func TestRemoveMaxConsumersAndProducersCmds(t *testing.T) { + ns := "public/test-remove-max-consumers-and-producers" + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"get-max-consumers-per-topic", ns} + initialConsumersPerTopicOut, execErr, _, _ := TestNamespaceCommands(GetMaxConsumersPerTopicCmd, args) + assert.Nil(t, execErr) + + args = []string{"set-max-consumers-per-topic", "--size", "10", ns} + _, execErr, _, _ = TestNamespaceCommands(SetMaxConsumersPerTopicCmd, args) + assert.Nil(t, execErr) + + args = []string{"remove-max-consumers-per-topic", ns} + removeConsumersPerTopicOut, execErr, _, _ := TestNamespaceCommands(RemoveMaxConsumersPerTopicCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, fmt.Sprintf("Removed max consumers per topic successfully for [%s]\n", ns), + removeConsumersPerTopicOut.String()) + + args = []string{"get-max-consumers-per-topic", ns} + currentConsumersPerTopicOut, execErr, _, _ := TestNamespaceCommands(GetMaxConsumersPerTopicCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, initialConsumersPerTopicOut.String(), currentConsumersPerTopicOut.String()) + + args = []string{"get-max-consumers-per-subscription", ns} + initialConsumersPerSubOut, execErr, _, _ := TestNamespaceCommands(GetMaxConsumersPerSubscriptionCmd, args) + assert.Nil(t, execErr) + + args = []string{"set-max-consumers-per-subscription", "--size", "9", ns} + _, execErr, _, _ = TestNamespaceCommands(SetMaxConsumersPerSubscriptionCmd, args) + assert.Nil(t, execErr) + + args = []string{"remove-max-consumers-per-subscription", ns} + removeConsumersPerSubOut, execErr, _, _ := TestNamespaceCommands(RemoveMaxConsumersPerSubscriptionCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, fmt.Sprintf("Removed max consumers per subscription successfully for [%s]\n", ns), + removeConsumersPerSubOut.String()) + + args = []string{"get-max-consumers-per-subscription", ns} + currentConsumersPerSubOut, execErr, _, _ := TestNamespaceCommands(GetMaxConsumersPerSubscriptionCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, initialConsumersPerSubOut.String(), currentConsumersPerSubOut.String()) + + args = []string{"get-max-producers-per-topic", ns} + initialProducersPerTopicOut, execErr, _, _ := TestNamespaceCommands(GetMaxProducersPerTopicCmd, args) + assert.Nil(t, execErr) + + args = []string{"set-max-producers-per-topic", "--size", "8", ns} + _, execErr, _, _ = TestNamespaceCommands(SetMaxProducersPerTopicCmd, args) + assert.Nil(t, execErr) + + args = []string{"remove-max-producers-per-topic", ns} + removeProducersPerTopicOut, execErr, _, _ := TestNamespaceCommands(RemoveMaxProducersPerTopicCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, fmt.Sprintf("Removed max producers per topic successfully for [%s]\n", ns), + removeProducersPerTopicOut.String()) + + args = []string{"get-max-producers-per-topic", ns} + currentProducersPerTopicOut, execErr, _, _ := TestNamespaceCommands(GetMaxProducersPerTopicCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, initialProducersPerTopicOut.String(), currentProducersPerTopicOut.String()) +} diff --git a/pkg/ctl/namespace/schema_compatibility_strategy.go b/pkg/ctl/namespace/schema_compatibility_strategy.go new file mode 100644 index 000000000..050de0fd9 --- /dev/null +++ b/pkg/ctl/namespace/schema_compatibility_strategy.go @@ -0,0 +1,139 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "strings" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetSchemaCompatibilityStrategyCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "This command is used for getting the schema compatibility strategy of a namespace." + desc.CommandPermission = "This command requires super-user permissions and broker has write policies permission." + + var examples []cmdutils.Example + get := cmdutils.Example{ + Desc: "Get the schema compatibility strategy of the namespace (namespace-name)", + Command: "pulsarctl namespaces get-schema-compatibility-strategy (namespace-name)", + } + examples = append(examples, get) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "The schema compatibility strategy of the namespace (namespace-name) is (strategy)", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "get-schema-compatibility-strategy", + "Get the schema compatibility strategy of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + vc.SetRunFuncWithNameArg(func() error { + return doGetSchemaCompatibilityStrategy(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doGetSchemaCompatibilityStrategy(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + s, err := admin.Namespaces().GetSchemaCompatibilityStrategy(*ns) + if err == nil { + vc.Command.Printf("The schema compatibility strategy of the namespace %s is %s\n", ns.String(), s.String()) + } + return err +} + +func SetSchemaCompatibilityStrategyCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "This command is used for setting the schema compatibility strategy of a namespace." + desc.CommandPermission = "This command requires super-user permissions and broker has write policies permission." + + var examples []cmdutils.Example + set := cmdutils.Example{ + Desc: "Set the schema compatibility strategy to (strategy)", + Command: "pulsarctl namespaces set-schema-compatibility-strategy --compatibility (strategy) (namespace-name)", + } + examples = append(examples, set) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "Successfully set the schema compatibility strategy of the namespace (namespace-name) to (strategy)", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "set-schema-compatibility-strategy", + "Set the schema compatibility strategy of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + var strategy string + vc.FlagSetGroup.InFlagSet("Schema Compatibility Strategy", func(set *pflag.FlagSet) { + set.StringVarP(&strategy, "compatibility", "c", "", + "Compatibility level required for new schemas created via a Producer. Possible values "+ + "(UNDEFINED, ALWAYS_INCOMPATIBLE, ALWAYS_COMPATIBLE, BACKWARD, FORWARD, FULL, "+ + "BACKWARD_TRANSITIVE, FORWARD_TRANSITIVE, FULL_TRANSITIVE)") + _ = cobra.MarkFlagRequired(set, "compatibility") + }) + + vc.SetRunFuncWithNameArg(func() error { + return doSetSchemaCompatibilityStrategy(vc, strategy) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doSetSchemaCompatibilityStrategy(vc *cmdutils.VerbCmd, strategy string) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + s, err := utils.ParseSchemaCompatibilityStrategy(strings.ToUpper(strategy)) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().SetSchemaCompatibilityStrategy(*ns, s) + if err == nil { + vc.Command.Printf("Successfully set the schema compatibility strategy of the namespace %s to %s\n", + ns.String(), s.String()) + } + return err +} diff --git a/pkg/ctl/namespace/schema_compatibility_strategy_test.go b/pkg/ctl/namespace/schema_compatibility_strategy_test.go new file mode 100644 index 000000000..a564c53c3 --- /dev/null +++ b/pkg/ctl/namespace/schema_compatibility_strategy_test.go @@ -0,0 +1,75 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "bytes" + "fmt" + "testing" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestSchemaCompatibilityStrategyCmd(t *testing.T) { + ns := "public/test-schema-compatibility-strategy" + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"get-schema-compatibility-strategy", ns} + initialOut, execErr, _, _ := TestNamespaceCommands(GetSchemaCompatibilityStrategyCmd, args) + assert.Nil(t, execErr) + assert.Contains(t, initialOut.String(), ns) + + var setOut, getOut *bytes.Buffer + args = []string{"set-schema-compatibility-strategy", "--compatibility", "FULL_TRANSITIVE", ns} + setOut, execErr, _, _ = TestNamespaceCommands(SetSchemaCompatibilityStrategyCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, + fmt.Sprintf("Successfully set the schema compatibility strategy of the namespace %s to %s\n", + ns, utils.SchemaCompatibilityStrategyFullTransitive.String()), + setOut.String()) + + args = []string{"get-schema-compatibility-strategy", ns} + getOut, execErr, _, _ = TestNamespaceCommands(GetSchemaCompatibilityStrategyCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, + fmt.Sprintf("The schema compatibility strategy of the namespace %s is %s\n", + ns, utils.SchemaCompatibilityStrategyFullTransitive.String()), + getOut.String()) + + args = []string{"set-schema-compatibility-strategy", ns} + _, _, _, err := TestNamespaceCommands(SetSchemaCompatibilityStrategyCmd, args) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "required flag(s) \"compatibility\" not set") + + args = []string{"set-schema-compatibility-strategy", "--compatibility", "INVALID", ns} + _, execErr, _, _ = TestNamespaceCommands(SetSchemaCompatibilityStrategyCmd, args) + assert.NotNil(t, execErr) + assert.Equal(t, "Invalid schema compatibility strategy INVALID", execErr.Error()) + + args = []string{"get-schema-compatibility-strategy", ns} + getOut, execErr, _, _ = TestNamespaceCommands(GetSchemaCompatibilityStrategyCmd, args) + assert.Nil(t, execErr) + assert.NotEqual(t, initialOut.String(), "") + assert.Equal(t, + fmt.Sprintf("The schema compatibility strategy of the namespace %s is %s\n", + ns, utils.SchemaCompatibilityStrategyFullTransitive.String()), + getOut.String()) +} diff --git a/pkg/ctl/namespace/set_replication_clusters.go b/pkg/ctl/namespace/set_replication_clusters.go index 608550f0e..1230ab949 100644 --- a/pkg/ctl/namespace/set_replication_clusters.go +++ b/pkg/ctl/namespace/set_replication_clusters.go @@ -20,7 +20,6 @@ package namespace import ( "strings" - "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -79,15 +78,15 @@ func setReplicationClusters(vc *cmdutils.VerbCmd) { "set-clusters", ) - var data utils.NamespacesData + var clusterIDs string vc.SetRunFuncWithNameArg(func() error { - return doSetReplicationClusters(vc, data) + return doSetReplicationClusters(vc, clusterIDs) }, "the cluster name is not specified or the cluster name is specified more than one") vc.FlagSetGroup.InFlagSet("Namespaces", func(flagSet *pflag.FlagSet) { flagSet.StringVarP( - &data.ClusterIDs, + &clusterIDs, "clusters", "c", "", @@ -98,11 +97,11 @@ func setReplicationClusters(vc *cmdutils.VerbCmd) { vc.EnableOutputFlagSet() } -func doSetReplicationClusters(vc *cmdutils.VerbCmd, data utils.NamespacesData) error { +func doSetReplicationClusters(vc *cmdutils.VerbCmd, clusterIDs string) error { ns := vc.NameArg admin := cmdutils.NewPulsarClient() - clusters := strings.Split(data.ClusterIDs, ",") + clusters := strings.Split(clusterIDs, ",") err := admin.Namespaces().SetNamespaceReplicationClusters(ns, clusters) if err == nil { vc.Command.Printf("Set replication clusters successfully for %s\n", ns) diff --git a/pkg/ctl/namespace/subscription_expiration_time.go b/pkg/ctl/namespace/subscription_expiration_time.go new file mode 100644 index 000000000..2d7e091bf --- /dev/null +++ b/pkg/ctl/namespace/subscription_expiration_time.go @@ -0,0 +1,188 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetSubscriptionExpirationTimeCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "This command is used for getting the subscription expiration time of a namespace." + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []cmdutils.Example + get := cmdutils.Example{ + Desc: "Get the subscription expiration time of the namespace (namespace-name)", + Command: "pulsarctl namespaces get-subscription-expiration-time (namespace-name)", + } + examples = append(examples, get) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "The subscription expiration time of the namespace (namespace-name) is (time)", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "get-subscription-expiration-time", + "Get the subscription expiration time of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + vc.SetRunFuncWithNameArg(func() error { + return doGetSubscriptionExpirationTime(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doGetSubscriptionExpirationTime(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + expirationTime, err := admin.Namespaces().GetSubscriptionExpirationTime(*ns) + if err == nil { + if expirationTime == -1 { + vc.Command.Printf("The subscription expiration time of the namespace %s is not set\n", ns.String()) + } else { + vc.Command.Printf("The subscription expiration time of the namespace %s is %d\n", ns.String(), expirationTime) + } + } + return err +} + +func SetSubscriptionExpirationTimeCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "This command is used for setting the subscription expiration time of a namespace." + desc.CommandPermission = "This command requires super-user permissions and broker has write policies permission." + + var examples []cmdutils.Example + set := cmdutils.Example{ + Desc: "Set the subscription expiration time of the namespace (namespace-name) to (time)", + Command: "pulsarctl namespaces set-subscription-expiration-time --time (time) (namespace-name)", + } + examples = append(examples, set) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "Successfully set the subscription expiration time of the namespace (namespace-name) to (time)", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "set-subscription-expiration-time", + "Set the subscription expiration time of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + var expirationTime int + vc.FlagSetGroup.InFlagSet("Subscription Expiration Time", func(set *pflag.FlagSet) { + set.IntVarP(&expirationTime, "time", "t", -1, "subscription expiration time in minutes") + _ = cobra.MarkFlagRequired(set, "time") + }) + + vc.SetRunFuncWithNameArg(func() error { + return doSetSubscriptionExpirationTime(vc, expirationTime) + }, "the namespace name is not specified or the namespace name is specified more than one") + + vc.EnableOutputFlagSet() +} + +func doSetSubscriptionExpirationTime(vc *cmdutils.VerbCmd, expirationTime int) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + if expirationTime < 0 { + return errors.New("the specified subscription expiration time must bigger than or equal to 0") + } + + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().SetSubscriptionExpirationTime(*ns, expirationTime) + if err == nil { + vc.Command.Printf("Successfully set the subscription expiration time of the namespace %s to %d\n", + ns.String(), expirationTime) + } + return err +} + +func RemoveSubscriptionExpirationTimeCmd(vc *cmdutils.VerbCmd) { + var desc cmdutils.LongDescription + desc.CommandUsedFor = "This command is used for removing the subscription expiration time of a namespace." + desc.CommandPermission = "This command requires tenant admin permissions." + + var examples []cmdutils.Example + remove := cmdutils.Example{ + Desc: "Remove the subscription expiration time of the namespace (namespace-name)", + Command: "pulsarctl namespaces remove-subscription-expiration-time (namespace-name)", + } + examples = append(examples, remove) + desc.CommandExamples = examples + + var out []cmdutils.Output + successOut := cmdutils.Output{ + Desc: "normal output", + Out: "Successfully removed the subscription expiration time of the namespace (namespace-name)", + } + out = append(out, successOut, ArgError, NsNotExistError) + out = append(out, NsErrors...) + desc.CommandOutput = out + + vc.SetDescription( + "remove-subscription-expiration-time", + "Remove the subscription expiration time of a namespace", + desc.ToString(), + desc.ExampleToString(), + ) + + vc.SetRunFuncWithNameArg(func() error { + return doRemoveSubscriptionExpirationTime(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") +} + +func doRemoveSubscriptionExpirationTime(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Namespaces().RemoveSubscriptionExpirationTime(*ns) + if err == nil { + vc.Command.Printf("Successfully removed the subscription expiration time of the namespace %s\n", ns.String()) + } + return err +} diff --git a/pkg/ctl/namespace/subscription_expiration_time_test.go b/pkg/ctl/namespace/subscription_expiration_time_test.go new file mode 100644 index 000000000..97bf59e43 --- /dev/null +++ b/pkg/ctl/namespace/subscription_expiration_time_test.go @@ -0,0 +1,69 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubscriptionExpirationTimeCmd(t *testing.T) { + ns := "public/test-subscription-expiration-time" + args := []string{"create", ns} + _, execErr, _, _ := TestNamespaceCommands(createNs, args) + assert.Nil(t, execErr) + + args = []string{"get-subscription-expiration-time", ns} + initialOut, execErr, _, _ := TestNamespaceCommands(GetSubscriptionExpirationTimeCmd, args) + assert.Nil(t, execErr) + + args = []string{"set-subscription-expiration-time", "--time", "60", ns} + setOut, execErr, _, _ := TestNamespaceCommands(SetSubscriptionExpirationTimeCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, + fmt.Sprintf("Successfully set the subscription expiration time of the namespace %s to %d\n", ns, 60), + setOut.String()) + + args = []string{"get-subscription-expiration-time", ns} + getOut, execErr, _, _ := TestNamespaceCommands(GetSubscriptionExpirationTimeCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, + fmt.Sprintf("The subscription expiration time of the namespace %s is %d\n", ns, 60), + getOut.String()) + + args = []string{"remove-subscription-expiration-time", ns} + removeOut, execErr, _, _ := TestNamespaceCommands(RemoveSubscriptionExpirationTimeCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, + fmt.Sprintf("Successfully removed the subscription expiration time of the namespace %s\n", ns), + removeOut.String()) + + args = []string{"get-subscription-expiration-time", ns} + getOut, execErr, _, _ = TestNamespaceCommands(GetSubscriptionExpirationTimeCmd, args) + assert.Nil(t, execErr) + assert.Equal(t, initialOut.String(), getOut.String()) +} + +func TestSetSubscriptionExpirationTimeWithInvalidValue(t *testing.T) { + args := []string{"set-subscription-expiration-time", "--time", "-1", "public/test-invalid-expiration-time"} + _, execErr, _, _ := TestNamespaceCommands(SetSubscriptionExpirationTimeCmd, args) + assert.NotNil(t, execErr) + assert.Equal(t, "the specified subscription expiration time must bigger than or equal to 0", execErr.Error()) +} diff --git a/pkg/ctl/namespace/subscription_permission.go b/pkg/ctl/namespace/subscription_permission.go new file mode 100644 index 000000000..72e1a1bed --- /dev/null +++ b/pkg/ctl/namespace/subscription_permission.go @@ -0,0 +1,63 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetSubPermissionsCmd(vc *cmdutils.VerbCmd) { + desc := cmdutils.LongDescription{} + desc.CommandUsedFor = "Get permissions to access subscription admin API" + desc.CommandPermission = "This command requires tenant admin permissions." + + desc.CommandExamples = []cmdutils.Example{ + { + Desc: "Get permissions to access subscription admin API", + Command: "pulsarctl namespaces subscription-permission tenant/namespace", + }, + } + + vc.SetDescription( + "subscription-permission", + "Get permissions to access subscription admin API", + desc.ToString(), + desc.ExampleToString(), + "subscription-permission", + ) + + vc.SetRunFuncWithNameArg(func() error { + return doGetSubPermissions(vc) + }, "the namespace name is not specified or the namespace name is specified more than one") + vc.EnableOutputFlagSet() +} + +func doGetSubPermissions(vc *cmdutils.VerbCmd) error { + ns, err := utils.GetNamespaceName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + permissions, err := admin.Namespaces().GetSubPermissions(*ns) + if err == nil { + err = vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), cmdutils.NewOutputContent().WithObject(permissions)) + } + return err +} diff --git a/pkg/ctl/namespace/subscription_permission_test.go b/pkg/ctl/namespace/subscription_permission_test.go new file mode 100644 index 000000000..d4b303cd5 --- /dev/null +++ b/pkg/ctl/namespace/subscription_permission_test.go @@ -0,0 +1,30 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 namespace + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubscriptionPermissionNameError(t *testing.T) { + _, _, nameErr, _ := TestNamespaceCommands(GetSubPermissionsCmd, []string{"subscription-permission"}) + assert.NotNil(t, nameErr) + assert.Equal(t, "the namespace name is not specified or the namespace name is specified more than one", nameErr.Error()) +} diff --git a/pkg/ctl/schemas/compatibility.go b/pkg/ctl/schemas/compatibility.go new file mode 100644 index 000000000..0be2852bb --- /dev/null +++ b/pkg/ctl/schemas/compatibility.go @@ -0,0 +1,83 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 schemas + +import ( + "encoding/json" + "os" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func testCompatibility(vc *cmdutils.VerbCmd) { + desc := cmdutils.LongDescription{} + desc.CommandUsedFor = "Test schema compatibility" + desc.CommandPermission = "This command requires namespace admin permissions." + + var examples []cmdutils.Example + examples = append(examples, cmdutils.Example{ + Desc: desc.CommandUsedFor, + Command: "pulsarctl schemas compatibility (topic name) --filename (schema file path)", + }) + desc.CommandExamples = examples + + vc.SetDescription( + "compatibility", + desc.CommandUsedFor, + desc.ToString(), + desc.ExampleToString(), + "compatibility", + ) + + schemaData := &utils.SchemaData{} + vc.FlagSetGroup.InFlagSet("SchemaConfig", func(flagSet *pflag.FlagSet) { + flagSet.StringVarP(&schemaData.Filename, "filename", "f", "", "filename") + _ = cobra.MarkFlagRequired(flagSet, "filename") + }) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + return doTestCompatibility(vc, schemaData) + }, "the topic name is not specified or the topic name is specified more than one") +} + +func doTestCompatibility(vc *cmdutils.VerbCmd, schemaData *utils.SchemaData) error { + var payload utils.PostSchemaPayload + + file, err := os.ReadFile(schemaData.Filename) + if err != nil { + return err + } + if err = json.Unmarshal(file, &payload); err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + compatibility, err := admin.Schemas().TestCompatibilityWithPostSchemaPayload(vc.NameArg, payload) + if err != nil { + return err + } + + return vc.OutputConfig.WriteOutput( + vc.Command.OutOrStdout(), + cmdutils.NewOutputContent().WithObject(compatibility), + ) +} diff --git a/pkg/ctl/schemas/delete.go b/pkg/ctl/schemas/delete.go index e9255e6e5..3c8fccc8b 100644 --- a/pkg/ctl/schemas/delete.go +++ b/pkg/ctl/schemas/delete.go @@ -18,6 +18,7 @@ package schemas import ( + "github.com/spf13/pflag" "github.com/streamnative/pulsarctl/pkg/cmdutils" ) @@ -31,7 +32,11 @@ func deleteSchema(vc *cmdutils.VerbCmd) { Desc: "Delete the latest schema for a topic", Command: "pulsarctl schemas delete (topic name)", } - examples = append(examples, del) + forceDel := cmdutils.Example{ + Desc: "Delete all schema resources for a topic", + Command: "pulsarctl schemas delete (topic name) --force", + } + examples = append(examples, del, forceDel) desc.CommandExamples = examples var out []cmdutils.Output @@ -54,16 +59,31 @@ func deleteSchema(vc *cmdutils.VerbCmd) { desc.ExampleToString(), "delete", ) + var force bool + vc.FlagSetGroup.InFlagSet("SchemaConfig", func(flagSet *pflag.FlagSet) { + flagSet.BoolVarP( + &force, + "force", + "f", + false, + "delete schema completely", + ) + }) vc.SetRunFuncWithNameArg(func() error { - return doDeleteSchema(vc) + return doDeleteSchema(vc, force) }, "the topic name is not specified or the topic name is specified more than one") } -func doDeleteSchema(vc *cmdutils.VerbCmd) error { +func doDeleteSchema(vc *cmdutils.VerbCmd, force bool) error { topic := vc.NameArg admin := cmdutils.NewPulsarClient() - err := admin.Schemas().DeleteSchema(topic) + var err error + if force { + err = admin.Schemas().ForceDeleteSchema(topic) + } else { + err = admin.Schemas().DeleteSchema(topic) + } if err == nil { vc.Command.Printf("Deleted %s successfully\n", topic) } diff --git a/pkg/ctl/schemas/get.go b/pkg/ctl/schemas/get.go index 2325fa562..bdec682fb 100644 --- a/pkg/ctl/schemas/get.go +++ b/pkg/ctl/schemas/get.go @@ -18,6 +18,7 @@ package schemas import ( + "errors" "io" "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" @@ -49,8 +50,13 @@ func getSchema(vc *cmdutils.VerbCmd) { Command: "pulsarctl schemas get (topic name) \n" + "\t--version 2", } + delWithAllVersion := cmdutils.Example{ + Desc: "Get all schema versions for a topic", + Command: "pulsarctl schemas get (topic name) \n" + + "\t--all-version", + } - examples = append(examples, del, delWithVersion) + examples = append(examples, del, delWithVersion, delWithAllVersion) desc.CommandExamples = examples var out []cmdutils.Output @@ -105,9 +111,10 @@ func getSchema(vc *cmdutils.VerbCmd) { ) schemaData := &utils.SchemaData{} + var allVersion bool vc.SetRunFuncWithNameArg(func() error { - return doGetSchema(vc, schemaData) + return doGetSchema(vc, schemaData, allVersion) }, "the topic name is not specified or the topic name is specified more than one") vc.FlagSetGroup.InFlagSet("SchemaConfig", func(flagSet *pflag.FlagSet) { @@ -116,14 +123,30 @@ func getSchema(vc *cmdutils.VerbCmd) { "version", 0, "the schema version info") + flagSet.BoolVarP( + &allVersion, + "all-version", + "a", + false, + "get all schema versions") }) vc.EnableOutputFlagSet() } -func doGetSchema(vc *cmdutils.VerbCmd, schemaData *utils.SchemaData) error { +func doGetSchema(vc *cmdutils.VerbCmd, schemaData *utils.SchemaData, allVersion bool) error { topic := vc.NameArg + if allVersion && vc.Command.Flag("version").Changed { + return errors.New("only one or neither of --version and --all-version can be specified") + } admin := cmdutils.NewPulsarClient() + if allVersion { + schemas, err := admin.Schemas().GetAllSchemas(topic) + if err == nil { + err = vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), cmdutils.NewOutputContent().WithObject(schemas)) + } + return err + } if !vc.Command.Flag("version").Changed { schemaInfoWithVersion, err := admin.Schemas().GetSchemaInfoWithVersion(topic) if err == nil { diff --git a/pkg/ctl/schemas/new_commands_test.go b/pkg/ctl/schemas/new_commands_test.go new file mode 100644 index 000000000..60ead183f --- /dev/null +++ b/pkg/ctl/schemas/new_commands_test.go @@ -0,0 +1,142 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 schemas + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/stretchr/testify/assert" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func withAdminURLForTest(t *testing.T, webServiceURL string) { + t.Helper() + oldURL := cmdutils.PulsarCtlConfig.WebServiceURL + cmdutils.PulsarCtlConfig.WebServiceURL = webServiceURL + t.Cleanup(func() { + cmdutils.PulsarCtlConfig.WebServiceURL = oldURL + }) +} + +func TestGetSchemaAllVersion(t *testing.T) { + topic := "persistent://public/default/test-schema" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/admin/v2/schemas/public/default/test-schema/schemas", r.URL.Path) + _, _ = w.Write([]byte(`{ + "getSchemaResponses": [ + { + "version": 2, + "type": "AVRO", + "timestamp": 1, + "data": "{\"type\":\"record\",\"name\":\"Test\",\"fields\":[]}", + "properties": {} + } + ] +}`)) + })) + defer srv.Close() + withAdminURLForTest(t, srv.URL) + + args := []string{"get", topic, "--all-version"} + out, execErr, err := TestSchemasCommands(getSchema, args) + assert.Nil(t, err) + assert.Nil(t, execErr) + + var infos []utils.SchemaInfoWithVersion + err = json.Unmarshal(out.Bytes(), &infos) + assert.Nil(t, err) + assert.Len(t, infos, 1) + assert.Equal(t, int64(2), infos[0].Version) + assert.Equal(t, "test-schema", infos[0].SchemaInfo.Name) +} + +func TestGetSchemaVersionConflict(t *testing.T) { + args := []string{"get", "persistent://public/default/test-schema", "--version", "1", "--all-version"} + _, execErr, err := TestSchemasCommands(getSchema, args) + assert.Nil(t, err) + assert.NotNil(t, execErr) + assert.Contains(t, execErr.Error(), "--version and --all-version") +} + +func TestDeleteSchemaWithForce(t *testing.T) { + topic := "persistent://public/default/test-schema" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method) + assert.Equal(t, "/admin/v2/schemas/public/default/test-schema/schema", r.URL.Path) + assert.Equal(t, "true", r.URL.Query().Get("force")) + w.WriteHeader(http.StatusNoContent) + })) + defer srv.Close() + withAdminURLForTest(t, srv.URL) + + args := []string{"delete", topic, "--force"} + out, execErr, err := TestSchemasCommands(deleteSchema, args) + assert.Nil(t, err) + assert.Nil(t, execErr) + assert.Equal(t, "Deleted persistent://public/default/test-schema successfully\n", out.String()) +} + +func TestSchemaCompatibility(t *testing.T) { + topic := "persistent://public/default/test-schema" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/admin/v2/schemas/public/default/test-schema/compatibility", r.URL.Path) + var payload utils.PostSchemaPayload + err := json.NewDecoder(r.Body).Decode(&payload) + assert.Nil(t, err) + assert.Equal(t, "AVRO", payload.SchemaType) + _, _ = w.Write([]byte(`{"compatibility":true,"schemaCompatibilityStrategy":"FULL"}`)) + })) + defer srv.Close() + withAdminURLForTest(t, srv.URL) + + tmpFile := filepath.Join(t.TempDir(), "schema.json") + err := os.WriteFile(tmpFile, []byte(`{ + "type":"AVRO", + "schema":"{\"type\":\"record\",\"name\":\"Test\",\"fields\":[]}", + "properties":{} +}`), 0o644) + assert.Nil(t, err) + + args := []string{"compatibility", topic, "-f", tmpFile} + out, execErr, err := TestSchemasCommands(testCompatibility, args) + assert.Nil(t, err) + assert.Nil(t, execErr) + + var result utils.IsCompatibility + err = json.Unmarshal(out.Bytes(), &result) + assert.Nil(t, err) + assert.True(t, result.IsCompatibility) + assert.Equal(t, utils.SchemaCompatibilityStrategyFull, result.SchemaCompatibilityStrategy) +} + +func TestSchemaCompatibilityMissingFile(t *testing.T) { + args := []string{"compatibility", "persistent://public/default/test-schema", "-f", "not-exist.json"} + _, execErr, err := TestSchemasCommands(testCompatibility, args) + assert.Nil(t, err) + assert.NotNil(t, execErr) + assert.Contains(t, execErr.Error(), "no such file or directory") +} diff --git a/pkg/ctl/schemas/schema_test.go b/pkg/ctl/schemas/schema_test.go index 62d44f42c..7621c74d7 100644 --- a/pkg/ctl/schemas/schema_test.go +++ b/pkg/ctl/schemas/schema_test.go @@ -64,6 +64,21 @@ func TestSchema(t *testing.T) { delOut, _, err := TestSchemasCommands(deleteSchema, delArgs) assert.Nil(t, err) assert.Equal(t, delOut.String(), "Deleted test-schema successfully\n") + + allArgs := []string{"get", "test-schema", "--all-version"} + allOut, _, err := TestSchemasCommands(getSchema, allArgs) + assert.NoError(t, err) + assert.True(t, strings.Contains(allOut.String(), "version")) + + compatibilityArgs := []string{"compatibility", "test-schema", "-f", fileName} + compatibilityOut, _, err := TestSchemasCommands(testCompatibility, compatibilityArgs) + assert.NoError(t, err) + assert.True(t, strings.Contains(compatibilityOut.String(), "compatibility")) + + forceDeleteArgs := []string{"delete", "test-schema", "--force"} + forceDeleteOut, _, err := TestSchemasCommands(deleteSchema, forceDeleteArgs) + assert.Nil(t, err) + assert.Equal(t, forceDeleteOut.String(), "Deleted test-schema successfully\n") } func TestFailSchema(t *testing.T) { diff --git a/pkg/ctl/schemas/schemas.go b/pkg/ctl/schemas/schemas.go index df93bba99..780c26a75 100644 --- a/pkg/ctl/schemas/schemas.go +++ b/pkg/ctl/schemas/schemas.go @@ -40,6 +40,7 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddVerbCmd(flagGrouping, resourceCmd, getSchema) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, deleteSchema) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, uploadSchema) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, testCompatibility) return resourceCmd } diff --git a/pkg/ctl/sinks/reload.go b/pkg/ctl/sinks/reload.go new file mode 100644 index 000000000..36fd64f41 --- /dev/null +++ b/pkg/ctl/sinks/reload.go @@ -0,0 +1,50 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 sinks + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/admin/config" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func reloadSinksCmd(vc *cmdutils.VerbCmd) { + desc := cmdutils.LongDescription{} + desc.CommandUsedFor = "Reload built-in Pulsar IO sinks" + desc.CommandPermission = "This command requires tenant admin permissions." + + desc.CommandExamples = []cmdutils.Example{ + { + Desc: "Reload built-in Pulsar IO sinks", + Command: "pulsarctl sinks reload", + }, + } + + vc.SetDescription("reload", "Reload built-in Pulsar IO sinks", desc.ToString(), desc.ExampleToString(), "reload") + vc.SetRunFunc(func() error { + return doReloadSinks(vc) + }) +} + +func doReloadSinks(vc *cmdutils.VerbCmd) error { + admin := cmdutils.NewPulsarClientWithAPIVersion(config.V3) + err := admin.Sinks().ReloadBuiltInSinks() + if err == nil { + vc.Command.Println("Reloaded built-in sinks successfully") + } + return err +} diff --git a/pkg/ctl/sinks/sinks.go b/pkg/ctl/sinks/sinks.go index fc18881c5..5950f5975 100644 --- a/pkg/ctl/sinks/sinks.go +++ b/pkg/ctl/sinks/sinks.go @@ -41,6 +41,7 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddVerbCmd(flagGrouping, resourceCmd, restartSinksCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, statusSinksCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, listBuiltInSinksCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, reloadSinksCmd) return resourceCmd } diff --git a/pkg/ctl/sources/reload.go b/pkg/ctl/sources/reload.go new file mode 100644 index 000000000..80b86863a --- /dev/null +++ b/pkg/ctl/sources/reload.go @@ -0,0 +1,50 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 sources + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/admin/config" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func reloadSourcesCmd(vc *cmdutils.VerbCmd) { + desc := cmdutils.LongDescription{} + desc.CommandUsedFor = "Reload built-in Pulsar IO sources" + desc.CommandPermission = "This command requires tenant admin permissions." + + desc.CommandExamples = []cmdutils.Example{ + { + Desc: "Reload built-in Pulsar IO sources", + Command: "pulsarctl sources reload", + }, + } + + vc.SetDescription("reload", "Reload built-in Pulsar IO sources", desc.ToString(), desc.ExampleToString(), "reload") + vc.SetRunFunc(func() error { + return doReloadSources(vc) + }) +} + +func doReloadSources(vc *cmdutils.VerbCmd) error { + admin := cmdutils.NewPulsarClientWithAPIVersion(config.V3) + err := admin.Sources().ReloadBuiltInSources() + if err == nil { + vc.Command.Println("Reloaded built-in sources successfully") + } + return err +} diff --git a/pkg/ctl/sources/sources.go b/pkg/ctl/sources/sources.go index db1210c33..383ebf108 100644 --- a/pkg/ctl/sources/sources.go +++ b/pkg/ctl/sources/sources.go @@ -41,6 +41,7 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddVerbCmd(flagGrouping, resourceCmd, restartSourcesCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, statusSourcesCmd) cmdutils.AddVerbCmd(flagGrouping, resourceCmd, listBuiltInSourcesCmd) + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, reloadSourcesCmd) return resourceCmd } diff --git a/pkg/ctl/topic/create.go b/pkg/ctl/topic/create.go index 24654eae1..9f7fdf1b0 100644 --- a/pkg/ctl/topic/create.go +++ b/pkg/ctl/topic/create.go @@ -22,6 +22,7 @@ import ( "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" "github.com/pkg/errors" + "github.com/spf13/pflag" "github.com/streamnative/pulsarctl/pkg/cmdutils" ) @@ -64,12 +65,17 @@ func CreateTopicCmd(vc *cmdutils.VerbCmd) { desc.ExampleToString(), "c") + metadata := map[string]string{} + vc.FlagSetGroup.InFlagSet("CreateTopic", func(set *pflag.FlagSet) { + set.StringToStringVarP(&metadata, "metadata", "m", nil, "topic metadata in key=value,key2=value2 format") + }) + vc.SetRunFuncWithMultiNameArgs(func() error { - return doCreateTopic(vc) + return doCreateTopic(vc, metadata) }, CheckTopicNameTwoArgs) } -func doCreateTopic(vc *cmdutils.VerbCmd) error { +func doCreateTopic(vc *cmdutils.VerbCmd, metadata map[string]string) error { // for testing if vc.NameError != nil { return vc.NameError @@ -86,7 +92,11 @@ func doCreateTopic(vc *cmdutils.VerbCmd) error { } admin := cmdutils.NewPulsarClient() - err = admin.Topics().Create(*topic, partitions) + if len(metadata) == 0 { + err = admin.Topics().Create(*topic, partitions) + } else { + err = admin.Topics().CreateWithProperties(*topic, partitions, metadata) + } if err == nil { vc.Command.Printf("Create topic %s with %d partitions successfully\n", topic.String(), partitions) } diff --git a/pkg/ctl/topic/get_message_id.go b/pkg/ctl/topic/get_message_id.go new file mode 100644 index 000000000..1e2ec38c6 --- /dev/null +++ b/pkg/ctl/topic/get_message_id.go @@ -0,0 +1,82 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topic + +import ( + "time" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetMessageIDCmd(vc *cmdutils.VerbCmd) { + desc := cmdutils.LongDescription{} + desc.CommandUsedFor = "Get message id by datetime for a topic." + desc.CommandPermission = "This command requires tenant admin permissions." + + desc.CommandExamples = []cmdutils.Example{ + { + Desc: "Get message id by datetime for a topic", + Command: "pulsarctl topics get-message-id --datetime 2021-06-28T16:53:08Z persistent://public/default/topic", + }, + } + + vc.SetDescription( + "get-message-id", + "Get message id by datetime for a topic", + desc.ToString(), + desc.ExampleToString(), + "get-message-id", + ) + + var datetime string + vc.FlagSetGroup.InFlagSet("GetMessageID", func(set *pflag.FlagSet) { + set.StringVarP(&datetime, "datetime", "d", "", "datetime in RFC3339 or RFC3339Nano format") + _ = cobra.MarkFlagRequired(set, "datetime") + }) + + vc.SetRunFuncWithNameArg(func() error { + return doGetMessageID(vc, datetime) + }, "the topic name is not specified or the topic name is specified more than one") + vc.EnableOutputFlagSet() +} + +func doGetMessageID(vc *cmdutils.VerbCmd, datetime string) error { + if vc.NameError != nil { + return vc.NameError + } + + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + + timestamp, err := time.Parse(time.RFC3339Nano, datetime) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + messageID, err := admin.Topics().GetMessageID(*topic, timestamp.UnixMilli()) + if err == nil { + err = vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), cmdutils.NewOutputContent().WithObject(messageID)) + } + return err +} diff --git a/pkg/ctl/topic/get_message_id_test.go b/pkg/ctl/topic/get_message_id_test.go new file mode 100644 index 000000000..e48aedd65 --- /dev/null +++ b/pkg/ctl/topic/get_message_id_test.go @@ -0,0 +1,36 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topic + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetMessageIDRequiresDatetime(t *testing.T) { + _, _, _, err := TestTopicCommands(GetMessageIDCmd, []string{"get-message-id", "persistent://public/default/test"}) + assert.NotNil(t, err) + assert.Equal(t, "required flag(s) \"datetime\" not set", err.Error()) +} + +func TestGetMessageIDNameError(t *testing.T) { + _, _, nameErr, _ := TestTopicCommands(GetMessageIDCmd, []string{"get-message-id", "--datetime", "2021-06-28T16:53:08Z"}) + assert.NotNil(t, nameErr) + assert.Equal(t, "the topic name is not specified or the topic name is specified more than one", nameErr.Error()) +} diff --git a/pkg/ctl/topic/max_consumers_per_subscription.go b/pkg/ctl/topic/max_consumers_per_subscription.go new file mode 100644 index 000000000..2ba2080c5 --- /dev/null +++ b/pkg/ctl/topic/max_consumers_per_subscription.go @@ -0,0 +1,113 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topic + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetMaxConsumersPerSubscriptionCmd(vc *cmdutils.VerbCmd) { + vc.SetDescription("get-max-consumers-per-subscription", "Get max consumers per subscription for a topic", "Get max consumers per subscription for a topic", "", "get-max-consumers-per-subscription") + vc.SetRunFuncWithNameArg(func() error { + return doGetMaxConsumersPerSubscription(vc) + }, "the topic name is not specified or the topic name is specified more than one") +} + +func doGetMaxConsumersPerSubscription(vc *cmdutils.VerbCmd) error { + if vc.NameError != nil { + return vc.NameError + } + + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + value, err := admin.Topics().GetMaxConsumersPerSubscription(*topic) + if err == nil { + if value == -1 { + vc.Command.Printf("The max consumers per subscription of the topic %s is not set\n", topic.String()) + return nil + } + vc.Command.Printf("%d\n", value) + } + return err +} + +func SetMaxConsumersPerSubscriptionCmd(vc *cmdutils.VerbCmd) { + var size int + vc.SetDescription("set-max-consumers-per-subscription", "Set max consumers per subscription for a topic", "Set max consumers per subscription for a topic", "", "set-max-consumers-per-subscription") + vc.FlagSetGroup.InFlagSet("MaxConsumersPerSubscription", func(set *pflag.FlagSet) { + set.IntVar(&size, "size", -1, "max consumers per subscription") + _ = cobra.MarkFlagRequired(set, "size") + }) + vc.SetRunFuncWithNameArg(func() error { + return doSetMaxConsumersPerSubscription(vc, size) + }, "the topic name is not specified or the topic name is specified more than one") +} + +func doSetMaxConsumersPerSubscription(vc *cmdutils.VerbCmd, size int) error { + if vc.NameError != nil { + return vc.NameError + } + if size < 0 { + return errors.New("the specified consumers value must bigger than 0") + } + + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Topics().SetMaxConsumersPerSubscription(*topic, size) + if err == nil { + vc.Command.Printf("Set max consumers per subscription successfully for [%s]\n", topic.String()) + } + return err +} + +func RemoveMaxConsumersPerSubscriptionCmd(vc *cmdutils.VerbCmd) { + vc.SetDescription("remove-max-consumers-per-subscription", "Remove max consumers per subscription for a topic", "Remove max consumers per subscription for a topic", "", "remove-max-consumers-per-subscription") + vc.SetRunFuncWithNameArg(func() error { + return doRemoveMaxConsumersPerSubscription(vc) + }, "the topic name is not specified or the topic name is specified more than one") +} + +func doRemoveMaxConsumersPerSubscription(vc *cmdutils.VerbCmd) error { + if vc.NameError != nil { + return vc.NameError + } + + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Topics().RemoveMaxConsumersPerSubscription(*topic) + if err == nil { + vc.Command.Printf("Removed max consumers per subscription successfully for [%s]\n", topic.String()) + } + return err +} diff --git a/pkg/ctl/topic/properties.go b/pkg/ctl/topic/properties.go new file mode 100644 index 000000000..de6a8c26d --- /dev/null +++ b/pkg/ctl/topic/properties.go @@ -0,0 +1,82 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topic + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/pflag" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func getPropertiesCmd(vc *cmdutils.VerbCmd) { + vc.SetDescription("get-properties", "Get the topic properties", "Get the topic properties", "") + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + admin := cmdutils.NewPulsarClient() + properties, err := admin.Topics().GetProperties(*topic) + if err != nil { + return err + } + return vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), cmdutils.NewOutputContent().WithObject(properties)) + }, "the topic name is not specified or the topic name is specified more than one") +} + +func updatePropertiesCmd(vc *cmdutils.VerbCmd) { + properties := map[string]string{} + vc.SetDescription("update-properties", "Update the topic properties", "Update the topic properties", "") + vc.FlagSetGroup.InFlagSet("TopicProperties", func(set *pflag.FlagSet) { + set.StringToStringVarP(&properties, "property", "p", nil, "properties in key=value,key2=value2 format") + }) + vc.SetRunFuncWithNameArg(func() error { + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + admin := cmdutils.NewPulsarClient() + err = admin.Topics().UpdateProperties(*topic, properties) + if err == nil { + vc.Command.Printf("Updated properties successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func removePropertiesCmd(vc *cmdutils.VerbCmd) { + var key string + vc.SetDescription("remove-properties", "Remove a property from a topic", "Remove a property from a topic", "") + vc.FlagSetGroup.InFlagSet("TopicProperties", func(set *pflag.FlagSet) { + set.StringVarP(&key, "key", "k", "", "property key") + }) + vc.SetRunFuncWithNameArg(func() error { + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + admin := cmdutils.NewPulsarClient() + err = admin.Topics().RemoveProperty(*topic, key) + if err == nil { + vc.Command.Printf("Removed property %s successfully for [%s]\n", key, topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} diff --git a/pkg/ctl/topic/schema_validation_enforce.go b/pkg/ctl/topic/schema_validation_enforce.go new file mode 100644 index 000000000..30e353c93 --- /dev/null +++ b/pkg/ctl/topic/schema_validation_enforce.go @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topic + +import ( + "errors" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/pflag" + + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func getSchemaValidationEnforceCmd(vc *cmdutils.VerbCmd) { + vc.SetDescription("get-schema-validation-enforce", "Get schema validation enforce flag for a topic", + "Get schema validation enforce flag for a topic", "") + vc.SetRunFuncWithNameArg(func() error { + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + admin := cmdutils.NewPulsarClient() + enabled, err := admin.Topics().GetSchemaValidationEnforced(*topic) + if err == nil { + vc.Command.Printf("%t\n", enabled) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func setSchemaValidationEnforceCmd(vc *cmdutils.VerbCmd) { + var enable bool + var disable bool + vc.SetDescription("set-schema-validation-enforce", "Set schema validation enforce flag for a topic", + "Set schema validation enforce flag for a topic", "") + vc.FlagSetGroup.InFlagSet("SchemaValidationEnforce", func(set *pflag.FlagSet) { + set.BoolVarP(&enable, "enable", "e", false, "enable schema validation enforce") + set.BoolVarP(&disable, "disable", "d", false, "disable schema validation enforce") + }) + vc.SetRunFuncWithNameArg(func() error { + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + if enable == disable { + return errors.New("need to specify either --enable or --disable") + } + admin := cmdutils.NewPulsarClient() + err = admin.Topics().SetSchemaValidationEnforced(*topic, enable) + if err == nil { + vc.Command.Printf("Set schema validation enforce successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveSchemaValidationEnforceCmd(vc *cmdutils.VerbCmd) { + vc.SetDescription("remove-schema-validation-enforce", "Remove schema validation enforce flag for a topic", + "Remove schema validation enforce flag for a topic", "") + vc.SetRunFuncWithNameArg(func() error { + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + admin := cmdutils.NewPulsarClient() + err = admin.Topics().RemoveSchemaValidationEnforced(*topic) + if err == nil { + vc.Command.Printf("Remove schema validation enforce successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} diff --git a/pkg/ctl/topic/schema_validation_enforce_test.go b/pkg/ctl/topic/schema_validation_enforce_test.go new file mode 100644 index 000000000..eb022f9d5 --- /dev/null +++ b/pkg/ctl/topic/schema_validation_enforce_test.go @@ -0,0 +1,32 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topic + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRemoveSchemaValidationEnforceNameError(t *testing.T) { + _, _, nameErr, _ := TestTopicCommands(RemoveSchemaValidationEnforceCmd, []string{ + "remove-schema-validation-enforce", + }) + assert.NotNil(t, nameErr) + assert.Equal(t, "the topic name is not specified or the topic name is specified more than one", nameErr.Error()) +} diff --git a/pkg/ctl/topic/subscribe_rate.go b/pkg/ctl/topic/subscribe_rate.go new file mode 100644 index 000000000..acbc0861f --- /dev/null +++ b/pkg/ctl/topic/subscribe_rate.go @@ -0,0 +1,106 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topic + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetSubscribeRateCmd(vc *cmdutils.VerbCmd) { + vc.SetDescription("get-subscribe-rate", "Get subscribe rate for a topic", "Get subscribe rate for a topic", "", "get-subscribe-rate") + vc.SetRunFuncWithNameArg(func() error { + return doGetSubscribeRate(vc) + }, "the topic name is not specified or the topic name is specified more than one") + vc.EnableOutputFlagSet() +} + +func doGetSubscribeRate(vc *cmdutils.VerbCmd) error { + if vc.NameError != nil { + return vc.NameError + } + + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + rate, err := admin.Topics().GetSubscribeRate(*topic) + if err == nil { + err = vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), cmdutils.NewOutputContent().WithObject(rate)) + } + return err +} + +func SetSubscribeRateCmd(vc *cmdutils.VerbCmd) { + data := *utils.NewSubscribeRate() + vc.SetDescription("set-subscribe-rate", "Set subscribe rate for a topic", "Set subscribe rate for a topic", "", "set-subscribe-rate") + vc.FlagSetGroup.InFlagSet("SubscribeRate", func(set *pflag.FlagSet) { + set.IntVarP(&data.SubscribeThrottlingRatePerConsumer, "subscribe-rate", "m", -1, "message dispatch rate") + set.IntVarP(&data.RatePeriodInSecond, "period", "p", 30, "dispatch rate period") + }) + vc.SetRunFuncWithNameArg(func() error { + return doSetSubscribeRate(vc, data) + }, "the topic name is not specified or the topic name is specified more than one") + vc.EnableOutputFlagSet() +} + +func doSetSubscribeRate(vc *cmdutils.VerbCmd, rate utils.SubscribeRate) error { + if vc.NameError != nil { + return vc.NameError + } + + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Topics().SetSubscribeRate(*topic, rate) + if err == nil { + vc.Command.Printf("Set subscribe rate successfully for [%s]\n", topic.String()) + } + return err +} + +func RemoveSubscribeRateCmd(vc *cmdutils.VerbCmd) { + vc.SetDescription("remove-subscribe-rate", "Remove subscribe rate for a topic", "Remove subscribe rate for a topic", "", "remove-subscribe-rate") + vc.SetRunFuncWithNameArg(func() error { + return doRemoveSubscribeRate(vc) + }, "the topic name is not specified or the topic name is specified more than one") +} + +func doRemoveSubscribeRate(vc *cmdutils.VerbCmd) error { + if vc.NameError != nil { + return vc.NameError + } + + topic, err := utils.GetTopicName(vc.NameArg) + if err != nil { + return err + } + + admin := cmdutils.NewPulsarClient() + err = admin.Topics().RemoveSubscribeRate(*topic) + if err == nil { + vc.Command.Printf("Removed subscribe rate successfully for [%s]\n", topic.String()) + } + return err +} diff --git a/pkg/ctl/topic/subscribe_rate_test.go b/pkg/ctl/topic/subscribe_rate_test.go new file mode 100644 index 000000000..74292ef44 --- /dev/null +++ b/pkg/ctl/topic/subscribe_rate_test.go @@ -0,0 +1,36 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topic + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRemoveSubscribeRateNameError(t *testing.T) { + _, _, nameErr, _ := TestTopicCommands(RemoveSubscribeRateCmd, []string{"remove-subscribe-rate"}) + assert.NotNil(t, nameErr) + assert.Equal(t, "the topic name is not specified or the topic name is specified more than one", nameErr.Error()) +} + +func TestSetMaxConsumersPerSubscriptionRejectsNegativeValue(t *testing.T) { + _, execErr, _, _ := TestTopicCommands(SetMaxConsumersPerSubscriptionCmd, []string{"set-max-consumers-per-subscription", "--size", "-1", "persistent://public/default/test"}) + assert.NotNil(t, execErr) + assert.Equal(t, "the specified consumers value must bigger than 0", execErr.Error()) +} diff --git a/pkg/ctl/topic/topic.go b/pkg/ctl/topic/topic.go index f81b661eb..db5c4f1c5 100644 --- a/pkg/ctl/topic/topic.go +++ b/pkg/ctl/topic/topic.go @@ -19,6 +19,7 @@ package topic import ( "github.com/streamnative/pulsarctl/pkg/cmdutils" + "github.com/streamnative/pulsarctl/pkg/ctl/topicpolicies" "github.com/spf13/cobra" ) @@ -48,6 +49,7 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { LookUpTopicCmd, GetBundleRangeCmd, GetLastMessageIDCmd, + GetMessageIDCmd, GetStatsCmd, GetInternalStatsCmd, GetInternalInfoCmd, @@ -60,6 +62,9 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { GetMaxConsumersCmd, SetMaxConsumersCmd, RemoveMaxConsumersCmd, + GetMaxConsumersPerSubscriptionCmd, + SetMaxConsumersPerSubscriptionCmd, + RemoveMaxConsumersPerSubscriptionCmd, GetMaxUnackMessagesPerConsumerCmd, SetMaxUnackMessagesPerConsumerCmd, RemoveMaxUnackMessagesPerConsumerCmd, @@ -93,11 +98,47 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { GetPublishRateCmd, SetPublishRateCmd, RemovePublishRateCmd, + GetSubscribeRateCmd, + SetSubscribeRateCmd, + RemoveSubscribeRateCmd, + getPropertiesCmd, + updatePropertiesCmd, + removePropertiesCmd, + getSchemaValidationEnforceCmd, + setSchemaValidationEnforceCmd, + RemoveSchemaValidationEnforceCmd, GetInactiveTopicCmd, SetInactiveTopicCmd, RemoveInactiveTopicCmd, } + commands = append(commands, + topicpolicies.GetMaxMessageSizeCmd, + topicpolicies.SetMaxMessageSizeCmd, + topicpolicies.RemoveMaxMessageSizeCmd, + topicpolicies.GetMaxSubscriptionsPerTopicCmd, + topicpolicies.SetMaxSubscriptionsPerTopicCmd, + topicpolicies.RemoveMaxSubscriptionsPerTopicCmd, + topicpolicies.GetDeduplicationSnapshotIntervalCmd, + topicpolicies.SetDeduplicationSnapshotIntervalCmd, + topicpolicies.RemoveDeduplicationSnapshotIntervalCmd, + topicpolicies.GetReplicatorDispatchRateCmd, + topicpolicies.SetReplicatorDispatchRateCmd, + topicpolicies.RemoveReplicatorDispatchRateCmd, + topicpolicies.GetOffloadPoliciesCmd, + topicpolicies.SetOffloadPoliciesCmd, + topicpolicies.RemoveOffloadPoliciesCmd, + topicpolicies.GetAutoSubscriptionCreationCmd, + topicpolicies.SetAutoSubscriptionCreationCmd, + topicpolicies.RemoveAutoSubscriptionCreationCmd, + topicpolicies.GetSchemaCompatibilityStrategyCmd, + topicpolicies.SetSchemaCompatibilityStrategyCmd, + topicpolicies.RemoveSchemaCompatibilityStrategyCmd, + topicpolicies.GetReplicationClustersCmd, + topicpolicies.SetReplicationClustersCmd, + topicpolicies.RemoveReplicationClustersCmd, + ) + cmdutils.AddVerbCmds(flagGrouping, resourceCmd, commands...) return resourceCmd diff --git a/pkg/ctl/topicpolicies/common.go b/pkg/ctl/topicpolicies/common.go new file mode 100644 index 000000000..6664aff11 --- /dev/null +++ b/pkg/ctl/topicpolicies/common.go @@ -0,0 +1,184 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "context" + "fmt" + "io" + + adminpkg "github.com/apache/pulsar-client-go/pulsaradmin/pkg/admin" + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func topicName(vc *cmdutils.VerbCmd) (*utils.TopicName, error) { + if vc.NameError != nil { + return nil, vc.NameError + } + return utils.GetTopicName(vc.NameArg) +} + +func topicPolicies(global bool) (adminpkg.TopicPolicies, error) { + return adminpkg.TopicPoliciesOf(cmdutils.NewPulsarClient(), global) +} + +func topicPolicyResources(vc *cmdutils.VerbCmd, global bool) (adminpkg.TopicPolicies, *utils.TopicName, error) { + topic, err := topicName(vc) + if err != nil { + return nil, nil, err + } + policies, err := topicPolicies(global) + if err != nil { + return nil, nil, err + } + return policies, topic, nil +} + +func addScopeFlags(vc *cmdutils.VerbCmd, global *bool, applied *bool) { + vc.FlagSetGroup.InFlagSet("TopicPolicyScope", func(set *pflag.FlagSet) { + if global != nil { + set.BoolVarP(global, "global", "g", false, "use global topic policies") + } + if applied != nil { + set.BoolVarP(applied, "applied", "a", false, "get the applied policy for the topic") + } + }) +} + +func writePolicyOutput(vc *cmdutils.VerbCmd, obj interface{}, text string, args ...interface{}) error { + if vc.OutputConfig == nil { + vc.EnableOutputFlagSet() + } + oc := cmdutils.NewOutputContent().WithObject(obj) + if text == "" { + return vc.OutputConfig.WriteOutput(vc.Command.OutOrStdout(), oc) + } + return vc.OutputConfig.WriteOutput( + vc.Command.OutOrStdout(), + oc. + WithTextFunc(func(w io.Writer) error { + _, err := fmt.Fprintf(w, text, args...) + return err + }), + ) +} + +func getOptionalIntPolicyCmd( + vc *cmdutils.VerbCmd, + use string, + short string, + getter func(context.Context, adminpkg.TopicPolicies, utils.TopicName, bool) (*int, error), +) { + var global bool + var applied bool + vc.SetDescription(use, short, short, "", use) + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + value, err := getter(vc.Command.Context(), policies, *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, value, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func getOptionalInt64PolicyCmd( + vc *cmdutils.VerbCmd, + use string, + short string, + getter func(context.Context, adminpkg.TopicPolicies, utils.TopicName, bool) (*int64, error), +) { + var global bool + var applied bool + vc.SetDescription(use, short, short, "", use) + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + value, err := getter(vc.Command.Context(), policies, *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, value, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func getOptionalBoolPolicyCmd( + vc *cmdutils.VerbCmd, + use string, + short string, + getter func(context.Context, adminpkg.TopicPolicies, utils.TopicName, bool) (*bool, error), +) { + var global bool + var applied bool + vc.SetDescription(use, short, short, "", use) + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + value, err := getter(vc.Command.Context(), policies, *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, value, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func setEnableDisablePolicyCmd( + vc *cmdutils.VerbCmd, + use string, + short string, + setter func(context.Context, adminpkg.TopicPolicies, utils.TopicName, bool) error, +) { + var global bool + var enable bool + var disable bool + vc.SetDescription(use, short, short, "", use) + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("Policy", func(set *pflag.FlagSet) { + set.BoolVarP(&enable, "enable", "e", false, "enable policy") + set.BoolVarP(&disable, "disable", "d", false, "disable policy") + }) + vc.SetRunFuncWithNameArg(func() error { + if enable == disable { + return fmt.Errorf("need to specify either --enable or --disable") + } + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = setter(vc.Command.Context(), policies, *topic, enable) + if err == nil { + vc.Command.Printf("%s successfully for [%s]\n", short, topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} diff --git a/pkg/ctl/topicpolicies/data_policies.go b/pkg/ctl/topicpolicies/data_policies.go new file mode 100644 index 000000000..e37be91c7 --- /dev/null +++ b/pkg/ctl/topicpolicies/data_policies.go @@ -0,0 +1,396 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "errors" + + util "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" + ctlutils "github.com/streamnative/pulsarctl/pkg/ctl/utils" +) + +type backlogQuotaArgs struct { + limitSize string + limitTime int64 + policy string + quotaType string +} + +type inactiveTopicPoliciesArgs struct { + enableDeleteWhileInactive bool + disableDeleteWhileInactive bool + maxInactiveDuration string + deleteMode string +} + +func GetRetentionCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-retention", "Get retention policy for a topic", "Get retention policy for a topic", "", "get-retention") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + retention, err := policies.GetRetention(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, retention, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetRetentionCmd(vc *cmdutils.VerbCmd) { + var global bool + var timeStr string + var sizeStr string + vc.SetDescription("set-retention", "Set retention policy for a topic", "Set retention policy for a topic", "", "set-retention") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("Retention", func(set *pflag.FlagSet) { + set.StringVarP(&timeStr, "time", "t", "", "retention time with optional time unit suffix") + set.StringVar(&sizeStr, "size", "", "retention size limit") + _ = cobra.MarkFlagRequired(set, "time") + _ = cobra.MarkFlagRequired(set, "size") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + retentionDuration, err := ctlutils.ParseRelativeTimeInSeconds(timeStr) + if err != nil { + return err + } + sizeLimit, err := ctlutils.ValidateSizeString(sizeStr) + if err != nil { + return err + } + retentionTimeInMin := -1 + if retentionDuration != -1 { + retentionTimeInMin = int(retentionDuration.Minutes()) + } + retentionSizeInMB := -1 + if sizeLimit != -1 { + retentionSizeInMB = int(sizeLimit / (1024 * 1024)) + } + err = policies.SetRetention(vc.Command.Context(), *topic, util.NewRetentionPolicies(retentionTimeInMin, retentionSizeInMB)) + if err == nil { + vc.Command.Printf("Set retention policy successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveRetentionCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-retention", "Removed retention policy for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveRetention(vc.Command.Context(), *topic) + }) +} + +func GetBacklogQuotaCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-backlog-quota", "Get backlog quota for a topic", "Get backlog quota for a topic", "", "get-backlog-quota") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + quota, err := policies.GetBacklogQuotaMap(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, quota, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetBacklogQuotaCmd(vc *cmdutils.VerbCmd) { + var global bool + args := backlogQuotaArgs{} + vc.SetDescription("set-backlog-quota", "Set backlog quota for a topic", "Set backlog quota for a topic", "", "set-backlog-quota") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("BacklogQuota", func(set *pflag.FlagSet) { + set.StringVarP(&args.limitSize, "limit-size", "", "", "size limit") + set.Int64VarP(&args.limitTime, "limit-time", "", -1, "time limit in seconds") + set.StringVarP(&args.policy, "policy", "p", "", "retention policy") + set.StringVarP(&args.quotaType, "type", "t", string(util.DestinationStorage), "backlog quota type") + _ = cobra.MarkFlagRequired(set, "policy") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + sizeLimit := int64(-1) + if args.limitSize != "" { + sizeLimit, err = ctlutils.ValidateSizeString(args.limitSize) + if err != nil { + return err + } + } + policy, err := util.ParseRetentionPolicy(args.policy) + if err != nil { + return err + } + quotaType, err := util.ParseBacklogQuotaType(args.quotaType) + if err != nil { + return err + } + err = policies.SetBacklogQuota( + vc.Command.Context(), + *topic, + util.NewBacklogQuota(sizeLimit, args.limitTime, policy), + quotaType, + ) + if err == nil { + vc.Command.Printf("Set backlog quota successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveBacklogQuotaCmd(vc *cmdutils.VerbCmd) { + var global bool + var quotaType string + vc.SetDescription("remove-backlog-quota", "Remove backlog quota for a topic", "Remove backlog quota for a topic", "", "remove-backlog-quota") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("BacklogQuota", func(set *pflag.FlagSet) { + set.StringVarP("aType, "type", "t", string(util.DestinationStorage), "backlog quota type") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + parsedType, err := util.ParseBacklogQuotaType(quotaType) + if err != nil { + return err + } + err = policies.RemoveBacklogQuota(vc.Command.Context(), *topic, parsedType) + if err == nil { + vc.Command.Printf("Removed backlog quota successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func GetPersistenceCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-persistence", "Get persistence policy for a topic", "Get persistence policy for a topic", "", "get-persistence") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + persistence, err := policies.GetPersistence(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, persistence, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetPersistenceCmd(vc *cmdutils.VerbCmd) { + var global bool + data := util.PersistenceData{} + vc.SetDescription("set-persistence", "Set persistence policy for a topic", "Set persistence policy for a topic", "", "set-persistence") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("Persistence", func(set *pflag.FlagSet) { + set.Int64VarP(&data.BookkeeperEnsemble, "bookkeeper-ensemble", "e", 0, "number of bookies") + set.Int64VarP(&data.BookkeeperWriteQuorum, "bookkeeper-write-quorum", "w", 0, "bookkeeper write quorum") + set.Int64VarP(&data.BookkeeperAckQuorum, "bookkeeper-ack-quorum", "a", 0, "bookkeeper ack quorum") + set.Float64VarP(&data.ManagedLedgerMaxMarkDeleteRate, "ml-mark-delete-max-rate", "r", 0.0, "managed ledger max mark delete rate") + }) + vc.SetRunFuncWithNameArg(func() error { + if data.BookkeeperEnsemble <= 0 || data.BookkeeperWriteQuorum <= 0 || data.BookkeeperAckQuorum <= 0 { + return errors.New("[--bookkeeper-ensemble], [--bookkeeper-write-quorum] and [--bookkeeper-ack-quorum] must greater than 0") + } + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = policies.SetPersistence(vc.Command.Context(), *topic, data) + if err == nil { + vc.Command.Printf("Set persistence policy successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemovePersistenceCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-persistence", "Removed persistence policy for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemovePersistence(vc.Command.Context(), *topic) + }) +} + +func GetDelayedDeliveryCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-delayed-delivery", "Get delayed delivery policy for a topic", "Get delayed delivery policy for a topic", "", "get-delayed-delivery") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + delayed, err := policies.GetDelayedDelivery(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, delayed, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetDelayedDeliveryCmd(vc *cmdutils.VerbCmd) { + var global bool + var enable bool + var disable bool + var tickTime string + var maxDelay string + vc.SetDescription("set-delayed-delivery", "Set delayed delivery policy for a topic", "Set delayed delivery policy for a topic", "", "set-delayed-delivery") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("DelayedDelivery", func(set *pflag.FlagSet) { + set.BoolVarP(&enable, "enable", "e", false, "enable delayed delivery messages") + set.BoolVarP(&disable, "disable", "d", false, "disable delayed delivery messages") + set.StringVarP(&tickTime, "time", "t", "1s", "tick time for delayed delivery") + set.StringVarP(&maxDelay, "max-delay", "", "0s", "max allowed delay for delayed delivery") + }) + vc.SetRunFuncWithNameArg(func() error { + if enable == disable { + return errors.New("need to specify either --enable or --disable") + } + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + tickTimeDuration, err := ctlutils.ParseRelativeTimeInSeconds(tickTime) + if err != nil { + return err + } + maxDelayDuration, err := ctlutils.ParseRelativeTimeInSeconds(maxDelay) + if err != nil { + return err + } + data := util.NewDelayedDeliveryDataWithMaxDelay( + tickTimeDuration.Seconds()*1000, + enable, + int64(maxDelayDuration.Seconds()*1000), + ) + err = policies.SetDelayedDelivery(vc.Command.Context(), *topic, *data) + if err == nil { + vc.Command.Printf("Set delayed delivery policy successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveDelayedDeliveryCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-delayed-delivery", "Removed delayed delivery policy for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveDelayedDelivery(vc.Command.Context(), *topic) + }) +} + +func GetInactiveTopicPoliciesCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-inactive-topic-policies", "Get inactive topic policies for a topic", "Get inactive topic policies for a topic", "", "get-inactive-topic-policies") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + inactive, err := policies.GetInactiveTopicPolicies(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, inactive, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetInactiveTopicPoliciesCmd(vc *cmdutils.VerbCmd) { + var global bool + args := inactiveTopicPoliciesArgs{} + vc.SetDescription("set-inactive-topic-policies", "Set inactive topic policies for a topic", "Set inactive topic policies for a topic", "", "set-inactive-topic-policies") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("InactiveTopicPolicies", func(set *pflag.FlagSet) { + set.BoolVarP(&args.enableDeleteWhileInactive, "enable-delete-while-inactive", "e", false, "enable delete while inactive") + set.BoolVarP(&args.disableDeleteWhileInactive, "disable-delete-while-inactive", "d", false, "disable delete while inactive") + set.StringVarP(&args.maxInactiveDuration, "max-inactive-duration", "t", "", "max inactive duration") + set.StringVarP(&args.deleteMode, "delete-mode", "m", "", "delete mode") + _ = cobra.MarkFlagRequired(set, "max-inactive-duration") + _ = cobra.MarkFlagRequired(set, "delete-mode") + }) + vc.SetRunFuncWithNameArg(func() error { + if args.enableDeleteWhileInactive == args.disableDeleteWhileInactive { + return errors.New("need to specify either --enable-delete-while-inactive or --disable-delete-while-inactive") + } + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + mode, err := util.ParseInactiveTopicDeleteMode(args.deleteMode) + if err != nil { + return err + } + maxInactiveDuration, err := ctlutils.ParseRelativeTimeInSeconds(args.maxInactiveDuration) + if err != nil { + return err + } + body := util.NewInactiveTopicPolicies(&mode, int(maxInactiveDuration.Seconds()), args.enableDeleteWhileInactive) + err = policies.SetInactiveTopicPolicies(vc.Command.Context(), *topic, body) + if err == nil { + vc.Command.Printf("Set inactive topic policies successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveInactiveTopicPoliciesCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-inactive-topic-policies", "Removed inactive topic policies for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveInactiveTopicPolicies(vc.Command.Context(), *topic) + }) +} diff --git a/pkg/ctl/topicpolicies/max_limits.go b/pkg/ctl/topicpolicies/max_limits.go new file mode 100644 index 000000000..58eea18fe --- /dev/null +++ b/pkg/ctl/topicpolicies/max_limits.go @@ -0,0 +1,281 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func getIntPolicyCmd(vc *cmdutils.VerbCmd, use, short string, getter func(admin bool, applied bool) (int, error)) { + var global bool + var applied bool + vc.SetDescription(use, short, short, "", use) + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + value, err := getter(global, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, value, "%d\n", value) + }, "the topic name is not specified or the topic name is specified more than one") +} + +func setIntPolicyCmd(vc *cmdutils.VerbCmd, use, short, flagName string, setter func(global bool, value int) error) { + var global bool + var value int + vc.SetDescription(use, short, short, "", use) + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("Policy", func(set *pflag.FlagSet) { + set.IntVarP(&value, flagName, "m", 0, flagName) + }) + vc.SetRunFuncWithNameArg(func() error { + err := setter(global, value) + if err == nil { + vc.Command.Printf("%s successfully for [%s]\n", short, vc.NameArg) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func removePolicyCmd(vc *cmdutils.VerbCmd, use, short string, remover func(global bool) error) { + var global bool + vc.SetDescription(use, short, short, "", use) + addScopeFlags(vc, &global, nil) + vc.SetRunFuncWithNameArg(func() error { + err := remover(global) + if err == nil { + vc.Command.Printf("%s successfully for [%s]\n", short, vc.NameArg) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func GetMaxMessageSizeCmd(vc *cmdutils.VerbCmd) { + getIntPolicyCmd(vc, "get-max-message-size", "Get max message size for a topic", func(global bool, applied bool) (int, error) { + topic, err := topicName(vc) + if err != nil { + return 0, err + } + policies, err := topicPolicies(global) + if err != nil { + return 0, err + } + value, err := policies.GetMaxMessageSize(vc.Command.Context(), *topic, applied) + if err != nil { + return 0, err + } + if value == nil { + return -1, nil + } + return *value, nil + }) +} + +func SetMaxMessageSizeCmd(vc *cmdutils.VerbCmd) { + setIntPolicyCmd(vc, "set-max-message-size", "Set max message size for a topic", "max-message-size", func(global bool, value int) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.SetMaxMessageSize(vc.Command.Context(), *topic, value) + }) +} + +func RemoveMaxMessageSizeCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-max-message-size", "Removed max message size for a topic", func(global bool) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.RemoveMaxMessageSize(vc.Command.Context(), *topic) + }) +} + +func GetMaxSubscriptionsPerTopicCmd(vc *cmdutils.VerbCmd) { + getIntPolicyCmd(vc, "get-max-subscriptions-per-topic", "Get max subscriptions per topic", func(global bool, applied bool) (int, error) { + topic, err := topicName(vc) + if err != nil { + return 0, err + } + policies, err := topicPolicies(global) + if err != nil { + return 0, err + } + value, err := policies.GetMaxSubscriptionsPerTopic(vc.Command.Context(), *topic, applied) + if err != nil { + return 0, err + } + if value == nil { + return -1, nil + } + return *value, nil + }) +} + +func SetMaxSubscriptionsPerTopicCmd(vc *cmdutils.VerbCmd) { + setIntPolicyCmd(vc, "set-max-subscriptions-per-topic", "Set max subscriptions per topic", "max-subscriptions-per-topic", func(global bool, value int) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.SetMaxSubscriptionsPerTopic(vc.Command.Context(), *topic, value) + }) +} + +func RemoveMaxSubscriptionsPerTopicCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-max-subscriptions-per-topic", "Removed max subscriptions per topic", func(global bool) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.RemoveMaxSubscriptionsPerTopic(vc.Command.Context(), *topic) + }) +} + +func GetDeduplicationSnapshotIntervalCmd(vc *cmdutils.VerbCmd) { + getIntPolicyCmd(vc, "get-deduplication-snapshot-interval", "Get deduplication snapshot interval", func(global bool, applied bool) (int, error) { + topic, err := topicName(vc) + if err != nil { + return 0, err + } + policies, err := topicPolicies(global) + if err != nil { + return 0, err + } + value, err := policies.GetDeduplicationSnapshotInterval(vc.Command.Context(), *topic, applied) + if err != nil { + return 0, err + } + if value == nil { + return -1, nil + } + return *value, nil + }) +} + +func SetDeduplicationSnapshotIntervalCmd(vc *cmdutils.VerbCmd) { + setIntPolicyCmd(vc, "set-deduplication-snapshot-interval", "Set deduplication snapshot interval", "interval", func(global bool, value int) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.SetDeduplicationSnapshotInterval(vc.Command.Context(), *topic, value) + }) +} + +func RemoveDeduplicationSnapshotIntervalCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-deduplication-snapshot-interval", "Removed deduplication snapshot interval", func(global bool) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.RemoveDeduplicationSnapshotInterval(vc.Command.Context(), *topic) + }) +} + +func GetReplicatorDispatchRateCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-replicator-dispatch-rate", "Get replicator dispatch rate for a topic", "Get replicator dispatch rate for a topic", "") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + rate, err := policies.GetReplicatorDispatchRate(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, rate, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetReplicatorDispatchRateCmd(vc *cmdutils.VerbCmd) { + var global bool + data := utils.DispatchRateData{} + vc.SetDescription("set-replicator-dispatch-rate", "Set replicator dispatch rate for a topic", "Set replicator dispatch rate for a topic", "") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("ReplicatorDispatchRate", func(set *pflag.FlagSet) { + set.Int64VarP(&data.DispatchThrottlingRateInMsg, "msg-dispatch-rate", "", -1, "message dispatch rate") + set.Int64VarP(&data.DispatchThrottlingRateInByte, "byte-dispatch-rate", "", -1, "byte dispatch rate") + set.Int64VarP(&data.RatePeriodInSecond, "dispatch-rate-period", "", 1, "dispatch rate period in seconds") + set.BoolVarP(&data.RelativeToPublishRate, "relative-to-publish-rate", "", false, "relative to publish rate") + }) + vc.SetRunFuncWithNameArg(func() error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + err = policies.SetReplicatorDispatchRate(vc.Command.Context(), *topic, data) + if err == nil { + vc.Command.Printf("Set replicator dispatch rate successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveReplicatorDispatchRateCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-replicator-dispatch-rate", "Removed replicator dispatch rate", func(global bool) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.RemoveReplicatorDispatchRate(vc.Command.Context(), *topic) + }) +} diff --git a/pkg/ctl/topicpolicies/offload_policies.go b/pkg/ctl/topicpolicies/offload_policies.go new file mode 100644 index 000000000..be8e72920 --- /dev/null +++ b/pkg/ctl/topicpolicies/offload_policies.go @@ -0,0 +1,99 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetOffloadPoliciesCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-offload-policies", "Get offload policies", "Get offload policies", "") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + value, err := policies.GetOffloadPolicies(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, value, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetOffloadPoliciesCmd(vc *cmdutils.VerbCmd) { + var global bool + policy := utils.NewOffloadPolicies() + vc.SetDescription("set-offload-policies", "Set offload policies", "Set offload policies", "") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("OffloadPolicies", func(set *pflag.FlagSet) { + set.StringVarP(&policy.ManagedLedgerOffloadDriver, "driver", "", "", "offload driver") + set.IntVarP(&policy.ManagedLedgerOffloadMaxThreads, "max-threads", "", 2, "max offload threads") + set.Int64VarP(&policy.ManagedLedgerOffloadThresholdInBytes, "threshold-bytes", "", -1, "offload threshold in bytes") + set.Int64VarP(&policy.ManagedLedgerOffloadDeletionLagInMillis, "deletion-lag-millis", "", 14400000, "offload deletion lag in millis") + set.Int64VarP(&policy.ManagedLedgerOffloadAutoTriggerSizeThresholdBytes, "auto-trigger-size-threshold-bytes", "", -1, "auto trigger size threshold bytes") + set.StringVarP(&policy.S3ManagedLedgerOffloadBucket, "bucket", "", "", "S3 bucket") + set.StringVarP(&policy.S3ManagedLedgerOffloadRegion, "region", "", "", "S3 region") + set.StringVarP(&policy.S3ManagedLedgerOffloadServiceEndpoint, "service-endpoint", "", "", "S3 service endpoint") + set.StringVarP(&policy.S3ManagedLedgerOffloadCredentialID, "credential-id", "", "", "credential id") + set.StringVarP(&policy.S3ManagedLedgerOffloadCredentialSecret, "credential-secret", "", "", "credential secret") + set.StringVarP(&policy.S3ManagedLedgerOffloadRole, "role", "", "", "S3 role") + set.StringVarP(&policy.S3ManagedLedgerOffloadRoleSessionName, "role-session-name", "", "", "S3 role session name") + set.StringVarP(&policy.OffloadersDirectory, "offloaders-directory", "", "", "offloaders directory") + set.StringToStringVarP(&policy.ManagedLedgerOffloadDriverMetadata, "driver-metadata", "", nil, "driver metadata in key=value,key2=value2 format") + }) + vc.SetRunFuncWithNameArg(func() error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + err = policies.SetOffloadPolicies(vc.Command.Context(), *topic, *policy) + if err == nil { + vc.Command.Printf("Set offload policies successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveOffloadPoliciesCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-offload-policies", "Removed offload policies", func(global bool) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.RemoveOffloadPolicies(vc.Command.Context(), *topic) + }) +} diff --git a/pkg/ctl/topicpolicies/rate_policies.go b/pkg/ctl/topicpolicies/rate_policies.go new file mode 100644 index 000000000..3f70c00ad --- /dev/null +++ b/pkg/ctl/topicpolicies/rate_policies.go @@ -0,0 +1,232 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetDispatchRateCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-dispatch-rate", "Get message dispatch rate for a topic", "Get message dispatch rate for a topic", "", "get-dispatch-rate") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + rate, err := policies.GetDispatchRate(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, rate, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetDispatchRateCmd(vc *cmdutils.VerbCmd) { + var global bool + data := utils.DispatchRateData{} + vc.SetDescription("set-dispatch-rate", "Set message dispatch rate for a topic", "Set message dispatch rate for a topic", "", "set-dispatch-rate") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("DispatchRate", func(set *pflag.FlagSet) { + set.Int64VarP(&data.DispatchThrottlingRateInMsg, "msg-dispatch-rate", "", -1, "message dispatch rate") + set.Int64VarP(&data.DispatchThrottlingRateInByte, "byte-dispatch-rate", "", -1, "byte dispatch rate") + set.Int64VarP(&data.RatePeriodInSecond, "dispatch-rate-period", "", 1, "dispatch rate period in seconds") + set.BoolVarP(&data.RelativeToPublishRate, "relative-to-publish-rate", "", false, "relative to publish rate") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = policies.SetDispatchRate(vc.Command.Context(), *topic, data) + if err == nil { + vc.Command.Printf("Set dispatch rate successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveDispatchRateCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-dispatch-rate", "Removed dispatch rate for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveDispatchRate(vc.Command.Context(), *topic) + }) +} + +func GetSubscriptionDispatchRateCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-subscription-dispatch-rate", "Get subscription dispatch rate for a topic", "Get subscription dispatch rate for a topic", "", "get-subscription-dispatch-rate") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + rate, err := policies.GetSubscriptionDispatchRate(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, rate, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetSubscriptionDispatchRateCmd(vc *cmdutils.VerbCmd) { + var global bool + data := utils.DispatchRateData{} + vc.SetDescription("set-subscription-dispatch-rate", "Set subscription dispatch rate for a topic", "Set subscription dispatch rate for a topic", "", "set-subscription-dispatch-rate") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("SubscriptionDispatchRate", func(set *pflag.FlagSet) { + set.Int64VarP(&data.DispatchThrottlingRateInMsg, "msg-dispatch-rate", "", -1, "message dispatch rate") + set.Int64VarP(&data.DispatchThrottlingRateInByte, "byte-dispatch-rate", "", -1, "byte dispatch rate") + set.Int64VarP(&data.RatePeriodInSecond, "dispatch-rate-period", "", 1, "dispatch rate period in seconds") + set.BoolVarP(&data.RelativeToPublishRate, "relative-to-publish-rate", "", false, "relative to publish rate") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = policies.SetSubscriptionDispatchRate(vc.Command.Context(), *topic, data) + if err == nil { + vc.Command.Printf("Set subscription dispatch rate successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveSubscriptionDispatchRateCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-subscription-dispatch-rate", "Removed subscription dispatch rate for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveSubscriptionDispatchRate(vc.Command.Context(), *topic) + }) +} + +func GetPublishRateCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-publish-rate", "Get publish rate for a topic", "Get publish rate for a topic", "", "get-publish-rate") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + rate, err := policies.GetPublishRate(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, rate, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetPublishRateCmd(vc *cmdutils.VerbCmd) { + var global bool + data := utils.PublishRateData{} + vc.SetDescription("set-publish-rate", "Set publish rate for a topic", "Set publish rate for a topic", "", "set-publish-rate") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("PublishRate", func(set *pflag.FlagSet) { + set.Int64VarP(&data.PublishThrottlingRateInMsg, "msg-publish-rate", "", -1, "message publish rate") + set.Int64VarP(&data.PublishThrottlingRateInByte, "byte-publish-rate", "", -1, "byte publish rate") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = policies.SetPublishRate(vc.Command.Context(), *topic, data) + if err == nil { + vc.Command.Printf("Set publish rate successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemovePublishRateCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-publish-rate", "Removed publish rate for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemovePublishRate(vc.Command.Context(), *topic) + }) +} + +func GetSubscribeRateCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-subscribe-rate", "Get subscribe rate for a topic", "Get subscribe rate for a topic", "", "get-subscribe-rate") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + rate, err := policies.GetSubscribeRate(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, rate, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetSubscribeRateCmd(vc *cmdutils.VerbCmd) { + var global bool + data := *utils.NewSubscribeRate() + vc.SetDescription("set-subscribe-rate", "Set subscribe rate for a topic", "Set subscribe rate for a topic", "", "set-subscribe-rate") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("SubscribeRate", func(set *pflag.FlagSet) { + set.IntVarP(&data.SubscribeThrottlingRatePerConsumer, "subscribe-rate", "m", -1, "message dispatch rate") + set.IntVarP(&data.RatePeriodInSecond, "period", "p", 30, "dispatch rate period") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = policies.SetSubscribeRate(vc.Command.Context(), *topic, data) + if err == nil { + vc.Command.Printf("Set subscribe rate successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveSubscribeRateCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-subscribe-rate", "Removed subscribe rate for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveSubscribeRate(vc.Command.Context(), *topic) + }) +} diff --git a/pkg/ctl/topicpolicies/replication_clusters.go b/pkg/ctl/topicpolicies/replication_clusters.go new file mode 100644 index 000000000..8fa70a196 --- /dev/null +++ b/pkg/ctl/topicpolicies/replication_clusters.go @@ -0,0 +1,157 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "errors" + "strings" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetReplicationClustersCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-replication-clusters", "Get replication clusters for a topic", "Get replication clusters for a topic", "") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + clusters, err := policies.GetReplicationClusters(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, clusters, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetReplicationClustersCmd(vc *cmdutils.VerbCmd) { + var global bool + var clusterIDs string + vc.SetDescription("set-replication-clusters", "Set replication clusters for a topic", "Set replication clusters for a topic", "") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("ReplicationClusters", func(set *pflag.FlagSet) { + set.StringVarP(&clusterIDs, "clusters", "c", "", "comma separated cluster names") + }) + vc.SetRunFuncWithNameArg(func() error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + err = policies.SetReplicationClusters(vc.Command.Context(), *topic, strings.Split(clusterIDs, ",")) + if err == nil { + vc.Command.Printf("Set replication clusters successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveReplicationClustersCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-replication-clusters", "Removed replication clusters", func(global bool) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.RemoveReplicationClusters(vc.Command.Context(), *topic) + }) +} + +func GetAutoSubscriptionCreationCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-auto-subscription-creation", "Get auto subscription creation policy", "Get auto subscription creation policy", "") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + override, err := policies.GetAutoSubscriptionCreation(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + return writePolicyOutput(vc, override, "") + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetAutoSubscriptionCreationCmd(vc *cmdutils.VerbCmd) { + var global bool + var enable bool + var disable bool + vc.SetDescription("set-auto-subscription-creation", "Set auto subscription creation policy", "Set auto subscription creation policy", "") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("AutoSubscriptionCreation", func(set *pflag.FlagSet) { + set.BoolVarP(&enable, "enable", "e", false, "enable auto subscription creation") + set.BoolVarP(&disable, "disable", "d", false, "disable auto subscription creation") + }) + vc.SetRunFuncWithNameArg(func() error { + topic, err := topicName(vc) + if err != nil { + return err + } + if enable == disable { + return errors.New("need to specify either --enable or --disable") + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + override := utils.AutoSubscriptionCreationOverride{AllowAutoSubscriptionCreation: enable} + err = policies.SetAutoSubscriptionCreation(vc.Command.Context(), *topic, override) + if err == nil { + vc.Command.Printf("Set auto subscription creation successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveAutoSubscriptionCreationCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-auto-subscription-creation", "Removed auto subscription creation policy", func(global bool) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.RemoveAutoSubscriptionCreation(vc.Command.Context(), *topic) + }) +} diff --git a/pkg/ctl/topicpolicies/scalar_policies.go b/pkg/ctl/topicpolicies/scalar_policies.go new file mode 100644 index 000000000..ab26e4f14 --- /dev/null +++ b/pkg/ctl/topicpolicies/scalar_policies.go @@ -0,0 +1,406 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "context" + + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/admin" + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" + ctlutils "github.com/streamnative/pulsarctl/pkg/ctl/utils" +) + +func GetMessageTTLCmd(vc *cmdutils.VerbCmd) { + getOptionalIntPolicyCmd( + vc, + "get-message-ttl", + "Get message TTL for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, applied bool) (*int, error) { + return policies.GetMessageTTL(ctx, topic, applied) + }, + ) +} + +func SetMessageTTLCmd(vc *cmdutils.VerbCmd) { + var global bool + var ttl string + vc.SetDescription("set-message-ttl", "Set message TTL for a topic", "Set message TTL for a topic", "", "set-message-ttl") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("MessageTTL", func(set *pflag.FlagSet) { + set.StringVarP(&ttl, "ttl", "t", "", "message TTL for topic with optional time unit suffix") + _ = cobra.MarkFlagRequired(set, "ttl") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + seconds, err := ctlutils.ParseRelativeTimeInSeconds(ttl) + if err != nil { + return err + } + messageTTL := -1 + if seconds != -1 { + messageTTL = int(seconds.Seconds()) + } + err = policies.SetMessageTTL(vc.Command.Context(), *topic, messageTTL) + if err == nil { + vc.Command.Printf("Set message TTL successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveMessageTTLCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-message-ttl", "Removed message TTL for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveMessageTTL(vc.Command.Context(), *topic) + }) +} + +func GetMaxUnackMessagesPerConsumerCmd(vc *cmdutils.VerbCmd) { + getOptionalIntPolicyCmd( + vc, + "get-max-unacked-messages-per-consumer", + "Get max unacked messages per consumer for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, applied bool) (*int, error) { + return policies.GetMaxUnackMessagesPerConsumer(ctx, topic, applied) + }, + ) +} + +func SetMaxUnackMessagesPerConsumerCmd(vc *cmdutils.VerbCmd) { + var global bool + var maxNum int + vc.SetDescription("set-max-unacked-messages-per-consumer", "Set max unacked messages per consumer for a topic", "Set max unacked messages per consumer for a topic", "", "set-max-unacked-messages-per-consumer") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("MaxUnackedMessagesPerConsumer", func(set *pflag.FlagSet) { + set.IntVarP(&maxNum, "maxNum", "m", 0, "max unacked messages num on consumer") + _ = cobra.MarkFlagRequired(set, "maxNum") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = policies.SetMaxUnackMessagesPerConsumer(vc.Command.Context(), *topic, maxNum) + if err == nil { + vc.Command.Printf("Set max unacked messages per consumer successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveMaxUnackMessagesPerConsumerCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-max-unacked-messages-per-consumer", "Removed max unacked messages per consumer for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveMaxUnackMessagesPerConsumer(vc.Command.Context(), *topic) + }) +} + +func GetMaxUnackMessagesPerSubscriptionCmd(vc *cmdutils.VerbCmd) { + getOptionalIntPolicyCmd( + vc, + "get-max-unacked-messages-per-subscription", + "Get max unacked messages per subscription for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, applied bool) (*int, error) { + return policies.GetMaxUnackMessagesPerSubscription(ctx, topic, applied) + }, + ) +} + +func SetMaxUnackMessagesPerSubscriptionCmd(vc *cmdutils.VerbCmd) { + var global bool + var maxNum int + vc.SetDescription("set-max-unacked-messages-per-subscription", "Set max unacked messages per subscription for a topic", "Set max unacked messages per subscription for a topic", "", "set-max-unacked-messages-per-subscription") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("MaxUnackedMessagesPerSubscription", func(set *pflag.FlagSet) { + set.IntVarP(&maxNum, "maxNum", "m", 0, "max unacked messages num on subscription") + _ = cobra.MarkFlagRequired(set, "maxNum") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = policies.SetMaxUnackMessagesPerSubscription(vc.Command.Context(), *topic, maxNum) + if err == nil { + vc.Command.Printf("Set max unacked messages per subscription successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveMaxUnackMessagesPerSubscriptionCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-max-unacked-messages-per-subscription", "Removed max unacked messages per subscription for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveMaxUnackMessagesPerSubscription(vc.Command.Context(), *topic) + }) +} + +func GetMaxConsumersPerSubscriptionCmd(vc *cmdutils.VerbCmd) { + getOptionalIntPolicyCmd( + vc, + "get-max-consumers-per-subscription", + "Get max consumers per subscription for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, applied bool) (*int, error) { + return policies.GetMaxConsumersPerSubscription(ctx, topic, applied) + }, + ) +} + +func SetMaxConsumersPerSubscriptionCmd(vc *cmdutils.VerbCmd) { + var global bool + var maxConsumers int + vc.SetDescription("set-max-consumers-per-subscription", "Set max consumers per subscription for a topic", "Set max consumers per subscription for a topic", "", "set-max-consumers-per-subscription") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("MaxConsumersPerSubscription", func(set *pflag.FlagSet) { + set.IntVarP(&maxConsumers, "max-consumers-per-subscription", "c", 0, "max consumers per subscription for a topic") + _ = cobra.MarkFlagRequired(set, "max-consumers-per-subscription") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = policies.SetMaxConsumersPerSubscription(vc.Command.Context(), *topic, maxConsumers) + if err == nil { + vc.Command.Printf("Set max consumers per subscription successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveMaxConsumersPerSubscriptionCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-max-consumers-per-subscription", "Removed max consumers per subscription for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveMaxConsumersPerSubscription(vc.Command.Context(), *topic) + }) +} + +func GetMaxConsumersCmd(vc *cmdutils.VerbCmd) { + getOptionalIntPolicyCmd( + vc, + "get-max-consumers", + "Get max consumers for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, applied bool) (*int, error) { + return policies.GetMaxConsumers(ctx, topic, applied) + }, + ) +} + +func SetMaxConsumersCmd(vc *cmdutils.VerbCmd) { + var global bool + var maxConsumers int + vc.SetDescription("set-max-consumers", "Set max consumers for a topic", "Set max consumers for a topic", "", "set-max-consumers") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("MaxConsumers", func(set *pflag.FlagSet) { + set.IntVarP(&maxConsumers, "max-consumers", "c", 0, "max consumers for a topic") + _ = cobra.MarkFlagRequired(set, "max-consumers") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = policies.SetMaxConsumers(vc.Command.Context(), *topic, maxConsumers) + if err == nil { + vc.Command.Printf("Set max consumers successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveMaxConsumersCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-max-consumers", "Removed max consumers for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveMaxConsumers(vc.Command.Context(), *topic) + }) +} + +func GetMaxProducersCmd(vc *cmdutils.VerbCmd) { + getOptionalIntPolicyCmd( + vc, + "get-max-producers", + "Get max producers for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, applied bool) (*int, error) { + return policies.GetMaxProducers(ctx, topic, applied) + }, + ) +} + +func SetMaxProducersCmd(vc *cmdutils.VerbCmd) { + var global bool + var maxProducers int + vc.SetDescription("set-max-producers", "Set max producers for a topic", "Set max producers for a topic", "", "set-max-producers") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("MaxProducers", func(set *pflag.FlagSet) { + set.IntVarP(&maxProducers, "max-producers", "p", 0, "max producers for a topic") + _ = cobra.MarkFlagRequired(set, "max-producers") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + err = policies.SetMaxProducers(vc.Command.Context(), *topic, maxProducers) + if err == nil { + vc.Command.Printf("Set max producers successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveMaxProducersCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-max-producers", "Removed max producers for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveMaxProducers(vc.Command.Context(), *topic) + }) +} + +func GetCompactionThresholdCmd(vc *cmdutils.VerbCmd) { + getOptionalInt64PolicyCmd( + vc, + "get-compaction-threshold", + "Get compaction threshold for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, applied bool) (*int64, error) { + return policies.GetCompactionThreshold(ctx, topic, applied) + }, + ) +} + +func SetCompactionThresholdCmd(vc *cmdutils.VerbCmd) { + var global bool + var threshold string + vc.SetDescription("set-compaction-threshold", "Set compaction threshold for a topic", "Set compaction threshold for a topic", "", "set-compaction-threshold") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("CompactionThreshold", func(set *pflag.FlagSet) { + set.StringVarP(&threshold, "threshold", "t", "", "maximum backlog before compaction is triggered") + _ = cobra.MarkFlagRequired(set, "threshold") + }) + vc.SetRunFuncWithNameArg(func() error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + value, err := ctlutils.ValidateSizeString(threshold) + if err != nil { + return err + } + err = policies.SetCompactionThreshold(vc.Command.Context(), *topic, value) + if err == nil { + vc.Command.Printf("Set compaction threshold successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveCompactionThresholdCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-compaction-threshold", "Removed compaction threshold for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveCompactionThreshold(vc.Command.Context(), *topic) + }) +} + +func GetDeduplicationCmd(vc *cmdutils.VerbCmd) { + getOptionalBoolPolicyCmd( + vc, + "get-deduplication", + "Get deduplication status for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, applied bool) (*bool, error) { + return policies.GetDeduplicationStatus(ctx, topic, applied) + }, + ) +} + +func SetDeduplicationCmd(vc *cmdutils.VerbCmd) { + setEnableDisablePolicyCmd( + vc, + "set-deduplication", + "Set deduplication status for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, enabled bool) error { + return policies.SetDeduplicationStatus(ctx, topic, enabled) + }, + ) +} + +func RemoveDeduplicationCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-deduplication", "Removed deduplication status for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveDeduplicationStatus(vc.Command.Context(), *topic) + }) +} + +func GetSchemaValidationEnforcedCmd(vc *cmdutils.VerbCmd) { + getOptionalBoolPolicyCmd( + vc, + "get-schema-validation-enforced", + "Get schema validation enforced for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, applied bool) (*bool, error) { + return policies.GetSchemaValidationEnforced(ctx, topic, applied) + }, + ) +} + +func SetSchemaValidationEnforcedCmd(vc *cmdutils.VerbCmd) { + setEnableDisablePolicyCmd( + vc, + "set-schema-validation-enforced", + "Set schema validation enforced for a topic", + func(ctx context.Context, policies admin.TopicPolicies, topic utils.TopicName, enabled bool) error { + return policies.SetSchemaValidationEnforced(ctx, topic, enabled) + }, + ) +} + +func RemoveSchemaValidationEnforcedCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-schema-validation-enforced", "Removed schema validation enforced for a topic", func(global bool) error { + policies, topic, err := topicPolicyResources(vc, global) + if err != nil { + return err + } + return policies.RemoveSchemaValidationEnforced(vc.Command.Context(), *topic) + }) +} diff --git a/pkg/ctl/topicpolicies/schema_compatibility_strategy.go b/pkg/ctl/topicpolicies/schema_compatibility_strategy.go new file mode 100644 index 000000000..5c80b38ff --- /dev/null +++ b/pkg/ctl/topicpolicies/schema_compatibility_strategy.go @@ -0,0 +1,93 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils" + "github.com/spf13/pflag" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func GetSchemaCompatibilityStrategyCmd(vc *cmdutils.VerbCmd) { + var global bool + var applied bool + vc.SetDescription("get-schema-compatibility-strategy", "Get schema compatibility strategy", "Get schema compatibility strategy", "") + addScopeFlags(vc, &global, &applied) + vc.EnableOutputFlagSet() + vc.SetRunFuncWithNameArg(func() error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + value, err := policies.GetSchemaCompatibilityStrategy(vc.Command.Context(), *topic, applied) + if err != nil { + return err + } + if value == nil { + return writePolicyOutput(vc, nil, "") + } + return writePolicyOutput(vc, value.String(), "%s\n", value.String()) + }, "the topic name is not specified or the topic name is specified more than one") +} + +func SetSchemaCompatibilityStrategyCmd(vc *cmdutils.VerbCmd) { + var global bool + var strategy string + vc.SetDescription("set-schema-compatibility-strategy", "Set schema compatibility strategy", "Set schema compatibility strategy", "") + addScopeFlags(vc, &global, nil) + vc.FlagSetGroup.InFlagSet("SchemaCompatibilityStrategy", func(set *pflag.FlagSet) { + set.StringVarP(&strategy, "compatibility", "c", "", "schema compatibility strategy") + }) + vc.SetRunFuncWithNameArg(func() error { + topic, err := topicName(vc) + if err != nil { + return err + } + parsed, err := utils.ParseSchemaCompatibilityStrategy(strategy) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + err = policies.SetSchemaCompatibilityStrategy(vc.Command.Context(), *topic, parsed) + if err == nil { + vc.Command.Printf("Set schema compatibility strategy successfully for [%s]\n", topic.String()) + } + return err + }, "the topic name is not specified or the topic name is specified more than one") +} + +func RemoveSchemaCompatibilityStrategyCmd(vc *cmdutils.VerbCmd) { + removePolicyCmd(vc, "remove-schema-compatibility-strategy", "Removed schema compatibility strategy", func(global bool) error { + topic, err := topicName(vc) + if err != nil { + return err + } + policies, err := topicPolicies(global) + if err != nil { + return err + } + return policies.RemoveSchemaCompatibilityStrategy(vc.Command.Context(), *topic) + }) +} diff --git a/pkg/ctl/topicpolicies/schema_validation_enforced_test.go b/pkg/ctl/topicpolicies/schema_validation_enforced_test.go new file mode 100644 index 000000000..6ad5ae192 --- /dev/null +++ b/pkg/ctl/topicpolicies/schema_validation_enforced_test.go @@ -0,0 +1,41 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTopicPoliciesSetSchemaValidationEnforcedValidation(t *testing.T) { + _, execErr, _, _ := TestTopicPoliciesCommands(SetSchemaValidationEnforcedCmd, []string{ + "set-schema-validation-enforced", + "persistent://public/default/test", + }) + assert.NotNil(t, execErr) + assert.Equal(t, "need to specify either --enable or --disable", execErr.Error()) +} + +func TestTopicPoliciesRemoveSchemaValidationEnforcedNameError(t *testing.T) { + _, _, nameErr, _ := TestTopicPoliciesCommands(RemoveSchemaValidationEnforcedCmd, []string{ + "remove-schema-validation-enforced", + }) + assert.NotNil(t, nameErr) + assert.Equal(t, "the topic name is not specified or the topic name is specified more than one", nameErr.Error()) +} diff --git a/pkg/ctl/topicpolicies/test_help.go b/pkg/ctl/topicpolicies/test_help.go new file mode 100644 index 000000000..307b5bcdf --- /dev/null +++ b/pkg/ctl/topicpolicies/test_help.go @@ -0,0 +1,65 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "bytes" + + "github.com/kris-nova/logger" + "github.com/spf13/cobra" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func TestTopicPoliciesCommands(newVerb func(cmd *cmdutils.VerbCmd), args []string) (out *bytes.Buffer, execErr, nameErr, err error) { + var execError error + cmdutils.ExecErrorHandler = func(err error) { + execError = err + } + + var parsedNameError error + cmdutils.CheckNameArgError = func(err error) { + parsedNameError = err + } + + rootCmd := &cobra.Command{ + Use: "pulsarctl [command]", + Short: "a CLI for Apache Pulsar", + Run: func(cmd *cobra.Command, _ []string) { + if err := cmd.Help(); err != nil { + logger.Debug("ignoring error %q", err.Error()) + } + }, + } + + buf := new(bytes.Buffer) + rootCmd.SetOut(buf) + rootCmd.SetArgs(append([]string{"topic-policies"}, args...)) + + resourceCmd := cmdutils.NewResourceCmd( + "topic-policies", + "Operations about topic policies", + "", + "topic-policy", + ) + flagGrouping := cmdutils.NewGrouping() + cmdutils.AddVerbCmd(flagGrouping, resourceCmd, newVerb) + rootCmd.AddCommand(resourceCmd) + err = rootCmd.Execute() + + return buf, execError, parsedNameError, err +} diff --git a/pkg/ctl/topicpolicies/topic_policies.go b/pkg/ctl/topicpolicies/topic_policies.go new file mode 100644 index 000000000..71b99e3cb --- /dev/null +++ b/pkg/ctl/topicpolicies/topic_policies.go @@ -0,0 +1,115 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "github.com/spf13/cobra" + "github.com/streamnative/pulsarctl/pkg/cmdutils" +) + +func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { + resourceCmd := cmdutils.NewResourceCmd( + "topic-policies", + "Operations about topic policies", + "", + "topic-policy", + ) + + cmdutils.AddVerbCmds(flagGrouping, resourceCmd, + GetMessageTTLCmd, + SetMessageTTLCmd, + RemoveMessageTTLCmd, + GetMaxUnackMessagesPerConsumerCmd, + SetMaxUnackMessagesPerConsumerCmd, + RemoveMaxUnackMessagesPerConsumerCmd, + GetMaxConsumersPerSubscriptionCmd, + SetMaxConsumersPerSubscriptionCmd, + RemoveMaxConsumersPerSubscriptionCmd, + GetRetentionCmd, + SetRetentionCmd, + RemoveRetentionCmd, + GetBacklogQuotaCmd, + SetBacklogQuotaCmd, + RemoveBacklogQuotaCmd, + GetMaxProducersCmd, + SetMaxProducersCmd, + RemoveMaxProducersCmd, + GetDeduplicationCmd, + SetDeduplicationCmd, + RemoveDeduplicationCmd, + GetPersistenceCmd, + SetPersistenceCmd, + RemovePersistenceCmd, + GetSubscriptionDispatchRateCmd, + SetSubscriptionDispatchRateCmd, + RemoveSubscriptionDispatchRateCmd, + GetPublishRateCmd, + SetPublishRateCmd, + RemovePublishRateCmd, + GetCompactionThresholdCmd, + SetCompactionThresholdCmd, + RemoveCompactionThresholdCmd, + GetSubscribeRateCmd, + SetSubscribeRateCmd, + RemoveSubscribeRateCmd, + GetMaxConsumersCmd, + SetMaxConsumersCmd, + RemoveMaxConsumersCmd, + GetDelayedDeliveryCmd, + SetDelayedDeliveryCmd, + RemoveDelayedDeliveryCmd, + GetDispatchRateCmd, + SetDispatchRateCmd, + RemoveDispatchRateCmd, + GetMaxUnackMessagesPerSubscriptionCmd, + SetMaxUnackMessagesPerSubscriptionCmd, + RemoveMaxUnackMessagesPerSubscriptionCmd, + GetInactiveTopicPoliciesCmd, + SetInactiveTopicPoliciesCmd, + RemoveInactiveTopicPoliciesCmd, + GetMaxMessageSizeCmd, + SetMaxMessageSizeCmd, + RemoveMaxMessageSizeCmd, + GetMaxSubscriptionsPerTopicCmd, + SetMaxSubscriptionsPerTopicCmd, + RemoveMaxSubscriptionsPerTopicCmd, + GetSchemaValidationEnforcedCmd, + SetSchemaValidationEnforcedCmd, + RemoveSchemaValidationEnforcedCmd, + GetDeduplicationSnapshotIntervalCmd, + SetDeduplicationSnapshotIntervalCmd, + RemoveDeduplicationSnapshotIntervalCmd, + GetReplicatorDispatchRateCmd, + SetReplicatorDispatchRateCmd, + RemoveReplicatorDispatchRateCmd, + GetOffloadPoliciesCmd, + SetOffloadPoliciesCmd, + RemoveOffloadPoliciesCmd, + GetAutoSubscriptionCreationCmd, + SetAutoSubscriptionCreationCmd, + RemoveAutoSubscriptionCreationCmd, + GetSchemaCompatibilityStrategyCmd, + SetSchemaCompatibilityStrategyCmd, + RemoveSchemaCompatibilityStrategyCmd, + GetReplicationClustersCmd, + SetReplicationClustersCmd, + RemoveReplicationClustersCmd, + ) + + return resourceCmd +} diff --git a/pkg/ctl/topicpolicies/topic_policies_test.go b/pkg/ctl/topicpolicies/topic_policies_test.go new file mode 100644 index 000000000..1560c74f7 --- /dev/null +++ b/pkg/ctl/topicpolicies/topic_policies_test.go @@ -0,0 +1,49 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 topicpolicies + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTopicPoliciesNameError(t *testing.T) { + _, _, nameErr, _ := TestTopicPoliciesCommands(GetMessageTTLCmd, []string{"get-message-ttl"}) + assert.NotNil(t, nameErr) + assert.Equal(t, "the topic name is not specified or the topic name is specified more than one", nameErr.Error()) +} + +func TestTopicPoliciesRequiredFlag(t *testing.T) { + _, _, _, err := TestTopicPoliciesCommands(SetMessageTTLCmd, []string{"set-message-ttl", "persistent://public/default/test"}) + assert.NotNil(t, err) + assert.Equal(t, "required flag(s) \"ttl\" not set", err.Error()) +} + +func TestTopicPoliciesEnableDisableValidation(t *testing.T) { + _, execErr, _, _ := TestTopicPoliciesCommands(SetDeduplicationCmd, []string{"set-deduplication", "persistent://public/default/test"}) + assert.NotNil(t, execErr) + assert.Equal(t, "need to specify either --enable or --disable", execErr.Error()) +} + +func TestTopicPoliciesSetRetentionHelp(t *testing.T) { + assert.NotPanics(t, func() { + _, _, _, err := TestTopicPoliciesCommands(SetRetentionCmd, []string{"set-retention", "--help"}) + assert.NoError(t, err) + }) +} diff --git a/pkg/pulsarctl.go b/pkg/pulsarctl.go index 171bb20a7..d4eecd979 100644 --- a/pkg/pulsarctl.go +++ b/pkg/pulsarctl.go @@ -36,6 +36,7 @@ import ( "github.com/streamnative/pulsarctl/pkg/ctl/tenant" "github.com/streamnative/pulsarctl/pkg/ctl/token" "github.com/streamnative/pulsarctl/pkg/ctl/topic" + "github.com/streamnative/pulsarctl/pkg/ctl/topicpolicies" "github.com/streamnative/pulsarctl/pkg/oauth2" function "github.com/streamnative/pulsarctl/pkg/ctl/functions" @@ -119,6 +120,7 @@ func NewPulsarctlCmd() *cobra.Command { rootCmd.AddCommand(source.Command(flagGrouping)) rootCmd.AddCommand(sink.Command(flagGrouping)) rootCmd.AddCommand(topic.Command(flagGrouping)) + rootCmd.AddCommand(topicpolicies.Command(flagGrouping)) rootCmd.AddCommand(namespace.Command(flagGrouping)) rootCmd.AddCommand(schema.Command(flagGrouping)) rootCmd.AddCommand(subscription.Command(flagGrouping)) diff --git a/pkg/pulsarctl_test.go b/pkg/pulsarctl_test.go new file mode 100644 index 000000000..9160b689a --- /dev/null +++ b/pkg/pulsarctl_test.go @@ -0,0 +1,67 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 pkg + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewPulsarctlCmdBuildsAndShowsHelp(t *testing.T) { + var cmd = NewPulsarctlCmd() + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stderr) + cmd.SetArgs([]string{"topic-policies", "set-retention", "--help"}) + + require.NotPanics(t, func() { + require.NoError(t, cmd.Execute()) + }) +} + +func TestTopicPoliciesHelpShowsSchemaValidationEnforced(t *testing.T) { + var cmd = NewPulsarctlCmd() + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stderr) + cmd.SetArgs([]string{"topic-policies", "--help"}) + + require.NoError(t, cmd.Execute()) + require.Contains(t, stdout.String(), "get-schema-validation-enforced") + require.Contains(t, stdout.String(), "set-schema-validation-enforced") + require.Contains(t, stdout.String(), "remove-schema-validation-enforced") +} + +func TestTopicsHelpShowsRemoveSchemaValidationEnforce(t *testing.T) { + var cmd = NewPulsarctlCmd() + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stderr) + cmd.SetArgs([]string{"topics", "--help"}) + + require.NoError(t, cmd.Execute()) + require.Contains(t, stdout.String(), "remove-schema-validation-enforce") +} diff --git a/site/gen-pulsarctldocs/generators/v1_1/toc.yaml b/site/gen-pulsarctldocs/generators/v1_1/toc.yaml index 651f00382..c8d894320 100644 --- a/site/gen-pulsarctldocs/generators/v1_1/toc.yaml +++ b/site/gen-pulsarctldocs/generators/v1_1/toc.yaml @@ -25,6 +25,7 @@ categories: - completion - functions - namespaces + - topic-policies - schemas - sinks - sources