consul/internal/tenancy/tenancytest/namespace_controller_test.go

160 lines
5.5 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package tenancytest
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing"
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/demo"
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
"github.com/hashicorp/consul/internal/tenancy/internal/controllers/common"
"github.com/hashicorp/consul/internal/tenancy/internal/controllers/namespace"
"github.com/hashicorp/consul/proto-public/pbresource"
pbtenancy "github.com/hashicorp/consul/proto-public/pbtenancy/v2beta1"
pbdemo "github.com/hashicorp/consul/proto/private/pbdemo/v1"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
)
// Due to a circular dependency, this test can't reside in the package next to the controller it is testing.
type nsTestSuite struct {
suite.Suite
client *rtest.Client
runtime controller.Runtime
ctx context.Context
}
func (ts *nsTestSuite) SetupTest() {
builder := svctest.NewResourceServiceBuilder().
WithV2Tenancy(true).
WithRegisterFns(demo.RegisterTypes)
ts.client = rtest.NewClient(builder.Run(ts.T()))
ts.runtime = controller.Runtime{Client: ts.client, Logger: testutil.Logger(ts.T())}
ts.ctx = testutil.TestContext(ts.T())
mgr := controller.NewManager(ts.client, testutil.Logger(ts.T()))
mgr.Register(namespace.Controller(builder.Registry()))
mgr.SetRaftLeader(true)
ctx, cancel := context.WithCancel(context.Background())
ts.T().Cleanup(cancel)
go mgr.Run(ctx)
}
func (ts *nsTestSuite) waitForReconciliation(id *pbresource.ID, reason string) {
ts.T().Helper()
retry.Run(ts.T(), func(r *retry.R) {
rsp, err := ts.client.Read(context.Background(), &pbresource.ReadRequest{
Id: id,
})
require.NoError(r, err)
status, found := rsp.Resource.Status[namespace.StatusKey]
require.True(r, found)
require.Len(r, status.Conditions, 1)
require.Equal(r, reason, status.Conditions[0].Reason)
})
}
func (ts *nsTestSuite) TestNamespaceController_HappyPath() {
// Create namespace ns1
ns1 := rtest.Resource(pbtenancy.NamespaceType, "ns1").
// Keep this CE friendly by using default partition
WithTenancy(resource.DefaultPartitionedTenancy()).
WithData(ts.T(), &pbtenancy.Namespace{Description: "namespace ns1"}).
Write(ts.T(), ts.client)
// Wait for it to be accepted
ts.waitForReconciliation(ns1.Id, common.ReasonAcceptedOK)
// Verify namespace finalizer added
ns1 = ts.client.RequireResourceMeta(ts.T(), ns1.Id, resource.FinalizerKey, namespace.StatusKey)
// Add a namespace scoped tenant to the namespace
artist1 := rtest.Resource(demo.TypeV1Artist, "moonchild").
WithTenancy(&pbresource.Tenancy{
Partition: resource.DefaultPartitionName,
Namespace: ns1.Id.Name,
PeerName: resource.DefaultPeerName,
}).
WithData(ts.T(), &pbdemo.Artist{Name: "Moonchild"}).
Write(ts.T(), ts.client)
// Delete the namespace
_, err := ts.client.Delete(ts.ctx, &pbresource.DeleteRequest{Id: ns1.Id})
require.NoError(ts.T(), err)
// Wait for the namespace to be deleted
ts.client.WaitForDeletion(ts.T(), ns1.Id)
// Verify tenants deleted.
ts.client.RequireResourceNotFound(ts.T(), artist1.Id)
}
func (ts *nsTestSuite) TestNamespaceController_DeleteBlockedByTenantsWithFinalizers() {
// Create namespace ns1
ns1 := rtest.Resource(pbtenancy.NamespaceType, "ns1").
WithTenancy(resource.DefaultPartitionedTenancy()).
WithData(ts.T(), &pbtenancy.Namespace{Description: "namespace ns1"}).
Write(ts.T(), ts.client)
// Wait for it to be accepted
ts.waitForReconciliation(ns1.Id, common.ReasonAcceptedOK)
// Add artist to namespace
_ = rtest.Resource(demo.TypeV1Artist, "weezer").
WithTenancy(&pbresource.Tenancy{
Partition: resource.DefaultPartitionName,
Namespace: ns1.Id.Name,
PeerName: resource.DefaultPeerName,
}).
WithData(ts.T(), &pbdemo.Artist{Name: "Weezer"}).
Write(ts.T(), ts.client)
// Add another artist to namespace with a finalizer so that is blocks namespace deletion.
artist2 := rtest.Resource(demo.TypeV1Artist, "foofighters").
WithTenancy(&pbresource.Tenancy{
Partition: resource.DefaultPartitionName,
Namespace: ns1.Id.Name,
PeerName: resource.DefaultPeerName,
}).
WithData(ts.T(), &pbdemo.Artist{Name: "Foo Fighters"}).
WithMeta(resource.FinalizerKey, "finalizer2").
Write(ts.T(), ts.client)
// Delete the namespace - this activates the controller logic to delete all tenants
ts.client.Delete(ts.ctx, &pbresource.DeleteRequest{Id: ns1.Id})
// Delete should be blocked by artist2 tenant with finalizer
ts.client.WaitForStatusConditionAnyGen(ts.T(), ns1.Id, namespace.StatusKey, &pbresource.Condition{
Type: common.ConditionDeleted,
State: pbresource.Condition_STATE_FALSE,
Reason: common.ReasonDeletionInProgress,
Message: common.ErrStillHasTenants.Error(),
})
// Remove the finalizer on artist2 to unblock deletion of ns1
artist2 = ts.client.RequireResourceExists(ts.T(), artist2.Id)
resource.RemoveFinalizer(artist2, "finalizer2")
_, err := ts.client.Write(ts.ctx, &pbresource.WriteRequest{Resource: artist2})
require.NoError(ts.T(), err)
// The final reconcile should delete artist since it was marked for deletion and
// and has no finalizers. Given no more tenants, wait for namespace to be deleted.
ts.client.WaitForDeletion(ts.T(), ns1.Id)
}
func TestNamespaceControllerSuite(t *testing.T) {
suite.Run(t, new(nsTestSuite))
}