Skip to content

Commit 199fcbd

Browse files
authored
feat: add resource graph command (#1296)
1 parent 74acaa7 commit 199fcbd

File tree

26 files changed

+1688
-41
lines changed

26 files changed

+1688
-41
lines changed

pkg/apis/api.kusion.io/v1/types.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,8 @@ const (
268268
FieldHealthPolicy = "healthPolicy"
269269
FieldKCLHealthCheckKCL = "health.kcl"
270270
// kind field in kubernetes resource Attributes
271-
FieldKind = "kind"
271+
FieldKind = "kind"
272+
FieldIsWorkload = "kusion.io/is-workload"
272273
)
273274

274275
// BackendConfigs contains the configuration of multiple backends and the current backend.
@@ -744,3 +745,59 @@ const (
744745
// The default maximum number of concurrent resource executions for Kusion is 10.
745746
DefaultMaxConcurrent = 10
746747
)
748+
749+
type Status string
750+
751+
// Status is to represent resource status displayed by resource graph after apply succeed
752+
const (
753+
ApplySucceed Status = "Apply succeeded"
754+
ApplyFail Status = "Apply failed"
755+
Reconciled Status = "Apply succeeded | Reconciled"
756+
ReconcileFail Status = "Apply succeeded | Reconcile failed"
757+
)
758+
759+
// Graph represents the structure of a project's resources within a workspace, used by `resource graph` command.
760+
type Graph struct {
761+
// Name of the project
762+
Project string `yaml:"Project" json:"Project"`
763+
// Name of the workspace where the app is deployed
764+
Workspace string `yaml:"Workspace" json:"Workspace"`
765+
// All the resources related to the app
766+
Resources *GraphResources `yaml:"Resources" json:"Resources"`
767+
}
768+
769+
// GraphResources defines the categorized resources related to the application.
770+
type GraphResources struct {
771+
// WorkloadResources contains the resources that are directly related to the workload.
772+
WorkloadResources map[string]*GraphResource `yaml:"WorkloadResources" json:"WorkloadResources"`
773+
// DependencyResources stores resources that are required dependencies for the workload.
774+
DependencyResources map[string]*GraphResource `yaml:"DependencyResources" json:"DependencyResources"`
775+
// OtherResources holds independent resources that are not directly tied to workloads or dependencies.
776+
OtherResources map[string]*GraphResource `yaml:"OtherResources" json:"OtherResources"`
777+
// ResourceIndex is a global mapping of resource IDs to their corresponding resource entries.
778+
ResourceIndex map[string]*ResourceEntry `yaml:"ResourceIndex,omitempty" json:"ResourceIndex,omitempty"`
779+
}
780+
781+
// GraphResource represents an individual resource in the cluster.
782+
type GraphResource struct {
783+
// ID refers to Resource ID.
784+
ID string `yaml:"ID" json:"ID"`
785+
// Type refers to Resource Type in the cluster.
786+
Type string `yaml:"Type" json:"Type"`
787+
// Name refers to Resource name in the cluster.
788+
Name string `yaml:"Name" json:"Name"`
789+
// CloudResourceID refers to Resource ID in the cloud provider.
790+
CloudResourceID string `yaml:"CloudResourceID" json:"CloudResourceID"`
791+
// Resource status after apply.
792+
Status Status `yaml:"Status" json:"Status"`
793+
// Dependents lists the resources that depend on this resource.
794+
Dependents []string `yaml:"Dependents" json:"Dependents"`
795+
// Dependencies lists the resources that this resource relies upon.
796+
Dependencies []string `yaml:"Dependencies" json:"Dependencies"`
797+
}
798+
799+
// ResourceEntry stores a GraphResource and its associated Resource mapping.
800+
type ResourceEntry struct {
801+
Resource *GraphResource
802+
Category map[string]*GraphResource
803+
}

pkg/backend/backend.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"kusionstack.io/kusion/pkg/backend/storages"
88
"kusionstack.io/kusion/pkg/config"
99
"kusionstack.io/kusion/pkg/engine/release"
10+
"kusionstack.io/kusion/pkg/engine/resource/graph"
1011
"kusionstack.io/kusion/pkg/workspace"
1112
)
1213

