Skip to content

Commit

Permalink
feat: add kusion resource graph api for server
Browse files Browse the repository at this point in the history
  • Loading branch information
Yangyang96 committed Nov 14, 2024
1 parent 210ef29 commit ac2b226
Show file tree
Hide file tree
Showing 10 changed files with 2,135 additions and 102 deletions.
781 changes: 735 additions & 46 deletions api/openapispec/docs.go

Large diffs are not rendered by default.

781 changes: 735 additions & 46 deletions api/openapispec/swagger.json

Large diffs are not rendered by default.

506 changes: 496 additions & 10 deletions api/openapispec/swagger.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pkg/domain/constant/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ const (
DefaultLogFilePath = "/home/admin/logs/kusion.log"
RepoCacheTTL = 60 * time.Minute
RunTimeOut = 60 * time.Minute
DefaultWorkloadSig = "kusion.io/is-workload"
)
74 changes: 74 additions & 0 deletions pkg/domain/entity/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ type Resource struct {
Status string `yaml:"status" json:"status"`
// Attributes is the attributes of the resource.
Attributes map[string]interface{} `yaml:"attributes,omitempty" json:"attributes,omitempty"`
// Extensions is the extensions of the resource.
Extensions map[string]interface{} `yaml:"extensions,omitempty" json:"extensions,omitempty"`
// DependsOn is the depends on of the resource.
DependsOn []string `yaml:"dependsOn,omitempty" json:"dependsOn,omitempty"`
// Provider is the provider of the resource.
Provider string `yaml:"provider" json:"provider"`
// Labels are custom labels associated with the resource.
Expand All @@ -46,6 +50,32 @@ type Resource struct {
UpdateTimestamp time.Time `yaml:"updateTimestamp,omitempty" json:"updateTimestamp,omitempty"`
}

type ResourceInfo struct {
// ResourceType is the type of the resource.
ResourceType string `yaml:"resourceType" json:"resourceType"`
// ResourcePlane is the plane of the resource.
ResourcePlane string `yaml:"resourcePlane" json:"resourcePlane"`
// ResourceName is the name of the resource.
ResourceName string `yaml:"resourceName" json:"resourceName"`
// IAMResourceID is the id of the resource in IAM.
IAMResourceID string `yaml:"iamResourceID" json:"iamResourceID"`
// CloudResourceID is the id of the resource in the cloud.
CloudResourceID string `yaml:"cloudResourceID" json:"cloudResourceID"`
// Status is the status of the resource.
Status string `yaml:"status" json:"status"`
}

type ResourceRelation struct {
DependentResource string
DependencyResource string
}

type ResourceGraph struct {
Resources map[string]ResourceInfo `yaml:"resources" json:"resources"`
Relations []ResourceRelation `yaml:"relations" json:"relations"`
Workload string `yaml:"workload" json:"workload"`
}

type ResourceFilter struct {
OrgID uint
ProjectID uint
Expand All @@ -69,3 +99,47 @@ func (r *Resource) Validate() error {

return nil
}

func NewResourceGraph() *ResourceGraph {
return &ResourceGraph{
Resources: make(map[string]ResourceInfo),
Relations: []ResourceRelation{},
Workload: "",
}
}

func (rg *ResourceGraph) SetWorkload(workload string) error {
rg.Workload = workload
return nil
}

// AddResourceRelation adds a directed edge from parent to child
func (rg *ResourceGraph) AddResourceRelation(dependentResource, dependencyResource string) {
rg.Relations = append(rg.Relations, ResourceRelation{
DependentResource: dependentResource,
DependencyResource: dependencyResource,
})
}

func (rg *ResourceGraph) ConstructResourceGraph(resources []*Resource) error {
for _, resource := range resources {
info := ResourceInfo{
ResourceType: resource.ResourceType,
ResourcePlane: resource.ResourcePlane,
ResourceName: resource.ResourceName,
IAMResourceID: resource.IAMResourceID,
CloudResourceID: resource.CloudResourceID,
Status: resource.Status,
}
rg.Resources[resource.KusionResourceID] = info
if resource.Extensions[constant.DefaultWorkloadSig] == true {
rg.SetWorkload(resource.KusionResourceID)
}
if resource.DependsOn != nil {
for _, dependent := range resource.DependsOn {
rg.AddResourceRelation(dependent, resource.KusionResourceID)
}
}
}
return nil
}
6 changes: 6 additions & 0 deletions pkg/infra/persistence/resource_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type ResourceModel struct {
LastAppliedTimestamp time.Time
Status string
Attributes map[string]any `gorm:"serializer:json" json:"attributes"`
Extensions map[string]any `gorm:"serializer:json" json:"extensions"`
DependsOn MultiString
Provider string
Labels MultiString
Owners MultiString
Expand Down Expand Up @@ -61,6 +63,8 @@ func (m *ResourceModel) ToEntity() (*entity.Resource, error) {
LastAppliedTimestamp: m.LastAppliedTimestamp,
Status: m.Status,
Attributes: m.Attributes,
Extensions: m.Extensions,
DependsOn: []string(m.DependsOn),
Provider: m.Provider,
Labels: []string(m.Labels),
Owners: []string(m.Owners),
Expand All @@ -86,6 +90,8 @@ func (m *ResourceModel) FromEntity(e *entity.Resource) error {
m.LastAppliedTimestamp = e.LastAppliedTimestamp
m.Status = e.Status
m.Attributes = e.Attributes
m.Extensions = e.Extensions
m.DependsOn = MultiString(e.DependsOn)
m.Provider = e.Provider
m.Labels = MultiString(e.Labels)
m.Owners = MultiString(e.Owners)
Expand Down
42 changes: 42 additions & 0 deletions pkg/server/handler/resource/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/httplog/v2"
"github.com/go-chi/render"
"kusionstack.io/kusion/pkg/domain/entity"
"kusionstack.io/kusion/pkg/server/handler"
resourcemanager "kusionstack.io/kusion/pkg/server/manager/resource"
logutil "kusionstack.io/kusion/pkg/server/util/logging"
Expand Down Expand Up @@ -77,6 +78,47 @@ func (h *Handler) GetResource() http.HandlerFunc {
}
}

// @Id getResourceGraph
// @Summary Get resource graph
// @Description Get resource graph by stack ID
// @Tags resource
// @Produce json
// @Param stack_id query uint true "Stack ID"
// @Success 200 {object} entity.ResourceGraph "Success"
// @Failure 400 {object} error "Bad Request"
// @Failure 401 {object} error "Unauthorized"
// @Failure 429 {object} error "Too Many Requests"
// @Failure 404 {object} error "Not Found"
// @Failure 500 {object} error "Internal Server Error"
// @Router /api/v1/resources/graph [get]
func (h *Handler) GetResourceGraph() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Getting stuff from context
ctx := r.Context()
logger := logutil.GetLogger(ctx)
logger.Info("Getting resource graph...")
stackIDParam := r.URL.Query().Get("stackID")
filter, err := h.resourceManager.BuildResourceFilter(ctx, "", "", stackIDParam, "", "")
if err != nil {
render.Render(w, r, handler.FailureResponse(ctx, err))
return
}

// List resources
resourceEntities, err := h.resourceManager.ListResources(ctx, filter)
if err != nil {
render.Render(w, r, handler.FailureResponse(ctx, err))
return
}
resourceGraph := entity.NewResourceGraph()
if err := resourceGraph.ConstructResourceGraph(resourceEntities); err != nil {
render.Render(w, r, handler.FailureResponse(ctx, err))
return
}
handler.HandleResult(w, r, ctx, nil, resourceGraph)
}
}

func requestHelper(r *http.Request) (context.Context, *httplog.Logger, *ResourceRequestParams, error) {
ctx := r.Context()
resourceID := chi.URLParam(r, "resourceID")
Expand Down
43 changes: 43 additions & 0 deletions pkg/server/handler/resource/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,49 @@ func TestResourceHandler(t *testing.T) {
assert.Equal(t, "Kubernetes", resp.Data.(map[string]any)["resourceType"])
assert.Equal(t, "Kubernetes", resp.Data.(map[string]any)["resourcePlane"])
})

t.Run("GetResourceGraph", func(t *testing.T) {
sqlMock, fakeGDB, recorder, resourceHandler := setupTest(t)
defer persistence.CloseDB(t, fakeGDB)
defer sqlMock.ExpectClose()

sqlMock.ExpectQuery("SELECT").
WillReturnRows(sqlmock.NewRows([]string{"id", "resource_type", "resource_plane", "resource_name", "kusion_resource_id", "stack_id", "depends_on", "extensions"}).
AddRow(1, "Kubernetes", "Kubernetes", resourceName, "a:b:c:d", "1", "e:f:g:h", `{"kusion.io/is-workload":true}`).
AddRow(2, "Terraform", "AWS", resourceNameSecond, "e:f:g:h", "1", nil, `{}`).
AddRow(3, "Terraform", "AWS", resourceNameSecond, "z:x:y:w", "1", "e:f:g:h", `{}`))
sqlMock.ExpectQuery("SELECT").
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "project_id"}).
AddRow(1, "test-stack", "1"))
sqlMock.ExpectQuery("SELECT").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "test-project"))

