blob: 602cef141059d202c38835f65be8a85da59d607a [file] [log] [blame]
// Copyright 2022 Google LLC
//
// Licensed 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 bigtable
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
btapb "google.golang.org/genproto/googleapis/bigtable/admin/v2"
"google.golang.org/genproto/googleapis/longrunning"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/anypb"
)
type mockAdminClock struct {
btapb.BigtableInstanceAdminClient
createInstanceReq *btapb.CreateInstanceRequest
createClusterReq *btapb.CreateClusterRequest
partialUpdateClusterReq *btapb.PartialUpdateClusterRequest
getClusterResp *btapb.Cluster
}
func (c *mockAdminClock) PartialUpdateCluster(
ctx context.Context, in *btapb.PartialUpdateClusterRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.partialUpdateClusterReq = in
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{},
}, nil
}
func (c *mockAdminClock) CreateInstance(
ctx context.Context, in *btapb.CreateInstanceRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.createInstanceReq = in
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{
Response: &anypb.Any{TypeUrl: "google.bigtable.admin.v2.Instance"},
},
}, nil
}
func (c *mockAdminClock) CreateCluster(
ctx context.Context, in *btapb.CreateClusterRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
c.createClusterReq = in
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{
Response: &anypb.Any{TypeUrl: "google.bigtable.admin.v2.Cluster"},
},
}, nil
}
func (c *mockAdminClock) PartialUpdateInstance(
ctx context.Context, in *btapb.PartialUpdateInstanceRequest, opts ...grpc.CallOption,
) (*longrunning.Operation, error) {
return &longrunning.Operation{
Done: true,
Result: &longrunning.Operation_Response{},
}, nil
}
func (c *mockAdminClock) GetCluster(
ctx context.Context, in *btapb.GetClusterRequest, opts ...grpc.CallOption,
) (*btapb.Cluster, error) {
return c.getClusterResp, nil
}
func (c *mockAdminClock) ListClusters(
ctx context.Context, in *btapb.ListClustersRequest, opts ...grpc.CallOption,
) (*btapb.ListClustersResponse, error) {
return &btapb.ListClustersResponse{Clusters: []*btapb.Cluster{c.getClusterResp}}, nil
}
func setupClient(t *testing.T, ac btapb.BigtableInstanceAdminClient) *InstanceAdminClient {
ctx := context.Background()
c, err := NewInstanceAdminClient(ctx, "my-cool-project")
if err != nil {
t.Fatalf("NewInstanceAdminClient failed: %v", err)
}
c.iClient = ac
return c
}
func TestInstanceAdmin_GetCluster(t *testing.T) {
tcs := []struct {
cluster *btapb.Cluster
wantConfig *AutoscalingConfig
desc string
}{
{
desc: "when autoscaling is not enabled",
cluster: &btapb.Cluster{
Name: ".../mycluster",
Location: ".../us-central1-a",
State: btapb.Cluster_READY,
DefaultStorageType: btapb.StorageType_SSD,
},
wantConfig: nil,
},
{
desc: "when autoscaling is enabled",
cluster: &btapb.Cluster{
Name: ".../mycluster",
Location: ".../us-central1-a",
State: btapb.Cluster_READY,
DefaultStorageType: btapb.StorageType_SSD,
Config: &btapb.Cluster_ClusterConfig_{
ClusterConfig: &btapb.Cluster_ClusterConfig{
ClusterAutoscalingConfig: &btapb.Cluster_ClusterAutoscalingConfig{
AutoscalingLimits: &btapb.AutoscalingLimits{
MinServeNodes: 1,
MaxServeNodes: 2,
},
AutoscalingTargets: &btapb.AutoscalingTargets{
CpuUtilizationPercent: 10,
},
},
},
},
},
wantConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
c := setupClient(t, &mockAdminClock{getClusterResp: tc.cluster})
info, err := c.GetCluster(context.Background(), "myinst", "mycluster")
if err != nil {
t.Fatalf("GetCluster failed: %v", err)
}
if gotConfig := info.AutoscalingConfig; !cmp.Equal(gotConfig, tc.wantConfig) {
t.Fatalf("want autoscaling config = %v, got = %v", tc.wantConfig, gotConfig)
}
})
}
}
func TestInstanceAdmin_Clusters(t *testing.T) {
tcs := []struct {
cluster *btapb.Cluster
wantConfig *AutoscalingConfig
desc string
}{
{
desc: "when autoscaling is not enabled",
cluster: &btapb.Cluster{
Name: ".../mycluster",
Location: ".../us-central1-a",
State: btapb.Cluster_READY,
DefaultStorageType: btapb.StorageType_SSD,
},
wantConfig: nil,
},
{
desc: "when autoscaling is enabled",
cluster: &btapb.Cluster{
Name: ".../mycluster",
Location: ".../us-central1-a",
State: btapb.Cluster_READY,
DefaultStorageType: btapb.StorageType_SSD,
Config: &btapb.Cluster_ClusterConfig_{
ClusterConfig: &btapb.Cluster_ClusterConfig{
ClusterAutoscalingConfig: &btapb.Cluster_ClusterAutoscalingConfig{
AutoscalingLimits: &btapb.AutoscalingLimits{
MinServeNodes: 1,
MaxServeNodes: 2,
},
AutoscalingTargets: &btapb.AutoscalingTargets{
CpuUtilizationPercent: 10,
},
},
},
},
},
wantConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10},
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
c := setupClient(t, &mockAdminClock{getClusterResp: tc.cluster})
infos, err := c.Clusters(context.Background(), "myinst")
if err != nil {
t.Fatalf("Clusters failed: %v", err)
}
if len(infos) != 1 {
t.Fatalf("Clusters len: want = 1, got = %v", len(infos))
}
info := infos[0]
if gotConfig := info.AutoscalingConfig; !cmp.Equal(gotConfig, tc.wantConfig) {
t.Fatalf("want autoscaling config = %v, got = %v", tc.wantConfig, gotConfig)
}
})
}
}
func TestInstanceAdmin_SetAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.SetAutoscaling(context.Background(), "myinst", "mycluster", AutoscalingConfig{
MinNodes: 1,
MaxNodes: 2,
CPUTargetPercent: 10,
})
if err != nil {
t.Fatalf("SetAutoscaling failed: %v", err)
}
wantMask := []string{"cluster_config.cluster_autoscaling_config"}
if gotMask := mock.partialUpdateClusterReq.UpdateMask.Paths; !cmp.Equal(wantMask, gotMask) {
t.Fatalf("want update mask = %v, got = %v", wantMask, gotMask)
}
wantName := "projects/my-cool-project/instances/myinst/clusters/mycluster"
if gotName := mock.partialUpdateClusterReq.Cluster.Name; gotName != wantName {
t.Fatalf("want name = %v, got = %v", wantName, gotName)
}
cc := mock.partialUpdateClusterReq.Cluster.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
}
func TestInstanceAdmin_UpdateCluster_RemovingAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.UpdateCluster(context.Background(), "myinst", "mycluster", 1)
if err != nil {
t.Fatalf("UpdateCluster failed: %v", err)
}
wantMask := []string{"serve_nodes", "cluster_config.cluster_autoscaling_config"}
if gotMask := mock.partialUpdateClusterReq.UpdateMask.Paths; !cmp.Equal(wantMask, gotMask) {
t.Fatalf("want update mask = %v, got = %v", wantMask, gotMask)
}
if gotConfig := mock.partialUpdateClusterReq.Cluster.Config; gotConfig != nil {
t.Fatalf("want config = nil, got = %v", gotConfig)
}
}
func TestInstanceAdmin_CreateInstance_WithAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.CreateInstance(context.Background(), &InstanceConf{
InstanceId: "myinst",
DisplayName: "myinst",
InstanceType: PRODUCTION,
ClusterId: "mycluster",
Zone: "us-central1-a",
StorageType: SSD,
AutoscalingConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10},
})
if err != nil {
t.Fatalf("CreateInstance failed: %v", err)
}
mycc := mock.createInstanceReq.Clusters["mycluster"]
cc := mycc.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
err = c.CreateInstance(context.Background(), &InstanceConf{
InstanceId: "myinst",
DisplayName: "myinst",
InstanceType: PRODUCTION,
ClusterId: "mycluster",
Zone: "us-central1-a",
StorageType: SSD,
NumNodes: 1,
})
if err != nil {
t.Fatalf("CreateInstance failed: %v", err)
}
// omitting autoscaling config results in a nil config in the request
mycc = mock.createInstanceReq.Clusters["mycluster"]
if cc := mycc.GetClusterConfig(); cc != nil {
t.Fatalf("want config = nil, got = %v", gotConfig)
}
}
func TestInstanceAdmin_CreateInstanceWithClusters_WithAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.CreateInstanceWithClusters(context.Background(), &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
InstanceType: PRODUCTION,
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
StorageType: SSD,
AutoscalingConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10},
},
},
})
if err != nil {
t.Fatalf("CreateInstanceWithClusters failed: %v", err)
}
mycc := mock.createInstanceReq.Clusters["mycluster"]
cc := mycc.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
}
func TestInstanceAdmin_CreateCluster_WithAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.CreateCluster(context.Background(), &ClusterConfig{
ClusterID: "mycluster",
Zone: "us-central1-a",
StorageType: SSD,
AutoscalingConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10},
})
if err != nil {
t.Fatalf("CreateCluster failed: %v", err)
}
cc := mock.createClusterReq.Cluster.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
err = c.CreateCluster(context.Background(), &ClusterConfig{
ClusterID: "mycluster",
Zone: "us-central1-a",
StorageType: SSD,
NumNodes: 1,
})
if err != nil {
t.Fatalf("CreateCluster failed: %v", err)
}
// omitting autoscaling config results in a nil config in the request
if cc := mock.createClusterReq.Cluster.GetClusterConfig(); cc != nil {
t.Fatalf("want config = nil, got = %v", gotConfig)
}
}
func TestInstanceAdmin_UpdateInstanceWithClusters_IgnoresInvalidClusters(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.UpdateInstanceWithClusters(context.Background(), &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
// Cluster has no autoscaling or num nodes
// It should be ignored
},
},
})
if err != nil {
t.Fatalf("UpdateInstanceWithClusters failed: %v", err)
}
if mock.partialUpdateClusterReq != nil {
t.Fatalf("PartialUpdateCluster should not have been called, got = %v",
mock.partialUpdateClusterReq)
}
}
func TestInstanceAdmin_UpdateInstanceWithClusters_WithAutoscaling(t *testing.T) {
mock := &mockAdminClock{}
c := setupClient(t, mock)
err := c.UpdateInstanceWithClusters(context.Background(), &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
AutoscalingConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10},
},
},
})
if err != nil {
t.Fatalf("UpdateInstanceWithClusters failed: %v", err)
}
cc := mock.partialUpdateClusterReq.Cluster.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
err = c.UpdateInstanceWithClusters(context.Background(), &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
NumNodes: 1,
// no autoscaling config
},
},
})
if err != nil {
t.Fatalf("UpdateInstanceWithClusters failed: %v", err)
}
got := mock.partialUpdateClusterReq.Cluster.Config
if got != nil {
t.Fatalf("want autoscaling config = nil, got = %v", gotConfig)
}
}
func TestInstanceAdmin_UpdateInstanceAndSyncClusters_WithAutoscaling(t *testing.T) {
mock := &mockAdminClock{
getClusterResp: &btapb.Cluster{
Name: ".../mycluster",
Location: ".../us-central1-a",
State: btapb.Cluster_READY,
DefaultStorageType: btapb.StorageType_SSD,
Config: &btapb.Cluster_ClusterConfig_{
ClusterConfig: &btapb.Cluster_ClusterConfig{
ClusterAutoscalingConfig: &btapb.Cluster_ClusterAutoscalingConfig{
AutoscalingLimits: &btapb.AutoscalingLimits{
MinServeNodes: 1,
MaxServeNodes: 2,
},
AutoscalingTargets: &btapb.AutoscalingTargets{
CpuUtilizationPercent: 10,
},
},
},
},
},
}
c := setupClient(t, mock)
_, err := UpdateInstanceAndSyncClusters(context.Background(), c, &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
AutoscalingConfig: &AutoscalingConfig{MinNodes: 1, MaxNodes: 2, CPUTargetPercent: 10},
},
},
})
if err != nil {
t.Fatalf("UpdateInstanceAndSyncClusters failed: %v", err)
}
cc := mock.partialUpdateClusterReq.Cluster.Config.(*btapb.Cluster_ClusterConfig_)
gotConfig := cc.ClusterConfig.ClusterAutoscalingConfig
wantMin := int32(1)
if gotMin := gotConfig.AutoscalingLimits.MinServeNodes; wantMin != gotMin {
t.Fatalf("want autoscaling min nodes = %v, got = %v", wantMin, gotMin)
}
wantMax := int32(2)
if gotMax := gotConfig.AutoscalingLimits.MaxServeNodes; wantMax != gotMax {
t.Fatalf("want autoscaling max nodes = %v, got = %v", wantMax, gotMax)
}
wantCPU := int32(10)
if gotCPU := gotConfig.AutoscalingTargets.CpuUtilizationPercent; wantCPU != gotCPU {
t.Fatalf("want autoscaling cpu = %v, got = %v", wantCPU, gotCPU)
}
_, err = UpdateInstanceAndSyncClusters(context.Background(), c, &InstanceWithClustersConfig{
InstanceID: "myinst",
DisplayName: "myinst",
Clusters: []ClusterConfig{
{
ClusterID: "mycluster",
Zone: "us-central1-a",
NumNodes: 1,
},
},
})
if err != nil {
t.Fatalf("UpdateInstanceAndSyncClusters failed: %v", err)
}
got := mock.partialUpdateClusterReq.Cluster.Config
if got != nil {
t.Fatalf("want autoscaling config = nil, got = %v", gotConfig)
}
}