diff --git a/internal/cmd/admin/admin.go b/internal/cmd/admin/admin.go index 0dfa5f47..8c2476f5 100644 --- a/internal/cmd/admin/admin.go +++ b/internal/cmd/admin/admin.go @@ -94,12 +94,13 @@ func toAdminDetail(admin *ps.NekiAdmin, parameters []*ps.NekiParameter) *adminDe } type parameterDisplay struct { - Namespace string `header:"namespace" json:"namespace"` - Name string `header:"name" json:"name"` - Value string `header:"value" json:"value"` - Default string `header:"default" json:"default_value"` - Type string `header:"type" json:"parameter_type"` - Restart bool `header:"restart" json:"restart"` + Namespace string `header:"namespace" json:"namespace"` + Name string `header:"name" json:"name"` + Value string `header:"value" json:"value"` + Default string `header:"default" json:"default_value"` + Type string `header:"type" json:"parameter_type"` + Restart bool `header:"restart" json:"restart"` + RequiresUpdate bool `header:"requires update" json:"requires_update"` orig *ps.NekiParameter } @@ -116,13 +117,14 @@ func toParameters(parameters []*ps.NekiParameter) []*parameterDisplay { out := make([]*parameterDisplay, 0, len(parameters)) for _, parameter := range parameters { out = append(out, ¶meterDisplay{ - Namespace: parameter.Namespace, - Name: parameter.Name, - Value: formatValue(parameter.Value), - Default: formatValue(parameter.DefaultValue), - Type: parameter.ParameterType, - Restart: parameter.Restart, - orig: parameter, + Namespace: parameter.Namespace, + Name: parameter.Name, + Value: formatValue(parameter.Value), + Default: formatValue(parameter.DefaultValue), + Type: parameter.ParameterType, + Restart: parameter.Restart, + RequiresUpdate: parameter.RequiresUpdate != nil, + orig: parameter, }) } return out diff --git a/internal/cmd/admin/admin_test.go b/internal/cmd/admin/admin_test.go index ab4618f0..bfd370d7 100644 --- a/internal/cmd/admin/admin_test.go +++ b/internal/cmd/admin/admin_test.go @@ -174,7 +174,7 @@ func TestAdminParametersCmd(t *testing.T) { c.Assert(out.String(), qt.JSONEquals, []map[string]interface{}{{ "name": "recovery-poll-interval", "display_name": "", "namespace": "admin", "advanced": false, "category": nil, "description": "", "parameter_type": "", "default_value": nil, "value": "20s", - "required": false, "created_at": "0001-01-01T00:00:00Z", "updated_at": nil, "restart": false, "url": "", + "required": false, "created_at": "0001-01-01T00:00:00Z", "updated_at": nil, "restart": false, "url": "", "requires_update": nil, }}) } diff --git a/internal/cmd/branch/parameters.go b/internal/cmd/branch/parameters.go index c86bf41e..2c049bbc 100644 --- a/internal/cmd/branch/parameters.go +++ b/internal/cmd/branch/parameters.go @@ -105,12 +105,13 @@ To change parameters, use 'pscale branch resize --parameters } type postgresParameter struct { - Namespace string `header:"namespace" json:"namespace"` - Name string `header:"name" json:"name"` - Value string `header:"value" json:"value"` - Default string `header:"default" json:"default_value"` - Type string `header:"type" json:"parameter_type"` - Restart bool `header:"restart" json:"restart"` + Namespace string `header:"namespace" json:"namespace"` + Name string `header:"name" json:"name"` + Value string `header:"value" json:"value"` + Default string `header:"default" json:"default_value"` + Type string `header:"type" json:"parameter_type"` + Restart bool `header:"restart" json:"restart"` + RequiresUpdate bool `header:"requires update" json:"requires_update"` orig *ps.PostgresParameter } @@ -119,13 +120,14 @@ func toPostgresParameters(parameters []*ps.PostgresParameter) []*postgresParamet out := make([]*postgresParameter, 0, len(parameters)) for _, param := range parameters { out = append(out, &postgresParameter{ - Namespace: param.Namespace, - Name: param.Name, - Value: formatParameterValue(param.Value), - Default: formatParameterValue(param.DefaultValue), - Type: param.ParameterType, - Restart: param.Restart, - orig: param, + Namespace: param.Namespace, + Name: param.Name, + Value: formatParameterValue(param.Value), + Default: formatParameterValue(param.DefaultValue), + Type: param.ParameterType, + Restart: param.Restart, + RequiresUpdate: param.RequiresUpdate != nil, + orig: param, }) } return out diff --git a/internal/cmd/branch/parameters_test.go b/internal/cmd/branch/parameters_test.go index 3c4beb87..21266a5d 100644 --- a/internal/cmd/branch/parameters_test.go +++ b/internal/cmd/branch/parameters_test.go @@ -3,6 +3,8 @@ package branch import ( "bytes" "context" + "encoding/json" + "strings" "testing" qt "github.com/frankban/quicktest" @@ -15,6 +17,7 @@ import ( func TestBranch_ParametersCmd(t *testing.T) { c := qt.New(t) + requiresUpdate := "cluster" var buf bytes.Buffer format := printer.JSON @@ -34,7 +37,7 @@ func TestBranch_ParametersCmd(t *testing.T) { c.Assert(req.Internal, qt.IsNil) return []*ps.PostgresParameter{ - {Namespace: "pgconf", Name: "max_connections", Value: "200", DefaultValue: "100", ParameterType: "integer", Restart: true}, + {Namespace: "pgconf", Name: "max_connections", Value: "200", DefaultValue: "100", ParameterType: "integer", Restart: true, RequiresUpdate: &requiresUpdate}, {Namespace: "pgbouncer", Name: "default_pool_size", Value: "20", DefaultValue: "20", ParameterType: "integer"}, }, nil }, @@ -58,6 +61,11 @@ func TestBranch_ParametersCmd(t *testing.T) { c.Assert(pgSvc.ListParametersFnInvoked, qt.IsTrue) c.Assert(buf.String(), qt.Contains, "max_connections") c.Assert(buf.String(), qt.Contains, "default_pool_size") + var parameters []map[string]interface{} + c.Assert(json.Unmarshal(buf.Bytes(), ¶meters), qt.IsNil) + c.Assert(parameters[0]["requires_update"], qt.Equals, "cluster") + c.Assert(parameters[1]["requires_update"], qt.IsNil) + c.Assert(buf.String(), qt.Contains, `"requires_update": null`) } func TestBranch_ParametersCmd_ListSubcommand(t *testing.T) { @@ -130,3 +138,24 @@ func TestBranch_ParametersCmd_NamespaceFilter(t *testing.T) { c.Assert(buf.String(), qt.Contains, "default_pool_size") c.Assert(buf.String(), qt.Not(qt.Contains), "max_connections") } + +func TestBranch_ParametersRequiresUpdateTable(t *testing.T) { + c := qt.New(t) + var parameters []*ps.PostgresParameter + c.Assert(json.Unmarshal([]byte(`[ + {"namespace":"pgconf","name":"locked","requires_update":"cluster"}, + {"namespace":"pgconf","name":"editable","requires_update":null} + ]`), ¶meters), qt.IsNil) + + var out bytes.Buffer + format := printer.Human + p := printer.NewPrinter(&format) + p.SetResourceOutput(&out) + c.Assert(p.PrintResource(toPostgresParameters(parameters)), qt.IsNil) + + lines := strings.Split(strings.TrimSpace(out.String()), "\n") + c.Assert(len(lines), qt.Equals, 4) + c.Assert(lines[0], qt.Contains, "REQUIRES UPDATE") + c.Assert(strings.Fields(lines[2]), qt.DeepEquals, []string{"pgconf", "locked", "No", "Yes"}) + c.Assert(strings.Fields(lines[3]), qt.DeepEquals, []string{"pgconf", "editable", "No", "No"}) +} diff --git a/internal/cmd/configprofile/config_profile.go b/internal/cmd/configprofile/config_profile.go index f7d4f275..e292eaa6 100644 --- a/internal/cmd/configprofile/config_profile.go +++ b/internal/cmd/configprofile/config_profile.go @@ -71,14 +71,15 @@ func toProfiles(profiles []*ps.NekiShardConfigurationProfile) []*profileDisplay } type parameterDisplay struct { - Namespace string `header:"namespace" json:"namespace"` - Name string `header:"name" json:"name"` - Value string `header:"value" json:"value"` - Default string `header:"default" json:"default_value"` - Type string `header:"type" json:"parameter_type"` - Restart bool `header:"restart" json:"restart"` - Required bool `header:"required" json:"required"` - orig *ps.NekiParameter + Namespace string `header:"namespace" json:"namespace"` + Name string `header:"name" json:"name"` + Value string `header:"value" json:"value"` + Default string `header:"default" json:"default_value"` + Type string `header:"type" json:"parameter_type"` + Restart bool `header:"restart" json:"restart"` + Required bool `header:"required" json:"required"` + RequiresUpdate bool `header:"requires update" json:"requires_update"` + orig *ps.NekiParameter } func (p *parameterDisplay) MarshalJSON() ([]byte, error) { return json.Marshal(p.orig) } @@ -99,14 +100,15 @@ func toParameters(parameters []*ps.NekiParameter) []*parameterDisplay { out := make([]*parameterDisplay, 0, len(parameters)) for _, p := range parameters { out = append(out, ¶meterDisplay{ - Namespace: p.Namespace, - Name: p.Name, - Value: formatValue(p.Value), - Default: formatValue(p.DefaultValue), - Type: p.ParameterType, - Restart: p.Restart, - Required: p.Required, - orig: p, + Namespace: p.Namespace, + Name: p.Name, + Value: formatValue(p.Value), + Default: formatValue(p.DefaultValue), + Type: p.ParameterType, + Restart: p.Restart, + Required: p.Required, + RequiresUpdate: p.RequiresUpdate != nil, + orig: p, }) } return out diff --git a/internal/cmd/configprofile/config_profile_test.go b/internal/cmd/configprofile/config_profile_test.go index fca7b2cf..e1fc4966 100644 --- a/internal/cmd/configprofile/config_profile_test.go +++ b/internal/cmd/configprofile/config_profile_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "strings" "testing" "time" @@ -309,13 +310,14 @@ func TestConfigProfileDefaultCommands(t *testing.T) { func TestConfigProfileParametersCmd(t *testing.T) { c := qt.New(t) + requiresUpdate := "configuration_profile" var out bytes.Buffer svc := &mock.NekiShardConfigurationProfilesService{ListParametersFn: func(_ context.Context, req *ps.ListNekiShardConfigurationProfileParametersRequest) ([]*ps.NekiParameter, error) { c.Assert(req.Extension, qt.IsNotNil) c.Assert(*req.Extension, qt.IsTrue) c.Assert(req.Internal, qt.IsNil) return []*ps.NekiParameter{ - {Name: "work_mem", Namespace: "pgconf", Value: "64MB"}, + {Name: "work_mem", Namespace: "pgconf", Value: "64MB", RequiresUpdate: &requiresUpdate}, {Name: "default_pool_size", Namespace: "pgbouncer", Value: "20"}, }, nil }} @@ -326,10 +328,33 @@ func TestConfigProfileParametersCmd(t *testing.T) { c.Assert(out.String(), qt.JSONEquals, []map[string]interface{}{{ "name": "work_mem", "display_name": "", "namespace": "pgconf", "advanced": false, "category": nil, "description": "", "parameter_type": "", "default_value": nil, "value": "64MB", - "required": false, "created_at": "0001-01-01T00:00:00Z", "updated_at": nil, "restart": false, "url": "", + "required": false, "created_at": "0001-01-01T00:00:00Z", "updated_at": nil, "restart": false, "url": "", "requires_update": "configuration_profile", }}) } +func TestConfigProfileParametersRequiresUpdateTable(t *testing.T) { + c := qt.New(t) + var parameters []*ps.NekiParameter + c.Assert(json.Unmarshal([]byte(`[ + {"namespace":"pgconf","name":"profile_locked","requires_update":"configuration_profile"}, + {"namespace":"pgconf","name":"cluster_locked","requires_update":"cluster"}, + {"namespace":"pgconf","name":"editable","requires_update":null} + ]`), ¶meters), qt.IsNil) + + var out bytes.Buffer + format := printer.Human + p := printer.NewPrinter(&format) + p.SetResourceOutput(&out) + c.Assert(p.PrintResource(toParameters(parameters)), qt.IsNil) + + lines := strings.Split(strings.TrimSpace(out.String()), "\n") + c.Assert(len(lines), qt.Equals, 5) + c.Assert(lines[0], qt.Contains, "REQUIRES UPDATE") + c.Assert(strings.Fields(lines[2]), qt.DeepEquals, []string{"pgconf", "profile_locked", "No", "No", "Yes"}) + c.Assert(strings.Fields(lines[3]), qt.DeepEquals, []string{"pgconf", "cluster_locked", "No", "No", "Yes"}) + c.Assert(strings.Fields(lines[4]), qt.DeepEquals, []string{"pgconf", "editable", "No", "No", "No"}) +} + func TestConfigProfileChangesCancelCmd(t *testing.T) { c := qt.New(t) var out bytes.Buffer diff --git a/internal/cmd/sidecar/sidecar.go b/internal/cmd/sidecar/sidecar.go index b6f10307..e3d2a25a 100644 --- a/internal/cmd/sidecar/sidecar.go +++ b/internal/cmd/sidecar/sidecar.go @@ -83,12 +83,13 @@ func toSidecarDetail(sidecar *ps.NekiSidecar, parameters []*ps.NekiParameter) *s } type parameterDisplay struct { - Namespace string `header:"namespace" json:"namespace"` - Name string `header:"name" json:"name"` - Value string `header:"value" json:"value"` - Default string `header:"default" json:"default_value"` - Type string `header:"type" json:"parameter_type"` - Restart bool `header:"restart" json:"restart"` + Namespace string `header:"namespace" json:"namespace"` + Name string `header:"name" json:"name"` + Value string `header:"value" json:"value"` + Default string `header:"default" json:"default_value"` + Type string `header:"type" json:"parameter_type"` + Restart bool `header:"restart" json:"restart"` + RequiresUpdate bool `header:"requires update" json:"requires_update"` orig *ps.NekiParameter } @@ -105,13 +106,14 @@ func toParameters(parameters []*ps.NekiParameter) []*parameterDisplay { out := make([]*parameterDisplay, 0, len(parameters)) for _, parameter := range parameters { out = append(out, ¶meterDisplay{ - Namespace: parameter.Namespace, - Name: parameter.Name, - Value: formatValue(parameter.Value), - Default: formatValue(parameter.DefaultValue), - Type: parameter.ParameterType, - Restart: parameter.Restart, - orig: parameter, + Namespace: parameter.Namespace, + Name: parameter.Name, + Value: formatValue(parameter.Value), + Default: formatValue(parameter.DefaultValue), + Type: parameter.ParameterType, + Restart: parameter.Restart, + RequiresUpdate: parameter.RequiresUpdate != nil, + orig: parameter, }) } return out diff --git a/internal/cmd/sidecar/sidecar_test.go b/internal/cmd/sidecar/sidecar_test.go index 6d611f49..e43db34c 100644 --- a/internal/cmd/sidecar/sidecar_test.go +++ b/internal/cmd/sidecar/sidecar_test.go @@ -189,7 +189,7 @@ func TestSidecarParametersCmd(t *testing.T) { c.Assert(out.String(), qt.JSONEquals, []map[string]interface{}{{ "name": "default_pool_size", "display_name": "", "namespace": "pgbouncer", "advanced": false, "category": nil, "description": "", "parameter_type": "", "default_value": nil, "value": "20", - "required": false, "created_at": "0001-01-01T00:00:00Z", "updated_at": nil, "restart": false, "url": "", + "required": false, "created_at": "0001-01-01T00:00:00Z", "updated_at": nil, "restart": false, "url": "", "requires_update": nil, }}) } diff --git a/internal/planetscale/neki_shard_configuration_profiles.go b/internal/planetscale/neki_shard_configuration_profiles.go index 0b50e356..9796e9e6 100644 --- a/internal/planetscale/neki_shard_configuration_profiles.go +++ b/internal/planetscale/neki_shard_configuration_profiles.go @@ -95,20 +95,21 @@ type SetDefaultNekiShardConfigurationProfileRequest struct { } type NekiParameter struct { - Type string `json:"type,omitempty"` - Name string `json:"name"` - DisplayName string `json:"display_name"` - Namespace string `json:"namespace"` - Advanced bool `json:"advanced"` - Category *string `json:"category"` - Description string `json:"description"` - ParameterType string `json:"parameter_type"` - DefaultValue interface{} `json:"default_value"` - Value interface{} `json:"value"` - Required bool `json:"required"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` - Restart bool `json:"restart"` + Type string `json:"type,omitempty"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Namespace string `json:"namespace"` + Advanced bool `json:"advanced"` + Category *string `json:"category"` + Description string `json:"description"` + ParameterType string `json:"parameter_type"` + DefaultValue interface{} `json:"default_value"` + Value interface{} `json:"value"` + Required bool `json:"required"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + Restart bool `json:"restart"` + RequiresUpdate *string `json:"requires_update"` // Min, Max, and Step are numbers for plain numeric parameters but strings // for byte- and time-typed ones (for example, "128kB" or "10min"). Max any `json:"max,omitempty"` diff --git a/internal/planetscale/postgres_branches.go b/internal/planetscale/postgres_branches.go index ed609df9..91bc9373 100644 --- a/internal/planetscale/postgres_branches.go +++ b/internal/planetscale/postgres_branches.go @@ -206,17 +206,18 @@ type ListPostgresParametersRequest struct { // PostgresParameter represents a configuration parameter of a Postgres branch. type PostgresParameter struct { - ID string `json:"id"` - Name string `json:"name"` - DisplayName string `json:"display_name"` - Namespace string `json:"namespace"` - Category string `json:"category"` - Description string `json:"description"` - ParameterType string `json:"parameter_type"` - DefaultValue any `json:"default_value"` - Value any `json:"value"` - Required bool `json:"required"` - Restart bool `json:"restart"` + ID string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Namespace string `json:"namespace"` + Category string `json:"category"` + Description string `json:"description"` + ParameterType string `json:"parameter_type"` + DefaultValue any `json:"default_value"` + Value any `json:"value"` + Required bool `json:"required"` + Restart bool `json:"restart"` + RequiresUpdate *string `json:"requires_update"` // Min, Max, and Step are numbers for plain numeric parameters but strings // for byte- and time-typed ones (e.g. "8kB", "10s").