// Create a new HTTP request
req, err := http.NewRequest("GET", "/resources", nil)
assert.NoError(t, err)

rctx := chi.NewRouteContext()
rctx.URLParams.Add("stack_id", "1")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))

// Call the ListResources handler function
resourceHandler.GetResourceGraph()(recorder, req)
fmt.Println(recorder.Body.String())
assert.Equal(t, http.StatusOK, recorder.Code)

// Unmarshal the response body
var resp handler.Response
err = json.Unmarshal(recorder.Body.Bytes(), &resp)
if err != nil {
t.Fatalf("Failed to unmarshal response: %v", err)
}

// Assertion
assert.Equal(t, "a:b:c:d", resp.Data.(map[string]any)["workload"])
assert.Equal(t, 2, len(resp.Data.(map[string]any)["relations"].([]any)))
assert.Equal(t, 3, len(resp.Data.(map[string]any)["resources"].(map[string]any)))
})
}

func setupTest(t *testing.T) (sqlmock.Sqlmock, *gorm.DB, *httptest.ResponseRecorder, *Handler) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/server/manager/stack/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ func (m *StackManager) WriteResources(ctx context.Context, release *v1.Release,
resourceEntity.LastAppliedTimestamp = release.ModifiedTime
resourceEntity.Attributes = resource.Attributes
resourceEntity.Status = constant.StatusResourceApplied
resourceEntity.Extensions = resource.Extensions
resourceEntity.DependsOn = resource.DependsOn
resourceEntitiesToInsert = append(resourceEntitiesToInsert, resourceEntity)
}
if err := m.resourceRepo.Create(ctx, resourceEntitiesToInsert); err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/server/route/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ func setupRestAPIV1(
r.Get("/", resourceHandler.GetResource())
})
r.Get("/", resourceHandler.ListResources())
r.Get("/graph", resourceHandler.GetResourceGraph())
})
r.Route("/modules", func(r chi.Router) {
r.Post("/", moduleHandler.CreateModule())
Expand Down

0 comments on commit ac2b226

Please sign in to comment.