Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ import (
)

type DeleteProductServicesRequest struct {
ServiceNames []string `json:"service_names"`
ServiceNames []string `json:"service_names"`
DeleteResources map[string][]*service.K8sServiceResource `json:"delete_resources,omitempty"`
}

type DeleteProductHelmReleaseRequest struct {
Expand Down Expand Up @@ -1743,7 +1744,7 @@ func DeleteProductServices(c *gin.Context) {

detail := fmt.Sprintf("%s:[%s]", envName, strings.Join(args.ServiceNames, ","))
internalhandler.InsertDetailedOperationLog(c, ctx.UserName, projectKey, setting.OperationSceneEnv, "删除", "环境的服务", detail, detail, "", types.RequestBodyTypeJSON, ctx.Logger, envName)
ctx.RespErr = service.DeleteProductServices(ctx.UserName, ctx.RequestID, envName, projectKey, args.ServiceNames, production, isDelete, ctx.Logger)
ctx.RespErr = service.DeleteProductServices(ctx.UserName, ctx.RequestID, envName, projectKey, args.ServiceNames, production, isDelete, args.DeleteResources, ctx.Logger)
}

// @Summary Delete helm release from envrionment
Expand Down
4 changes: 2 additions & 2 deletions pkg/microservice/aslan/core/environment/handler/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ func OpenAPIDeleteYamlServiceFromEnv(c *gin.Context) {
}
}

ctx.RespErr = service.DeleteProductServices(ctx.UserName, ctx.RequestID, req.EnvName, projectKey, req.ServiceNames, false, !req.NotDeleteResource, ctx.Logger)
ctx.RespErr = service.DeleteProductServices(ctx.UserName, ctx.RequestID, req.EnvName, projectKey, req.ServiceNames, false, !req.NotDeleteResource, nil, ctx.Logger)
}

func OpenAPIDeleteProductionYamlServiceFromEnv(c *gin.Context) {
Expand Down Expand Up @@ -550,7 +550,7 @@ func OpenAPIDeleteProductionYamlServiceFromEnv(c *gin.Context) {
return
}

ctx.RespErr = service.DeleteProductServices(ctx.UserName, ctx.RequestID, req.EnvName, projectKey, req.ServiceNames, true, !req.NotDeleteResource, ctx.Logger)
ctx.RespErr = service.DeleteProductServices(ctx.UserName, ctx.RequestID, req.EnvName, projectKey, req.ServiceNames, true, !req.NotDeleteResource, nil, ctx.Logger)
}

