Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add release validation #1122

Merged
merged 1 commit into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pkg/apis/api.kusion.io/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,7 @@ type Release struct {
Stack string `yaml:"stack" json:"stack"`

// Spec of the Release, which can be provided when creating release or generated during Release.
Spec *Spec `yaml:"spec" json:"spec"`
Spec *Spec `yaml:"spec,omitempty" json:"spec,omitempty"`

// State of the Release, which will be generated and updated during Release. When a Release is created,
// the State will be filled with the latest State, which indicates the current infra resources.
Expand All @@ -870,7 +870,7 @@ type Release struct {
CreateTime time.Time `yaml:"createTime" json:"createTime"`

// ModifiedTime is the time that the Release is modified.
ModifiedTime time.Time `yaml:"modifiedTime,omitempty" json:"modifiedTime,omitempty"`
ModifiedTime time.Time `yaml:"modifiedTime" json:"modifiedTime"`
}

// DeprecatedState is a record of an operation's result. It is a mapping between resources in KCL and the actual infra
Expand Down
85 changes: 85 additions & 0 deletions pkg/engine/release/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package release

import (
"errors"
"fmt"

v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
)

var (
ErrEmptyRelease = errors.New("empty release")
ErrEmptyProject = errors.New("empty project")
ErrEmptyWorkspace = errors.New("empty workspace")
ErrEmptyRevision = errors.New("empty revision")
ErrEmptyStack = errors.New("empty stack")
ErrEmptySpec = errors.New("empty spec")
ErrEmptyState = errors.New("empty state")
ErrEmptyPhase = errors.New("empty phase")
ErrEmptyCreateTime = errors.New("empty create time")
ErrEmptyModifiedTime = errors.New("empty modified time")
ErrDuplicateResourceKey = errors.New("duplicate resource key")
)

func ValidateRelease(r *v1.Release) error {
if r == nil {
return ErrEmptyRelease
}
if r.Project == "" {
return ErrEmptyProject
}
if r.Workspace == "" {
return ErrEmptyWorkspace
}
if r.Revision == 0 {
return ErrEmptyRevision
}
if r.Stack == "" {
return ErrEmptyStack
}
if err := ValidateState(r.State); err != nil {
return err
}
if r.Phase == "" {
return ErrEmptyPhase
}
if r.CreateTime.IsZero() {
return ErrEmptyCreateTime
}
if r.ModifiedTime.IsZero() {
return ErrEmptyModifiedTime
}
return nil
}

func ValidateSpec(spec *v1.Spec) error {
if spec == nil {
return ErrEmptySpec
}
if err := validateResources(spec.Resources); err != nil {
return err
}
return nil
}

func ValidateState(state *v1.State) error {
if state == nil {
return ErrEmptyState
}
if err := validateResources(state.Resources); err != nil {
return err
}
return nil
}

func validateResources(resources v1.Resources) error {
resourceKeyMap := make(map[string]bool)
for _, resource := range resources {
key := resource.ResourceKey()
if _, ok := resourceKeyMap[key]; ok {
return fmt.Errorf("%w: %s", ErrDuplicateResourceKey, key)
}
resourceKeyMap[key] = true
}
return nil
}
266 changes: 266 additions & 0 deletions pkg/engine/release/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
package release

import (
"testing"
"time"

"github.com/stretchr/testify/assert"

v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
)

func mockResource() v1.Resource {
return v1.Resource{
ID: "apps.kusionstack.io/v1alpha1:PodTransitionRule:fakeNs:default-dev-foo",
Type: "Kubernetes",
Attributes: map[string]interface{}{
"apiVersion": "apps.kusionstack.io/v1alpha1",
"kind": "PodTransitionRule",
"metadata": map[string]interface{}{
"creationTimestamp": interface{}(nil),
"name": "default-dev-foo",
"namespace": "fakeNs",
},
"spec": map[string]interface{}{
"rules": []interface{}{map[string]interface{}{
"availablePolicy": map[string]interface{}{
"maxUnavailableValue": "30%",
},
"name": "maxUnavailable",
}},
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app.kubernetes.io/name": "foo", "app.kubernetes.io/part-of": "default",
},
},
}, "status": map[string]interface{}{},
},
DependsOn: []string(nil),
Extensions: map[string]interface{}{
"GVK": "apps.kusionstack.io/v1alpha1, Kind=PodTransitionRule",
},
}
}

