2023-05-09 18:57:40 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
2023-08-11 13:12:13 +00:00
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
2023-05-09 18:57:40 +00:00
|
|
|
|
|
|
|
package reaper
|
|
|
|
|
|
|
|
import (
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
"google.golang.org/grpc/status"
|
|
|
|
|
|
|
|
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"
|
2023-12-05 19:00:06 +00:00
|
|
|
"github.com/hashicorp/consul/internal/resource/resourcetest"
|
|
|
|
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
|
2023-05-09 18:57:40 +00:00
|
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestReconcile_ResourceWithNoChildren(t *testing.T) {
|
2023-12-05 19:00:06 +00:00
|
|
|
client := setupResourceService(t)
|
2023-11-09 19:44:33 +00:00
|
|
|
runReaperTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
|
|
|
|
// Seed the database with an artist.
|
|
|
|
res, err := demo.GenerateV2Artist()
|
|
|
|
|
|
|
|
// set resource tenancy from default to test tenancy
|
|
|
|
res.Id.Tenancy = tenancy
|
|
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
ctx := testutil.TestContext(t)
|
|
|
|
writeRsp, err := client.Write(ctx, &pbresource.WriteRequest{Resource: res})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Delete the artist to create a tombstone
|
|
|
|
_, err = client.Delete(ctx, &pbresource.DeleteRequest{Id: writeRsp.Resource.Id})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Retrieve tombstone
|
|
|
|
listRsp, err := client.List(ctx, &pbresource.ListRequest{
|
|
|
|
Type: resource.TypeV1Tombstone,
|
|
|
|
Tenancy: tenancy,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, listRsp.Resources, 1)
|
|
|
|
tombstone := listRsp.Resources[0]
|
|
|
|
|
|
|
|
// Verify reconcile does first pass and queues up for a second pass
|
|
|
|
rec := newReconciler()
|
|
|
|
runtime := controller.Runtime{
|
|
|
|
Client: client,
|
|
|
|
Logger: testutil.Logger(t),
|
|
|
|
}
|
|
|
|
req := controller.Request{ID: tombstone.Id}
|
|
|
|
require.ErrorIs(t, controller.RequeueAfterError(secondPassDelay), rec.Reconcile(ctx, runtime, req))
|
|
|
|
|
|
|
|
// Verify condition FirstPassCompleted is true
|
|
|
|
readRsp, err := client.Read(ctx, &pbresource.ReadRequest{Id: tombstone.Id})
|
|
|
|
require.NoError(t, err)
|
|
|
|
tombstone = readRsp.Resource
|
|
|
|
condition := tombstone.Status[statusKeyReaperController].Conditions[0]
|
|
|
|
require.Equal(t, conditionTypeFirstPassCompleted, condition.Type)
|
|
|
|
require.Equal(t, pbresource.Condition_STATE_TRUE, condition.State)
|
|
|
|
|
|
|
|
// Verify reconcile does second pass and tombstone is deleted
|
|
|
|
// Fake out time so elapsed time > secondPassDelay
|
|
|
|
rec.timeNow = func() time.Time { return time.Now().Add(secondPassDelay + time.Second) }
|
|
|
|
require.NoError(t, rec.Reconcile(ctx, runtime, req))
|
|
|
|
_, err = client.Read(ctx, &pbresource.ReadRequest{Id: tombstone.Id})
|
|
|
|
require.Error(t, err)
|
|
|
|
require.Equal(t, codes.NotFound.String(), status.Code(err).String())
|
|
|
|
|
|
|
|
// Reconcile again to verify no-op on an already deleted tombstone
|
|
|
|
require.NoError(t, rec.Reconcile(ctx, runtime, req))
|
2023-05-09 18:57:40 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestReconcile_ResourceWithChildren(t *testing.T) {
|
2023-12-05 19:00:06 +00:00
|
|
|
client := setupResourceService(t)
|
2023-11-09 19:44:33 +00:00
|
|
|
runReaperTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
|
|
|
|
// Seed the database with an artist
|
|
|
|
res, err := demo.GenerateV2Artist()
|
2023-05-09 18:57:40 +00:00
|
|
|
|
2023-11-09 19:44:33 +00:00
|
|
|
// set resource tenancy from default to test tenancy
|
|
|
|
res.Id.Tenancy = tenancy
|
2023-05-09 18:57:40 +00:00
|
|
|
|
|
|
|
require.NoError(t, err)
|
2023-11-09 19:44:33 +00:00
|
|
|
ctx := testutil.TestContext(t)
|
|
|
|
writeRsp, err := client.Write(ctx, &pbresource.WriteRequest{Resource: res})
|
|
|
|
require.NoError(t, err)
|
|
|
|
artist := writeRsp.Resource
|
|
|
|
|
|
|
|
// Create 3 albums owned by the artist
|
|
|
|
numAlbums := 3
|
|
|
|
for i := 0; i < numAlbums; i++ {
|
|
|
|
res, err = demo.GenerateV2Album(artist.Id)
|
|
|
|
require.NoError(t, err)
|
|
|
|
_, err := client.Write(ctx, &pbresource.WriteRequest{Resource: res})
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete the artist to create a tombstone
|
|
|
|
_, err = client.Delete(ctx, &pbresource.DeleteRequest{Id: writeRsp.Resource.Id})
|
2023-05-09 18:57:40 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-11-09 19:44:33 +00:00
|
|
|
// Retrieve the tombstone
|
|
|
|
listRsp, err := client.List(ctx, &pbresource.ListRequest{
|
|
|
|
Type: resource.TypeV1Tombstone,
|
|
|
|
Tenancy: writeRsp.Resource.Id.Tenancy,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, listRsp.Resources, 1)
|
|
|
|
tombstone := listRsp.Resources[0]
|
|
|
|
|
|
|
|
// Verify reconcile does first pass delete and queues up for a second pass
|
|
|
|
rec := newReconciler()
|
|
|
|
runtime := controller.Runtime{
|
|
|
|
Client: client,
|
|
|
|
Logger: testutil.Logger(t),
|
|
|
|
}
|
|
|
|
req := controller.Request{ID: tombstone.Id}
|
|
|
|
require.ErrorIs(t, controller.RequeueAfterError(secondPassDelay), rec.Reconcile(ctx, runtime, req))
|
|
|
|
|
|
|
|
// Verify 3 albums deleted
|
|
|
|
listRsp, err = client.List(ctx, &pbresource.ListRequest{
|
|
|
|
Type: demo.TypeV2Album,
|
|
|
|
Tenancy: artist.Id.Tenancy,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, listRsp.Resources)
|
2023-05-09 18:57:40 +00:00
|
|
|
|
2023-11-09 19:44:33 +00:00
|
|
|
// Verify condition FirstPassCompleted is true
|
|
|
|
readRsp, err := client.Read(ctx, &pbresource.ReadRequest{Id: tombstone.Id})
|
|
|
|
require.NoError(t, err)
|
|
|
|
tombstone = readRsp.Resource
|
|
|
|
condition := tombstone.Status[statusKeyReaperController].Conditions[0]
|
|
|
|
require.Equal(t, conditionTypeFirstPassCompleted, condition.Type)
|
|
|
|
require.Equal(t, pbresource.Condition_STATE_TRUE, condition.State)
|
|
|
|
|
|
|
|
// Verify reconcile does second pass
|
|
|
|
// Fake out time so elapsed time > secondPassDelay
|
|
|
|
rec.timeNow = func() time.Time { return time.Now().Add(secondPassDelay + time.Second) }
|
|
|
|
require.NoError(t, rec.Reconcile(ctx, runtime, req))
|
|
|
|
|
|
|
|
// Verify artist tombstone deleted
|
|
|
|
_, err = client.Read(ctx, &pbresource.ReadRequest{Id: tombstone.Id})
|
|
|
|
require.Error(t, err)
|
|
|
|
require.Equal(t, codes.NotFound.String(), status.Code(err).String())
|
|
|
|
|
|
|
|
// Verify tombstones for 3 albums created
|
|
|
|
listRsp, err = client.List(ctx, &pbresource.ListRequest{
|
|
|
|
Type: resource.TypeV1Tombstone,
|
|
|
|
Tenancy: artist.Id.Tenancy,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, listRsp.Resources, numAlbums)
|
2023-05-09 18:57:40 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestReconcile_RequeueWithDelayWhenSecondPassDelayNotElapsed(t *testing.T) {
|
2023-12-05 19:00:06 +00:00
|
|
|
client := setupResourceService(t)
|
2023-11-09 19:44:33 +00:00
|
|
|
runReaperTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
|
|
|
|
// Seed the database with an artist.
|
|
|
|
res, err := demo.GenerateV2Artist()
|
|
|
|
|
|
|
|
// set resource tenancy from default to test tenancy
|
|
|
|
res.Id.Tenancy = tenancy
|
|
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
ctx := testutil.TestContext(t)
|
|
|
|
writeRsp, err := client.Write(ctx, &pbresource.WriteRequest{Resource: res})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Delete the artist to create a tombstone
|
|
|
|
_, err = client.Delete(ctx, &pbresource.DeleteRequest{Id: writeRsp.Resource.Id})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Retrieve tombstone
|
|
|
|
listRsp, err := client.List(ctx, &pbresource.ListRequest{
|
|
|
|
Type: resource.TypeV1Tombstone,
|
|
|
|
Tenancy: writeRsp.Resource.Id.Tenancy,
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, listRsp.Resources, 1)
|
|
|
|
tombstone := listRsp.Resources[0]
|
|
|
|
|
|
|
|
// Verify reconcile does first pass and queues up for a second pass
|
|
|
|
rec := newReconciler()
|
|
|
|
runtime := controller.Runtime{
|
|
|
|
Client: client,
|
|
|
|
Logger: testutil.Logger(t),
|
|
|
|
}
|
|
|
|
req := controller.Request{ID: tombstone.Id}
|
|
|
|
require.ErrorIs(t, controller.RequeueAfterError(secondPassDelay), rec.Reconcile(ctx, runtime, req))
|
|
|
|
|
|
|
|
// Verify condition FirstPassCompleted is true
|
|
|
|
readRsp, err := client.Read(ctx, &pbresource.ReadRequest{Id: tombstone.Id})
|
|
|
|
require.NoError(t, err)
|
|
|
|
tombstone = readRsp.Resource
|
|
|
|
condition := tombstone.Status[statusKeyReaperController].Conditions[0]
|
|
|
|
require.Equal(t, conditionTypeFirstPassCompleted, condition.Type)
|
|
|
|
require.Equal(t, pbresource.Condition_STATE_TRUE, condition.State)
|
|
|
|
|
|
|
|
// Verify requeued for second pass since secondPassDelay time has not elapsed
|
|
|
|
require.ErrorIs(t, controller.RequeueAfterError(secondPassDelay), rec.Reconcile(ctx, runtime, req))
|
2023-05-09 18:57:40 +00:00
|
|
|
})
|
2023-11-09 19:44:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func runReaperTestCaseWithTenancies(testCase func(tenancy *pbresource.Tenancy)) {
|
|
|
|
for _, tenancy := range resourcetest.TestTenancies() {
|
|
|
|
testCase(tenancy)
|
2023-05-09 18:57:40 +00:00
|
|
|
}
|
|
|
|
}
|
2023-12-05 19:00:06 +00:00
|
|
|
|
|
|
|
func setupResourceService(t *testing.T) pbresource.ResourceServiceClient {
|
|
|
|
return svctest.NewResourceServiceBuilder().
|
|
|
|
WithTenancies(rtest.TestTenancies()...).
|
|
|
|
WithRegisterFns(demo.RegisterTypes).
|
|
|
|
Run(t)
|
|
|
|
}
|