diff --git a/MODULE.bazel b/MODULE.bazel index efc7ee10f30ae..b2990f5c61dc4 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -175,6 +175,8 @@ use_repo( "build_buf_gen_go_redpandadata_common_protocolbuffers_go", "build_buf_gen_go_redpandadata_dataplane_connectrpc_go", "build_buf_gen_go_redpandadata_dataplane_protocolbuffers_go", + "build_buf_gen_go_redpandadata_gatekeeper_connectrpc_go", + "build_buf_gen_go_redpandadata_gatekeeper_protocolbuffers_go", "com_connectrpc_connect", "com_github_actgardner_gogen_avro_v10", "com_github_alecaivazis_survey_v2", diff --git a/src/go/rpk/go.mod b/src/go/rpk/go.mod index 684414c564c6f..de2c266d0ea42 100644 --- a/src/go/rpk/go.mod +++ b/src/go/rpk/go.mod @@ -11,8 +11,10 @@ require ( buf.build/gen/go/redpandadata/common/protocolbuffers/go v1.35.1-20240917150400-3f349e63f44a.1 buf.build/gen/go/redpandadata/dataplane/connectrpc/go v1.17.0-20241112225414-3759fedba3f3.1 buf.build/gen/go/redpandadata/dataplane/protocolbuffers/go v1.35.1-20241112225414-3759fedba3f3.1 + buf.build/gen/go/redpandadata/gatekeeper/connectrpc/go v1.18.0-20241209180130-05cf059c71c1.1 + buf.build/gen/go/redpandadata/gatekeeper/protocolbuffers/go v1.36.2-20241209180130-05cf059c71c1.1 cloud.google.com/go/compute/metadata v0.5.2 - connectrpc.com/connect v1.17.0 + connectrpc.com/connect v1.18.0 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/avast/retry-go v3.0.0+incompatible github.com/aws/aws-sdk-go v1.55.5 @@ -61,7 +63,7 @@ require ( golang.org/x/sync v0.8.0 golang.org/x/sys v0.28.0 golang.org/x/term v0.25.0 - google.golang.org/protobuf v1.35.1 + google.golang.org/protobuf v1.36.2 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.2 k8s.io/apimachinery v0.31.2 @@ -69,7 +71,7 @@ require ( ) require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.2-20240920164238-5a7b106cbb87.1 // indirect buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.35.1-20240617172850-a48fcebcf8f1.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect diff --git a/src/go/rpk/go.sum b/src/go/rpk/go.sum index 7fba1eb882b25..7e3bae2fb0de8 100644 --- a/src/go/rpk/go.sum +++ b/src/go/rpk/go.sum @@ -1,5 +1,5 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1 h1:9wP6ZZYWnF2Z0TxmII7m3XNykxnP4/w8oXeth6ekcRI= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1/go.mod h1:Duw/9JoXkXIydyASnLYIiufkzySThoqavOsF+IihqvM= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.2-20240920164238-5a7b106cbb87.1 h1:laCIQalEieFOxgzV19GyoOXwrdKjZhn7zFXt3YNkeAc= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.2-20240920164238-5a7b106cbb87.1/go.mod h1:JnMVLi3qrNYPODVpEKG7UjHLl/d2zR221e66YCSmP2Q= buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.35.1-20240617172850-a48fcebcf8f1.1 h1:56K2aAfywpsJln2seD16Sfp2NJy7kYH8q3bxwbvR3J8= buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.35.1-20240617172850-a48fcebcf8f1.1/go.mod h1:Gob4yM1VtJ2LFWFjGhPVK32vpn1ftYpKEr72JEqRJDk= buf.build/gen/go/redpandadata/cloud/connectrpc/go v1.17.0-20241024195046-353ea4645e3d.1 h1:8RGH8Fw8/mHvoAQSRwYPHI4JK5Xu3goqwplWTklP5RI= @@ -12,10 +12,14 @@ buf.build/gen/go/redpandadata/dataplane/connectrpc/go v1.17.0-20241112225414-375 buf.build/gen/go/redpandadata/dataplane/connectrpc/go v1.17.0-20241112225414-3759fedba3f3.1/go.mod h1:lAVv5Nv6SZUV8+UFtUfFF2mMS4WlDp1CsOSPtNgrjPE= buf.build/gen/go/redpandadata/dataplane/protocolbuffers/go v1.35.1-20241112225414-3759fedba3f3.1 h1:FoxR0Huu43isy8t/JcQkeORWN6KYb0SDoCKLrpU529E= buf.build/gen/go/redpandadata/dataplane/protocolbuffers/go v1.35.1-20241112225414-3759fedba3f3.1/go.mod h1:+/pdQipFpdMztKw+xaZFHGUrwMfHLu1qyKOGpTsWFeA= +buf.build/gen/go/redpandadata/gatekeeper/connectrpc/go v1.18.0-20241209180130-05cf059c71c1.1 h1:XlPYQ+gKAnbp81oWIkaKl5g0bVDwp9/QH7EsT6wrr1U= +buf.build/gen/go/redpandadata/gatekeeper/connectrpc/go v1.18.0-20241209180130-05cf059c71c1.1/go.mod h1:KHqtiR23YDDMkNkjB50+ffEDpPMFfmvzfdgL6BH2QK0= +buf.build/gen/go/redpandadata/gatekeeper/protocolbuffers/go v1.36.2-20241209180130-05cf059c71c1.1 h1:MTirPdYgthT0qc9r2ftBQ4bHwyOJzrX780Cx702e6GA= +buf.build/gen/go/redpandadata/gatekeeper/protocolbuffers/go v1.36.2-20241209180130-05cf059c71c1.1/go.mod h1:JR47NAfo6qCtr01EWCX9DTgO5C6dsL1331MLz7+SnRg= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= -connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= -connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +connectrpc.com/connect v1.18.0 h1:7ZHAkx8fTaRO4YIyvV00XiS8bx4XjWp0grk9oh0PIQ0= +connectrpc.com/connect v1.18.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= @@ -376,8 +380,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= +google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/src/go/rpk/pkg/cli/generate/BUILD b/src/go/rpk/pkg/cli/generate/BUILD index 243ad853204ff..9c15557574f21 100644 --- a/src/go/rpk/pkg/cli/generate/BUILD +++ b/src/go/rpk/pkg/cli/generate/BUILD @@ -7,6 +7,7 @@ go_library( "autocomplete.go", "generate.go", "grafana.go", + "license.go", "prometheus.go", ], embedsrcs = [ @@ -26,6 +27,9 @@ go_library( "//src/go/rpk/pkg/kafka", "//src/go/rpk/pkg/os", "//src/go/rpk/pkg/out", + "//src/go/rpk/pkg/publicapi", + "@build_buf_gen_go_redpandadata_gatekeeper_protocolbuffers_go//redpanda/api/gatekeeper/v1alpha1", + "@com_connectrpc_connect//:connect", "@com_github_prometheus_client_model//go", "@com_github_prometheus_common//expfmt", "@com_github_spf13_afero//:afero", diff --git a/src/go/rpk/pkg/cli/generate/generate.go b/src/go/rpk/pkg/cli/generate/generate.go index a6012e1060d65..2dee7872596b6 100644 --- a/src/go/rpk/pkg/cli/generate/generate.go +++ b/src/go/rpk/pkg/cli/generate/generate.go @@ -25,6 +25,7 @@ func NewCommand(fs afero.Fs, p *config.Params) *cobra.Command { cmd.AddCommand( newAppCmd(fs, p), newGrafanaDashboardCmd(p), + newLicenseCommand(fs, p), newPrometheusConfigCmd(fs, p), newShellCompletionCommand(), ) diff --git a/src/go/rpk/pkg/cli/generate/license.go b/src/go/rpk/pkg/cli/generate/license.go new file mode 100644 index 0000000000000..0c988900edef2 --- /dev/null +++ b/src/go/rpk/pkg/cli/generate/license.go @@ -0,0 +1,208 @@ +// Copyright 2025 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package generate + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "time" + + gatekeeperv1alpha1 "buf.build/gen/go/redpandadata/gatekeeper/protocolbuffers/go/redpanda/api/gatekeeper/v1alpha1" + "connectrpc.com/connect" + "github.com/redpanda-data/redpanda/src/go/rpk/pkg/config" + rpkos "github.com/redpanda-data/redpanda/src/go/rpk/pkg/os" + "github.com/redpanda-data/redpanda/src/go/rpk/pkg/out" + "github.com/redpanda-data/redpanda/src/go/rpk/pkg/publicapi" + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +type licenseRequest struct { + name string + lastname string + company string + email string +} + +func newLicenseCommand(fs afero.Fs, p *config.Params) *cobra.Command { + var ( + lr licenseRequest + path string + noConfirm bool + ) + cmd := &cobra.Command{ + Use: "license", + Short: "Generate a trial license", + Long: `Generate a trial license + +This command allows you to sign up for a 30-day trial of Redpanda Enterprise. + +If you require a permanent license, contact us: https://www.redpanda.com/contact + +The license will be saved in your working directory or the specified path, based +on the --path flag. +`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, _ []string) { + cfg, err := p.Load(fs) + out.MaybeDie(err, "rpk unable to load config: %v", err) + + if lr.isEmpty() { + err := lr.prompt() + out.MaybeDieErr(err) + } + err = lr.validate() + out.MaybeDieErr(err) + + req := connect.NewRequest( + &gatekeeperv1alpha1.LicenseSignupRequest{ + GivenName: lr.name, + FamilyName: lr.lastname, + CompanyName: lr.company, + Email: lr.email, + ClusterInfo: &gatekeeperv1alpha1.EnterpriseClusterInfo{ + ClusterId: "rpk-generated", + Platform: gatekeeperv1alpha1.EnterpriseClusterInfo_PLATFORM_REDPANDA, + }, + RequestOrigin: gatekeeperv1alpha1.LicenseSignupRequest_REQUEST_ORIGIN_CLI, + }, + ) + + savePath, err := preparePath(fs, path, noConfirm) + out.MaybeDieErr(err) + + cl := publicapi.NewEnterpriseClientSet(cfg.DevOverrides().PublicAPIURL) + signup, err := cl.Gatekeeper.LicenseSignup(cmd.Context(), req) + out.MaybeDie(err, "unable to request trial license: %v", err) + + expirationDate := time.Now().Add(30 * 24 * time.Hour).Format(time.DateOnly) + err = rpkos.ReplaceFile(fs, savePath, []byte(signup.Msg.GetLicense().LicenseKey), 0o644) + if err != nil { + fmt.Printf(` +Successfully generated a license but we were unable to save it to a file: %v + +License: %v + +This license expires on %v + +You may set this license by running: + rpk license set %[2]v + +Or through Redpanda Console. + +For more information, please visit: https://docs.redpanda.com/current/get-started/licensing/overview/#license-keys +`, err, signup.Msg.GetLicense().LicenseKey, expirationDate) + return + } + + fmt.Printf(` +Successfully generated a license and it has been saved to %q. + +This license expires on %v + +You may set this license by running: + rpk license set --path %[1]v + +Or through Redpanda Console. + +For more information, please visit: https://docs.redpanda.com/current/get-started/licensing/overview/#license-keys +`, savePath, expirationDate) + }, + } + cmd.Flags().StringVar(&path, "path", "", "File path for generating the license") + cmd.Flags().BoolVar(&noConfirm, "no-confirm", false, "Disable confirmation prompt for overwriting the generated license file") + // License request info. + cmd.Flags().StringVar(&lr.name, "name", "", "First name for trial license registration") + cmd.Flags().StringVar(&lr.lastname, "last-name", "", "Last name for register trial license registration") + cmd.Flags().StringVar(&lr.company, "company", "", "Company name for trial license registration") + cmd.Flags().StringVar(&lr.email, "email", "", "Company email for trial license registration") + + cmd.MarkFlagsRequiredTogether("name", "last-name", "company", "email") + return cmd +} + +func (l *licenseRequest) isEmpty() bool { + return l.name == "" && l.lastname == "" && l.company == "" && l.email == "" +} + +func (l *licenseRequest) prompt() error { + name, err := out.Prompt("First Name:") + if err != nil { + return fmt.Errorf("unable to get the firt name: %v", err) + } + l.name = name + lastname, err := out.Prompt("Last Name:") + if err != nil { + return fmt.Errorf("unable to get the last name: %v", err) + } + l.lastname = lastname + company, err := out.Prompt("Company Name:") + if err != nil { + return fmt.Errorf("unable to get the company name: %v", err) + } + l.company = company + email, err := out.Prompt("Business Email:") + if err != nil { + return fmt.Errorf("unable to get the business email: %v", err) + } + l.email = email + return nil +} + +func (l *licenseRequest) validate() error { + if l.name == "" { + return errors.New("name cannot be empty") + } + if l.lastname == "" { + return errors.New("lastname cannot be empty") + } + if l.email == "" { + return errors.New("company email cannot be empty") + } + if l.company == "" { + return errors.New("company name cannot be empty") + } + return nil +} + +func preparePath(fs afero.Fs, path string, noConfirm bool) (string, error) { + if path == "" { + workingDir, err := os.Getwd() + if err != nil { + return "", err + } + path = filepath.Join(workingDir, "redpanda.license") + } else { + isDir, err := afero.IsDir(fs, path) + if err != nil { + return "", fmt.Errorf("unable to determine if path %q is a directory: %v", path, err) + } + if !isDir { + return path, nil + } + path = filepath.Join(path, "redpanda.license") + } + exists, err := afero.Exists(fs, path) + if err != nil { + return "", fmt.Errorf("unable to check if file %q exists: %v", path, err) + } + if exists && !noConfirm { + confirm, err := out.Confirm("%q already exists. Do you want to overwrite it?", path) + if err != nil { + return "", errors.New("cancelled; unable to confirm license file overwrite; you may select a new saving path using the '--path' flag") + } + if !confirm { + return "", fmt.Errorf("cancelled; overwrite not allowed on %q; you may select a new saving path using the '--path' flag", path) + } + } + return path, nil +} diff --git a/src/go/rpk/pkg/publicapi/BUILD b/src/go/rpk/pkg/publicapi/BUILD index bb85cb5344cc1..f173bd0affadf 100644 --- a/src/go/rpk/pkg/publicapi/BUILD +++ b/src/go/rpk/pkg/publicapi/BUILD @@ -5,6 +5,7 @@ go_library( srcs = [ "controlplane.go", "dataplane.go", + "enterprise.go", "publicapi.go", "transform.go", ], @@ -17,6 +18,7 @@ go_library( "@build_buf_gen_go_redpandadata_common_protocolbuffers_go//redpanda/api/common/v1alpha1", "@build_buf_gen_go_redpandadata_dataplane_connectrpc_go//redpanda/api/dataplane/v1alpha2/dataplanev1alpha2connect", "@build_buf_gen_go_redpandadata_dataplane_protocolbuffers_go//redpanda/api/dataplane/v1alpha2", + "@build_buf_gen_go_redpandadata_gatekeeper_connectrpc_go//redpanda/api/gatekeeper/v1alpha1/gatekeeperv1alpha1connect", "@com_connectrpc_connect//:connect", "@org_uber_go_zap//:zap", ], diff --git a/src/go/rpk/pkg/publicapi/enterprise.go b/src/go/rpk/pkg/publicapi/enterprise.go new file mode 100644 index 0000000000000..22913b0bdb18e --- /dev/null +++ b/src/go/rpk/pkg/publicapi/enterprise.go @@ -0,0 +1,43 @@ +// Copyright 2025 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +package publicapi + +import ( + "net/http" + "time" + + "buf.build/gen/go/redpandadata/gatekeeper/connectrpc/go/redpanda/api/gatekeeper/v1alpha1/gatekeeperv1alpha1connect" + "connectrpc.com/connect" +) + +// EnterpriseClientSet holds the respective service clients to interact with +// the enterprise endpoints of the Public API. +type EnterpriseClientSet struct { + Gatekeeper gatekeeperv1alpha1connect.EnterpriseServiceClient +} + +// NewEnterpriseClientSet creates a Public API client set with the service +// clients of each resource available to interact with this package. +func NewEnterpriseClientSet(host string, opts ...connect.ClientOption) *EnterpriseClientSet { + if host == "" { + host = ControlPlaneProdURL + } + opts = append([]connect.ClientOption{ + connect.WithInterceptors( + newLoggerInterceptor(), // Add logs to every request. + ), + }, opts...) + + httpCl := &http.Client{Timeout: 30 * time.Second} + + return &EnterpriseClientSet{ + Gatekeeper: gatekeeperv1alpha1connect.NewEnterpriseServiceClient(httpCl, host, opts...), + } +}