func TestValidateRelease(t *testing.T) {
loc, _ := time.LoadLocation("Asia/Shanghai")
testcases := []struct {
name string
success bool
release *v1.Release
}{
{
name: "valid release",
success: true,
release: &v1.Release{
Project: "fake-project",
Workspace: "fake-ws",
Revision: 1,
Stack: "fake-stack",
State: &v1.State{Resources: v1.Resources{mockResource()}},
Phase: v1.ReleasePhaseApplying,
CreateTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
ModifiedTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
},
},
{
name: "invalid release empty project",
success: false,
release: &v1.Release{
Project: "",
Workspace: "fake-ws",
Revision: 1,
Stack: "fake-stack",
State: &v1.State{Resources: v1.Resources{mockResource()}},
Phase: v1.ReleasePhaseApplying,
CreateTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
ModifiedTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
},
},
{
name: "invalid release empty workspace",
success: false,
release: &v1.Release{
Project: "fake-project",
Workspace: "",
Revision: 1,
Stack: "fake-stack",
State: &v1.State{Resources: v1.Resources{mockResource()}},
Phase: v1.ReleasePhaseApplying,
CreateTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
ModifiedTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
},
},
{
name: "invalid release empty revision",
success: false,
release: &v1.Release{
Project: "fake-project",
Workspace: "fake-ws",
Revision: 0,
Stack: "fake-stack",
State: &v1.State{Resources: v1.Resources{mockResource()}},
Phase: v1.ReleasePhaseApplying,
CreateTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
ModifiedTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
},
},
{
name: "invalid release empty stack",
success: false,
release: &v1.Release{
Project: "fake-project",
Workspace: "fake-ws",
Revision: 1,
Stack: "",
State: &v1.State{Resources: v1.Resources{mockResource()}},
Phase: v1.ReleasePhaseApplying,
CreateTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
ModifiedTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
},
},
{
name: "invalid release empty state",
success: false,
release: &v1.Release{
Project: "fake-project",
Workspace: "fake-ws",
Revision: 1,
Stack: "fake-stack",
State: nil,
Phase: v1.ReleasePhaseApplying,
CreateTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
ModifiedTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
},
},
{
name: "invalid release empty phase",
success: false,
release: &v1.Release{
Project: "fake-project",
Workspace: "fake-ws",
Revision: 1,
Stack: "fake-stack",
State: &v1.State{Resources: v1.Resources{mockResource()}},
Phase: "",
CreateTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
ModifiedTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
},
},
{
name: "invalid release empty create time",
success: false,
release: &v1.Release{
Project: "fake-project",
Workspace: "fake-ws",
Revision: 1,
Stack: "fake-stack",
State: &v1.State{Resources: v1.Resources{mockResource()}},
Phase: v1.ReleasePhaseApplying,
ModifiedTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
},
},
{
name: "invalid release empty modified time",
success: false,
release: &v1.Release{
Project: "fake-project",
Workspace: "fake-ws",
Revision: 1,
Stack: "fake-stack",
State: &v1.State{Resources: v1.Resources{mockResource()}},
Phase: v1.ReleasePhaseApplying,
CreateTime: time.Date(2024, 5, 10, 16, 48, 0, 0, loc),
},
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateRelease(tc.release)
assert.Equal(t, tc.success, err == nil)
})
}
}

func TestValidateState(t *testing.T) {
testcases := []struct {
name string
success bool
state *v1.State
}{
{
name: "valid state",
success: true,
state: &v1.State{
Resources: v1.Resources{mockResource()},
},
},
{
name: "invalid state nil state",
success: false,
state: nil,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateState(tc.state)
assert.Equal(t, tc.success, err == nil)
})
}
}

func TestValidateSpec(t *testing.T) {
testcases := []struct {
name string
success bool
spec *v1.Spec
}{
{
name: "valid spec",
success: true,
spec: &v1.Spec{
Resources: v1.Resources{mockResource()},
},
},
{
name: "invalid spec nil spec",
success: false,
spec: nil,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateSpec(tc.spec)
assert.Equal(t, tc.success, err == nil)
})
}
}

func TestValidateResources(t *testing.T) {
testcases := []struct {
name string
success bool
resources v1.Resources
}{
{
name: "valid resources",
success: true,
resources: v1.Resources{mockResource()},
},
{
name: "invalid resources duplicate resource key",
success: false,
resources: v1.Resources{mockResource(), mockResource()},
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := validateResources(tc.resources)
assert.Equal(t, tc.success, err == nil)
})
}
}
Loading