func OpenAPIApplyProductionYamlService(c *gin.Context) {
Expand Down
1 change: 1 addition & 0 deletions pkg/microservice/aslan/core/environment/handler/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ func (*Router) Inject(router *gin.RouterGroup) {
environments.GET("/:name/services/:serviceName", GetService)
environments.PUT("/:name/services/:serviceName", UpdateService)
environments.GET("/:name/services/:serviceName/yaml", FetchServiceYaml)
environments.GET("/:name/services/:serviceName/resources", ListServiceResources)
environments.POST("/:name/services/:serviceName/preview", PreviewService)
environments.POST("/:name/services/preview/batch", BatchPreviewServices)
environments.POST("/:name/services/:serviceName/restart", RestartService)
Expand Down
66 changes: 66 additions & 0 deletions pkg/microservice/aslan/core/environment/handler/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ type FetchServiceYamlResponse struct {
Yaml string `json:"yaml"`
}

type ListServiceResourcesResponse struct {
Resources []*service.K8sServiceResource `json:"resources"`
}

// @Summary Fetch Service Yaml
// @Description Fetch Service Yaml
// @Tags environment
Expand Down Expand Up @@ -284,6 +288,68 @@ func FetchServiceYaml(c *gin.Context) {
ctx.Resp = resp
}

// @Summary List service resources
// @Description List k8s yaml service resources
// @Tags environment
// @Accept json
// @Produce json
// @Param projectName query string true "project name"
// @Param name path string true "env name"
// @Param serviceName path string true "service name"
// @Success 200 {object} ListServiceResourcesResponse
// @Router /api/aslan/environment/environments/{name}/services/{serviceName}/resources [get]
func ListServiceResources(c *gin.Context) {
ctx, err := internalhandler.NewContextWithAuthorization(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
if err != nil {
ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
ctx.UnAuthorized = true
return
}

serviceName := c.Param("serviceName")
envName := c.Param("name")
projectKey := c.Query("projectName")
production := c.Query("production") == "true"

// authorization checks
if !ctx.Resources.IsSystemAdmin {
if _, ok := ctx.Resources.ProjectAuthInfo[projectKey]; !ok {
ctx.UnAuthorized = true
return
}
if production {
if !ctx.Resources.ProjectAuthInfo[projectKey].IsProjectAdmin &&
!ctx.Resources.ProjectAuthInfo[projectKey].ProductionEnv.View {
permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, projectKey, types.ResourceTypeEnvironment, envName, types.ProductionEnvActionView)
if err != nil || !permitted {
ctx.UnAuthorized = true
return
}
}

err = commonutil.CheckZadigProfessionalLicense()
if err != nil {
ctx.RespErr = err
return
}
} else {
if !ctx.Resources.ProjectAuthInfo[projectKey].IsProjectAdmin &&
!ctx.Resources.ProjectAuthInfo[projectKey].Env.View {
permitted, err := internalhandler.GetCollaborationModePermission(ctx.UserID, projectKey, types.ResourceTypeEnvironment, envName, types.EnvActionView)
if err != nil || !permitted {
ctx.UnAuthorized = true
return
}
}
}
}

resp := new(ListServiceResourcesResponse)
resp.Resources, ctx.RespErr = service.ListK8sServiceResources(projectKey, envName, serviceName, production, ctx.Logger)
ctx.Resp = resp
}

// @Summary Preview service
// @Description Preview service
// @Tags environment
Expand Down
22 changes: 18 additions & 4 deletions pkg/microservice/aslan/core/environment/service/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2574,7 +2574,7 @@ func DeleteProduct(username, envName, productName, requestID string, isDelete bo
}

// @todo fix env already deleted issue, may cause service not really deleted in k8s
err = DeleteProductServices("", requestID, envName, productName, svcNames, false, isDelete, log)
err = DeleteProductServices("", requestID, envName, productName, svcNames, false, isDelete, nil, log)
if err != nil {
log.Warnf("DeleteProductServices error: %v", err)
}
Expand Down Expand Up @@ -2617,7 +2617,7 @@ func DeleteProduct(username, envName, productName, requestID string, isDelete bo
return nil
}

func DeleteProductServices(userName, requestID, envName, productName string, serviceNames []string, production, isDelete bool, log *zap.SugaredLogger) (err error) {
func DeleteProductServices(userName, requestID, envName, productName string, serviceNames []string, production, isDelete bool, deleteResources map[string][]*K8sServiceResource, log *zap.SugaredLogger) (err error) {
productInfo, err := commonrepo.NewProductColl().Find(&commonrepo.ProductFindOptions{Name: productName, EnvName: envName, Production: util.GetBoolPointer(production)})
if err != nil {
err = fmt.Errorf("failed to find product, productName: %s, envName: %s, production: %v, error: %v", productName, envName, production, err)
Expand All @@ -2627,7 +2627,7 @@ func DeleteProductServices(userName, requestID, envName, productName string, ser
if getProjectType(productName) == setting.HelmDeployType {
return deleteHelmProductServices(userName, requestID, productInfo, serviceNames, isDelete, log)
}
return deleteK8sProductServices(productInfo, serviceNames, isDelete, log)
return deleteK8sProductServices(productInfo, serviceNames, isDelete, deleteResources, log)
}

func DeleteProductHelmReleases(userName, requestID, envName, productName string, releases []string, production, isDelete bool, log *zap.SugaredLogger) (err error) {
Expand All @@ -2643,7 +2643,7 @@ func deleteHelmProductServices(userName, requestID string, productInfo *commonmo
return kube.DeleteHelmServiceFromEnv(userName, requestID, productInfo, serviceNames, isDelete, log)
}

func deleteK8sProductServices(productInfo *commonmodels.Product, serviceNames []string, isDelete bool, log *zap.SugaredLogger) error {
func deleteK8sProductServices(productInfo *commonmodels.Product, serviceNames []string, isDelete bool, deleteResources map[string][]*K8sServiceResource, log *zap.SugaredLogger) error {
serviceRelatedYaml := make(map[string]string)
for _, service := range productInfo.GetServiceMap() {
if !commonutil.ServiceIsDeployed(service.ServiceName, productInfo.ServiceDeployStrategy) || !isDelete {
Expand All @@ -2660,6 +2660,20 @@ func deleteK8sProductServices(productInfo *commonmodels.Product, serviceNames []
log.Errorf("failed to remove k8s resources when rendering yaml for service : %s, err: %s", service.ServiceName, err)
return fmt.Errorf("failed to remove k8s resources when rendering yaml for service : %s, err: %s", service.ServiceName, err)
}

if deleteResources != nil {
selectedResources, ok := deleteResources[service.ServiceName]
if ok {
yaml, err = filterSelectedServiceResourceYaml(yaml, selectedResources)
if err != nil {
log.Errorf("failed to filter selected k8s resources for service : %s, err: %s", service.ServiceName, err)
return fmt.Errorf("failed to filter selected k8s resources for service : %s, err: %s", service.ServiceName, err)
}
if len(selectedResources) == 0 {
log.Infof("all k8s resources are retained when deleting service %s in env %s/%s", service.ServiceName, productInfo.ProductName, productInfo.EnvName)
}
}
}
serviceRelatedYaml[service.ServiceName] = yaml
}
}
Expand Down
95 changes: 95 additions & 0 deletions pkg/microservice/aslan/core/environment/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"context"
"fmt"
"slices"
"sort"
"strings"

"github.com/koderover/zadig/v2/pkg/tool/clientmanager"
"github.com/pkg/errors"
Expand Down Expand Up @@ -313,6 +315,99 @@ func FetchServiceYaml(productName, envName, serviceName string, _ *zap.SugaredLo
return curYaml, nil
}

func ListK8sServiceResources(productName, envName, serviceName string, production bool, log *zap.SugaredLogger) ([]*K8sServiceResource, error) {
if getProjectType(productName) != setting.K8SDeployType {
return nil, e.ErrInvalidParam.AddDesc("only k8s yaml service resources are supported")
}

productInfo, err := commonrepo.NewProductColl().Find(&commonrepo.ProductFindOptions{
Name: productName,
EnvName: envName,
Production: util.GetBoolPointer(production),
})
if err != nil {
return nil, e.ErrGetService.AddErr(err)
}
if productInfo.GetServiceMap()[serviceName] == nil {
return nil, e.ErrGetService.AddDesc(fmt.Sprintf("service %s not found in environment", serviceName))
}

serviceYaml, err := FetchServiceYaml(productName, envName, serviceName, log)
if err != nil {
return nil, err
}

return parseK8sServiceResources(serviceYaml)
}

func parseK8sServiceResources(serviceYaml string) ([]*K8sServiceResource, error) {
resources, _, err := kube.ManifestToUnstructured(serviceYaml)
if err != nil {
return nil, err
}

ret := make([]*K8sServiceResource, 0, len(resources))
for _, resource := range resources {
ret = append(ret, &K8sServiceResource{
APIVersion: resource.GetAPIVersion(),
Kind: resource.GetKind(),
Name: resource.GetName(),
})
}
return ret, nil
}

func filterSelectedServiceResourceYaml(serviceYaml string, selectedResources []*K8sServiceResource) (string, error) {
if len(selectedResources) == 0 {
return "", nil
}

selectedResourceMap := make(map[string]struct{}, len(selectedResources))
for _, resource := range selectedResources {
if resource == nil || resource.APIVersion == "" || resource.Kind == "" || resource.Name == "" {
return "", fmt.Errorf("selected resource api_version, kind and name cannot be empty")
}
selectedResourceMap[k8sServiceResourceKey(resource.APIVersion, resource.Kind, resource.Name)] = struct{}{}
}

resources, resourceMap, err := kube.ManifestToUnstructured(serviceYaml)
if err != nil {
return "", err
}

filteredManifests := make([]string, 0, len(selectedResourceMap))
foundResourceMap := make(map[string]struct{}, len(selectedResourceMap))
for _, resource := range resources {
key := k8sServiceResourceKey(resource.GetAPIVersion(), resource.GetKind(), resource.GetName())
if _, ok := selectedResourceMap[key]; !ok {
continue
}

gvkn := fmt.Sprintf("%s-%s", resource.GetObjectKind().GroupVersionKind(), resource.GetName())
if resourceInfo, ok := resourceMap[gvkn]; ok {
filteredManifests = append(filteredManifests, resourceInfo.Manifest)
foundResourceMap[key] = struct{}{}
}
}

if len(foundResourceMap) != len(selectedResourceMap) {
missingResources := make([]string, 0, len(selectedResourceMap)-len(foundResourceMap))
for key := range selectedResourceMap {
if _, ok := foundResourceMap[key]; !ok {
missingResources = append(missingResources, key)
}
}
sort.Strings(missingResources)
return "", fmt.Errorf("selected resources not found in service yaml: %s", strings.Join(missingResources, ", "))
}

return util.JoinYamls(filteredManifests), nil
}

func k8sServiceResourceKey(apiVersion, kind, name string) string {
return fmt.Sprintf("%s/%s/%s", apiVersion, kind, name)
}

func PreviewService(args *PreviewServiceArgs, _ *zap.SugaredLogger) (*SvcDiffResult, error) {
envInfo, err := commonrepo.NewProductColl().Find(&commonrepo.ProductFindOptions{
Name: args.ProductName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package service

import (
"strings"
"testing"
)

const testServiceResourceYaml = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
---
apiVersion: v1
kind: Service
metadata:
name: demo
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo
`

func TestParseK8sServiceResources(t *testing.T) {
resources, err := parseK8sServiceResources(testServiceResourceYaml)
if err != nil {
t.Fatalf("parseK8sServiceResources() error = %v", err)
}

if len(resources) != 3 {
t.Fatalf("expected 3 resources, got %d", len(resources))
}
if resources[0].APIVersion != "apps/v1" || resources[0].Kind != "Deployment" || resources[0].Name != "demo" {
t.Fatalf("unexpected first resource: %+v", resources[0])
}
}

func TestFilterSelectedServiceResourceYaml(t *testing.T) {
filtered, err := filterSelectedServiceResourceYaml(testServiceResourceYaml, []*K8sServiceResource{
{APIVersion: "v1", Kind: "Service", Name: "demo"},
{APIVersion: "networking.k8s.io/v1", Kind: "Ingress", Name: "demo"},
})
if err != nil {
t.Fatalf("filterSelectedServiceResourceYaml() error = %v", err)
}

resources, err := parseK8sServiceResources(filtered)
if err != nil {
t.Fatalf("parse filtered yaml error = %v", err)
}
if len(resources) != 2 {
t.Fatalf("expected 2 resources, got %d", len(resources))
}
if resources[0].Kind != "Service" || resources[1].Kind != "Ingress" {
t.Fatalf("unexpected filtered resources: %+v", resources)
}
if strings.Contains(filtered, "kind: Deployment") {
t.Fatalf("filtered yaml should not contain Deployment: %s", filtered)
}
}

func TestFilterSelectedServiceResourceYamlEmptySelection(t *testing.T) {
filtered, err := filterSelectedServiceResourceYaml(testServiceResourceYaml, nil)
if err != nil {
t.Fatalf("filterSelectedServiceResourceYaml() error = %v", err)
}
if filtered != "" {
t.Fatalf("expected empty yaml, got %q", filtered)
}
}

func TestFilterSelectedServiceResourceYamlMissingResource(t *testing.T) {
_, err := filterSelectedServiceResourceYaml(testServiceResourceYaml, []*K8sServiceResource{
{APIVersion: "v1", Kind: "ConfigMap", Name: "missing"},
})
if err == nil {
t.Fatal("expected missing resource error")
}
}
6 changes: 6 additions & 0 deletions pkg/microservice/aslan/core/environment/service/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ type PreviewServiceArgs struct {
DeployContents []config.DeployContent `json:"deploy_contents"`
}

type K8sServiceResource struct {
APIVersion string `json:"api_version"`
Kind string `json:"kind"`
Name string `json:"name"`
}

type RestartScaleArgs struct {
Type string `json:"type"`
ProductName string `json:"product_name"`
Expand Down
Loading