Skip to content

Commit

Permalink
feat: add release validation (#1122)
Browse files Browse the repository at this point in the history
  • Loading branch information
healthjyk committed May 21, 2024
1 parent 7feee90 commit 2aa9fdb
Show file tree
Hide file tree
Showing 3 changed files with 353 additions and 2 deletions.
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 @@ -893,7 +893,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 @@ -906,7 +906,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)
})
}
}

0 comments on commit 2aa9fdb

Please sign in to comment.