feat: allow limiting clusterResourceWhitelist by resource name (#12208) (#24674)

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
This commit is contained in:
Michael Crenshaw
2025-12-03 15:55:28 -05:00
committed by GitHub
parent c43088265e
commit e77acec858
42 changed files with 2909 additions and 1042 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -396,8 +396,8 @@ func (proj *AppProject) ProjectPoliciesString() string {
return strings.Join(policies, "\n")
}
// IsGroupKindPermitted validates if the given resource group/kind is permitted to be deployed in the project
func (proj AppProject) IsGroupKindPermitted(gk schema.GroupKind, namespaced bool) bool {
// IsGroupKindNamePermitted validates if the given resource group/kind is permitted to be deployed in the project
func (proj AppProject) IsGroupKindNamePermitted(gk schema.GroupKind, name string, namespaced bool) bool {
var isWhiteListed, isBlackListed bool
res := metav1.GroupKind{Group: gk.Group, Kind: gk.Kind}
@@ -413,18 +413,18 @@ func (proj AppProject) IsGroupKindPermitted(gk schema.GroupKind, namespaced bool
clusterWhitelist := proj.Spec.ClusterResourceWhitelist
clusterBlacklist := proj.Spec.ClusterResourceBlacklist
isWhiteListed = len(clusterWhitelist) != 0 && isResourceInList(res, clusterWhitelist)
isBlackListed = len(clusterBlacklist) != 0 && isResourceInList(res, clusterBlacklist)
isWhiteListed = len(clusterWhitelist) != 0 && isNamedResourceInList(res, name, clusterWhitelist)
isBlackListed = len(clusterBlacklist) != 0 && isNamedResourceInList(res, name, clusterBlacklist)
return isWhiteListed && !isBlackListed
}
// IsLiveResourcePermitted returns whether a live resource found in the cluster is permitted by an AppProject
func (proj AppProject) IsLiveResourcePermitted(un *unstructured.Unstructured, destCluster *Cluster, projectClusters func(project string) ([]*Cluster, error)) (bool, error) {
return proj.IsResourcePermitted(un.GroupVersionKind().GroupKind(), un.GetNamespace(), destCluster, projectClusters)
return proj.IsResourcePermitted(un.GroupVersionKind().GroupKind(), un.GetName(), un.GetNamespace(), destCluster, projectClusters)
}
func (proj AppProject) IsResourcePermitted(groupKind schema.GroupKind, namespace string, destCluster *Cluster, projectClusters func(project string) ([]*Cluster, error)) (bool, error) {
if !proj.IsGroupKindPermitted(groupKind, namespace != "") {
func (proj AppProject) IsResourcePermitted(groupKind schema.GroupKind, name string, namespace string, destCluster *Cluster, projectClusters func(project string) ([]*Cluster, error)) (bool, error) {
if !proj.IsGroupKindNamePermitted(groupKind, name, namespace != "") {
return false, nil
}
if namespace != "" {

File diff suppressed because it is too large Load Diff

View File

@@ -83,7 +83,7 @@ message AppProjectSpec {
repeated ProjectRole roles = 4;
// ClusterResourceWhitelist contains list of whitelisted cluster level resources
repeated .k8s.io.apimachinery.pkg.apis.meta.v1.GroupKind clusterResourceWhitelist = 5;
repeated ClusterResourceRestrictionItem clusterResourceWhitelist = 5;
// NamespaceResourceBlacklist contains list of blacklisted namespace level resources
repeated .k8s.io.apimachinery.pkg.apis.meta.v1.GroupKind namespaceResourceBlacklist = 6;
@@ -101,7 +101,7 @@ message AppProjectSpec {
repeated SignatureKey signatureKeys = 10;
// ClusterResourceBlacklist contains list of blacklisted cluster level resources
repeated .k8s.io.apimachinery.pkg.apis.meta.v1.GroupKind clusterResourceBlacklist = 11;
repeated ClusterResourceRestrictionItem clusterResourceBlacklist = 11;
// SourceNamespaces defines the namespaces application resources are allowed to be created in
repeated string sourceNamespaces = 12;
@@ -936,6 +936,17 @@ message ClusterList {
repeated Cluster items = 2;
}
// ClusterResourceRestrictionItem is a cluster resource that is restricted by the project's whitelist or blacklist
message ClusterResourceRestrictionItem {
optional string group = 1;
optional string kind = 2;
// Name is the name of the restricted resource. Glob patterns using Go's filepath.Match syntax are supported.
// Unlike the group and kind fields, if no name is specified, all resources of the specified group/kind are matched.
optional string name = 3;
}
// Command holds binary path and arguments list
message Command {
repeated string command = 1;

View File

@@ -2707,7 +2707,7 @@ type AppProjectSpec struct {
// Roles are user defined RBAC roles associated with this project
Roles []ProjectRole `json:"roles,omitempty" protobuf:"bytes,4,rep,name=roles"`
// ClusterResourceWhitelist contains list of whitelisted cluster level resources
ClusterResourceWhitelist []metav1.GroupKind `json:"clusterResourceWhitelist,omitempty" protobuf:"bytes,5,opt,name=clusterResourceWhitelist"`
ClusterResourceWhitelist []ClusterResourceRestrictionItem `json:"clusterResourceWhitelist,omitempty" protobuf:"bytes,5,opt,name=clusterResourceWhitelist"`
// NamespaceResourceBlacklist contains list of blacklisted namespace level resources
NamespaceResourceBlacklist []metav1.GroupKind `json:"namespaceResourceBlacklist,omitempty" protobuf:"bytes,6,opt,name=namespaceResourceBlacklist"`
// OrphanedResources specifies if controller should monitor orphaned resources of apps in this project
@@ -2719,7 +2719,7 @@ type AppProjectSpec struct {
// SignatureKeys contains a list of PGP key IDs that commits in Git must be signed with in order to be allowed for sync
SignatureKeys []SignatureKey `json:"signatureKeys,omitempty" protobuf:"bytes,10,opt,name=signatureKeys"`
// ClusterResourceBlacklist contains list of blacklisted cluster level resources
ClusterResourceBlacklist []metav1.GroupKind `json:"clusterResourceBlacklist,omitempty" protobuf:"bytes,11,opt,name=clusterResourceBlacklist"`
ClusterResourceBlacklist []ClusterResourceRestrictionItem `json:"clusterResourceBlacklist,omitempty" protobuf:"bytes,11,opt,name=clusterResourceBlacklist"`
// SourceNamespaces defines the namespaces application resources are allowed to be created in
SourceNamespaces []string `json:"sourceNamespaces,omitempty" protobuf:"bytes,12,opt,name=sourceNamespaces"`
// PermitOnlyProjectScopedClusters determines whether destinations can only reference clusters which are project-scoped
@@ -2728,6 +2728,15 @@ type AppProjectSpec struct {
DestinationServiceAccounts []ApplicationDestinationServiceAccount `json:"destinationServiceAccounts,omitempty" protobuf:"bytes,14,name=destinationServiceAccounts"`
}
// ClusterResourceRestrictionItem is a cluster resource that is restricted by the project's whitelist or blacklist
type ClusterResourceRestrictionItem struct {
Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
Kind string `json:"kind" protobuf:"bytes,2,opt,name=kind"`
// Name is the name of the restricted resource. Glob patterns using Go's filepath.Match syntax are supported.
// Unlike the group and kind fields, if no name is specified, all resources of the specified group/kind are matched.
Name string `json:"name,omitempty" protobuf:"bytes,3,opt,name=name"`
}
// SyncWindows is a collection of sync windows in this project
type SyncWindows []*SyncWindow
@@ -3532,6 +3541,28 @@ func isResourceInList(res metav1.GroupKind, list []metav1.GroupKind) bool {
return false
}
func isNamedResourceInList(res metav1.GroupKind, name string, list []ClusterResourceRestrictionItem) bool {
for _, item := range list {
ok, err := filepath.Match(item.Kind, res.Kind)
if !ok || err != nil {
continue
}
ok, err = filepath.Match(item.Group, res.Group)
if !ok || err != nil {
continue
}
if item.Name == "" {
return true
}
ok, err = filepath.Match(item.Name, name)
if !ok || err != nil {
continue
}
return true
}
return false
}
// getFinalizerIndex returns finalizer index in the list of object finalizers or -1 if finalizer does not exist
func getFinalizerIndex(meta metav1.ObjectMeta, name string) int {
for i, finalizer := range meta.Finalizers {

View File

@@ -513,8 +513,8 @@ func TestAppProject_IsGroupKindPermitted(t *testing.T) {
NamespaceResourceBlacklist: []metav1.GroupKind{{Group: "apps", Kind: "Deployment"}},
},
}
assert.False(t, proj.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}, true))
assert.False(t, proj.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "Deployment"}, true))
assert.False(t, proj.IsGroupKindNamePermitted(schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}, "", true))
assert.False(t, proj.IsGroupKindNamePermitted(schema.GroupKind{Group: "apps", Kind: "Deployment"}, "", true))
proj2 := AppProject{
Spec: AppProjectSpec{
@@ -522,39 +522,47 @@ func TestAppProject_IsGroupKindPermitted(t *testing.T) {
NamespaceResourceBlacklist: []metav1.GroupKind{{Group: "apps", Kind: "Deployment"}},
},
}
assert.True(t, proj2.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}, true))
assert.False(t, proj2.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, true))
assert.True(t, proj2.IsGroupKindNamePermitted(schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}, "", true))
assert.False(t, proj2.IsGroupKindNamePermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, "", true))
proj3 := AppProject{
Spec: AppProjectSpec{
ClusterResourceBlacklist: []metav1.GroupKind{{Group: "", Kind: "Namespace"}},
ClusterResourceBlacklist: []ClusterResourceRestrictionItem{{Group: "", Kind: "Namespace"}},
},
}
assert.False(t, proj3.IsGroupKindPermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, false))
assert.False(t, proj3.IsGroupKindNamePermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, "", false))
proj4 := AppProject{
Spec: AppProjectSpec{
ClusterResourceWhitelist: []metav1.GroupKind{{Group: "*", Kind: "*"}},
ClusterResourceBlacklist: []metav1.GroupKind{{Group: "*", Kind: "*"}},
ClusterResourceWhitelist: []ClusterResourceRestrictionItem{{Group: "*", Kind: "*"}},
ClusterResourceBlacklist: []ClusterResourceRestrictionItem{{Group: "*", Kind: "*"}},
},
}
assert.False(t, proj4.IsGroupKindPermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, false))
assert.True(t, proj4.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, true))
assert.False(t, proj4.IsGroupKindNamePermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, "", false))
assert.True(t, proj4.IsGroupKindNamePermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, "", true))
proj5 := AppProject{
Spec: AppProjectSpec{
ClusterResourceWhitelist: []metav1.GroupKind{},
ClusterResourceWhitelist: []ClusterResourceRestrictionItem{},
NamespaceResourceWhitelist: []metav1.GroupKind{{Group: "*", Kind: "*"}},
},
}
assert.False(t, proj5.IsGroupKindPermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, false))
assert.True(t, proj5.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, true))
assert.False(t, proj5.IsGroupKindNamePermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, "", false))
assert.True(t, proj5.IsGroupKindNamePermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, "", true))
proj6 := AppProject{
Spec: AppProjectSpec{},
}
assert.False(t, proj6.IsGroupKindPermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, false))
assert.True(t, proj6.IsGroupKindPermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, true))
assert.False(t, proj6.IsGroupKindNamePermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, "", false))
assert.True(t, proj6.IsGroupKindNamePermitted(schema.GroupKind{Group: "apps", Kind: "Action"}, "", true))
proj7 := AppProject{
Spec: AppProjectSpec{
ClusterResourceWhitelist: []ClusterResourceRestrictionItem{{Group: "", Kind: "Namespace", Name: "team1-*"}},
},
}
assert.False(t, proj7.IsGroupKindNamePermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, "other-namespace", false))
assert.True(t, proj7.IsGroupKindNamePermitted(schema.GroupKind{Group: "", Kind: "Namespace"}, "team1-namespace", false))
}
func TestAppProject_GetRoleByName(t *testing.T) {

View File

@@ -145,7 +145,7 @@ func (in *AppProjectSpec) DeepCopyInto(out *AppProjectSpec) {
}
if in.ClusterResourceWhitelist != nil {
in, out := &in.ClusterResourceWhitelist, &out.ClusterResourceWhitelist
*out = make([]v1.GroupKind, len(*in))
*out = make([]ClusterResourceRestrictionItem, len(*in))
copy(*out, *in)
}
if in.NamespaceResourceBlacklist != nil {
@@ -181,7 +181,7 @@ func (in *AppProjectSpec) DeepCopyInto(out *AppProjectSpec) {
}
if in.ClusterResourceBlacklist != nil {
in, out := &in.ClusterResourceBlacklist, &out.ClusterResourceBlacklist
*out = make([]v1.GroupKind, len(*in))
*out = make([]ClusterResourceRestrictionItem, len(*in))
copy(*out, *in)
}
if in.SourceNamespaces != nil {
@@ -1800,6 +1800,22 @@ func (in *ClusterList) DeepCopy() *ClusterList {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClusterResourceRestrictionItem) DeepCopyInto(out *ClusterResourceRestrictionItem) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterResourceRestrictionItem.
func (in *ClusterResourceRestrictionItem) DeepCopy() *ClusterResourceRestrictionItem {
if in == nil {
return nil
}
out := new(ClusterResourceRestrictionItem)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Command) DeepCopyInto(out *Command) {
*out = *in