@@ -21,6 +22,9 @@ type Backend interface {
2122
// StateStorageWithPath returns the state storage with the specified path.
2223
StateStorageWithPath(path string) (release.Storage, error)
2324

25+
// GraphStorage returns the graph storage.
26+
GraphStorage(project, workspace string) (graph.Storage, error)
27+
2428
// ProjectStorage returns the project directory under release folder.
2529
ProjectStorage() (map[string][]string, error)
2630
}

pkg/backend/storages/local.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
55
"kusionstack.io/kusion/pkg/engine/release"
66
releasestorages "kusionstack.io/kusion/pkg/engine/release/storages"
7+
"kusionstack.io/kusion/pkg/engine/resource/graph"
8+
graphstorages "kusionstack.io/kusion/pkg/engine/resource/graph/storages"
79
projectstorages "kusionstack.io/kusion/pkg/project/storages"
810
"kusionstack.io/kusion/pkg/workspace"
911
workspacestorages "kusionstack.io/kusion/pkg/workspace/storages"
@@ -32,6 +34,10 @@ func (s *LocalStorage) StateStorageWithPath(path string) (release.Storage, error
3234
return releasestorages.NewLocalStorage(releasestorages.GenReleasePrefixKeyWithPath(s.path, path))
3335
}
3436

37+
func (s *LocalStorage) GraphStorage(project, workspace string) (graph.Storage, error) {
38+
return graphstorages.NewLocalStorage(graphstorages.GenGraphDirPath(s.path, project, workspace))
39+
}
40+
3541
func (s *LocalStorage) ProjectStorage() (map[string][]string, error) {
3642
return projectstorages.NewLocalStorage(projectstorages.GenProjectDirPath(s.path)).Get()
3743
}

pkg/backend/storages/local_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88

99
v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
1010
releasestorages "kusionstack.io/kusion/pkg/engine/release/storages"
11+
graphstorages "kusionstack.io/kusion/pkg/engine/resource/graph/storages"
12+
projectstorages "kusionstack.io/kusion/pkg/project/storages"
1113
workspacestorages "kusionstack.io/kusion/pkg/workspace/storages"
1214
)
1315

@@ -86,3 +88,59 @@ func TestLocalStorage_ReleaseStorage(t *testing.T) {
8688
})
8789
}
8890
}
91+
92+
func TestLocalStorage_GraphStorage(t *testing.T) {
93+
testcases := []struct {
94+
name string
95+
success bool
96+
localStorage *LocalStorage
97+
project, workspace string
98+
}{
99+
{
100+
name: "graph storage from local backend",
101+
success: true,
102+
localStorage: &LocalStorage{
103+
path: "kusion",
104+
},
105+
project: "wordpress",
106+
workspace: "dev",
107+
},
108+
}
109+
110+
for _, tc := range testcases {
111+
t.Run(tc.name, func(t *testing.T) {
112+
mockey.PatchConvey("mock new local graph storage", t, func() {
113+
mockey.Mock(graphstorages.NewLocalStorage).Return(&graphstorages.LocalStorage{}, nil).Build()
114+
_, err := tc.localStorage.GraphStorage(tc.project, tc.workspace)
115+
assert.Equal(t, tc.success, err == nil)
116+
})
117+
})
118+
}
119+
}
120+
121+
func TestLocalStorage_ProjectStorage(t *testing.T) {
122+
testcases := []struct {
123+
name string
124+
success bool
125+
localStorage *LocalStorage
126+
}{
127+
{
128+
name: "project storage from local backend",
129+
success: true,
130+
localStorage: &LocalStorage{
131+
path: "kusion",
132+
},
133+
},
134+
}
135+
136+
for _, tc := range testcases {
137+
t.Run(tc.name, func(t *testing.T) {
138+
mockey.PatchConvey("mock new local project storage", t, func() {
139+
mockey.Mock((*projectstorages.LocalStorage).Get).Return(map[string][]string{}, nil).Build()
140+
_, err := tc.localStorage.ProjectStorage()
141+
142+
assert.Equal(t, tc.success, err == nil)
143+
})
144+
})
145+
}
146+
}

