diff --git a/go.mod b/go.mod index 13b6f835d2..eb08bccfef 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/aws/aws-sdk-go v1.44.289 github.com/coredns/coredns v1.10.1 github.com/coreos/go-oidc v2.1.0+incompatible + github.com/deckarep/golang-set/v2 v2.3.1 github.com/docker/go-connections v0.4.0 github.com/envoyproxy/go-control-plane v0.11.1 github.com/envoyproxy/go-control-plane/xdsmatcher v0.0.0-20230524161521-aaaacbfbe53e diff --git a/go.sum b/go.sum index 3598121de6..7ba17c40bc 100644 --- a/go.sum +++ b/go.sum @@ -210,6 +210,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.3.1 h1:vjmkvJt/IV27WXPyYQpAh4bRyWJc5Y435D17XQ9QU5A= +github.com/deckarep/golang-set/v2 v2.3.1/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/denverdino/aliyungo v0.0.0-20170926055100-d3308649c661 h1:lrWnAyy/F72MbxIxFUzKmcMCdt9Oi8RzpAxzTNQHD7o= github.com/denverdino/aliyungo v0.0.0-20170926055100-d3308649c661/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= diff --git a/internal/resource/resource.go b/internal/resource/resource.go index b5100f0029..2f3a7a05ca 100644 --- a/internal/resource/resource.go +++ b/internal/resource/resource.go @@ -7,11 +7,23 @@ import ( "fmt" "strings" + mapset "github.com/deckarep/golang-set/v2" + "github.com/hashicorp/consul/agent/dns" + "github.com/hashicorp/consul/proto-public/pbresource" ) +// MaxNameLength is the maximum length of a resource name. const MaxNameLength = 63 +// DeletionTimestampKey is the key in a resource's metadata that stores the timestamp +// when a resource was marked for deletion. This only applies to resources with finalizers. +const DeletionTimestampKey = "deletionTimestamp" + +// FinalizerKey is the key in resource's metadata that stores the whitespace separated +// list of finalizers. +const FinalizerKey = "finalizers" + // ValidateName returns an error a name is not a valid resource name. // The error will contain reference to what constitutes a valid resource name. func ValidateName(name string) error { @@ -20,3 +32,55 @@ func ValidateName(name string) error { } return nil } + +// IsMarkedForDeletion returns true if a resource has been marked for deletion, +// false otherwise. +func IsMarkedForDeletion(res *pbresource.Resource) bool { + if res.Metadata == nil { + return false + } + _, ok := res.Metadata[DeletionTimestampKey] + return ok +} + +// HasFinalizers returns true if a resource has one or more finalizers, false otherwise. +func HasFinalizers(res *pbresource.Resource) bool { + return GetFinalizers(res).Cardinality() >= 1 +} + +// HasFinalizer returns true if a resource has a given finalizers, false otherwise. +func HasFinalizer(res *pbresource.Resource, finalizer string) bool { + return GetFinalizers(res).Contains(finalizer) +} + +// AddFinalizer adds a finalizer to the given resource. +func AddFinalizer(res *pbresource.Resource, finalizer string) { + finalizerSet := GetFinalizers(res) + finalizerSet.Add(finalizer) + if res.Metadata == nil { + res.Metadata = map[string]string{} + } + res.Metadata[FinalizerKey] = strings.Join(finalizerSet.ToSlice(), " ") +} + +// RemoveFinalizer removes a finalizer from the given resource. +func RemoveFinalizer(res *pbresource.Resource, finalizer string) { + finalizerSet := GetFinalizers(res) + finalizerSet.Remove(finalizer) + if res.Metadata == nil { + res.Metadata = map[string]string{} + } + res.Metadata[FinalizerKey] = strings.Join(finalizerSet.ToSlice(), " ") +} + +// GetFinalizers returns the set of finalizers for the given resource. +func GetFinalizers(res *pbresource.Resource) mapset.Set[string] { + if res.Metadata == nil { + return mapset.NewSet[string]() + } + finalizers, ok := res.Metadata[FinalizerKey] + if !ok { + return mapset.NewSet[string]() + } + return mapset.NewSet[string](strings.Fields(finalizers)...) +} diff --git a/internal/resource/resource_test.go b/internal/resource/resource_test.go new file mode 100644 index 0000000000..5057bd4cf8 --- /dev/null +++ b/internal/resource/resource_test.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resource_test + +import ( + "testing" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/internal/resource" + rtest "github.com/hashicorp/consul/internal/resource/resourcetest" + pbtenancy "github.com/hashicorp/consul/proto-public/pbtenancy/v2beta1" +) + +func TestFinalizer(t *testing.T) { + t.Run("no finalizers", func(t *testing.T) { + res := rtest.Resource(pbtenancy.NamespaceType, "ns1").Build() + require.False(t, resource.HasFinalizers(res)) + require.False(t, resource.HasFinalizer(res, "finalizer1")) + require.Equal(t, mapset.NewSet[string](), resource.GetFinalizers(res)) + resource.RemoveFinalizer(res, "finalizer") + }) + + t.Run("add finalizer", func(t *testing.T) { + res := rtest.Resource(pbtenancy.NamespaceType, "ns1").Build() + resource.AddFinalizer(res, "finalizer1") + require.True(t, resource.HasFinalizers(res)) + require.True(t, resource.HasFinalizer(res, "finalizer1")) + require.False(t, resource.HasFinalizer(res, "finalizer2")) + require.Equal(t, mapset.NewSet[string]("finalizer1"), resource.GetFinalizers(res)) + + // add duplicate -> noop + resource.AddFinalizer(res, "finalizer1") + require.Equal(t, mapset.NewSet[string]("finalizer1"), resource.GetFinalizers(res)) + }) + + t.Run("remove finalizer", func(t *testing.T) { + res := rtest.Resource(pbtenancy.NamespaceType, "ns1").Build() + resource.AddFinalizer(res, "finalizer1") + resource.AddFinalizer(res, "finalizer2") + resource.RemoveFinalizer(res, "finalizer1") + require.False(t, resource.HasFinalizer(res, "finalizer1")) + require.True(t, resource.HasFinalizer(res, "finalizer2")) + require.Equal(t, mapset.NewSet[string]("finalizer2"), resource.GetFinalizers(res)) + + // remove non-existent -> noop + resource.RemoveFinalizer(res, "finalizer3") + require.Equal(t, mapset.NewSet[string]("finalizer2"), resource.GetFinalizers(res)) + }) + +} diff --git a/test-integ/go.mod b/test-integ/go.mod index 034753efec..03d7c9f60f 100644 --- a/test-integ/go.mod +++ b/test-integ/go.mod @@ -53,6 +53,7 @@ require ( github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.3.1 // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker v24.0.5+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect diff --git a/test-integ/go.sum b/test-integ/go.sum index 09543e235c..5229b90cd7 100644 --- a/test-integ/go.sum +++ b/test-integ/go.sum @@ -163,6 +163,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.3.1 h1:vjmkvJt/IV27WXPyYQpAh4bRyWJc5Y435D17XQ9QU5A= +github.com/deckarep/golang-set/v2 v2.3.1/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY= diff --git a/test/integration/consul-container/go.mod b/test/integration/consul-container/go.mod index 0b471b264d..6874859717 100644 --- a/test/integration/consul-container/go.mod +++ b/test/integration/consul-container/go.mod @@ -68,6 +68,7 @@ require ( github.com/coreos/pkg v0.0.0-20220810130054-c7d1c02cb6cf // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.3.1 // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.10.1 // indirect diff --git a/test/integration/consul-container/go.sum b/test/integration/consul-container/go.sum index 1cbb9315f5..3c027337c0 100644 --- a/test/integration/consul-container/go.sum +++ b/test/integration/consul-container/go.sum @@ -159,6 +159,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.3.1 h1:vjmkvJt/IV27WXPyYQpAh4bRyWJc5Y435D17XQ9QU5A= +github.com/deckarep/golang-set/v2 v2.3.1/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY=