Skip to content

Commit

Permalink
feature: API Start and Stop mechanism for RpaasInstance. (#150)
Browse files Browse the repository at this point in the history
* Start and stop routes and client command

* Update copyright date

* Adding missing implementations

* Fix linter

* Shutdown flag showing in the cli info cmd

* Bug fixes and creating innitial testing

* Fetching reference after patch

* Completed test suite

* Showing Shutdown when using kubectl get rpaasinstance

* Remove content type

* Remove content-type assertions
  • Loading branch information
gvicentin committed Apr 8, 2024
1 parent 27db984 commit f69ce91
Show file tree
Hide file tree
Showing 23 changed files with 505 additions and 3 deletions.
1 change: 1 addition & 0 deletions api/v1alpha1/rpaasinstance_types.go
Expand Up @@ -382,6 +382,7 @@ type RpaasInstanceExternalAddressesStatus struct {
// +kubebuilder:subresource:status
// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.currentReplicas,selectorpath=.status.podSelector
// +kubebuilder:printcolumn:name="Suspended",type=boolean,JSONPath=`.spec.suspend`
// +kubebuilder:printcolumn:name="Shutdown",type=boolean,JSONPath=`.spec.shutdown`
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:printcolumn:name="IPs",type=string,JSONPath=`.status.externalAddresses.ips[*]`
// +kubebuilder:printcolumn:name="Hostnames",type=string,JSONPath=`.status.externalAddresses.hostnames[*]`
Expand Down
2 changes: 2 additions & 0 deletions cmd/plugin/rpaasv2/cmd/app.go
Expand Up @@ -34,6 +34,8 @@ func NewApp(o, e io.Writer, client rpaasclient.Client) (app *cli.App) {
app.Writer = o
app.Commands = []*cli.Command{
NewCmdScale(),
NewCmdStart(),
NewCmdStop(),
NewCmdAccessControlList(),
NewCmdCertificates(),
NewCmdBlocks(),
Expand Down
1 change: 1 addition & 0 deletions cmd/plugin/rpaasv2/cmd/info.go
Expand Up @@ -88,6 +88,7 @@ Tags: {{ join ", " .Tags }}
Team owner: {{ .Team }}
Plan: {{ .Plan }}
Flavors: {{ join ", " .Flavors }}
Shutdown: {{ .Shutdown }}
{{- with .Cluster}}
Cluster: {{ . }}
{{- end }}
Expand Down
6 changes: 5 additions & 1 deletion cmd/plugin/rpaasv2/cmd/info_test.go
Expand Up @@ -328,6 +328,7 @@ Tags: tag1, tag2, tag3
Team owner: some-team
Plan: basic
Flavors: flavor1, flavor2, flavor-N
Shutdown: false
Cluster: my-dedicated-cluster
Pool: my-pool
Expand Down Expand Up @@ -585,6 +586,7 @@ Tags: tag1, tag2, tag3
Team owner: some-team
Plan: basic
Flavors: flavor1, flavor2, flavor-N
Shutdown: false
Cluster: my-dedicated-cluster
Pods: (current: 2 / desired: 3)
Expand Down Expand Up @@ -680,6 +682,7 @@ Tags: tag1, tag2, tag3
Team owner: some-team
Plan: basic
Flavors: flavor1, flavor2, flavor-N
Shutdown: false
Cluster: my-dedicated-cluster
Pods: (current: 2 / desired: 3)
Expand Down Expand Up @@ -737,10 +740,11 @@ Pods: (current: 2 / desired: 3)
Team: "some team",
Description: "some description",
Tags: []string{"tag1", "tag2", "tag3"},
Shutdown: true,
}, nil
},
},
expected: "{\n\t\"addresses\": [\n\t\t{\n\t\t\t\"type\": \"cluster-external\",\n\t\t\t\"hostname\": \"some-host\",\n\t\t\t\"ip\": \"0.0.0.0\",\n\t\t\t\"status\": \"ready\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"cluster-external\",\n\t\t\t\"hostname\": \"some-host2\",\n\t\t\t\"ip\": \"0.0.0.1\",\n\t\t\t\"status\": \"ready\"\n\t\t}\n\t],\n\t\"replicas\": 5,\n\t\"plan\": \"basic\",\n\t\"routes\": [\n\t\t{\n\t\t\t\"path\": \"some-path\",\n\t\t\t\"destination\": \"some-destination\"\n\t\t}\n\t],\n\t\"binds\": [\n\t\t{\n\t\t\t\"name\": \"some-name\",\n\t\t\t\"host\": \"some-host\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"some-name2\",\n\t\t\t\"host\": \"some-host2\"\n\t\t}\n\t],\n\t\"team\": \"some team\",\n\t\"name\": \"my-instance\",\n\t\"description\": \"some description\",\n\t\"tags\": [\n\t\t\"tag1\",\n\t\t\"tag2\",\n\t\t\"tag3\"\n\t]\n}\n",
expected: "{\n\t\"addresses\": [\n\t\t{\n\t\t\t\"type\": \"cluster-external\",\n\t\t\t\"hostname\": \"some-host\",\n\t\t\t\"ip\": \"0.0.0.0\",\n\t\t\t\"status\": \"ready\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"cluster-external\",\n\t\t\t\"hostname\": \"some-host2\",\n\t\t\t\"ip\": \"0.0.0.1\",\n\t\t\t\"status\": \"ready\"\n\t\t}\n\t],\n\t\"replicas\": 5,\n\t\"plan\": \"basic\",\n\t\"routes\": [\n\t\t{\n\t\t\t\"path\": \"some-path\",\n\t\t\t\"destination\": \"some-destination\"\n\t\t}\n\t],\n\t\"binds\": [\n\t\t{\n\t\t\t\"name\": \"some-name\",\n\t\t\t\"host\": \"some-host\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"some-name2\",\n\t\t\t\"host\": \"some-host2\"\n\t\t}\n\t],\n\t\"team\": \"some team\",\n\t\"name\": \"my-instance\",\n\t\"description\": \"some description\",\n\t\"tags\": [\n\t\t\"tag1\",\n\t\t\"tag2\",\n\t\t\"tag3\"\n\t],\n\t\"shutdown\": true\n}\n",
},
}

