mirror of
https://github.com/kyverno/kyverno.git
synced 2024-12-14 11:57:48 +00:00
feat: add cleanupPolicy validation code (#5279)
* validate the cleanupPolicy Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com> * add validation for DELETE permission for cleanupPolicy Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com> * add separate binary for cleanupPolicy Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com> * fix linter issues Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com> Signed-off-by: Nikhil Sharma <nikhilsharma230303@gmail.com> Co-authored-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>
This commit is contained in:
parent
2b4ff1ef6d
commit
d44dc97990
10 changed files with 574 additions and 0 deletions
18
api/kyverno/v1alpha1/cleanup_policy_interface.go
Normal file
18
api/kyverno/v1alpha1/cleanup_policy_interface.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CleanupPolicyInterface abstracts the concrete policy type (Policy vs ClusterPolicy)
|
||||||
|
// +kubebuilder:object:generate=false
|
||||||
|
type CleanupPolicyInterface interface {
|
||||||
|
metav1.Object
|
||||||
|
GetSpec() *CleanupPolicySpec
|
||||||
|
GetStatus() *CleanupPolicyStatus
|
||||||
|
Validate(sets.String) field.ErrorList
|
||||||
|
GetKind() string
|
||||||
|
GetSchedule() string
|
||||||
|
}
|
158
api/kyverno/v1alpha1/cleanup_policy_test.go
Normal file
158
api/kyverno/v1alpha1/cleanup_policy_test.go
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
package v1alpha1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_CleanupPolicy_Name(t *testing.T) {
|
||||||
|
subject := CleanupPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "this-is-a-way-too-long-policy-name-that-should-trigger-an-error-when-calling-the-policy-validation-method",
|
||||||
|
},
|
||||||
|
Spec: CleanupPolicySpec{
|
||||||
|
Schedule: "* * * * *",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
errs := subject.Validate(nil)
|
||||||
|
assert.Assert(t, len(errs) == 1)
|
||||||
|
assert.Equal(t, errs[0].Field, "metadata.name")
|
||||||
|
assert.Equal(t, errs[0].Type, field.ErrorTypeTooLong)
|
||||||
|
assert.Equal(t, errs[0].Detail, "must have at most 63 bytes")
|
||||||
|
assert.Equal(t, errs[0].Error(), "metadata.name: Too long: must have at most 63 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CleanupPolicy_Schedule(t *testing.T) {
|
||||||
|
subject := CleanupPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-policy",
|
||||||
|
},
|
||||||
|
Spec: CleanupPolicySpec{
|
||||||
|
Schedule: "schedule-not-in-proper-cron-format",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
errs := subject.Validate(nil)
|
||||||
|
assert.Assert(t, len(errs) == 1)
|
||||||
|
assert.Equal(t, errs[0].Field, "spec.schedule")
|
||||||
|
assert.Equal(t, errs[0].Type, field.ErrorTypeInvalid)
|
||||||
|
assert.Equal(t, errs[0].Detail, "schedule spec in the cleanupPolicy is not in proper cron format")
|
||||||
|
assert.Equal(t, errs[0].Error(), fmt.Sprintf(`spec.schedule: Invalid value: "%s": schedule spec in the cleanupPolicy is not in proper cron format`, subject.Spec.Schedule))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ClusterCleanupPolicy_Name(t *testing.T) {
|
||||||
|
subject := ClusterCleanupPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "this-is-a-way-too-long-policy-name-that-should-trigger-an-error-when-calling-the-policy-validation-method",
|
||||||
|
},
|
||||||
|
Spec: CleanupPolicySpec{
|
||||||
|
Schedule: "* * * * *",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
errs := subject.Validate(nil)
|
||||||
|
assert.Assert(t, len(errs) == 1)
|
||||||
|
assert.Equal(t, errs[0].Field, "metadata.name")
|
||||||
|
assert.Equal(t, errs[0].Type, field.ErrorTypeTooLong)
|
||||||
|
assert.Equal(t, errs[0].Detail, "must have at most 63 bytes")
|
||||||
|
assert.Equal(t, errs[0].Error(), "metadata.name: Too long: must have at most 63 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ClusterCleanupPolicy_Schedule(t *testing.T) {
|
||||||
|
subject := ClusterCleanupPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-policy",
|
||||||
|
},
|
||||||
|
Spec: CleanupPolicySpec{
|
||||||
|
Schedule: "schedule-not-in-proper-cron-format",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
errs := subject.Validate(nil)
|
||||||
|
assert.Assert(t, len(errs) == 1)
|
||||||
|
assert.Equal(t, errs[0].Field, "spec.schedule")
|
||||||
|
assert.Equal(t, errs[0].Type, field.ErrorTypeInvalid)
|
||||||
|
assert.Equal(t, errs[0].Detail, "schedule spec in the cleanupPolicy is not in proper cron format")
|
||||||
|
assert.Equal(t, errs[0].Error(), fmt.Sprintf(`spec.schedule: Invalid value: "%s": schedule spec in the cleanupPolicy is not in proper cron format`, subject.Spec.Schedule))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_doesMatchExcludeConflict(t *testing.T) {
|
||||||
|
path := field.NewPath("dummy")
|
||||||
|
testcases := []struct {
|
||||||
|
description string
|
||||||
|
policySpec []byte
|
||||||
|
errors func(r *CleanupPolicySpec) field.ErrorList
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "Same match and exclude",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
|
||||||
|
errors: func(r *CleanupPolicySpec) (errs field.ErrorList) {
|
||||||
|
return append(errs, field.Invalid(path, r, "CleanupPolicy is matching an empty set"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Failed to exclude kind",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Failed to exclude name",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something-*","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Failed to exclude namespace",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something3","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Failed to exclude labels",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"higha"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Failed to exclude expression",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["databases"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Failed to exclude subjects",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something2","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Failed to exclude clusterroles",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something3","something1"],"roles":["something","something1"]}, "schedule": "* * * * *"}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Failed to exclude roles",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something","something1"]},"exclude":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"],"selector":{"matchLabels":{"memory":"high"},"matchExpressions":[{"key":"tier","operator":"In","values":["database"]}]}},"subjects":[{"name":"something","kind":"something","Namespace":"something","apiGroup":"something"},{"name":"something1","kind":"something1","Namespace":"something1","apiGroup":"something1"}],"clusterroles":["something","something1"],"roles":["something3","something1"]}, "schedule": "* * * * *"}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "simple",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"something","namespaces":["something","something1"]}},"exclude":{"resources":{"kinds":["Pod","Namespace","Job"],"name":"some*","namespaces":["something","something1","something2"]}}, "schedule": "* * * * *"}`),
|
||||||
|
errors: func(r *CleanupPolicySpec) (errs field.ErrorList) {
|
||||||
|
return append(errs, field.Invalid(path, r, "CleanupPolicy is matching an empty set"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "simple - fail",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"kinds":["Pod","Namespace"],"name":"somxething","namespaces":["something","something1"]}},"exclude":{"resources":{"kinds":["Pod","Namespace","Job"],"name":"some*","namespaces":["something","something1","something2"]}}, "schedule": "* * * * *"}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "empty case",
|
||||||
|
policySpec: []byte(`{"match":{"resources":{"selector":{"matchLabels":{"allow-deletes":"false"}}}},"exclude":{"clusterRoles":["random"]},"validate":{"message":"Deleting {{request.object.kind}}/{{request.object.metadata.name}} is not allowed","deny":{"conditions":{"all":[{"key":"{{request.operation}}","operator":"Equal","value":"DELETE"}]}}}, "schedule": "* * * * *"}`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
var policySpec CleanupPolicySpec
|
||||||
|
err := json.Unmarshal(testcase.policySpec, &policySpec)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
errs := policySpec.ValidateMatchExcludeConflict(path)
|
||||||
|
var expectedErrs field.ErrorList
|
||||||
|
if testcase.errors != nil {
|
||||||
|
expectedErrs = testcase.errors(&policySpec)
|
||||||
|
}
|
||||||
|
assert.Equal(t, len(errs), len(expectedErrs))
|
||||||
|
for i := range errs {
|
||||||
|
fmt.Println(i)
|
||||||
|
assert.Equal(t, errs[i].Error(), expectedErrs[i].Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,15 @@ limitations under the License.
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
|
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
|
||||||
|
"github.com/kyverno/kyverno/pkg/utils/wildcard"
|
||||||
|
"github.com/robfig/cron"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
// +genclient
|
// +genclient
|
||||||
|
@ -41,6 +48,32 @@ type CleanupPolicy struct {
|
||||||
Status CleanupPolicyStatus `json:"status,omitempty"`
|
Status CleanupPolicyStatus `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSpec returns the policy spec
|
||||||
|
func (p *CleanupPolicy) GetSpec() *CleanupPolicySpec {
|
||||||
|
return &p.Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatus returns the policy status
|
||||||
|
func (p *CleanupPolicy) GetStatus() *CleanupPolicyStatus {
|
||||||
|
return &p.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSchedule returns the schedule from the policy spec
|
||||||
|
func (p *CleanupPolicy) GetSchedule() string {
|
||||||
|
return p.Spec.Schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CleanupPolicy) GetKind() string {
|
||||||
|
return p.Kind
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate implements programmatic validation
|
||||||
|
func (p *CleanupPolicy) Validate(clusterResources sets.String) (errs field.ErrorList) {
|
||||||
|
errs = append(errs, kyvernov1.ValidatePolicyName(field.NewPath("metadata").Child("name"), p.Name)...)
|
||||||
|
errs = append(errs, p.Spec.Validate(field.NewPath("spec"), clusterResources, true)...)
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
@ -72,6 +105,32 @@ type ClusterCleanupPolicy struct {
|
||||||
Status CleanupPolicyStatus `json:"status,omitempty"`
|
Status CleanupPolicyStatus `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSpec returns the policy spec
|
||||||
|
func (p *ClusterCleanupPolicy) GetSpec() *CleanupPolicySpec {
|
||||||
|
return &p.Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatus returns the policy status
|
||||||
|
func (p *ClusterCleanupPolicy) GetStatus() *CleanupPolicyStatus {
|
||||||
|
return &p.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSchedule returns the schedule from the policy spec
|
||||||
|
func (p *ClusterCleanupPolicy) GetSchedule() string {
|
||||||
|
return p.Spec.Schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ClusterCleanupPolicy) GetKind() string {
|
||||||
|
return p.Kind
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate implements programmatic validation
|
||||||
|
func (p *ClusterCleanupPolicy) Validate(clusterResources sets.String) (errs field.ErrorList) {
|
||||||
|
errs = append(errs, kyvernov1.ValidatePolicyName(field.NewPath("metadata").Child("name"), p.Name)...)
|
||||||
|
errs = append(errs, p.Spec.Validate(field.NewPath("spec"), clusterResources, false)...)
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
// +kubebuilder:object:root=true
|
// +kubebuilder:object:root=true
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
|
@ -109,3 +168,185 @@ type CleanupPolicySpec struct {
|
||||||
type CleanupPolicyStatus struct {
|
type CleanupPolicyStatus struct {
|
||||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
|
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate implements programmatic validation
|
||||||
|
func (p *CleanupPolicySpec) Validate(path *field.Path, clusterResources sets.String, namespaced bool) (errs field.ErrorList) {
|
||||||
|
errs = append(errs, ValidateSchedule(path.Child("schedule"), p.Schedule)...)
|
||||||
|
errs = append(errs, p.MatchResources.Validate(path.Child("match"), namespaced, clusterResources)...)
|
||||||
|
errs = append(errs, p.ExcludeResources.Validate(path.Child("exclude"), namespaced, clusterResources)...)
|
||||||
|
errs = append(errs, p.ValidateMatchExcludeConflict(path)...)
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSchedule validates whether the schedule specified is in proper cron format or not.
|
||||||
|
func ValidateSchedule(path *field.Path, schedule string) (errs field.ErrorList) {
|
||||||
|
if _, err := cron.ParseStandard(schedule); err != nil {
|
||||||
|
errs = append(errs, field.Invalid(path, schedule, "schedule spec in the cleanupPolicy is not in proper cron format"))
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateMatchExcludeConflict checks if the resultant of match and exclude block is not an empty set
|
||||||
|
func (spec *CleanupPolicySpec) ValidateMatchExcludeConflict(path *field.Path) (errs field.ErrorList) {
|
||||||
|
if len(spec.ExcludeResources.All) > 0 || len(spec.MatchResources.All) > 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
// if both have any then no resource should be common
|
||||||
|
if len(spec.MatchResources.Any) > 0 && len(spec.ExcludeResources.Any) > 0 {
|
||||||
|
for _, rmr := range spec.MatchResources.Any {
|
||||||
|
for _, rer := range spec.ExcludeResources.Any {
|
||||||
|
if reflect.DeepEqual(rmr, rer) {
|
||||||
|
return append(errs, field.Invalid(path, spec, "CleanupPolicy is matching an empty set"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
if reflect.DeepEqual(spec.ExcludeResources, kyvernov1.MatchResources{}) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
excludeRoles := sets.NewString(spec.ExcludeResources.Roles...)
|
||||||
|
excludeClusterRoles := sets.NewString(spec.ExcludeResources.ClusterRoles...)
|
||||||
|
excludeKinds := sets.NewString(spec.ExcludeResources.Kinds...)
|
||||||
|
excludeNamespaces := sets.NewString(spec.ExcludeResources.Namespaces...)
|
||||||
|
excludeSubjects := sets.NewString()
|
||||||
|
for _, subject := range spec.ExcludeResources.Subjects {
|
||||||
|
subjectRaw, _ := json.Marshal(subject)
|
||||||
|
excludeSubjects.Insert(string(subjectRaw))
|
||||||
|
}
|
||||||
|
excludeSelectorMatchExpressions := sets.NewString()
|
||||||
|
if spec.ExcludeResources.Selector != nil {
|
||||||
|
for _, matchExpression := range spec.ExcludeResources.Selector.MatchExpressions {
|
||||||
|
matchExpressionRaw, _ := json.Marshal(matchExpression)
|
||||||
|
excludeSelectorMatchExpressions.Insert(string(matchExpressionRaw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
excludeNamespaceSelectorMatchExpressions := sets.NewString()
|
||||||
|
if spec.ExcludeResources.NamespaceSelector != nil {
|
||||||
|
for _, matchExpression := range spec.ExcludeResources.NamespaceSelector.MatchExpressions {
|
||||||
|
matchExpressionRaw, _ := json.Marshal(matchExpression)
|
||||||
|
excludeNamespaceSelectorMatchExpressions.Insert(string(matchExpressionRaw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(excludeRoles) > 0 {
|
||||||
|
if len(spec.MatchResources.Roles) == 0 || !excludeRoles.HasAll(spec.MatchResources.Roles...) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(excludeClusterRoles) > 0 {
|
||||||
|
if len(spec.MatchResources.ClusterRoles) == 0 || !excludeClusterRoles.HasAll(spec.MatchResources.ClusterRoles...) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(excludeSubjects) > 0 {
|
||||||
|
if len(spec.MatchResources.Subjects) == 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
for _, subject := range spec.MatchResources.UserInfo.Subjects {
|
||||||
|
subjectRaw, _ := json.Marshal(subject)
|
||||||
|
if !excludeSubjects.Has(string(subjectRaw)) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if spec.ExcludeResources.Name != "" {
|
||||||
|
if !wildcard.Match(spec.ExcludeResources.Name, spec.MatchResources.Name) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(spec.ExcludeResources.Names) > 0 {
|
||||||
|
excludeSlice := spec.ExcludeResources.Names
|
||||||
|
matchSlice := spec.MatchResources.Names
|
||||||
|
|
||||||
|
// if exclude block has something and match doesn't it means we
|
||||||
|
// have a non empty set
|
||||||
|
if len(spec.MatchResources.Names) == 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// if *any* name in match and exclude conflicts
|
||||||
|
// we want user to fix that
|
||||||
|
for _, matchName := range matchSlice {
|
||||||
|
for _, excludeName := range excludeSlice {
|
||||||
|
if wildcard.Match(excludeName, matchName) {
|
||||||
|
return append(errs, field.Invalid(path, spec, "CleanupPolicy is matching an empty set"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
if len(excludeNamespaces) > 0 {
|
||||||
|
if len(spec.MatchResources.Namespaces) == 0 || !excludeNamespaces.HasAll(spec.MatchResources.Namespaces...) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(excludeKinds) > 0 {
|
||||||
|
if len(spec.MatchResources.Kinds) == 0 || !excludeKinds.HasAll(spec.MatchResources.Kinds...) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if spec.MatchResources.Selector != nil && spec.ExcludeResources.Selector != nil {
|
||||||
|
if len(excludeSelectorMatchExpressions) > 0 {
|
||||||
|
if len(spec.MatchResources.Selector.MatchExpressions) == 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
for _, matchExpression := range spec.MatchResources.Selector.MatchExpressions {
|
||||||
|
matchExpressionRaw, _ := json.Marshal(matchExpression)
|
||||||
|
if !excludeSelectorMatchExpressions.Has(string(matchExpressionRaw)) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(spec.ExcludeResources.Selector.MatchLabels) > 0 {
|
||||||
|
if len(spec.MatchResources.Selector.MatchLabels) == 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
for label, value := range spec.MatchResources.Selector.MatchLabels {
|
||||||
|
if spec.ExcludeResources.Selector.MatchLabels[label] != value {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if spec.MatchResources.NamespaceSelector != nil && spec.ExcludeResources.NamespaceSelector != nil {
|
||||||
|
if len(excludeNamespaceSelectorMatchExpressions) > 0 {
|
||||||
|
if len(spec.MatchResources.NamespaceSelector.MatchExpressions) == 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
for _, matchExpression := range spec.MatchResources.NamespaceSelector.MatchExpressions {
|
||||||
|
matchExpressionRaw, _ := json.Marshal(matchExpression)
|
||||||
|
if !excludeNamespaceSelectorMatchExpressions.Has(string(matchExpressionRaw)) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(spec.ExcludeResources.NamespaceSelector.MatchLabels) > 0 {
|
||||||
|
if len(spec.MatchResources.NamespaceSelector.MatchLabels) == 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
for label, value := range spec.MatchResources.NamespaceSelector.MatchLabels {
|
||||||
|
if spec.ExcludeResources.NamespaceSelector.MatchLabels[label] != value {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (spec.MatchResources.Selector == nil && spec.ExcludeResources.Selector != nil) ||
|
||||||
|
(spec.MatchResources.Selector != nil && spec.ExcludeResources.Selector == nil) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
if (spec.MatchResources.NamespaceSelector == nil && spec.ExcludeResources.NamespaceSelector != nil) ||
|
||||||
|
(spec.MatchResources.NamespaceSelector != nil && spec.ExcludeResources.NamespaceSelector == nil) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
if spec.MatchResources.Annotations != nil && spec.ExcludeResources.Annotations != nil {
|
||||||
|
if !(reflect.DeepEqual(spec.MatchResources.Annotations, spec.ExcludeResources.Annotations)) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (spec.MatchResources.Annotations == nil && spec.ExcludeResources.Annotations != nil) ||
|
||||||
|
(spec.MatchResources.Annotations != nil && spec.ExcludeResources.Annotations == nil) {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
return append(errs, field.Invalid(path, spec, "CleanupPolicy is matching an empty set"))
|
||||||
|
}
|
||||||
|
|
5
cmd/cleanup/logger/log.go
Normal file
5
cmd/cleanup/logger/log.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import "github.com/kyverno/kyverno/pkg/logging"
|
||||||
|
|
||||||
|
var Logger = logging.WithName("cleanupwebhooks")
|
1
cmd/cleanup/main.go
Normal file
1
cmd/cleanup/main.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package main
|
45
cmd/cleanup/utils/utils.go
Normal file
45
cmd/cleanup/utils/utils.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
kyvernov1alpha1 "github.com/kyverno/kyverno/api/kyverno/v1alpha1"
|
||||||
|
admissionv1 "k8s.io/api/admission/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UnmarshalCleanupPolicy(kind string, raw []byte) (kyvernov1alpha1.CleanupPolicyInterface, error) {
|
||||||
|
var policy kyvernov1alpha1.CleanupPolicyInterface
|
||||||
|
if kind == "CleanupPolicy" {
|
||||||
|
if err := json.Unmarshal(raw, &policy); err != nil {
|
||||||
|
return policy, err
|
||||||
|
}
|
||||||
|
return policy, nil
|
||||||
|
}
|
||||||
|
return policy, fmt.Errorf("admission request does not contain a cleanuppolicy")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCleanupPolicy(request *admissionv1.AdmissionRequest) (kyvernov1alpha1.CleanupPolicyInterface, error) {
|
||||||
|
return UnmarshalCleanupPolicy(request.Kind.Kind, request.Object.Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCleanupPolicies(request *admissionv1.AdmissionRequest) (kyvernov1alpha1.CleanupPolicyInterface, kyvernov1alpha1.CleanupPolicyInterface, error) {
|
||||||
|
var emptypolicy kyvernov1alpha1.CleanupPolicyInterface
|
||||||
|
policy, err := UnmarshalCleanupPolicy(request.Kind.Kind, request.Object.Raw)
|
||||||
|
if err != nil {
|
||||||
|
return policy, emptypolicy, err
|
||||||
|
}
|
||||||
|
if request.Operation == admissionv1.Update {
|
||||||
|
oldPolicy, err := UnmarshalCleanupPolicy(request.Kind.Kind, request.OldObject.Raw)
|
||||||
|
return policy, oldPolicy, err
|
||||||
|
}
|
||||||
|
return policy, emptypolicy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetResourceName(request *admissionv1.AdmissionRequest) string {
|
||||||
|
resourceName := request.Kind.Kind + "/" + request.Name
|
||||||
|
if request.Namespace != "" {
|
||||||
|
resourceName = request.Namespace + "/" + resourceName
|
||||||
|
}
|
||||||
|
return resourceName
|
||||||
|
}
|
98
cmd/cleanup/validate/validate.go
Normal file
98
cmd/cleanup/validate/validate.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package validate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
kyvernov1alpha1 "github.com/kyverno/kyverno/api/kyverno/v1alpha1"
|
||||||
|
"github.com/kyverno/kyverno/cmd/cleanup/logger"
|
||||||
|
"github.com/kyverno/kyverno/pkg/clients/dclient"
|
||||||
|
"github.com/kyverno/kyverno/pkg/engine/variables"
|
||||||
|
"github.com/kyverno/kyverno/pkg/logging"
|
||||||
|
"github.com/kyverno/kyverno/pkg/openapi"
|
||||||
|
"github.com/kyverno/kyverno/pkg/policy/generate"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/client-go/discovery"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cleanup provides implementation to validate permission for using DELETE operation by CleanupPolicy
|
||||||
|
type Cleanup struct {
|
||||||
|
// rule to hold CleanupPolicy specifications
|
||||||
|
spec kyvernov1alpha1.CleanupPolicySpec
|
||||||
|
// authCheck to check access for operations
|
||||||
|
authCheck generate.Operations
|
||||||
|
// logger
|
||||||
|
log logr.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCleanup returns a new instance of Cleanup validation checker
|
||||||
|
func NewCleanup(client dclient.Interface, cleanup kyvernov1alpha1.CleanupPolicySpec, log logr.Logger) *Cleanup {
|
||||||
|
c := Cleanup{
|
||||||
|
spec: cleanup,
|
||||||
|
authCheck: generate.NewAuth(client, log),
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
// canIDelete returns a error if kyverno cannot perform operations
|
||||||
|
func (c *Cleanup) CanIDelete(kind, namespace string) error {
|
||||||
|
// Skip if there is variable defined
|
||||||
|
authCheck := c.authCheck
|
||||||
|
if !variables.IsVariable(kind) && !variables.IsVariable(namespace) {
|
||||||
|
// DELETE
|
||||||
|
ok, err := authCheck.CanIDelete(kind, namespace)
|
||||||
|
if err != nil {
|
||||||
|
// machinery error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("kyverno does not have permissions to 'delete' resource %s/%s. Update permissions in ClusterRole", kind, namespace)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.log.V(4).Info("name & namespace uses variables, so cannot be resolved. Skipping Auth Checks.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks the policy and rules declarations for required configurations
|
||||||
|
func ValidateCleanupPolicy(cleanuppolicy kyvernov1alpha1.CleanupPolicyInterface, client dclient.Interface, mock bool, openApiManager openapi.Manager) error {
|
||||||
|
namespace := cleanuppolicy.GetNamespace()
|
||||||
|
var res []*metav1.APIResourceList
|
||||||
|
clusterResources := sets.NewString()
|
||||||
|
|
||||||
|
// Get all the cluster type kind supported by cluster
|
||||||
|
res, err := discovery.ServerPreferredResources(client.Discovery().DiscoveryInterface())
|
||||||
|
if err != nil {
|
||||||
|
if discovery.IsGroupDiscoveryFailedError(err) {
|
||||||
|
err := err.(*discovery.ErrGroupDiscoveryFailed)
|
||||||
|
for gv, err := range err.Groups {
|
||||||
|
logger.Logger.Error(err, "failed to list api resources", "group", gv)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, resList := range res {
|
||||||
|
for _, r := range resList.APIResources {
|
||||||
|
if !r.Namespaced {
|
||||||
|
clusterResources.Insert(r.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs := cleanuppolicy.Validate(clusterResources); len(errs) != 0 {
|
||||||
|
return errs.ToAggregate()
|
||||||
|
}
|
||||||
|
|
||||||
|
for kind := range clusterResources {
|
||||||
|
checker := NewCleanup(client, *cleanuppolicy.GetSpec(), logging.GlobalLogger())
|
||||||
|
if err := checker.CanIDelete(kind, namespace); err != nil {
|
||||||
|
return fmt.Errorf("cannot delete kind %s in namespace %s", kind, namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -4247,6 +4247,11 @@ CleanupPolicyStatus
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<hr />
|
<hr />
|
||||||
|
<h3 id="kyverno.io/v1alpha1.CleanupPolicyInterface">CleanupPolicyInterface
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
<p>CleanupPolicyInterface abstracts the concrete policy type (Policy vs ClusterPolicy)</p>
|
||||||
|
</p>
|
||||||
<h3 id="kyverno.io/v1alpha1.CleanupPolicySpec">CleanupPolicySpec
|
<h3 id="kyverno.io/v1alpha1.CleanupPolicySpec">CleanupPolicySpec
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -33,6 +33,7 @@ require (
|
||||||
github.com/onsi/gomega v1.22.1
|
github.com/onsi/gomega v1.22.1
|
||||||
github.com/orcaman/concurrent-map/v2 v2.0.0
|
github.com/orcaman/concurrent-map/v2 v2.0.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/robfig/cron v1.2.0
|
||||||
github.com/sigstore/cosign v1.13.0
|
github.com/sigstore/cosign v1.13.0
|
||||||
github.com/sigstore/k8s-manifest-sigstore v0.4.2
|
github.com/sigstore/k8s-manifest-sigstore v0.4.2
|
||||||
github.com/sigstore/sigstore v1.4.4
|
github.com/sigstore/sigstore v1.4.4
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1812,6 +1812,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||||
|
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
|
|
Loading…
Reference in a new issue