From 37326b79560187730a65f4d6dafbd812a84bc81c Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Fri, 27 Mar 2026 13:55:31 +0800 Subject: [PATCH 1/2] [rest]: port Go SDK generator bug fixes from ZSV-11399 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port critical Go SDK generation fixes from MR !9249 (ZSV-11399): GoApiTemplate.groovy: - Add context.Context parameter to all generated Go method signatures - Add allTo response key extraction for PostWithRespKey - Add valid flag + isValid() for safe template initialization - Add reset() to prevent stale state across repeated SDK generation - Add getApiOptPath() for optional path handling - Fix pluralization: only replace y→ies after consonants - Pass allTo to GetWithSpec for correct response unwrapping GoInventory.groovy: - Add reset() + call GoApiTemplate.reset() at start of generation - Use template.isValid() instead of template.at != null - Fix time.Now → time.Now() in generated Go code (4 occurrences) - Remove deprecated generateClientFile() (~400 lines of dead code) base_param_types.go.template: - Fix time.Now → time.Now() (missing parentheses) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../resources/scripts/GoApiTemplate.groovy | 223 ++++++---- .../main/resources/scripts/GoInventory.groovy | 416 +----------------- .../templates/base_param_types.go.template | 2 +- 3 files changed, 150 insertions(+), 491 deletions(-) diff --git a/rest/src/main/resources/scripts/GoApiTemplate.groovy b/rest/src/main/resources/scripts/GoApiTemplate.groovy index fefe945ba3c..1141156f7f2 100644 --- a/rest/src/main/resources/scripts/GoApiTemplate.groovy +++ b/rest/src/main/resources/scripts/GoApiTemplate.groovy @@ -2,6 +2,8 @@ package scripts import org.zstack.header.query.APIQueryMessage import org.zstack.header.rest.RestRequest +import org.zstack.header.rest.SDK +import org.zstack.header.rest.RestResponse import org.zstack.rest.sdk.SdkFile import org.zstack.rest.sdk.SdkTemplate import org.zstack.utils.Utils @@ -20,6 +22,7 @@ class GoApiTemplate implements SdkTemplate { private RestRequest at private String path private Class responseClass + private String allTo; private String replyName private SdkTemplate inventoryGenerator @@ -54,6 +57,9 @@ class GoApiTemplate implements SdkTemplate { // Track APIs that should be skipped during generation private static Set skippedApis = new HashSet<>() + // Flag to indicate if the template was successfully initialized + private boolean valid = false + GoApiTemplate(Class apiMsgClass, SdkTemplate inventoryGenerator) { try { apiMsgClazz = apiMsgClass @@ -85,6 +91,12 @@ class GoApiTemplate implements SdkTemplate { } } + allTo = "" + if (responseClass != null) { + RestResponse restResponse = responseClass.getAnnotation(RestResponse) + allTo = restResponse != null ? restResponse.allTo() : "" + } + if (responseClass != null) { replyName = responseClass.simpleName.replaceAll('^API', '').replaceAll('Reply$', '').replaceAll('Event$', '') } else { @@ -103,6 +115,7 @@ class GoApiTemplate implements SdkTemplate { queryInventoryClass = findInventoryClass() logger.warn("[GoSDK] Processing API: " + clzName + " -> action=" + actionType + ", resource=" + resourceName + ", response=" + responseClass?.simpleName) + valid = true } catch (Throwable e) { logger.error("[GoSDK] CRITICAL ERROR constructing GoApiTemplate for ${apiMsgClass.name}: ${e.class.name}: ${e.message}", e) throw e @@ -113,6 +126,14 @@ class GoApiTemplate implements SdkTemplate { return at } + /** + * Check if the template was successfully initialized. + * Templates without @RestRequest annotation are invalid. + */ + boolean isValid() { + return valid + } + String getActionType() { return actionType } @@ -184,6 +205,21 @@ class GoApiTemplate implements SdkTemplate { logger.warn("[GoSDK] Registered ${mappings.size()} LongJob mappings") } + /** + * Reset all static state for clean re-generation. + * Should be called at the beginning of generate() or before each SDK generation run. + */ + static void reset() { + generatedParamFiles.clear() + generatedActionFiles.clear() + generatedViewFiles.clear() + knownInventoryClasses = null + groupedApiNames.clear() + longJobMappings.clear() + skippedApis.clear() + logger.warn("[GoSDK] Reset all static state") + } + /** * Check if current API supports async operation */ @@ -373,6 +409,13 @@ class GoApiTemplate implements SdkTemplate { return "v1/" + path } + private String getApiOptPath(String optPath) { + if (optPath.startsWith("/")) { + return "v1" + optPath + } + return "v1/" + optPath + } + List generate() { return [] } @@ -509,18 +552,18 @@ class GoApiTemplate implements SdkTemplate { if (!hasParams) { // No params: don't require user to pass params, use empty map internally if (unwrap) { - return """func (cli *ZSClient) ${clzName}() (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\tif err := cli.Post("${apiPath}", map[string]interface{}{}, &resp); err != nil { +\tif err := cli.Post(ctx, "${apiPath}", map[string]interface{}{}, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp.${fieldName}, nil } """ } else { - return """func (cli *ZSClient) ${clzName}() (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context) (*view.${viewStructName}, error) { \tresp := view.${viewStructName}{} -\tif err := cli.Post("${apiPath}", map[string]interface{}{}, &resp); err != nil { +\tif err := cli.Post(ctx, "${apiPath}", map[string]interface{}{}, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp, nil @@ -531,18 +574,18 @@ class GoApiTemplate implements SdkTemplate { // Has params: require user to pass params if (unwrap) { - return """func (cli *ZSClient) ${clzName}(params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, params param.${clzName}Param) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\tif err := cli.Post("${apiPath}", params, &resp); err != nil { +\tif err := cli.Post(ctx, "${apiPath}", params, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp.${fieldName}, nil } """ } else { - return """func (cli *ZSClient) ${clzName}(params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, params param.${clzName}Param) (*view.${viewStructName}, error) { \tresp := view.${viewStructName}{} -\tif err := cli.Post("${apiPath}", params, &resp); err != nil { +\tif err := cli.Post(ctx, "${apiPath}", params, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp, nil @@ -552,9 +595,9 @@ class GoApiTemplate implements SdkTemplate { } private String generateQueryMethod(String apiPath, String viewStructName) { - return """func (cli *ZSClient) ${clzName}(params *param.QueryParam) ([]view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, params *param.QueryParam) ([]view.${viewStructName}, error) { \tvar resp []view.${viewStructName} -\treturn resp, cli.List("${apiPath}", params, &resp) +\treturn resp, cli.List(ctx, "${apiPath}", params, &resp) } """ } @@ -566,7 +609,7 @@ class GoApiTemplate implements SdkTemplate { private String generatePageMethod(String apiPath, String viewStructName) { String pageMethodName = clzName.replaceFirst('^Query', 'Page') String varName = resourceName.substring(0, 1).toLowerCase() + resourceName.substring(1) - if (varName.endsWith("y")) { + if (varName.endsWith("y") && varName.length() > 1 && !"aeiou".contains(varName.charAt(varName.length() - 2).toString())) { varName = varName.substring(0, varName.length() - 1) + "ies" } else if (!varName.endsWith("s")) { varName = varName + "s" @@ -574,9 +617,9 @@ class GoApiTemplate implements SdkTemplate { return """ // ${pageMethodName} Pagination -func (cli *ZSClient) ${pageMethodName}(params *param.QueryParam) ([]view.${viewStructName}, int, error) { +func (cli *ZSClient) ${pageMethodName}(ctx context.Context, params *param.QueryParam) ([]view.${viewStructName}, int, error) { \tvar ${varName} []view.${viewStructName} -\ttotal, err := cli.Page("${apiPath}", params, &${varName}) +\ttotal, err := cli.Page(ctx, "${apiPath}", params, &${varName}) \treturn ${varName}, total, err } """ @@ -602,9 +645,9 @@ func (cli *ZSClient) ${pageMethodName}(params *param.QueryParam) ([]view.${viewS String spec = buildSpecPath(remainingPlaceholders) return """ -func (cli *ZSClient) ${getMethodName}(${params}) (*view.${viewStructName}, error) { +func (cli *ZSClient) ${getMethodName}(ctx context.Context, ${params}) (*view.${viewStructName}, error) { \tvar resp view.${viewStructName} -\terr := cli.GetWithSpec("${cleanPath}", ${firstParam}, ${spec}, "", nil, &resp) +\terr := cli.GetWithSpec(ctx, "${cleanPath}", ${firstParam}, ${spec}, "${allTo}", nil, &resp) \tif err != nil { \t\treturn nil, err \t} @@ -615,9 +658,9 @@ func (cli *ZSClient) ${getMethodName}(${params}) (*view.${viewStructName}, error // Standard case: single uuid parameter return """ -func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, error) { +func (cli *ZSClient) ${getMethodName}(ctx context.Context, uuid string) (*view.${viewStructName}, error) { \tvar resp view.${viewStructName} -\tif err := cli.Get("${cleanPath}", uuid, nil, &resp); err != nil { +\tif err := cli.Get(ctx, "${cleanPath}", uuid, nil, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp, nil @@ -639,9 +682,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err if (placeholders.size() == 0) { // No placeholder: no uuid parameter needed // Use GetWithRespKey to extract the inventory field - return """func (cli *ZSClient) ${clzName}() (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\tif err := cli.GetWithRespKey("${cleanPath}", "", "inventory", nil, &resp); err != nil { +\tif err := cli.GetWithRespKey(ctx, "${cleanPath}", "", "inventory", nil, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp.${fieldName}, nil @@ -649,9 +692,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err """ } else { // Single placeholder: use GetWithRespKey with uuid to extract inventory - return """func (cli *ZSClient) ${clzName}(uuid string) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, uuid string) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\tif err := cli.GetWithRespKey("${cleanPath}", uuid, "inventory", nil, &resp); err != nil { +\tif err := cli.GetWithRespKey(ctx, "${cleanPath}", uuid, "inventory", nil, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp.${fieldName}, nil @@ -666,9 +709,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String params = placeholders.collect { "${toSafeGoParamName(it)} string" }.join(", ") String spec = buildSpecPath(remainingPlaceholders) - return """func (cli *ZSClient) ${clzName}(${params}) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${params}) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\terr := cli.GetWithSpec("${cleanPath}", ${firstParam}, ${spec}, "", nil, &resp) +\terr := cli.GetWithSpec(ctx, "${cleanPath}", ${firstParam}, ${spec}, "", nil, &resp) \tif err != nil { \t\treturn nil, err \t} @@ -682,9 +725,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err if (placeholders.size() == 0) { // No placeholder: no uuid parameter needed // Use GetWithRespKey with empty responseKey to parse whole response - return """func (cli *ZSClient) ${clzName}() (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context) (*view.${viewStructName}, error) { \tvar resp view.${viewStructName} -\tif err := cli.GetWithRespKey("${cleanPath}", "", "", nil, &resp); err != nil { +\tif err := cli.GetWithRespKey(ctx, "${cleanPath}", "", "", nil, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp, nil @@ -692,9 +735,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err """ } else { // Single placeholder: use GetWithRespKey with uuid - return """func (cli *ZSClient) ${clzName}(uuid string) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, uuid string) (*view.${viewStructName}, error) { \tvar resp view.${viewStructName} -\tif err := cli.GetWithRespKey("${cleanPath}", uuid, "", nil, &resp); err != nil { +\tif err := cli.GetWithRespKey(ctx, "${cleanPath}", uuid, "", nil, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp, nil @@ -709,9 +752,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String params = placeholders.collect { "${toSafeGoParamName(it)} string" }.join(", ") String spec = buildSpecPath(remainingPlaceholders) - return """func (cli *ZSClient) ${clzName}(${params}) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${params}) (*view.${viewStructName}, error) { \tvar resp view.${viewStructName} -\terr := cli.GetWithSpec("${cleanPath}", ${firstParam}, ${spec}, "", nil, &resp) +\terr := cli.GetWithSpec(ctx, "${cleanPath}", ${firstParam}, ${spec}, "", nil, &resp) \tif err != nil { \t\treturn nil, err \t} @@ -756,9 +799,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String paramName = toSafeGoParamName(placeholders[0]) if (isActionApi) { // Action APIs wrap params.Params inside a map - return """func (cli *ZSClient) ${clzName}(${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\tif err := cli.Put("${cleanPath}", ${paramName}, map[string]interface{}{ +\tif err := cli.Put(ctx, "${cleanPath}", ${paramName}, map[string]interface{}{ \t\t"${actionKey}": params.Params, \t}, &resp); err != nil { \t\treturn nil, err @@ -767,9 +810,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err } """ } else { - return """func (cli *ZSClient) ${clzName}(${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\tif err := cli.Put("${cleanPath}", ${paramName}, params, &resp); err != nil { +\tif err := cli.Put(ctx, "${cleanPath}", ${paramName}, params, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp.${fieldName}, nil @@ -781,9 +824,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err if (!hasParams) { // No params: don't require user input if (isActionApi) { - return """func (cli *ZSClient) ${clzName}() (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\tif err := cli.Put("${cleanPath}", "", map[string]interface{}{ +\tif err := cli.Put(ctx, "${cleanPath}", "", map[string]interface{}{ \t\t"${actionKey}": map[string]interface{}{}, \t}, &resp); err != nil { \t\treturn nil, err @@ -792,9 +835,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err } """ } else { - return """func (cli *ZSClient) ${clzName}() (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\tif err := cli.Put("${cleanPath}", "", map[string]interface{}{}, &resp); err != nil { +\tif err := cli.Put(ctx, "${cleanPath}", "", map[string]interface{}{}, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp.${fieldName}, nil @@ -802,9 +845,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err """ } } else if (isActionApi) { - return """func (cli *ZSClient) ${clzName}(params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, params param.${clzName}Param) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\tif err := cli.Put("${cleanPath}", "", map[string]interface{}{ +\tif err := cli.Put(ctx, "${cleanPath}", "", map[string]interface{}{ \t\t"${actionKey}": params.Params, \t}, &resp); err != nil { \t\treturn nil, err @@ -813,9 +856,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err } """ } else { - return """func (cli *ZSClient) ${clzName}(uuid string, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, uuid string, params param.${clzName}Param) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\tif err := cli.Put("${cleanPath}", uuid, params, &resp); err != nil { +\tif err := cli.Put(ctx, "${cleanPath}", uuid, params, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp.${fieldName}, nil @@ -831,9 +874,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String params = placeholders.collect { "${toSafeGoParamName(it)} string" }.join(", ") String spec = buildSpecPath(remainingPlaceholders) - return """func (cli *ZSClient) ${clzName}(${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { \tvar resp view.${responseStructName} -\terr := cli.PutWithSpec("${cleanPath}", ${firstParam}, ${spec}, "", params, &resp) +\terr := cli.PutWithSpec(ctx, "${cleanPath}", ${firstParam}, ${spec}, "", params, &resp) \tif err != nil { \t\treturn nil, err \t} @@ -850,9 +893,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String paramName = toSafeGoParamName(placeholders[0]) if (isActionApi) { // Action APIs wrap params.Params inside a map - return """func (cli *ZSClient) ${clzName}(${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { \tresp := view.${viewStructName}{} -\tif err := cli.PutWithRespKey("${cleanPath}", ${paramName}, "", map[string]interface{}{ +\tif err := cli.PutWithRespKey(ctx, "${cleanPath}", ${paramName}, "", map[string]interface{}{ \t\t"${actionKey}": params.Params, \t}, &resp); err != nil { \t\treturn nil, err @@ -861,9 +904,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err } """ } else { - return """func (cli *ZSClient) ${clzName}(${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { \tresp := view.${viewStructName}{} -\tif err := cli.PutWithRespKey("${cleanPath}", ${paramName}, "", params, &resp); err != nil { +\tif err := cli.PutWithRespKey(ctx, "${cleanPath}", ${paramName}, "", params, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp, nil @@ -875,9 +918,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err if (!hasParams) { // No params: don't require user input if (isActionApi) { - return """func (cli *ZSClient) ${clzName}() (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context) (*view.${viewStructName}, error) { \tresp := view.${viewStructName}{} -\tif err := cli.PutWithRespKey("${cleanPath}", "", "", map[string]interface{}{ +\tif err := cli.PutWithRespKey(ctx, "${cleanPath}", "", "", map[string]interface{}{ \t\t"${actionKey}": map[string]interface{}{}, \t}, &resp); err != nil { \t\treturn nil, err @@ -886,9 +929,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err } """ } else { - return """func (cli *ZSClient) ${clzName}() (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context) (*view.${viewStructName}, error) { \tresp := view.${viewStructName}{} -\tif err := cli.PutWithRespKey("${cleanPath}", "", "", map[string]interface{}{}, &resp); err != nil { +\tif err := cli.PutWithRespKey(ctx, "${cleanPath}", "", "", map[string]interface{}{}, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp, nil @@ -896,9 +939,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err """ } } else if (isActionApi) { - return """func (cli *ZSClient) ${clzName}(params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, params param.${clzName}Param) (*view.${viewStructName}, error) { \tresp := view.${viewStructName}{} -\tif err := cli.PutWithRespKey("${cleanPath}", "", "", map[string]interface{}{ +\tif err := cli.PutWithRespKey(ctx, "${cleanPath}", "", "", map[string]interface{}{ \t\t"${actionKey}": params.Params, \t}, &resp); err != nil { \t\treturn nil, err @@ -907,9 +950,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err } """ } else { - return """func (cli *ZSClient) ${clzName}(uuid string, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, uuid string, params param.${clzName}Param) (*view.${viewStructName}, error) { \tresp := view.${viewStructName}{} -\tif err := cli.PutWithRespKey("${cleanPath}", uuid, "", params, &resp); err != nil { +\tif err := cli.PutWithRespKey(ctx, "${cleanPath}", uuid, "", params, &resp); err != nil { \t\treturn nil, err \t} \treturn &resp, nil @@ -925,9 +968,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String params = placeholders.collect { "${toSafeGoParamName(it)} string" }.join(", ") String spec = buildSpecPath(remainingPlaceholders) - return """func (cli *ZSClient) ${clzName}(${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { \tresp := view.${viewStructName}{} -\terr := cli.PutWithSpec("${cleanPath}", ${firstParam}, ${spec}, "", params, &resp) +\terr := cli.PutWithSpec(ctx, "${cleanPath}", ${firstParam}, ${spec}, "", params, &resp) \tif err != nil { \t\treturn nil, err \t} @@ -948,8 +991,8 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err if (!useSpec) { // Single or no placeholder: use the standard Delete method - return """func (cli *ZSClient) ${clzName}(uuid string, deleteMode param.DeleteMode) error { -\treturn cli.Delete("${cleanPath}", uuid, string(deleteMode)) + return """func (cli *ZSClient) ${clzName}(ctx context.Context, uuid string, deleteMode param.DeleteMode) error { +\treturn cli.Delete(ctx, "${cleanPath}", uuid, string(deleteMode)) } """ } else { @@ -961,8 +1004,8 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String spec = buildSpecPath(remainingPlaceholders) String paramsStr = "fmt.Sprintf(\"deleteMode=%s\", deleteMode)" - return """func (cli *ZSClient) ${clzName}(${params}, deleteMode param.DeleteMode) error { -\treturn cli.DeleteWithSpec("${cleanPath}", ${firstParam}, ${spec}, ${paramsStr}, nil) + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${params}, deleteMode param.DeleteMode) error { +\treturn cli.DeleteWithSpec(ctx, "${cleanPath}", ${firstParam}, ${spec}, ${paramsStr}, nil) } """ } @@ -985,11 +1028,11 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err // Build parameter key, for example expungeImage String paramKey = clzName.substring(0, 1).toLowerCase() + clzName.substring(1) - return """func (cli *ZSClient) ${clzName}(uuid string) error { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, uuid string) error { \tparams := map[string]interface{}{ \t\t"${paramKey}": map[string]interface{}{}, \t} -\treturn cli.Put("${cleanPath}", uuid, params, nil) +\treturn cli.Put(ctx, "${cleanPath}", uuid, params, nil) } """ } @@ -1025,9 +1068,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err switch (httpMethod) { case "GET": if (!useSpec) { - return """func (cli *ZSClient) ${clzName}(params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, params param.${clzName}Param) (*view.${viewStructName}, error) { \tvar resp ${respType} -\tif err := cli.Get("${cleanPath}", "", params, &resp); err != nil { +\tif err := cli.Get(ctx, "${cleanPath}", "", params, &resp); err != nil { \t\treturn nil, err \t} \t${returnStmt} @@ -1036,9 +1079,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err } else { String params = placeholders.collect { "${toSafeGoParamName(it)} string" }.join(", ") String pathSpec = buildPathSpec(placeholders) - return """func (cli *ZSClient) ${clzName}(${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { \tvar resp ${respType} -\terr := cli.GetWithSpec("${cleanPath}", ${pathSpec}, "", "", params, &resp) +\terr := cli.GetWithSpec(ctx, "${cleanPath}", ${pathSpec}, "", "${allTo}", params, &resp) \tif err != nil { \t\treturn nil, err \t} @@ -1048,9 +1091,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err } case "POST": if (!useSpec) { - return """func (cli *ZSClient) ${clzName}(params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, params param.${clzName}Param) (*view.${viewStructName}, error) { \t${respDecl} -\tif err := cli.Post("${cleanPath}", params, &resp); err != nil { +\tif err := cli.Post(ctx, "${cleanPath}", params, &resp); err != nil { \t\treturn nil, err \t} \t${returnStmt} @@ -1060,9 +1103,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err // POST lacks *WithSpec helpers; build the full URL manually String params = placeholders.collect { "${toSafeGoParamName(it)} string" }.join(", ") String fullPath = buildFullPath(placeholders) - return """func (cli *ZSClient) ${clzName}(${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { \t${respDecl} -\terr := cli.Post(${fullPath}, params, &resp) +\terr := cli.Post(ctx, ${fullPath}, params, &resp) \tif err != nil { \t\treturn nil, err \t} @@ -1087,7 +1130,7 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err """("${cleanPath}", ${paramName}, "", map[string]interface{}{ \t\t"${actionKey}": map[string]interface{}{}, \t}, &resp)""" - return """func (cli *ZSClient) ${clzName}(${paramName} string) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${paramName} string) (*view.${viewStructName}, error) { \t${respDecl} \tif err := ${putMethod}${putArgs}; err != nil { \t\treturn nil, err @@ -1100,7 +1143,7 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String putArgs = unwrap ? """("${cleanPath}", ${paramName}, map[string]interface{}{}, &resp)""" : """("${cleanPath}", ${paramName}, "", map[string]interface{}{}, &resp)""" - return """func (cli *ZSClient) ${clzName}(${paramName} string) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${paramName} string) (*view.${viewStructName}, error) { \t${respDecl} \tif err := ${putMethod}${putArgs}; err != nil { \t\treturn nil, err @@ -1119,7 +1162,7 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err """("${cleanPath}", ${paramName}, "", map[string]interface{}{ \t\t"${actionKey}": params.Params, \t}, &resp)""" - return """func (cli *ZSClient) ${clzName}(${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { \t${respDecl} \tif err := ${putMethod}${putArgs}; err != nil { \t\treturn nil, err @@ -1132,7 +1175,7 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String putArgs = unwrap ? """("${cleanPath}", ${paramName}, params, &resp)""" : """("${cleanPath}", ${paramName}, "", params, &resp)""" - return """func (cli *ZSClient) ${clzName}(${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${paramName} string, params param.${clzName}Param) (*view.${viewStructName}, error) { \t${respDecl} \tif err := ${putMethod}${putArgs}; err != nil { \t\treturn nil, err @@ -1154,7 +1197,7 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err """("${cleanPath}", "", "", map[string]interface{}{ \t\t"${actionKey}": map[string]interface{}{}, \t}, &resp)""" - return """func (cli *ZSClient) ${clzName}() (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context) (*view.${viewStructName}, error) { \t${respDecl} \tif err := ${putMethod}${putArgs}; err != nil { \t\treturn nil, err @@ -1167,7 +1210,7 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String putArgs = unwrap ? """("${cleanPath}", "", map[string]interface{}{}, &resp)""" : """("${cleanPath}", "", "", map[string]interface{}{}, &resp)""" - return """func (cli *ZSClient) ${clzName}() (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context) (*view.${viewStructName}, error) { \t${respDecl} \tif err := ${putMethod}${putArgs}; err != nil { \t\treturn nil, err @@ -1185,7 +1228,7 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err """("${cleanPath}", "", "", map[string]interface{}{ \t\t"${actionKey}": params.Params, \t}, &resp)""" - return """func (cli *ZSClient) ${clzName}(params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, params param.${clzName}Param) (*view.${viewStructName}, error) { \t${respDecl} \tif err := ${putMethod}${putArgs}; err != nil { \t\treturn nil, err @@ -1198,7 +1241,7 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String putArgs = unwrap ? """("${cleanPath}", uuid, params, &resp)""" : """("${cleanPath}", uuid, "", params, &resp)""" - return """func (cli *ZSClient) ${clzName}(uuid string, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, uuid string, params param.${clzName}Param) (*view.${viewStructName}, error) { \t${respDecl} \tif err := ${putMethod}${putArgs}; err != nil { \t\treturn nil, err @@ -1215,9 +1258,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String params = placeholders.collect { "${toSafeGoParamName(it)} string" }.join(", ") String spec = buildSpecPath(remainingPlaceholders) - return """func (cli *ZSClient) ${clzName}(${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { \t${respDecl} -\terr := cli.PutWithSpec("${cleanPath}", ${firstParam}, ${spec}, "", params, &resp) +\terr := cli.PutWithSpec(ctx, "${cleanPath}", ${firstParam}, ${spec}, "", params, &resp) \tif err != nil { \t\treturn nil, err \t} @@ -1227,8 +1270,8 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err } case "DELETE": if (!useSpec) { - return """func (cli *ZSClient) ${clzName}(uuid string, deleteMode param.DeleteMode) error { -\treturn cli.Delete("${cleanPath}", uuid, string(deleteMode)) + return """func (cli *ZSClient) ${clzName}(ctx context.Context, uuid string, deleteMode param.DeleteMode) error { +\treturn cli.Delete(ctx, "${cleanPath}", uuid, string(deleteMode)) } """ } else { @@ -1239,16 +1282,16 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err String spec = buildSpecPath(remainingPlaceholders) String paramsStr = "fmt.Sprintf(\"deleteMode=%s\", deleteMode)" - return """func (cli *ZSClient) ${clzName}(${params}, deleteMode param.DeleteMode) error { - return cli.DeleteWithSpec("${cleanPath}", ${firstParam}, ${spec}, ${paramsStr}, nil) + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${params}, deleteMode param.DeleteMode) error { + return cli.DeleteWithSpec(ctx, "${cleanPath}", ${firstParam}, ${spec}, ${paramsStr}, nil) } """ } default: if (!useSpec) { - return """func (cli *ZSClient) ${clzName}(params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, params param.${clzName}Param) (*view.${viewStructName}, error) { \t${respDecl} -\tif err := cli.Post("${cleanPath}", params, &resp); err != nil { +\tif err := cli.Post(ctx, "${cleanPath}", params, &resp); err != nil { \t\treturn nil, err \t} \t${returnStmt} @@ -1258,9 +1301,9 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err // POST lacks *WithSpec helpers; build the full URL manually String params = placeholders.collect { "${toSafeGoParamName(it)} string" }.join(", ") String fullPath = buildFullPath(placeholders) - return """func (cli *ZSClient) ${clzName}(${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { + return """func (cli *ZSClient) ${clzName}(ctx context.Context, ${params}, params param.${clzName}Param) (*view.${viewStructName}, error) { \t${respDecl} -\terr := cli.Post(${fullPath}, params, &resp) +\terr := cli.Post(ctx, ${fullPath}, params, &resp) \tif err != nil { \t\treturn nil, err \t} @@ -1291,7 +1334,7 @@ func (cli *ZSClient) ${getMethodName}(uuid string) (*view.${viewStructName}, err def builder = new StringBuilder() builder.append("\n// ${asyncMethodName} Async\n") - builder.append("func (cli *ZSClient) ${asyncMethodName}(params param.${clzName}Param) (string, error) {\n") + builder.append("func (cli *ZSClient) ${asyncMethodName}(ctx context.Context, params param.${clzName}Param) (string, error) {\n") builder.append("\n") builder.append("\tresource := \"${resource}\"\n") builder.append("\tresponseKey := \"\"\n") diff --git a/rest/src/main/resources/scripts/GoInventory.groovy b/rest/src/main/resources/scripts/GoInventory.groovy index c31e44149c6..335260e671d 100644 --- a/rest/src/main/resources/scripts/GoInventory.groovy +++ b/rest/src/main/resources/scripts/GoInventory.groovy @@ -106,6 +106,14 @@ class GoInventory implements SdkTemplate { return longJobMappings } + /** + * Reset all static state for clean re-generation. + */ + static void reset() { + longJobMappings.clear() + logger.warn("[GoSDK] Reset GoInventory static state") + } + /** * Pre-analyze all API classes once and cache metadata. * This avoids expensive re-instantiation of GoApiTemplate and redundant logging. @@ -124,7 +132,7 @@ class GoInventory implements SdkTemplate { try { GoApiTemplate template = new GoApiTemplate(apiClass, this) // If it's a valid template (has @RestRequest) - if (template.at != null) { + if (template.isValid()) { allApiTemplates.add(template) } } catch (Throwable e) { @@ -160,6 +168,9 @@ class GoInventory implements SdkTemplate { List generate() { def files = [] + GoApiTemplate.reset() + reset() + logger.warn("[GoSDK] ===== GoInventory.generate() START =====") logger.warn("[GoSDK] GoInventory.generate() starting...") @@ -296,7 +307,7 @@ class GoInventory implements SdkTemplate { content.append("// Copyright (c) ZStack.io, Inc.\n\n") content.append("package view\n\n") content.append("import \"time\"\n\n") - content.append("var _ = time.Now // avoid unused import\n\n") + content.append("var _ = time.Now() // avoid unused import\n\n") classes.each { Class clz -> String structName = getViewStructName(clz) @@ -314,401 +325,6 @@ class GoInventory implements SdkTemplate { return files } - /** - * Generate ZSClient base file - * @deprecated client.go is manually maintained, this method should not be used - */ - @Deprecated - private SdkFile generateClientFile() { - def sdkFile = new SdkFile() - sdkFile.subPath = "/pkg/client/" - sdkFile.fileName = "client.go" - - def content = new StringBuilder() - content.append("// Copyright (c) ZStack.io, Inc.\n\n") - content.append("package client\n\n") - content.append("import (\n") - content.append("\t\"bytes\"\n") - content.append("\t\"crypto/sha512\"\n") - content.append("\t\"encoding/hex\"\n") - content.append("\t\"encoding/json\"\n") - content.append("\t\"fmt\"\n") - content.append("\t\"io\"\n") - content.append("\t\"net/http\"\n") - content.append("\t\"net/url\"\n") - content.append("\t\"strconv\"\n") - content.append("\t\"strings\"\n") - content.append("\t\"github.com/zstackio/zstack-sdk-go-v2/pkg/param\"\n") - content.append("\t\"time\"\n") - content.append(")\n\n") - content.append("// AuthType authentication type\n") - content.append("type AuthType string\n\n") - content.append("const (\n") - content.append("\tAuthTypeAccessKey AuthType = \"accesskey\"\n") - content.append("\tAuthTypeLogin AuthType = \"login\"\n") - content.append(")\n\n") - content.append("const (\n") - content.append("\tdefaultZStackPort = 8080\n") - content.append(")\n\n") - content.append("// ZSConfig client configuration\n") - content.append("type ZSConfig struct {\n") - content.append("\thostname string\n") - content.append("\tport int\n") - content.append("\tcontextPath string\n") - content.append("\taccessKeyId string\n") - content.append("\taccessKeySecret string\n") - content.append("\tusername string\n") - content.append("\tpassword string\n") - content.append("\tauthType AuthType\n") - content.append("\tdebug bool\n") - content.append("\ttimeout time.Duration\n") - content.append("}\n\n") - content.append("// NewZSConfig creates a new configuration\n") - content.append("func NewZSConfig(hostname string, port int, contextPath string) *ZSConfig {\n") - content.append("\treturn &ZSConfig{\n") - content.append("\t\thostname: hostname,\n") - content.append("\t\tport: port,\n") - content.append("\t\tcontextPath: contextPath,\n") - content.append("\t\ttimeout: 30 * time.Second,\n") - content.append("\t}\n") - content.append("}\n\n") - content.append("// DefaultZSConfig creates a default configuration\n") - content.append("func DefaultZSConfig(hostname, contextPath string) *ZSConfig {\n") - content.append("\treturn NewZSConfig(hostname, defaultZStackPort, contextPath)\n") - content.append("}\n\n") - content.append("// AccessKey sets access key authentication\n") - content.append("func (config *ZSConfig) AccessKey(id, secret string) *ZSConfig {\n") - content.append("\tconfig.accessKeyId = id\n") - content.append("\tconfig.accessKeySecret = secret\n") - content.append("\tconfig.authType = AuthTypeAccessKey\n") - content.append("\treturn config\n") - content.append("}\n\n") - content.append("// Login sets login authentication\n") - content.append("func (config *ZSConfig) Login(username, password string) *ZSConfig {\n") - content.append("\tconfig.username = username\n") - content.append("\tconfig.password = password\n") - content.append("\tconfig.authType = AuthTypeLogin\n") - content.append("\treturn config\n") - content.append("}\n\n") - content.append("// Debug enables debug mode\n") - content.append("func (config *ZSConfig) Debug(debug bool) *ZSConfig {\n") - content.append("\tconfig.debug = debug\n") - content.append("\treturn config\n") - content.append("}\n\n") - content.append("// ZSClient ZStack API client\n") - content.append("type ZSClient struct {\n") - content.append("\tconfig *ZSConfig\n") - content.append("\thttpClient *http.Client\n") - content.append("\tsessionId string\n") - content.append("}\n\n") - content.append("// JobView job inventory view\n") - content.append("type JobView struct {\n") - content.append("\tUUID string `json:\"uuid\"`\n") - content.append("\tState string `json:\"state\"`\n") - content.append("\tResult interface{} `json:\"result,omitempty\"`\n") - content.append("\tError interface{} `json:\"error,omitempty\"`\n") - content.append("\tCreateDate string `json:\"createDate\"`\n") - content.append("}\n\n") - content.append("const (\n") - content.append("\tJobStateProcessing = \"Processing\"\n") - content.append("\tJobStateSucceeded = \"Succeeded\"\n") - content.append("\tJobStateFailed = \"Failed\"\n") - content.append(")\n\n") - content.append("// NewZSClient creates a new ZStack client\n") - content.append("func NewZSClient(config *ZSConfig) *ZSClient {\n") - content.append("\t// Auto-encrypt password for login authentication\n") - content.append("\tif config.authType == AuthTypeLogin && config.password != \"\" {\n") - content.append("\t\tconfig.password = hashPasswordSHA512(config.password)\n") - content.append("\t\tif config.debug {\n") - content.append("\t\t\tfmt.Printf(\"[DEBUG] Password hashed: %s...\\n\", config.password[:16])\n") - content.append("\t\t}\n") - content.append("\t}\n") - content.append("\treturn &ZSClient{\n") - content.append("\t\tconfig: config,\n") - content.append("\t\thttpClient: &http.Client{\n") - content.append("\t\t\tTimeout: config.timeout,\n") - content.append("\t\t},\n") - content.append("\t}\n") - content.append("}\n\n") - content.append("// hashPasswordSHA512 encrypts password using SHA512\n") - content.append("func hashPasswordSHA512(password string) string {\n") - content.append("\thash := sha512.Sum512([]byte(password))\n") - content.append("\treturn hex.EncodeToString(hash[:])\n") - content.append("}\n\n") - content.append("func (cli *ZSClient) baseURL() string {\n") - content.append("\treturn fmt.Sprintf(\"http://%s:%d%s\", cli.config.hostname, cli.config.port, cli.config.contextPath)\n") - content.append("}\n\n") - content.append("// Get performs a GET request\n") - content.append("func (cli *ZSClient) Get(path string, uuid string, params interface{}, result interface{}) error {\n") - content.append("\turl := fmt.Sprintf(\"%s/%s\", cli.baseURL(), path)\n") - content.append("\tif uuid != \"\" {\n") - content.append("\t\turl = fmt.Sprintf(\"%s/%s\", url, uuid)\n") - content.append("\t}\n") - content.append("\treturn cli.doRequest(\"GET\", url, nil, result)\n") - content.append("}\n\n") - content.append("func (cli *ZSClient) QueryJob(uuid string) (*JobView, error) {\n") - content.append("\tvar resp JobView\n") - content.append("\turl := fmt.Sprintf(\"%s/v1/api-jobs/%s\", cli.baseURL(), uuid)\n") - content.append("\terr := cli.doRequest(\"GET\", url, nil, &resp)\n") - content.append("\treturn &resp, err\n") - content.append("}\n\n") - content.append("// List performs a list query\n") - content.append("func (cli *ZSClient) List(path string, params interface{}, result interface{}) error {\n") - content.append("\tbaseURL := cli.baseURL()\n") - content.append("\trequestURL := fmt.Sprintf(\"%s/%s\", baseURL, path)\n") - content.append("\n") - content.append("\tif params != nil {\n") - content.append("\t\tif queryParam, ok := params.(*param.QueryParam); ok {\n") - content.append("\t\t\tqueryString := cli.buildQueryString(queryParam)\n") - content.append("\t\t\tif queryString != \"\" {\n") - content.append("\t\t\t\trequestURL = fmt.Sprintf(\"%s?%s\", requestURL, queryString)\n") - content.append("\t\t\t}\n") - content.append("\t\t}\n") - content.append("\t}\n") - content.append("\n") - content.append("\t// Unmarshal response into wrapper with inventories field\n") - content.append("\tvar wrapper struct {\n") - content.append("\t\tInventories interface{} `json:\"inventories\"`\n") - content.append("\t\tInventory interface{} `json:\"inventory\"`\n") - content.append("\t}\n") - content.append("\n") - content.append("\tif err := cli.doRequest(\"GET\", requestURL, nil, &wrapper); err != nil {\n") - content.append("\t\treturn err\n") - content.append("\t}\n") - content.append("\n") - content.append("\t// Try inventories first (plural), then inventory (singular)\n") - content.append("\tvar data interface{}\n") - content.append("\tif wrapper.Inventories != nil {\n") - content.append("\t\tdata = wrapper.Inventories\n") - content.append("\t} else if wrapper.Inventory != nil {\n") - content.append("\t\tdata = wrapper.Inventory\n") - content.append("\t}\n") - content.append("\n") - content.append("\t// Re-marshal and unmarshal into the actual result type\n") - content.append("\tif data != nil {\n") - content.append("\t\tdataBytes, err := json.Marshal(data)\n") - content.append("\t\tif err != nil {\n") - content.append("\t\t\treturn fmt.Errorf(\"failed to marshal data: %v\", err)\n") - content.append("\t\t}\n") - content.append("\t\tif cli.config.debug {\n") - content.append("\t\t\tfmt.Printf(\"[DEBUG] Received %d bytes of inventory data\\n\", len(dataBytes))\n") - content.append("\t\t}\n") - content.append("\t\terr = json.Unmarshal(dataBytes, result)\n") - content.append("\t\tif err != nil {\n") - content.append("\t\t\treturn fmt.Errorf(\"failed to unmarshal data into result: %v\", err)\n") - content.append("\t\t}\n") - content.append("\t\treturn nil\n") - content.append("\t}\n") - content.append("\tif cli.config.debug {\n") - content.append("\t\tfmt.Println(\"[DEBUG] Both inventories and inventory are nil, returning empty result\")\n") - content.append("\t}\n") - content.append("\treturn nil\n") - content.append("}\n\n") - content.append("// Post performs a POST request\n") - content.append("func (cli *ZSClient) Post(path string, params interface{}, result interface{}) error {\n") - content.append("\turl := fmt.Sprintf(\"%s/%s\", cli.baseURL(), path)\n") - content.append("\treturn cli.doRequest(\"POST\", url, params, result)\n") - content.append("}\n\n") - content.append("// Put performs a PUT request\n") - content.append("func (cli *ZSClient) Put(path string, uuid string, params interface{}, result interface{}) error {\n") - content.append("\turl := fmt.Sprintf(\"%s/%s/%s\", cli.baseURL(), path, uuid)\n") - content.append("\treturn cli.doRequest(\"PUT\", url, params, result)\n") - content.append("}\n\n") - content.append("// Delete performs a DELETE request\n") - content.append("func (cli *ZSClient) Delete(path string, uuid string, deleteMode string) error {\n") - content.append("\turl := fmt.Sprintf(\"%s/%s/%s?deleteMode=%s\", cli.baseURL(), path, uuid, deleteMode)\n") - content.append("\treturn cli.doRequest(\"DELETE\", url, nil, nil)\n") - content.append("}\n\n") - content.append("func (cli *ZSClient) doRequest(method, url string, body interface{}, result interface{}) error {\n") - content.append("\t// Auto-login if using login auth and no session yet\n") - content.append("\tif cli.config.authType == AuthTypeLogin && cli.sessionId == \"\" && !strings.HasSuffix(url, \"/accounts/login\") {\n") - content.append("\t\terr := cli.Login(cli.config.username, cli.config.password)\n") - content.append("\t\tif err != nil {\n") - content.append("\t\t\treturn fmt.Errorf(\"auto-login failed: %v\", err)\n") - content.append("\t\t}\n") - content.append("\t}\n\n") - content.append("\tvar bodyReader io.Reader\n") - content.append("\tvar bodyBytes []byte\n") - content.append("\tif body != nil {\n") - content.append("\t\tvar err error\n") - content.append("\t\tbodyBytes, err = json.Marshal(body)\n") - content.append("\t\tif err != nil {\n") - content.append("\t\t\treturn err\n") - content.append("\t\t}\n") - content.append("\t\tbodyReader = bytes.NewBuffer(bodyBytes)\n") - content.append("\t}\n\n") - content.append("\treq, err := http.NewRequest(method, url, bodyReader)\n") - content.append("\tif err != nil {\n") - content.append("\t\treturn err\n") - content.append("\t}\n\n") - content.append("\treq.Header.Set(\"Content-Type\", \"application/json\")\n") - content.append("\tcli.addAuthHeaders(req)\n\n") - content.append("\tif cli.config.debug && bodyBytes != nil {\n") - content.append("\t\tfmt.Printf(\"[DEBUG] %s %s\\n\", method, url)\n") - content.append("\t\tfmt.Printf(\"[DEBUG] Body: %s\\n\", string(bodyBytes))\n") - content.append("\t\tfmt.Printf(\"[DEBUG] Headers: Authorization=%s\\n\", req.Header.Get(\"Authorization\"))\n") - content.append("\t}\n\n") - content.append("\tresp, err := cli.httpClient.Do(req)\n") - content.append("\tif err != nil {\n") - content.append("\t\treturn err\n") - content.append("\t}\n") - content.append("\tdefer resp.Body.Close()\n\n") - content.append("\tif resp.StatusCode == 202 {\n") - content.append("\t\tvar location struct {\n") - content.append("\t\t\tLocation string `json:\"location\"`\n") - content.append("\t\t\tUuid string `json:\"org.zstack.header.rest.APIEvent/uuid\"`\n") - content.append("\t\t}\n") - content.append("\t\tif err := json.NewDecoder(resp.Body).Decode(&location); err != nil {\n") - content.append("\t\t\treturn fmt.Errorf(\"failed to decode 202 response: %v\", err)\n") - content.append("\t\t}\n") - content.append("\t\tjobUUID := location.Uuid\n") - content.append("\t\tif jobUUID == \"\" {\n") - content.append("\t\t\tparts := bytes.Split([]byte(location.Location), []byte(\"/\"))\n") - content.append("\t\t\tif len(parts) > 0 {\n") - content.append("\t\t\t\tjobUUID = string(parts[len(parts)-1])\n") - content.append("\t\t\t}\n") - content.append("\t\t}\n") - content.append("\n") - content.append("\t\tif jobUUID == \"\" {\n") - content.append("\t\t\treturn fmt.Errorf(\"failed to extract job uuid from 202 response\")\n") - content.append("\t\t}\n") - content.append("\n") - content.append("\t\treturn cli.waitForJob(jobUUID, result)\n") - content.append("\t}\n\n") - content.append("\tif resp.StatusCode >= 400 {\n") - content.append("\t\trespBody, _ := io.ReadAll(resp.Body)\n") - content.append("\t\terrMsg := fmt.Sprintf(\"API error: %s %s returned status code %d\\n\", method, url, resp.StatusCode)\n") - content.append("\t\terrMsg += fmt.Sprintf(\"Authorization: %s\\n\", req.Header.Get(\"Authorization\"))\n") - content.append("\t\terrMsg += fmt.Sprintf(\"Response: %s\", string(respBody))\n") - content.append("\t\treturn fmt.Errorf(errMsg)\n") - content.append("\t}\n\n") - content.append("\tif result != nil {\n") - content.append("\t\treturn json.NewDecoder(resp.Body).Decode(result)\n") - content.append("\t}\n") - content.append("\treturn nil\n") - content.append("}\n\n") - content.append("func (cli *ZSClient) waitForJob(jobUUID string, result interface{}) error {\n") - content.append("\tticker := time.NewTicker(500 * time.Millisecond)\n") - content.append("\tdefer ticker.Stop()\n") - content.append("\n") - content.append("\ttimeout := time.After(30 * time.Minute)\n") - content.append("\n") - content.append("\tfor {\n") - content.append("\t\tselect {\n") - content.append("\t\tcase <-timeout:\n") - content.append("\t\t\treturn fmt.Errorf(\"job %s timeout\", jobUUID)\n") - content.append("\t\tcase <-ticker.C:\n") - content.append("\t\t\tjob, err := cli.QueryJob(jobUUID)\n") - content.append("\t\t\tif err != nil {\n") - content.append("\t\t\t\tcontinue\n") - content.append("\t\t\t}\n") - content.append("\n") - content.append("\t\t\tif job.State == JobStateSucceeded {\n") - content.append("\t\t\t\tif result != nil && job.Result != nil {\n") - content.append("\t\t\t\t\tdata, err := json.Marshal(job.Result)\n") - content.append("\t\t\t\t\tif err != nil {\n") - content.append("\t\t\t\t\t\treturn fmt.Errorf(\"failed to marshal job result: %v\", err)\n") - content.append("\t\t\t\t\t}\n") - content.append("\t\t\t\t\treturn json.Unmarshal(data, result)\n") - content.append("\t\t\t\t}\n") - content.append("\t\t\t\treturn nil\n") - content.append("\t\t\t}\n") - content.append("\n") - content.append("\t\t\tif job.State == JobStateFailed {\n") - content.append("\t\t\t\treturn fmt.Errorf(\"job failed: %v\", job.Error)\n") - content.append("\t\t\t}\n") - content.append("\t\t}\n") - content.append("\t}\n") - content.append("}\n\n") - content.append("func (cli *ZSClient) buildQueryString(params *param.QueryParam) string {\n") - content.append("\tif params == nil {\n") - content.append("\t\treturn \"\"\n") - content.append("\t}\n") - content.append("\tu := url.Values{}\n") - content.append("\n") - content.append("\tfor _, q := range params.Conditions {\n") - content.append("\t\tif q.Name != \"\" && q.Op != \"\" {\n") - content.append("\t\t\tu.Add(\"q\", fmt.Sprintf(\"%s%s%s\", q.Name, q.Op, q.Value))\n") - content.append("\t\t} else if q.Value != \"\" {\n") - content.append("\t\t\tu.Add(\"q\", q.Value)\n") - content.append("\t\t}\n") - content.append("\t}\n") - content.append("\n") - content.append("\tif params.LimitNum != nil {\n") - content.append("\t\tu.Set(\"limit\", strconv.Itoa(*params.LimitNum))\n") - content.append("\t}\n") - content.append("\tif params.StartNum != nil {\n") - content.append("\t\tu.Set(\"start\", strconv.Itoa(*params.StartNum))\n") - content.append("\t}\n") - content.append("\tif params.Count {\n") - content.append("\t\tu.Set(\"count\", \"true\")\n") - content.append("\t}\n") - content.append("\tif params.ReplyWithCount {\n") - content.append("\t\tu.Set(\"replyWithCount\", \"true\")\n") - content.append("\t}\n") - content.append("\tif params.GroupBy != \"\" {\n") - content.append("\t\tu.Set(\"groupBy\", params.GroupBy)\n") - content.append("\t}\n") - content.append("\tif params.SortBy != \"\" {\n") - content.append("\t\tu.Set(\"sortBy\", params.SortBy)\n") - content.append("\t}\n") - content.append("\tif params.SortDirection != \"\" {\n") - content.append("\t\tu.Set(\"sortDirection\", params.SortDirection)\n") - content.append("\t}\n") - content.append("\tfor _, f := range params.Fields {\n") - content.append("\t\tu.Add(\"fields\", f)\n") - content.append("\t}\n") - content.append("\n") - content.append("\treturn u.Encode()\n") - content.append("}\n\n") - content.append("func (cli *ZSClient) addAuthHeaders(req *http.Request) {\n") - content.append("\tif cli.config.authType == AuthTypeAccessKey {\n") - content.append("\t\treq.Header.Set(\"X-Access-Key-Id\", cli.config.accessKeyId)\n") - content.append("\t\treq.Header.Set(\"X-Access-Key-Secret\", cli.config.accessKeySecret)\n") - content.append("\t} else if cli.sessionId != \"\" {\n") - content.append("\t\treq.Header.Set(\"Authorization\", \"OAuth \"+cli.sessionId)\n") - content.append("\t}\n") - content.append("}\n\n") - content.append("// Login authenticates with username and password\n") - content.append("func (cli *ZSClient) Login(username, password string) error {\n") - content.append("\tif cli.config.authType != AuthTypeLogin {\n") - content.append("\t\treturn fmt.Errorf(\"client is not configured for login authentication\")\n") - content.append("\t}\n\n") - content.append("\tvar loginReq = map[string]map[string]string{\n") - content.append("\t\t\"logInByAccount\": {\n") - content.append("\t\t\t\"accountName\": username,\n") - content.append("\t\t\t\"password\": password, // Already hashed in NewZSClient\n") - content.append("\t\t},\n") - content.append("\t}\n\n") - content.append("\tvar loginResp struct {\n") - content.append("\t\tInventory struct {\n") - content.append("\t\t\tUUID string `json:\"uuid\"`\n") - content.append("\t\t} `json:\"inventory\"`\n") - content.append("\t}\n\n") - content.append("\turl := fmt.Sprintf(\"%s/v1/accounts/login\", cli.baseURL())\n") - content.append("\terr := cli.doRequest(\"PUT\", url, loginReq, &loginResp)\n") - content.append("\tif err != nil {\n") - content.append("\t\treturn fmt.Errorf(\"login failed: %v\", err)\n") - content.append("\t}\n\n") - content.append("\tcli.sessionId = loginResp.Inventory.UUID\n") - content.append("\tif cli.config.debug {\n") - content.append("\t\tfmt.Printf(\"[DEBUG] Login successful, sessionId=%s\\n\", cli.sessionId)\n") - content.append("\t}\n") - content.append("\treturn nil\n") - content.append("}\n\n") - content.append("func (cli *ZSClient) Logout() error {\n") - content.append("\tif cli.sessionId == \"\" {\n") - content.append("\t\treturn nil\n") - content.append("\t}\n\n") - content.append("\turl := fmt.Sprintf(\"%s/v1/accounts/sessions/%s\", cli.baseURL(), cli.sessionId)\n") - content.append("\terr := cli.doRequest(\"DELETE\", url, nil, nil)\n") - content.append("\tcli.sessionId = \"\"\n") - content.append("\treturn err\n") - content.append("}\n") - - sdkFile.content = content.toString() - return sdkFile - } /** * Generate base view file @@ -1760,7 +1376,7 @@ class GoInventory implements SdkTemplate { content.append("// Copyright (c) ZStack.io, Inc.\n\n") content.append("package view\n\n") content.append("import \"time\"\n\n") - content.append("var _ = time.Now // avoid unused import\n\n") + content.append("var _ = time.Now() // avoid unused import\n\n") int addedCount = 0 Set processedViews = new HashSet<>() @@ -1912,7 +1528,7 @@ class GoInventory implements SdkTemplate { content.append("// Copyright (c) ZStack.io, Inc.\n\n") content.append("package view\n\n") content.append("import \"time\"\n\n") - content.append("var _ = time.Now // avoid unused import\n\n") + content.append("var _ = time.Now() // avoid unused import\n\n") logger.warn("[GoSDK] Generating new view file for: ${structName} (${fileName})") } @@ -2086,7 +1702,7 @@ class GoInventory implements SdkTemplate { content.append("// Copyright (c) ZStack.io, Inc.\n\n") content.append("package param\n\n") content.append("import \"time\"\n\n") - content.append("var _ = time.Now // avoid unused import\n\n") + content.append("var _ = time.Now() // avoid unused import\n\n") boolean hasParams = false allApiTemplates.each { GoApiTemplate template -> diff --git a/rest/src/main/resources/scripts/templates/base_param_types.go.template b/rest/src/main/resources/scripts/templates/base_param_types.go.template index 962ccda910b..1c3e8ec9ffa 100644 --- a/rest/src/main/resources/scripts/templates/base_param_types.go.template +++ b/rest/src/main/resources/scripts/templates/base_param_types.go.template @@ -4,7 +4,7 @@ package param import "time" -var _ = time.Now // avoid unused import +var _ = time.Now() // avoid unused import type DeleteMode string From 326ca6b6d74e140c00aa1bf19c29f4d8d465460e Mon Sep 17 00:00:00 2001 From: "ye.zou" Date: Fri, 27 Mar 2026 14:29:31 +0800 Subject: [PATCH 2/2] [rest]: GoInventory.reset() must clear all instance caches The previous reset() only cleared longJobMappings (static), leaving instance-level caches (allApiTemplates, generatedViewStructs, generatedParamStructs, generatedClientMethods, additionalClasses, etc.) intact across repeated generate() calls on the same instance. This caused skipped or duplicate output when TestGenerateGoSDK ran more than once in the same JVM. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/resources/scripts/GoInventory.groovy | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/rest/src/main/resources/scripts/GoInventory.groovy b/rest/src/main/resources/scripts/GoInventory.groovy index 335260e671d..b38ae30a412 100644 --- a/rest/src/main/resources/scripts/GoInventory.groovy +++ b/rest/src/main/resources/scripts/GoInventory.groovy @@ -107,11 +107,24 @@ class GoInventory implements SdkTemplate { } /** - * Reset all static state for clean re-generation. + * Reset all static and instance state for clean re-generation. + * Must be called before each generate() to avoid stale caches + * causing skipped or duplicate output across repeated runs. */ - static void reset() { + void reset() { longJobMappings.clear() - logger.warn("[GoSDK] Reset GoInventory static state") + allApiTemplates.clear() + inventories.clear() + markedInventories.clear() + additionalClasses.clear() + generatedViewStructs.clear() + generatedViewFiles.clear() + paramNestedTypes.clear() + generatedParamStructs.clear() + generatedClientMethods.clear() + generatingForParam = false + currentGeneratingClass = null + logger.warn("[GoSDK] Reset GoInventory state (static + instance)") } /**