Expand Down
48 changes: 48 additions & 0 deletions cmd/plugin/rpaasv2/cmd/start.go
@@ -0,0 +1,48 @@
// Copyright 2024 tsuru authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cmd

import (
"fmt"

"github.com/urfave/cli/v2"
)

func NewCmdStart() *cli.Command {
return &cli.Command{
Name: "start",
Usage: "Starts instance if the current state is shutdown",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "service",
Aliases: []string{"tsuru-service", "s"},
Usage: "the Tsuru service name",
},
&cli.StringFlag{
Name: "instance",
Aliases: []string{"tsuru-service-instance", "i"},
Usage: "the reverse proxy instance name",
Required: true,
},
},
Before: setupClient,
Action: runStart,
}
}

func runStart(c *cli.Context) error {
client, err := getClient(c)
if err != nil {
return err
}

err = client.Start(c.Context, c.String("instance"))
if err != nil {
return err
}

fmt.Fprintf(c.App.Writer, "Started instance %s\n", formatInstanceName(c))
return nil
}
34 changes: 34 additions & 0 deletions cmd/plugin/rpaasv2/cmd/start_test.go
@@ -0,0 +1,34 @@
// Copyright 2024 tsuru authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cmd

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/tsuru/rpaas-operator/pkg/rpaas/client/fake"
)

func TestStart(t *testing.T) {
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}

args := []string{"./rpaasv2", "start", "-s", "some-service", "-i", "my-instance"}

client := &fake.FakeClient{
FakeStart: func(instance string) error {
require.Equal(t, instance, "my-instance")
return nil
},
}

app := NewApp(stdout, stderr, client)
err := app.Run(args)
require.NoError(t, err)
assert.Equal(t, stdout.String(), "Started instance some-service/my-instance\n")
}
48 changes: 48 additions & 0 deletions cmd/plugin/rpaasv2/cmd/stop.go
@@ -0,0 +1,48 @@
// Copyright 2024 tsuru authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cmd

import (
"fmt"

"github.com/urfave/cli/v2"
)

func NewCmdStop() *cli.Command {
return &cli.Command{
Name: "stop",
Usage: "Shutdown instance (halt autoscale and scale in all replicas)",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "service",
Aliases: []string{"tsuru-service", "s"},
Usage: "the Tsuru service name",
},
&cli.StringFlag{
Name: "instance",
Aliases: []string{"tsuru-service-instance", "i"},
Usage: "the reverse proxy instance name",
Required: true,
},
},
Before: setupClient,
Action: runStop,
}
}

