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
1,500 changes: 1,432 additions & 68 deletions rest/src/main/resources/scripts/GoApiTemplate.groovy

Large diffs are not rendered by default.

1,729 changes: 1,652 additions & 77 deletions rest/src/main/resources/scripts/GoInventory.groovy

Large diffs are not rendered by default.

551 changes: 551 additions & 0 deletions rest/src/main/resources/scripts/GoTestTemplate.groovy

Large diffs are not rendered by default.

347 changes: 141 additions & 206 deletions rest/src/main/resources/scripts/RestDocumentationGenerator.groovy

Large diffs are not rendered by default.

936 changes: 936 additions & 0 deletions rest/src/main/resources/scripts/SDK生成器开发指南.md

Large diffs are not rendered by default.

606 changes: 305 additions & 301 deletions rest/src/main/resources/scripts/SdkApiTemplate.groovy

Large diffs are not rendered by default.

1,088 changes: 1,088 additions & 0 deletions rest/src/main/resources/scripts/ZStack SDK Go 开发规范与标准.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) ZStack.io, Inc.

package param

import "time"

var _ = time.Now // avoid unused import

type DeleteMode string

const (
DeleteModePermissive DeleteMode = "Permissive"
DeleteModeEnforcing DeleteMode = "Enforcing"
)

type BaseParam struct {
SystemTags []string `json:"systemTags,omitempty"` // System tags
UserTags []string `json:"userTags,omitempty"` // User tags
RequestIp string `json:"requestIp,omitempty"` // Request IP
}

type HqlParam struct {
OperationName string `json:"operationName"` // Request name
Query string `json:"query"` // Query statement
Variables Variables `json:"variables"` // Parameters for the statement
}

type Variables struct {
Conditions []Condition `json:"conditions"` // Conditions
ExtraConditions []Condition `json:"extraConditions"` // Extra conditions
Input map[string]interface{} `json:"input"` // Input parameters
PageVar `json:",inline,omitempty"`
Type string `json:"type"` // Type
}

type Condition struct {
Key string `json:"key"` // Key
Op string `json:"op"` // Operator
Value string `json:"value"` // Value
}

type PageVar struct {
Start int `json:"start,omitempty"` // Start page
Limit int `json:"limit,omitempty"` // Limit per page
}
102 changes: 102 additions & 0 deletions rest/src/main/resources/scripts/templates/base_params.go.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) ZStack.io, Inc.

package param

import (
"errors"
"fmt"
"net/url"
"reflect"
"strings"

"github.com/fatih/structs"
)

type QueryParam struct {
url.Values
}

func NewQueryParam() QueryParam {
return QueryParam{
Values: make(url.Values),
}
}

// AddQ adds a query condition, similar to a MySQL database query.
// Omitting this field will return all records, with the number of returned records limited by the 'limit' field.
func (params *QueryParam) AddQ(q string) *QueryParam {
if params.Get("q") == "" {
params.Set("q", q)
} else {
params.Add("q", q)
}
return params
}

// Limit sets the maximum number of records to return, similar to MySQL's 'limit'. Default value is 1000.
func (params *QueryParam) Limit(limit int) *QueryParam {
params.Set("limit", fmt.Sprintf("%d", limit))
return params
}

// Start sets the starting position for the query, similar to MySQL's 'offset'. Used with 'limit' for pagination.
func (params *QueryParam) Start(start int) *QueryParam {
params.Set("start", fmt.Sprintf("%d", start))
return params
}

// Count sets the query to return the count of records that match the query conditions, similar to MySQL's 'count()' function.
func (params *QueryParam) Count(count bool) *QueryParam {
params.Set("count", fmt.Sprintf("%t", count))
return params
}

// GroupBy groups the results by a specified field, similar to MySQL's 'group by' keyword.
func (params *QueryParam) GroupBy(groupBy string) *QueryParam {
params.Set("groupBy", groupBy)
return params
}

// ReplyWithCount, when set to true, includes the total count of records that match the query in the response.
func (params *QueryParam) ReplyWithCount(replyWithCount bool) *QueryParam {
params.Set("replyWithCount", fmt.Sprintf("%t", replyWithCount))
return params
}

// FilterName sets a filter name, functionality is unknown from ZStack Java SDK (sdk-4.4.0.jar).
func (params *QueryParam) FilterName(filterName string) *QueryParam {
params.Set("filterName", filterName)
return params
}

// Sort sorts the results by a specified field, similar to MySQL's 'sort by' keyword.
// Use '+' for ascending order and '-' for descending order, followed by the field name.
func (params *QueryParam) Sort(sort string) *QueryParam {
params.Set("sort", sort)
return params
}

