diff --git a/cli/cmd/cluster_addon.go b/cli/cmd/cluster_addon.go index bb5057ee..374f0111 100644 --- a/cli/cmd/cluster_addon.go +++ b/cli/cmd/cluster_addon.go @@ -11,9 +11,8 @@ import ( func (r *runners) InitClusterAddon(parent *cobra.Command) *cobra.Command { cmd := &cobra.Command{ - Use: "addon", - Short: "Manage cluster add-ons", - Hidden: true, // this feature is not fully implemented and controlled behind a feature toggle in the api until ready + Use: "addon", + Short: "Manage cluster add-ons", } parent.AddCommand(cmd) diff --git a/cli/cmd/cluster_addon_create_objectstore.go b/cli/cmd/cluster_addon_create_objectstore.go index 98422ecc..5630f065 100644 --- a/cli/cmd/cluster_addon_create_objectstore.go +++ b/cli/cmd/cluster_addon_create_objectstore.go @@ -101,7 +101,7 @@ func (r *runners) createAndWaitForClusterAddonCreateObjectStore(opts kotsclient. return addon, nil } - // if the wait flag was provided, we poll the api until the cluster is ready, or a timeout + // if the wait flag was provided, we poll the api until the addon is ready, or a timeout if waitDuration > 0 { return waitForAddon(r.kotsAPI, opts.ClusterID, addon.ID, waitDuration) } diff --git a/cli/cmd/cluster_addon_create_postgres.go b/cli/cmd/cluster_addon_create_postgres.go new file mode 100644 index 00000000..4405e08a --- /dev/null +++ b/cli/cmd/cluster_addon_create_postgres.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "fmt" + "os" + "time" + + "github.com/pkg/errors" + "github.com/replicatedhq/replicated/cli/print" + "github.com/replicatedhq/replicated/pkg/kotsclient" + "github.com/replicatedhq/replicated/pkg/platformclient" + "github.com/replicatedhq/replicated/pkg/types" + "github.com/spf13/cobra" +) + +type clusterAddonCreatePostgresArgs struct { + version string + diskGiB int64 + instanceType string + + clusterID string + waitDuration time.Duration + dryRun bool + outputFormat string +} + +func (r *runners) InitClusterAddonCreatePostgres(parent *cobra.Command) *cobra.Command { + args := clusterAddonCreatePostgresArgs{} + + cmd := &cobra.Command{ + Use: "postgres CLUSTER_ID", + Short: "Create a Postgres database for a cluster", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, cmdArgs []string) error { + args.clusterID = cmdArgs[0] + return r.clusterAddonCreatePostgresCreateRun(args) + }, + } + parent.AddCommand(cmd) + + _ = clusterAddonCreatePostgresFlags(cmd, &args) + + return cmd +} + +func clusterAddonCreatePostgresFlags(cmd *cobra.Command, args *clusterAddonCreatePostgresArgs) error { + cmd.Flags().StringVar(&args.version, "version", "", "The Postgres version to create") + cmd.Flags().Int64Var(&args.diskGiB, "disk", 200, "Disk Size (GiB) for the Postgres database") + cmd.Flags().StringVar(&args.instanceType, "instance-type", "db.t3.micro", "The type of instance to use for the Postgres database") + + cmd.Flags().DurationVar(&args.waitDuration, "wait", 0, "Wait duration for add-on to be ready before exiting (leave empty to not wait)") + cmd.Flags().BoolVar(&args.dryRun, "dry-run", false, "Simulate creation to verify that your inputs are valid without actually creating an add-on") + cmd.Flags().StringVar(&args.outputFormat, "output", "table", "The output format to use. One of: json|table|wide (default: table)") + return nil +} + +func (r *runners) clusterAddonCreatePostgresCreateRun(args clusterAddonCreatePostgresArgs) error { + opts := kotsclient.CreateClusterAddonPostgresOpts{ + ClusterID: args.clusterID, + Version: args.version, + DiskGiB: args.diskGiB, + InstanceType: args.instanceType, + DryRun: args.dryRun, + } + + addon, err := r.createAndWaitForClusterAddonCreatePostgres(opts, args.waitDuration) + if err != nil { + if errors.Cause(err) == ErrWaitDurationExceeded { + defer func() { + os.Exit(124) + }() + } else { + return err + } + } + + if opts.DryRun { + _, err := fmt.Fprintln(r.w, "Dry run succeeded.") + return err + } + + return print.Addon(args.outputFormat, r.w, addon) +} + +func (r *runners) createAndWaitForClusterAddonCreatePostgres(opts kotsclient.CreateClusterAddonPostgresOpts, waitDuration time.Duration) (*types.ClusterAddon, error) { + addon, err := r.kotsAPI.CreateClusterAddonPostgres(opts) + if errors.Cause(err) == platformclient.ErrForbidden { + return nil, ErrCompatibilityMatrixTermsNotAccepted + } else if err != nil { + return nil, errors.Wrap(err, "create cluster add-on postgres") + } + + if opts.DryRun { + return addon, nil + } + + // if the wait flag was provided, we poll the api until the add-on is ready, or a timeout + if waitDuration > 0 { + return waitForAddon(r.kotsAPI, opts.ClusterID, addon.ID, waitDuration) + } + + return addon, nil +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index b316a3ec..acffd6b7 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -229,6 +229,7 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i runCmds.InitClusterAddonRm(clusterAddonCmd) clusterAddonCreateCmd := runCmds.InitClusterAddonCreate(clusterAddonCmd) runCmds.InitClusterAddonCreateObjectStore(clusterAddonCreateCmd) + runCmds.InitClusterAddonCreatePostgres(clusterAddonCreateCmd) clusterPortCmd := runCmds.InitClusterPort(clusterCmd) runCmds.InitClusterPortLs(clusterPortCmd) diff --git a/cli/print/cluster_addons.go b/cli/print/cluster_addons.go index 09787e33..16cddfa1 100644 --- a/cli/print/cluster_addons.go +++ b/cli/print/cluster_addons.go @@ -81,6 +81,8 @@ func addonData(addon *types.ClusterAddon) string { switch { case addon.ObjectStore != nil: return addonObjectStoreData(*addon.ObjectStore) + case addon.Postgres != nil: + return addonPostgresData(*addon.Postgres) default: return "" } @@ -93,3 +95,11 @@ func addonObjectStoreData(data types.ClusterAddonObjectStore) string { } return string(b) } + +func addonPostgresData(data types.ClusterAddonPostgres) string { + b, err := json.Marshal(data) + if err != nil { + log.Printf("failed to marshal postgres data: %v", err) + } + return string(b) +} diff --git a/pkg/kotsclient/cluster_addon_objectstore_create.go b/pkg/kotsclient/cluster_addon_objectstore_create.go index f9f81f3f..3221420a 100644 --- a/pkg/kotsclient/cluster_addon_objectstore_create.go +++ b/pkg/kotsclient/cluster_addon_objectstore_create.go @@ -25,7 +25,7 @@ type CreateClusterAddonObjectStoreResponse struct { } type CreateClusterAddonErrorResponse struct { - Error string `json:"error"` + Message string `json:"message"` } func (c *VendorV3Client) CreateClusterAddonObjectStore(opts CreateClusterAddonObjectStoreOpts) (*types.ClusterAddon, error) { @@ -51,7 +51,7 @@ func (c *VendorV3Client) doCreateClusterAddonObjectStoreRequest(clusterID string if jsonErr := json.Unmarshal(apiErr.Body, errResp); jsonErr != nil { return nil, fmt.Errorf("unmarshal error response: %w", err) } - return nil, errors.New(errResp.Error) + return nil, errors.New(errResp.Message) } } diff --git a/pkg/kotsclient/cluster_addon_postgres_create.go b/pkg/kotsclient/cluster_addon_postgres_create.go new file mode 100644 index 00000000..c43d9895 --- /dev/null +++ b/pkg/kotsclient/cluster_addon_postgres_create.go @@ -0,0 +1,60 @@ +package kotsclient + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/pkg/errors" + "github.com/replicatedhq/replicated/pkg/platformclient" + "github.com/replicatedhq/replicated/pkg/types" +) + +type CreateClusterAddonPostgresOpts struct { + ClusterID string + Version string + DiskGiB int64 + InstanceType string + DryRun bool +} + +type CreateClusterAddonPostgresRequest struct { + Version string `json:"version"` + DiskGiB int64 `json:"disk_gib"` + InstanceType string `json:"instance_type"` +} + +func (c *VendorV3Client) CreateClusterAddonPostgres(opts CreateClusterAddonPostgresOpts) (*types.ClusterAddon, error) { + req := CreateClusterAddonPostgresRequest{ + Version: opts.Version, + DiskGiB: opts.DiskGiB, + InstanceType: opts.InstanceType, + } + return c.doCreateClusterAddonPostgresRequest(opts.ClusterID, req, opts.DryRun) +} + +func (c *VendorV3Client) doCreateClusterAddonPostgresRequest(clusterID string, req CreateClusterAddonPostgresRequest, dryRun bool) (*types.ClusterAddon, error) { + resp := CreateClusterAddonObjectStoreResponse{} + endpoint := fmt.Sprintf("/v3/cluster/%s/addons/postgres", clusterID) + if dryRun { + endpoint = fmt.Sprintf("%s?dry-run=true", endpoint) + } + err := c.DoJSON("POST", endpoint, http.StatusCreated, req, &resp) + if err != nil { + // if err is APIError and the status code is 400, then we have a validation error + // and we can return the validation error + if apiErr, ok := errors.Cause(err).(platformclient.APIError); ok { + if apiErr.StatusCode == http.StatusBadRequest { + errResp := &CreateClusterAddonErrorResponse{} + if jsonErr := json.Unmarshal(apiErr.Body, errResp); jsonErr != nil { + return nil, fmt.Errorf("unmarshal error response: %w", err) + } + return nil, errors.New(errResp.Message) + } + } + + return nil, err + } + + return resp.Addon, nil +} diff --git a/pkg/types/cluster.go b/pkg/types/cluster.go index 8160494f..4dff23d6 100644 --- a/pkg/types/cluster.go +++ b/pkg/types/cluster.go @@ -82,6 +82,7 @@ type ClusterAddon struct { CreatedAt time.Time `json:"created_at"` ObjectStore *ClusterAddonObjectStore `json:"object_store,omitempty"` + Postgres *ClusterAddonPostgres `json:"postgres,omitempty"` } type ClusterAddonObjectStore struct { @@ -92,10 +93,20 @@ type ClusterAddonObjectStore struct { ServiceAccountNameReadOnly string `json:"service_account_name_read_only,omitempty"` } +type ClusterAddonPostgres struct { + Version string `json:"version"` + DiskGiB int64 `json:"disk_gib"` + InstanceType string `json:"instance_type"` + + URI string `json:"uri,omitempty"` +} + func (addon *ClusterAddon) TypeName() string { switch { case addon.ObjectStore != nil: return "Object Store" + case addon.Postgres != nil: + return "Postgres" default: return "Unknown" }