func runStop(c *cli.Context) error {
client, err := getClient(c)
if err != nil {
return err
}

err = client.Stop(c.Context, c.String("instance"))
if err != nil {
return err
}

fmt.Fprintf(c.App.Writer, "Shutting down instance %s\n", formatInstanceName(c))
return nil
}
34 changes: 34 additions & 0 deletions cmd/plugin/rpaasv2/cmd/stop_test.go
@@ -0,0 +1,34 @@
// Copyright 2024 tsuru authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cmd

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/tsuru/rpaas-operator/pkg/rpaas/client/fake"
)

func TestStop(t *testing.T) {
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}

args := []string{"./rpaasv2", "stop", "-s", "some-service", "-i", "my-instance"}

client := &fake.FakeClient{
FakeStop: func(instance string) error {
require.Equal(t, instance, "my-instance")
return nil
},
}

app := NewApp(stdout, stderr, client)
err := app.Run(args)
require.NoError(t, err)
assert.Equal(t, stdout.String(), "Shutting down instance some-service/my-instance\n")
}
3 changes: 3 additions & 0 deletions config/crd/bases/extensions.tsuru.io_rpaasinstances.yaml
Expand Up @@ -22,6 +22,9 @@ spec:
- jsonPath: .spec.suspend
name: Suspended
type: boolean
- jsonPath: .spec.shutdown
name: Shutdown
type: boolean
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
Expand Down
16 changes: 16 additions & 0 deletions internal/pkg/rpaas/fake/manager.go
Expand Up @@ -32,6 +32,8 @@ type RpaasManager struct {
FakeInstanceAddress func(name string) (string, error)
FakeInstanceStatus func(name string) (*nginxv1alpha1.Nginx, rpaas.PodStatusMap, error)
FakeScale func(instanceName string, replicas int32) error
FakeStart func(instanceName string) error
FakeStop func(instanceName string) error
FakeGetPlans func() ([]rpaas.Plan, error)
FakeGetFlavors func() ([]rpaas.Flavor, error)
FakeCreateExtraFiles func(instanceName string, files ...rpaas.File) error
Expand Down Expand Up @@ -166,6 +168,20 @@ func (m *RpaasManager) Scale(ctx context.Context, instanceName string, replicas
return nil
}

func (m *RpaasManager) Start(ctx context.Context, instanceName string) error {
if m.FakeStart != nil {
return m.FakeStart(instanceName)
}
return nil
}

func (m *RpaasManager) Stop(ctx context.Context, instanceName string) error {
if m.FakeStop != nil {
return m.FakeStop(instanceName)
}
return nil
}

func (m *RpaasManager) GetPlans(ctx context.Context) ([]rpaas.Plan, error) {
if m.FakeGetPlans != nil {
return m.FakeGetPlans()
Expand Down
30 changes: 30 additions & 0 deletions internal/pkg/rpaas/k8s.go
Expand Up @@ -611,10 +611,39 @@ func (m *k8sRpaasManager) Scale(ctx context.Context, instanceName string, replic
if replicas < 0 {
return ValidationError{Msg: fmt.Sprintf("invalid replicas number: %d", replicas)}
}

oldReplicas := originalInstance.Spec.Replicas
if replicas > 0 && oldReplicas != nil && *oldReplicas == 0 {
// When scaling out from zero, disable shutdown automatically
instance.Spec.Shutdown = false
}

instance.Spec.Replicas = &replicas
return m.patchInstance(ctx, originalInstance, instance)
}

func (m *k8sRpaasManager) Start(ctx context.Context, instanceName string) error {
instance, err := m.GetInstance(ctx, instanceName)
if err != nil {
return err
}

originalInstance := instance.DeepCopy()
instance.Spec.Shutdown = false
return m.patchInstance(ctx, originalInstance, instance)
}

func (m *k8sRpaasManager) Stop(ctx context.Context, instanceName string) error {
instance, err := m.GetInstance(ctx, instanceName)
if err != nil {
return err
}

originalInstance := instance.DeepCopy()
instance.Spec.Shutdown = true
return m.patchInstance(ctx, originalInstance, instance)
}

func (m *k8sRpaasManager) GetCertificates(ctx context.Context, instanceName string) ([]CertificateData, error) {
instance, err := m.GetInstance(ctx, instanceName)
if err != nil {
Expand Down Expand Up @@ -1631,6 +1660,7 @@ func (m *k8sRpaasManager) GetInstanceInfo(ctx context.Context, instanceName stri
Plan: instance.Spec.PlanName,
Binds: instance.Spec.Binds,
Flavors: instance.Spec.Flavors,
Shutdown: instance.Spec.Shutdown,
PlanOverride: instance.Spec.PlanTemplate,
Autoscale: m.getAutoscale(instance),
}
Expand Down
52 changes: 51 additions & 1 deletion internal/pkg/rpaas/k8s_test.go
Expand Up @@ -3645,7 +3645,13 @@ func Test_k8sRpaasManager_Scale(t *testing.T) {
instance2.Name = "another-instance"
instance2.Spec.Autoscale = nil

resources := []runtime.Object{instance1, instance2}
instance3 := newEmptyRpaasInstance()
instance3.Name = "instance-scale-from-zero"
instance3.Spec.Shutdown = true
instance3.Spec.Autoscale = nil
instance3.Spec.Replicas = pointerToInt32(0)

resources := []runtime.Object{instance1, instance2, instance3}

testCases := []struct {
instance string
Expand Down Expand Up @@ -3684,6 +3690,19 @@ func Test_k8sRpaasManager_Scale(t *testing.T) {
assert.Equal(t, int32(30), *instance.Spec.Replicas)
},
},
{
instance: "instance-scale-from-zero",
assertion: func(t *testing.T, err error, m *k8sRpaasManager) {
assert.NoError(t, err)

instance := v1alpha1.RpaasInstance{}
err = m.cli.Get(context.Background(), types.NamespacedName{Name: "instance-scale-from-zero", Namespace: getServiceName()}, &instance)
require.NoError(t, err)

assert.Equal(t, int32(30), *instance.Spec.Replicas)
assert.False(t, instance.Spec.Shutdown)
},
},
}

for _, tt := range testCases {
Expand All @@ -3695,6 +3714,37 @@ func Test_k8sRpaasManager_Scale(t *testing.T) {
}
}

func Test_k8sRpaasManager_Start(t *testing.T) {
scheme := newScheme()
instance := newEmptyRpaasInstance()
instance.Name = "my-instance"
instance.Spec.Shutdown = true

manager := &k8sRpaasManager{cli: fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(instance).Build()}
err := manager.Start(context.Background(), "my-instance")
require.NoError(t, err)

err = manager.cli.Get(context.Background(), types.NamespacedName{Name: "my-instance", Namespace: getServiceName()}, instance)
require.NoError(t, err)

assert.False(t, instance.Spec.Shutdown)
}

func Test_k8sRpaasManager_Stop(t *testing.T) {
scheme := newScheme()
instance := newEmptyRpaasInstance()
instance.Name = "my-instance"
instance.Spec.Shutdown = false

manager := &k8sRpaasManager{cli: fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(instance).Build()}
err := manager.Stop(context.Background(), "my-instance")
require.NoError(t, err)

err = manager.cli.Get(context.Background(), types.NamespacedName{Name: "my-instance", Namespace: getServiceName()}, instance)
require.NoError(t, err)
assert.True(t, instance.Spec.Shutdown)
}

func Test_k8sRpaasManager_GetInstanceInfo(t *testing.T) {
cfg := config.Get()
defer func() { config.Set(cfg) }()
Expand Down
2 changes: 2 additions & 0 deletions internal/pkg/rpaas/manager.go
Expand Up @@ -264,6 +264,8 @@ type RpaasManager interface {
GetInstanceAddress(ctx context.Context, name string) (string, error)
GetInstanceStatus(ctx context.Context, name string) (*nginxv1alpha1.Nginx, PodStatusMap, error)
Scale(ctx context.Context, name string, replicas int32) error
Start(ctx context.Context, name string) error
Stop(ctx context.Context, name string) error
GetPlans(ctx context.Context) ([]Plan, error)
GetFlavors(ctx context.Context) ([]Flavor, error)
BindApp(ctx context.Context, instanceName string, args BindAppArgs) error
Expand Down

0 comments on commit f69ce91

Please sign in to comment.