pkg/backend/storages/oss.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
77
"kusionstack.io/kusion/pkg/engine/release"
88
releasestorages "kusionstack.io/kusion/pkg/engine/release/storages"
9+
"kusionstack.io/kusion/pkg/engine/resource/graph"
10+
graphstorages "kusionstack.io/kusion/pkg/engine/resource/graph/storages"
911
projectstorages "kusionstack.io/kusion/pkg/project/storages"
1012
"kusionstack.io/kusion/pkg/workspace"
1113
workspacestorages "kusionstack.io/kusion/pkg/workspace/storages"
@@ -44,6 +46,10 @@ func (s *OssStorage) StateStorageWithPath(path string) (release.Storage, error)
4446
return releasestorages.NewOssStorage(s.bucket, releasestorages.GenReleasePrefixKeyWithPath(s.prefix, path))
4547
}
4648

49+
func (s *OssStorage) GraphStorage(project, workspace string) (graph.Storage, error) {
50+
return graphstorages.NewOssStorage(s.bucket, graphstorages.GenGenericOssResourcePrefixKey(s.prefix, project, workspace))
51+
}
52+
4753
func (s *OssStorage) ProjectStorage() (map[string][]string, error) {
4854
return projectstorages.NewOssStorage(s.bucket, projectstorages.GenGenericOssReleasePrefixKey(s.prefix)).Get()
4955
}

pkg/backend/storages/oss_test.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
1111
releasestorages "kusionstack.io/kusion/pkg/engine/release/storages"
12+
graphstorages "kusionstack.io/kusion/pkg/engine/resource/graph/storages"
13+
projectstorages "kusionstack.io/kusion/pkg/project/storages"
1214
workspacestorages "kusionstack.io/kusion/pkg/workspace/storages"
1315
)
1416

@@ -78,7 +80,7 @@ func TestOssStorage_ReleaseStorage(t *testing.T) {
7880
project, workspace string
7981
}{
8082
{
81-
name: "release storage from s3 backend",
83+
name: "release storage from oss backend",
8284
success: true,
8385
ossStorage: &OssStorage{
8486
bucket: &oss.Bucket{},
@@ -99,3 +101,61 @@ func TestOssStorage_ReleaseStorage(t *testing.T) {
99101
})
100102
}
101103
}
104+
105+
func TestOssStorage_GraphStorage(t *testing.T) {
106+
testcases := []struct {
107+
name string
108+
success bool
109+
ossStorage *OssStorage
110+
project, workspace string
111+
}{
112+
{
113+
name: "graph storage from oss backend",
114+
success: true,
115+
ossStorage: &OssStorage{
116+
bucket: &oss.Bucket{},
117+
prefix: "kusion",
118+
},
119+
project: "wordpress",
120+
workspace: "dev",
121+
},
122+
}
123+
124+
for _, tc := range testcases {
125+
t.Run(tc.name, func(t *testing.T) {
126+
mockey.PatchConvey("mock new oss graph storage", t, func() {
127+
mockey.Mock(graphstorages.NewOssStorage).Return(&graphstorages.OssStorage{}, nil).Build()
128+
_, err := tc.ossStorage.GraphStorage(tc.project, tc.workspace)
129+
assert.Equal(t, tc.success, err == nil)
130+
})
131+
})
132+
}
133+
}
134+
135+
func TestOssStorage_ProjectStorage(t *testing.T) {
136+
testcases := []struct {
137+
name string
138+
success bool
139+
ossStorage *OssStorage
140+
}{
141+
{
142+
name: "project storage from oss backend",
143+
success: true,
144+
ossStorage: &OssStorage{
145+
bucket: &oss.Bucket{},
146+
prefix: "kusion",
147+
},
148+
},
149+
}
150+
151+
for _, tc := range testcases {
152+
t.Run(tc.name, func(t *testing.T) {
153+
mockey.PatchConvey("mock new oss project storage", t, func() {
154+
mockey.Mock((*projectstorages.OssStorage).Get).Return(map[string][]string{}, nil).Build()
155+
_, err := tc.ossStorage.ProjectStorage()
156+
157+
assert.Equal(t, tc.success, err == nil)
158+
})
159+
})
160+
}
161+
}

