diff --git a/Makefile b/Makefile index a4b32ce3f..2f57c880d 100644 --- a/Makefile +++ b/Makefile @@ -167,6 +167,7 @@ run-unit-tests: $(GOBUILDDIR) $(SOURCES) golang:$(GOVERSION) \ go test -v \ $(REPOPATH)/pkg/apis/arangodb/v1alpha \ + $(REPOPATH)/pkg/deployment \ $(REPOPATH)/pkg/util/k8sutil $(TESTBIN): $(GOBUILDDIR) $(SOURCES) diff --git a/pkg/deployment/plan_builder.go b/pkg/deployment/plan_builder.go index d48de4521..bcada6660 100644 --- a/pkg/deployment/plan_builder.go +++ b/pkg/deployment/plan_builder.go @@ -31,15 +31,37 @@ import ( // get the status in line with the specification. // If a plan already exists, nothing is done. func (d *Deployment) createPlan() error { - if len(d.status.Plan) > 0 { - // Plan already exists, complete that first + // Create plan + newPlan, changed := createPlan(d.deps.Log, d.status.Plan, d.apiObject.Spec, d.status) + + // If not change, we're done + if !changed { + return nil + } + + // Save plan + if len(newPlan) == 0 { + // Nothing to do return nil } + d.status.Plan = newPlan + if err := d.updateCRStatus(); err != nil { + return maskAny(err) + } + return nil +} + +// createPlan considers the given specification & status and creates a plan to get the status in line with the specification. +// If a plan already exists, the given plan is returned with false. +// Otherwise the new plan is returned with a boolean true. +func createPlan(log zerolog.Logger, currentPlan api.Plan, spec api.DeploymentSpec, status api.DeploymentStatus) (api.Plan, bool) { + if len(currentPlan) > 0 { + // Plan already exists, complete that first + return currentPlan, false + } // Check for various scenario's - spec := d.apiObject.Spec var plan api.Plan - log := d.deps.Log // Check for scale up/down switch spec.Mode { @@ -47,25 +69,17 @@ func (d *Deployment) createPlan() error { // Never scale down case api.DeploymentModeResilientSingle: // Only scale singles - plan = append(plan, createScalePlan(log, d.status.Members.Single, api.ServerGroupSingle, spec.Single.Count)...) + plan = append(plan, createScalePlan(log, status.Members.Single, api.ServerGroupSingle, spec.Single.Count)...) case api.DeploymentModeCluster: // Scale dbservers, coordinators, syncmasters & syncworkers - plan = append(plan, createScalePlan(log, d.status.Members.DBServers, api.ServerGroupDBServers, spec.DBServers.Count)...) - plan = append(plan, createScalePlan(log, d.status.Members.Coordinators, api.ServerGroupCoordinators, spec.Coordinators.Count)...) - plan = append(plan, createScalePlan(log, d.status.Members.SyncMasters, api.ServerGroupSyncMasters, spec.SyncMasters.Count)...) - plan = append(plan, createScalePlan(log, d.status.Members.SyncWorkers, api.ServerGroupSyncWorkers, spec.SyncWorkers.Count)...) + plan = append(plan, createScalePlan(log, status.Members.DBServers, api.ServerGroupDBServers, spec.DBServers.Count)...) + plan = append(plan, createScalePlan(log, status.Members.Coordinators, api.ServerGroupCoordinators, spec.Coordinators.Count)...) + plan = append(plan, createScalePlan(log, status.Members.SyncMasters, api.ServerGroupSyncMasters, spec.SyncMasters.Count)...) + plan = append(plan, createScalePlan(log, status.Members.SyncWorkers, api.ServerGroupSyncWorkers, spec.SyncWorkers.Count)...) } - // Save plan - if len(plan) == 0 { - // Nothing to do - return nil - } - d.status.Plan = plan - if err := d.updateCRStatus(); err != nil { - return maskAny(err) - } - return nil + // Return plan + return plan, true } // createScalePlan creates a scaling plan for a single server group diff --git a/pkg/deployment/plan_builder_test.go b/pkg/deployment/plan_builder_test.go new file mode 100644 index 000000000..dd9a29b73 --- /dev/null +++ b/pkg/deployment/plan_builder_test.go @@ -0,0 +1,227 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// 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. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package deployment + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + api "github.com/arangodb/k8s-operator/pkg/apis/arangodb/v1alpha" +) + +// TestCreatePlanSingleScale creates a `single` deployment to test the creating of scaling plan. +func TestCreatePlanSingleScale(t *testing.T) { + log := zerolog.Nop() + spec := api.DeploymentSpec{ + Mode: api.DeploymentModeSingle, + } + spec.SetDefaults("test") + + // Test with empty status + var status api.DeploymentStatus + newPlan, changed := createPlan(log, nil, spec, status) + assert.True(t, changed) + assert.Len(t, newPlan, 0) // Single mode does not scale + + // Test with 1 single member + status.Members.Single = api.MemberStatusList{ + api.MemberStatus{ + ID: "id", + PodName: "something", + }, + } + newPlan, changed = createPlan(log, nil, spec, status) + assert.True(t, changed) + assert.Len(t, newPlan, 0) // Single mode does not scale + + // Test with 2 single members (which should not happen) and try to scale down + status.Members.Single = api.MemberStatusList{ + api.MemberStatus{ + ID: "id1", + PodName: "something1", + }, + api.MemberStatus{ + ID: "id1", + PodName: "something1", + }, + } + newPlan, changed = createPlan(log, nil, spec, status) + assert.True(t, changed) + assert.Len(t, newPlan, 0) // Single mode does not scale +} + +// TestCreatePlanResilientSingleScale creates a `resilientsingle` deployment to test the creating of scaling plan. +func TestCreatePlanResilientSingleScale(t *testing.T) { + log := zerolog.Nop() + spec := api.DeploymentSpec{ + Mode: api.DeploymentModeResilientSingle, + } + spec.SetDefaults("test") + spec.Single.Count = 2 + + // Test with empty status + var status api.DeploymentStatus + newPlan, changed := createPlan(log, nil, spec, status) + assert.True(t, changed) + require.Len(t, newPlan, 2) + assert.Equal(t, api.ActionTypeAddMember, newPlan[0].Type) + assert.Equal(t, api.ActionTypeAddMember, newPlan[1].Type) + + // Test with 1 single member + status.Members.Single = api.MemberStatusList{ + api.MemberStatus{ + ID: "id", + PodName: "something", + }, + } + newPlan, changed = createPlan(log, nil, spec, status) + assert.True(t, changed) + require.Len(t, newPlan, 1) + assert.Equal(t, api.ActionTypeAddMember, newPlan[0].Type) + assert.Equal(t, api.ServerGroupSingle, newPlan[0].Group) + + // Test scaling down from 4 members to 2 + status.Members.Single = api.MemberStatusList{ + api.MemberStatus{ + ID: "id1", + PodName: "something1", + }, + api.MemberStatus{ + ID: "id2", + PodName: "something2", + }, + api.MemberStatus{ + ID: "id3", + PodName: "something3", + }, + api.MemberStatus{ + ID: "id4", + PodName: "something4", + }, + } + newPlan, changed = createPlan(log, nil, spec, status) + assert.True(t, changed) + require.Len(t, newPlan, 2) // Note: Downscaling is only down 1 at a time + assert.Equal(t, api.ActionTypeShutdownMember, newPlan[0].Type) + assert.Equal(t, api.ActionTypeRemoveMember, newPlan[1].Type) + assert.Equal(t, api.ServerGroupSingle, newPlan[0].Group) + assert.Equal(t, api.ServerGroupSingle, newPlan[1].Group) +} + +// TestCreatePlanClusterScale creates a `cluster` deployment to test the creating of scaling plan. +func TestCreatePlanClusterScale(t *testing.T) { + log := zerolog.Nop() + spec := api.DeploymentSpec{ + Mode: api.DeploymentModeCluster, + } + spec.SetDefaults("test") + + // Test with empty status + var status api.DeploymentStatus + newPlan, changed := createPlan(log, nil, spec, status) + assert.True(t, changed) + require.Len(t, newPlan, 6) // Adding 3 dbservers & 3 coordinators (note: agents do not scale now) + assert.Equal(t, api.ActionTypeAddMember, newPlan[0].Type) + assert.Equal(t, api.ActionTypeAddMember, newPlan[1].Type) + assert.Equal(t, api.ActionTypeAddMember, newPlan[2].Type) + assert.Equal(t, api.ActionTypeAddMember, newPlan[3].Type) + assert.Equal(t, api.ActionTypeAddMember, newPlan[4].Type) + assert.Equal(t, api.ActionTypeAddMember, newPlan[5].Type) + assert.Equal(t, api.ServerGroupDBServers, newPlan[0].Group) + assert.Equal(t, api.ServerGroupDBServers, newPlan[1].Group) + assert.Equal(t, api.ServerGroupDBServers, newPlan[2].Group) + assert.Equal(t, api.ServerGroupCoordinators, newPlan[3].Group) + assert.Equal(t, api.ServerGroupCoordinators, newPlan[4].Group) + assert.Equal(t, api.ServerGroupCoordinators, newPlan[5].Group) + + // Test with 2 dbservers & 1 coordinator + status.Members.DBServers = api.MemberStatusList{ + api.MemberStatus{ + ID: "db1", + PodName: "something1", + }, + api.MemberStatus{ + ID: "db2", + PodName: "something2", + }, + } + status.Members.Coordinators = api.MemberStatusList{ + api.MemberStatus{ + ID: "cr1", + PodName: "coordinator1", + }, + } + newPlan, changed = createPlan(log, nil, spec, status) + assert.True(t, changed) + require.Len(t, newPlan, 3) + assert.Equal(t, api.ActionTypeAddMember, newPlan[0].Type) + assert.Equal(t, api.ActionTypeAddMember, newPlan[1].Type) + assert.Equal(t, api.ActionTypeAddMember, newPlan[2].Type) + assert.Equal(t, api.ServerGroupDBServers, newPlan[0].Group) + assert.Equal(t, api.ServerGroupCoordinators, newPlan[1].Group) + assert.Equal(t, api.ServerGroupCoordinators, newPlan[2].Group) + + // Now scale down + status.Members.DBServers = api.MemberStatusList{ + api.MemberStatus{ + ID: "db1", + PodName: "something1", + }, + api.MemberStatus{ + ID: "db2", + PodName: "something2", + }, + api.MemberStatus{ + ID: "db3", + PodName: "something3", + }, + } + status.Members.Coordinators = api.MemberStatusList{ + api.MemberStatus{ + ID: "cr1", + PodName: "coordinator1", + }, + api.MemberStatus{ + ID: "cr2", + PodName: "coordinator2", + }, + } + spec.DBServers.Count = 1 + spec.Coordinators.Count = 1 + newPlan, changed = createPlan(log, nil, spec, status) + assert.True(t, changed) + require.Len(t, newPlan, 5) // Note: Downscaling is done 1 at a time + assert.Equal(t, api.ActionTypeCleanOutMember, newPlan[0].Type) + assert.Equal(t, api.ActionTypeShutdownMember, newPlan[1].Type) + assert.Equal(t, api.ActionTypeRemoveMember, newPlan[2].Type) + assert.Equal(t, api.ActionTypeShutdownMember, newPlan[3].Type) + assert.Equal(t, api.ActionTypeRemoveMember, newPlan[4].Type) + assert.Equal(t, api.ServerGroupDBServers, newPlan[0].Group) + assert.Equal(t, api.ServerGroupDBServers, newPlan[1].Group) + assert.Equal(t, api.ServerGroupDBServers, newPlan[2].Group) + assert.Equal(t, api.ServerGroupCoordinators, newPlan[3].Group) + assert.Equal(t, api.ServerGroupCoordinators, newPlan[4].Group) +}