// Fields specifies the fields to return, similar to MySQL's 'select' fields functionality.
func (params *QueryParam) Fields(fields []string) *QueryParam {
params.Set("fields", strings.Join(fields, ","))
return params
}

// ConvertStruct2UrlValues converts a struct to url.Values.
func ConvertStruct2UrlValues(param interface{}) (url.Values, error) {
if reflect.Ptr != reflect.TypeOf(param).Kind() {
return nil, errors.New("model should be pointer kind")
}
result := url.Values{}
if param == nil || reflect.ValueOf(param).IsNil() {
return nil, errors.New("param is nil")
}

s := structs.New(param)
s.TagName = "json"
mappedOpts := s.Map()
Comment on lines +87 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Locate and read the specific file
fd 'base_params.go.template$' -type f

Repository: MatheMatrix/zstack

Length of output: 232


🏁 Script executed:

# Find the file
fd 'base_params.go.template'

Repository: MatheMatrix/zstack

Length of output: 127


🏁 Script executed:

# Read the file to see the actual code around lines 87-97
sed -n '85,105p' rest/src/main/resources/scripts/templates/base_params.go.template

Repository: MatheMatrix/zstack

Length of output: 603


🏁 Script executed:

# Verify Go reflect behavior with nil value
python3 << 'EOF'
# Let's document Go reflect behavior to confirm the issue
print("Go reflect API behavior analysis:")
print()
print("1. reflect.TypeOf(nil) returns nil Type")
print("2. Calling .Kind() on a nil Type causes panic with:")
print("   'reflect: call of reflect.Kind on nil Type'")
print()
print("Current code order:")
print("  Line 87: reflect.TypeOf(param).Kind() <- CALLED FIRST")
print("  Line 90: if param == nil <- CHECKED SECOND")
print()
print("When param == nil:")
print("  1. reflect.TypeOf(nil) returns nil Type")
print("  2. nil.Kind() panics immediately")
print("  3. Never reaches the nil check at line 90")
print()
print("Conclusion: The review comment's critical issue is VALID")
EOF

Repository: MatheMatrix/zstack

Length of output: 556


ConvertStruct2UrlValuesparam == nil 时会 panic

第 87 行在判空之前调用 reflect.TypeOf(param).Kind() 会导致异常。当 param == nil 时,reflect.TypeOf(nil) 返回 nil,对 nil 调用 .Kind() 会触发 panic。需要先判空再进行反射操作,同时补充对指向 struct 的指针的验证。

建议修复
 func ConvertStruct2UrlValues(param interface{}) (url.Values, error) {
-	if reflect.Ptr != reflect.TypeOf(param).Kind() {
-		return nil, errors.New("model should be pointer kind")
-	}
-	result := url.Values{}
-	if param == nil || reflect.ValueOf(param).IsNil() {
+	if param == nil {
 		return nil, errors.New("param is nil")
 	}
+	t := reflect.TypeOf(param)
+	v := reflect.ValueOf(param)
+	if t.Kind() != reflect.Ptr || v.IsNil() {
+		return nil, errors.New("param should be a non-nil pointer")
+	}
+	if t.Elem().Kind() != reflect.Struct {
+		return nil, errors.New("param should point to a struct")
+	}
+	result := url.Values{}
 
 	s := structs.New(param)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if reflect.Ptr != reflect.TypeOf(param).Kind() {
return nil, errors.New("model should be pointer kind")
}
result := url.Values{}
if param == nil || reflect.ValueOf(param).IsNil() {
return nil, errors.New("param is nil")
}
s := structs.New(param)
s.TagName = "json"
mappedOpts := s.Map()
if param == nil {
return nil, errors.New("param is nil")
}
t := reflect.TypeOf(param)
v := reflect.ValueOf(param)
if t.Kind() != reflect.Ptr || v.IsNil() {
return nil, errors.New("param should be a non-nil pointer")
}
if t.Elem().Kind() != reflect.Struct {
return nil, errors.New("param should point to a struct")
}
result := url.Values{}
s := structs.New(param)
s.TagName = "json"
mappedOpts := s.Map()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rest/src/main/resources/scripts/templates/base_params.go.template` around
lines 87 - 97, ConvertStruct2UrlValues currently calls
reflect.TypeOf(param).Kind() before checking param == nil which panics for nil;
move the nil check first and then validate that param is a non-nil pointer to a
struct. Specifically: in ConvertStruct2UrlValues, first if param == nil return
error; then get t := reflect.TypeOf(param) and verify t.Kind() == reflect.Ptr
and t.Elem().Kind() == reflect.Struct; also use v := reflect.ValueOf(param) and
check v.IsNil() only after confirming it's a pointer; after those checks proceed
to call structs.New(param) and set s.TagName = "json".

for k, v := range mappedOpts {
result.Set(k, fmt.Sprintf("%v", v))
}
return result, nil
}
232 changes: 232 additions & 0 deletions rest/src/main/resources/scripts/templates/client.go.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Copyright (c) ZStack.io, Inc.

package client

import (
"context"
"crypto/sha512"
"fmt"
"net/http"
"net/url"

"github.com/kataras/golog"
"github.com/terraform-zstack-modules/zsphere-sdk-go-v2/pkg/errors"
"github.com/terraform-zstack-modules/zsphere-sdk-go-v2/pkg/param"
"github.com/terraform-zstack-modules/zsphere-sdk-go-v2/pkg/view"
)

type ZSClient struct {
*ZSHttpClient
}

func NewZSClient(config *ZSConfig) *ZSClient {
return &ZSClient{
ZSHttpClient: NewZSHttpClient(config),
}
}

func (cli *ZSClient) Login(ctx context.Context) (*view.SessionInventoryView, error) {
if cli.authType != AuthTypeAccountUser && cli.authType != AuthTypeAccount {
return nil, errors.ErrNotSupported
}

var sessionView *view.SessionInventoryView
var err error
if cli.authType == AuthTypeAccountUser {
sessionView, err = cli.logInByAccountUser(ctx)
} else {
sessionView, err = cli.logInByAccount(ctx)
}

if err != nil {
golog.Errorf("ZSClient.Login error:%v", err)
return nil, err
}

cli.LoadSession(sessionView.Uuid)
return sessionView, nil
}

func (cli *ZSClient) logInByAccountUser(ctx context.Context) (*view.SessionInventoryView, error) {
if cli.authType != AuthTypeAccountUser {
return nil, errors.ErrNotSupported
}

if len(cli.accountName) == 0 || len(cli.accountUserName) == 0 || len(cli.password) == 0 {
return nil, errors.ErrParameter
}

params := param.LogInByUserParam{
LogInByUser: param.LogInByUserDetailParam{
AccountName: cli.accountName,
UserName: cli.accountUserName,
Password: fmt.Sprintf("%x", sha512.Sum512([]byte(cli.password))),
},
}
sessionView := view.SessionInventoryView{}
err := cli.Put(ctx, "v1/accounts/users/login", "", params, &sessionView)
if err != nil {
golog.Errorf("ZSClient.logInByAccountUser Account[%s] User[%s] error:%v",
cli.accountName, cli.accountUserName, err)
return nil, err
}

return &sessionView, nil
}

func (cli *ZSClient) logInByAccount(ctx context.Context) (*view.SessionInventoryView, error) {
if cli.authType != AuthTypeAccount {
return nil, errors.ErrNotSupported
}

if len(cli.accountName) == 0 || len(cli.password) == 0 {
return nil, errors.ErrParameter
}

params := param.LoginByAccountParam{
LoginByAccount: param.LoginByAccountDetailParam{
AccountName: cli.accountName,
Password: fmt.Sprintf("%x", sha512.Sum512([]byte(cli.password))),
},
}
sessionView := view.SessionInventoryView{}
err := cli.Put(ctx, "v1/accounts/login", "", params, &sessionView)
if err != nil {
golog.Errorf("ZSClient.logInByAccount Account[%s] error:%v", cli.accountName, err)
return nil, err
}

return &sessionView, nil
}

func (cli *ZSClient) ValidateSession(ctx context.Context) (map[string]bool, error) {
if cli.authType != AuthTypeAccountUser && cli.authType != AuthTypeAccount {
return nil, errors.ErrNotSupported
}

if len(cli.sessionId) == 0 {
return nil, errors.ErrNotSupported
}

return cli.ValidateSessionId(ctx, cli.sessionId)
}

func (cli *ZSClient) ValidateSessionId(ctx context.Context, sessionId string) (map[string]bool, error) {
validSession := make(map[string]bool)
err := cli.GetWithSpec(ctx, "v1/accounts/sessions", sessionId, "valid", "", nil, &validSession)
if err != nil {
golog.Errorf("ZSClient.ValidateSession sessionId[%s] error:%v", sessionId, err)
return nil, err
}

golog.Debugf("ZSClient.ValidateSession sessionId[%s]:%v", sessionId, validSession)
return validSession, nil
}

func (cli *ZSClient) Logout(ctx context.Context) error {
if cli.authType != AuthTypeAccountUser && cli.authType != AuthTypeAccount {
return errors.ErrNotSupported
}

if len(cli.sessionId) == 0 {
return errors.ErrNotSupported
}

err := cli.Delete(ctx, "v1/accounts/sessions", cli.sessionId, "")
if err != nil {
golog.Errorf("ZSClient.Logout sessionId[%s] error:%v", cli.sessionId, err)
return err
}

cli.unloadSession()
return nil
}

func (cli *ZSClient) WebLogin(ctx context.Context) (*view.WebUISessionView, error) {
if cli.authType != AuthTypeAccountUser && cli.authType != AuthTypeAccount {
return nil, errors.ErrNotSupported
}

var operationName, username, loginType, query string
var input map[string]interface{}
if cli.authType == AuthTypeAccount {
operationName, username, loginType = "loginByAccount", cli.accountName, "iam1"
input = map[string]interface{}{
"accountName": cli.accountName,
"password": fmt.Sprintf("%x", sha512.Sum512([]byte(cli.password))),
}
query = `mutation loginByAccount($input:LoginByAccountInput!) {
loginByAccount(input: $input) {
sessionId,
accountUuid,
userUuid,
currentIdentity
}
}`
} else {
operationName, username, loginType = "loginIAM2VirtualID", cli.accountUserName, "iam2"
input = map[string]interface{}{
"name": cli.accountUserName,
"password": fmt.Sprintf("%x", sha512.Sum512([]byte(cli.password))),
}
query = `mutation loginIAM2VirtualID($input:LoginIAM2VirtualIDInput!) {
loginIAM2VirtualID(input: $input) {
sessionId,
accountUuid,
userUuid,
currentIdentity
}
}`
}

result := new(view.WebUISessionView)
params := param.HqlParam{
OperationName: operationName,
Query: query,
Variables: param.Variables{
Input: input,
},
}
respHeader, err := cli.hql(ctx, params, result, responseKeyData, operationName)
if err != nil {
return nil, err
}
result.UserName = username
result.LoginType = loginType
result.ZSVersion = respHeader.Get("Zs-Version")
return result, nil
}

func (cli *ZSClient) hql(ctx context.Context, params param.HqlParam, retVal interface{}, unMarshalKeys ...string) (http.Header, error) {
urlStr := fmt.Sprintf("http://%s:%d/graphql", cli.hostname, WebZStackPort)
_, respHeader, resp, err := cli.httpPost(ctx, urlStr, jsonMarshal(params), false)
if err != nil {
return nil, err
}

if retVal == nil {
return nil, nil
}

return respHeader, resp.Unmarshal(retVal, unMarshalKeys...)
}
Comment on lines +206 to +212
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

hql 方法在 retVal == nil 时丢失响应头

retVal == nil 时,方法返回 nil, nil,但函数签名表明它应该返回 (http.Header, error)。这会导致调用者丢失可能有用的响应头信息。

 	if retVal == nil {
-		return nil, nil
+		return respHeader, nil
 	}

同样的问题也存在于第 223-225 行的 Zql 方法中。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if retVal == nil {
return nil, nil
}
return respHeader, resp.Unmarshal(retVal, unMarshalKeys...)
}
if retVal == nil {
return respHeader, nil
}
return respHeader, resp.Unmarshal(retVal, unMarshalKeys...)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rest/src/main/resources/scripts/templates/client.go.template` around lines
206 - 212, The hql method currently returns nil, nil when retVal == nil, losing
the http.Header; change the retVal == nil branch in function hql to return
respHeader, nil so callers receive the response headers; apply the same fix to
the Zql method (the retVal == nil branch around lines referenced in the diff) so
both hql and Zql return (respHeader, nil) instead of (nil, nil).


func (cli *ZSClient) Zql(ctx context.Context, querySt string, retVal interface{}, unMarshalKeys ...string) (http.Header, error) {
encodedQuery := url.QueryEscape(querySt)
baseUrl := cli.getRequestURL("v1/zql")
urlStr := fmt.Sprintf("%s?zql=%s", baseUrl, encodedQuery)
_, respHeader, resp, err := cli.httpGet(ctx, urlStr, false)
if err != nil {
return nil, err
}

if retVal == nil {
return nil, nil
}

return respHeader, resp.Unmarshal(retVal, unMarshalKeys...)
}

const (
AuthTypeLogin AuthType = "login"
)
Loading