pkg/backend/storages/s3.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
1010
"kusionstack.io/kusion/pkg/engine/release"
1111
releasestorages "kusionstack.io/kusion/pkg/engine/release/storages"
12+
"kusionstack.io/kusion/pkg/engine/resource/graph"
13+
graphstorages "kusionstack.io/kusion/pkg/engine/resource/graph/storages"
1214
projectstorages "kusionstack.io/kusion/pkg/project/storages"
1315
"kusionstack.io/kusion/pkg/workspace"
1416
workspacestorages "kusionstack.io/kusion/pkg/workspace/storages"
@@ -57,6 +59,10 @@ func (s *S3Storage) StateStorageWithPath(path string) (release.Storage, error) {
5759
return releasestorages.NewS3Storage(s.s3, s.bucket, releasestorages.GenReleasePrefixKeyWithPath(s.prefix, path))
5860
}
5961

62+
func (s *S3Storage) GraphStorage(project, workspace string) (graph.Storage, error) {
63+
return graphstorages.NewS3Storage(s.s3, s.bucket, graphstorages.GenGenericOssResourcePrefixKey(s.prefix, project, workspace))
64+
}
65+
6066
func (s *S3Storage) ProjectStorage() (map[string][]string, error) {
6167
return projectstorages.NewS3Storage(s.s3, s.bucket, projectstorages.GenGenericOssReleasePrefixKey(s.prefix)).Get()
6268
}

pkg/backend/storages/s3_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010

1111
v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1"
1212
releasestorages "kusionstack.io/kusion/pkg/engine/release/storages"
13+
graphstorages "kusionstack.io/kusion/pkg/engine/resource/graph/storages"
14+
projectstorages "kusionstack.io/kusion/pkg/project/storages"
1315
workspacestorages "kusionstack.io/kusion/pkg/workspace/storages"
1416
)
1517

@@ -103,3 +105,63 @@ func TestS3Storage_ReleaseStorage(t *testing.T) {
103105
})
104106
}
105107
}
108+
109+
func TestS3Storage_GraphStorage(t *testing.T) {
110+
testcases := []struct {
111+
name string
112+
success bool
113+
s3Storage *S3Storage
114+
project, workspace string
115+
}{
116+
{
117+
name: "graph storage from s3 backend",
118+
success: true,
119+
s3Storage: &S3Storage{
120+
s3: &s3.S3{},
121+
bucket: "infra",
122+
prefix: "kusion",
123+
},
124+
project: "wordpress",
125+
workspace: "dev",
126+
},
127+
}
128+
129+
for _, tc := range testcases {
130+
t.Run(tc.name, func(t *testing.T) {
131+
mockey.PatchConvey("mock new s3 graph storage", t, func() {
132+
mockey.Mock(graphstorages.NewS3Storage).Return(&graphstorages.S3Storage{}, nil).Build()
133+
_, err := tc.s3Storage.GraphStorage(tc.project, tc.workspace)
134+
assert.Equal(t, tc.success, err == nil)
135+
})
136+
})
137+
}
138+
}
139+
140+
func TestS3Storage_ProjectStorage(t *testing.T) {
141+
testcases := []struct {
142+
name string
143+
success bool
144+
s3Storage *S3Storage
145+
}{
146+
{
147+
name: "project storage from s3 backend",
148+
success: true,
149+
s3Storage: &S3Storage{
150+
s3: &s3.S3{},
151+
bucket: "infra",
152+
prefix: "kusion",
153+
},
154+
},
155+
}
156+
157+
for _, tc := range testcases {
158+
t.Run(tc.name, func(t *testing.T) {
159+
mockey.PatchConvey("mock new s3 project storage", t, func() {
160+
mockey.Mock((*projectstorages.S3Storage).Get).Return(map[string][]string{}, nil).Build()
161+
_, err := tc.s3Storage.ProjectStorage()
162+
163+
assert.Equal(t, tc.success, err == nil)
164+
})
165+
})
166+
}
167+
}

0 commit comments

Comments
 (0)