From 4c57108d3833a6a4812921a008393b2524e11734 Mon Sep 17 00:00:00 2001 From: naman-nirwan Date: Thu, 16 Jul 2026 20:24:26 +0530 Subject: [PATCH 1/2] Fixes for Getting started CD page, yaml parsing according to different wrappings --- pkg/registry/endpoint.go | 120 +++++++++++++++++++++++++++++++++--- pkg/spec/cd.spec.yaml | 18 ++++-- pkg/spec/platform.spec.yaml | 4 +- pkg/spec/spec.go | 9 +++ 4 files changed, 136 insertions(+), 15 deletions(-) diff --git a/pkg/registry/endpoint.go b/pkg/registry/endpoint.go index 50de15d..776118f 100644 --- a/pkg/registry/endpoint.go +++ b/pkg/registry/endpoint.go @@ -76,11 +76,42 @@ func callEndpointFull(ctx *cmdctx.Ctx, ep *spec.EndpointSpec, extraQueryParams m // Priority 1: file body — wins over strategies when -f is provided. if ep.FileBody != spec.FileBodyNone && cmdctx.GetString(ctx.FlagValues, "file") != "" { - body, err := cmdctx.SlurpInputFile(ctx.FlagValues) + rawBody, err := cmdctx.SlurpInputFile(ctx.FlagValues) if err != nil { return nil, nil, err } - body, ct, err := cmdctx.NormalizeFileBody(body, resolveFileBodyContentType(ep, method), cmdctx.GetString(ctx.FlagValues, "file")) + + // CD-style DTO envelope: the -f file may be given as the raw resource YAML + // (wrapped or flat) rather than the API's {identifier, ..., yaml: "..."} shape. + // Build the envelope here instead of going through the generic normalize/wrap path. + if ep.FileBodyYamlEnvelope != "" { + env, err := buildYamlEnvelope(ctx, ep.FileBodyYamlEnvelope, rawBody) + if err != nil { + return nil, nil, err + } + qp := evalQueryParams(ctx, ep.QueryParams, true, extraQueryParams) + if err := runEndpointValidators(ctx, ep, cmdctx.EndpointRequest{ + Method: method, + Path: path, + QueryParams: qp, + Body: env, + ContentType: "application/json", + }); err != nil { + return nil, nil, err + } + if method == "PUT" || method == "PATCH" { + return c.DoRequest(client.Request{ + Method: method, + Path: path, + QueryParams: qp, + Body: env, + BodyContentType: "application/json", + }) + } + return c.Post(path, qp, env) + } + + body, ct, err := cmdctx.NormalizeFileBody(rawBody, resolveFileBodyContentType(ep, method), cmdctx.GetString(ctx.FlagValues, "file")) if err != nil { return nil, nil, err } @@ -113,7 +144,7 @@ func callEndpointFull(ctx *cmdctx.Ctx, ep *spec.EndpointSpec, extraQueryParams m Method: method, Path: path, QueryParams: qp, - Body: map[string]any{ep.UpdateBodyWrap: parsed}, + Body: map[string]any{ep.UpdateBodyWrap: unwrapIfAlreadyWrapped(parsed, ep.UpdateBodyWrap)}, BodyContentType: ct, }) } @@ -133,20 +164,26 @@ func callEndpointFull(ctx *cmdctx.Ctx, ep *spec.EndpointSpec, extraQueryParams m if err := json.Unmarshal([]byte(body), &parsed); err != nil { return nil, nil, fmt.Errorf("parsing -f body: %w", err) } + inner := parsed + if ep.CreateBodyWrap != "" { + if wrapped, ok := parsed[ep.CreateBodyWrap].(map[string]any); ok { + inner = wrapped + } + } if len(ep.CreateBodyInit) > 0 { exprEnv := exprenv.Make(ctx) for dotPath, exprStr := range ep.CreateBodyInit { - if _, exists := parsed[dotPath]; !exists { + if _, exists := inner[dotPath]; !exists { if result, ok := exprenv.EvalExprAny(exprEnv, exprStr); ok && result != nil { - setDotPath(parsed, dotPath, result) + setDotPath(inner, dotPath, result) } } } } if ep.CreateBodyWrap != "" { - return c.Post(path, qp, map[string]any{ep.CreateBodyWrap: parsed}) + return c.Post(path, qp, map[string]any{ep.CreateBodyWrap: inner}) } - return c.Post(path, qp, parsed) + return c.Post(path, qp, inner) } return c.PostRaw(path, qp, body, ct) } @@ -561,6 +598,75 @@ func setDotPath(m map[string]any, path string, val any) { setDotPath(child, parts[1], val) } +// unwrapIfAlreadyWrapped guards against double-wrapping when the -f file is a full +// Harness Studio YAML export that already has the wrapper key at the top level +func unwrapIfAlreadyWrapped(parsed any, wrapperKey string) any { + m, ok := parsed.(map[string]any) + if !ok { + return parsed + } + if wrapped, ok := m[wrapperKey].(map[string]any); ok { + return wrapped + } + return parsed +} + +// envelopeIdentityFields lists the keys lifted from the inner resource object to the +// top level of a CD-style DTO envelope (see buildYamlEnvelope). +var envelopeIdentityFields = []string{ + "identifier", "name", "orgIdentifier", "projectIdentifier", + "description", "tags", "type", "environmentRef", +} + +// buildYamlEnvelope wraps a raw -f YAML file into a Harness CD-style DTO envelope: +// {identifier, name, orgIdentifier, projectIdentifier, ..., yaml: ""}. +func buildYamlEnvelope(ctx *cmdctx.Ctx, wrapperKey, rawBody string) (map[string]any, error) { + var parsed map[string]any + if err := yaml.Unmarshal([]byte(rawBody), &parsed); err != nil { + return nil, fmt.Errorf("parsing -f YAML: %w", err) + } + + if _, ok := parsed["yaml"].(string); ok { + seedEnvelopeScope(parsed, ctx) + return parsed, nil + } + + inner := parsed + yamlStr := rawBody + if wrapped, ok := parsed[wrapperKey].(map[string]any); ok { + inner = wrapped + } else { + b, err := yaml.Marshal(map[string]any{wrapperKey: parsed}) + if err != nil { + return nil, fmt.Errorf("re-wrapping -f YAML under %q: %w", wrapperKey, err) + } + yamlStr = string(b) + } + + env := map[string]any{"yaml": yamlStr} + for _, k := range envelopeIdentityFields { + if v, ok := inner[k]; ok { + env[k] = v + } + } + seedEnvelopeScope(env, ctx) + return env, nil +} + +// seedEnvelopeScope defaults orgIdentifier/projectIdentifier on env from ctx.Auth +// when the -f file did not already specify them. +func seedEnvelopeScope(env map[string]any, ctx *cmdctx.Ctx) { + if ctx.Auth == nil { + return + } + if _, ok := env["orgIdentifier"]; !ok && ctx.Auth.OrgID != "" { + env["orgIdentifier"] = ctx.Auth.OrgID + } + if _, ok := env["projectIdentifier"]; !ok && ctx.Auth.ProjectID != "" { + env["projectIdentifier"] = ctx.Auth.ProjectID + } +} + // resolveFileBodyContentType returns the format to validate/normalize the -f file // against: ep.FileBodyContentType if set, otherwise resolveContentType(ep, method). func resolveFileBodyContentType(ep *spec.EndpointSpec, method string) string { diff --git a/pkg/spec/cd.spec.yaml b/pkg/spec/cd.spec.yaml index fd5b5e7..03be461 100644 --- a/pkg/spec/cd.spec.yaml +++ b/pkg/spec/cd.spec.yaml @@ -143,11 +143,12 @@ commands: - command: create service verb: create noun: service - short: "Create a service: harness create service -f service.json" + short: "Create a service: harness create service -f service.yaml" handler_type: endpoint endpoint: method: POST file_body: required + file_body_yaml_envelope: service path: /ng/api/servicesV2 item_expr: it.data text_header: "\nCreated service {{it.service.identifier}}\n" @@ -155,7 +156,7 @@ commands: - command: update service verb: update noun: service - short: Update a service (name, description, tags) + short: Update a service (name, description, tags, or full YAML via -f) handler_type: endpoint flags_builtin: set: true @@ -163,6 +164,7 @@ commands: endpoint: method: PUT file_body: optional + file_body_yaml_envelope: service path: /ng/api/servicesV2 get_path: /ng/api/servicesV2/{{ctx.id}} update_strategy: get-then-put @@ -230,11 +232,12 @@ commands: - command: create environment verb: create noun: environment - short: "Create an environment: harness create environment -f environment.json" + short: "Create an environment: harness create environment -f environment.yaml" handler_type: endpoint endpoint: method: POST file_body: required + file_body_yaml_envelope: environment path: /ng/api/environmentsV2 item_expr: it.data text_header: "\nCreated environment {{it.environment.identifier}}\n" @@ -242,7 +245,7 @@ commands: - command: update environment verb: update noun: environment - short: Update an environment (name, type, description, tags) + short: Update an environment (name, type, description, tags, or full YAML via -f) handler_type: endpoint flags_builtin: set: true @@ -250,6 +253,7 @@ commands: endpoint: method: PUT file_body: optional + file_body_yaml_envelope: environment path: /ng/api/environmentsV2 get_path: /ng/api/environmentsV2/{{ctx.id}} update_strategy: get-then-put @@ -326,11 +330,12 @@ commands: - command: create infrastructure verb: create noun: infrastructure - short: "Create an infrastructure: harness create infrastructure -f infrastructure.json" + short: "Create an infrastructure: harness create infrastructure -f infrastructure.yaml" handler_type: endpoint endpoint: method: POST file_body: required + file_body_yaml_envelope: infrastructureDefinition path: /ng/api/infrastructures item_expr: it.data text_header: "\nCreated infrastructure {{it.infrastructure.identifier}}\n" @@ -338,7 +343,7 @@ commands: - command: update infrastructure verb: update noun: infrastructure - short: Update an infrastructure definition + short: Update an infrastructure definition (name, description, tags, or full YAML via -f) handler_type: endpoint flags_builtin: set: true @@ -346,6 +351,7 @@ commands: endpoint: method: PUT file_body: optional + file_body_yaml_envelope: infrastructureDefinition path: /ng/api/infrastructures get_path: /ng/api/infrastructures/{{ctx.id}} update_strategy: get-then-put diff --git a/pkg/spec/platform.spec.yaml b/pkg/spec/platform.spec.yaml index b42d752..26663c0 100644 --- a/pkg/spec/platform.spec.yaml +++ b/pkg/spec/platform.spec.yaml @@ -1334,8 +1334,8 @@ commands: endpoint: method: POST path: /ng/api/delegate-token-ng - body_params: - name: ctx.id + query_params: + tokenName: ctx.id item_expr: it.resource text_header: "\nCreated delegate token {{it.name}}\n" diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index ce3bfc2..4f82fe9 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -335,6 +335,15 @@ type EndpointSpec struct { // FileBodyContentType. Used by APIs that expect { "": "" } (e.g. the v1 template API's template_yaml envelope). FileBodyWrapAsString string `yaml:"file_body_wrap_as_string,omitempty"` + // FileBodyYamlEnvelope wraps the -f file into a Harness CD-style DTO envelope: + // lifts identity fields (identifier/name/orgIdentifier/projectIdentifier/type/ + // environmentRef) to the top level and embeds the full resource YAML under a + // "yaml" string field. The value is the inner wrapper key (e.g. "service", + // "environment", "infrastructureDefinition") used to detect/re-wrap the resource + // when the -f file is given in unwrapped or flat form. If the -f file already + // has a top-level "yaml" string field, it is passed through as-is (only missing + // org/project scope is defaulted). + FileBodyYamlEnvelope string `yaml:"file_body_yaml_envelope,omitempty"` // TextFormatter names a registered TextFormatterFn used when --format text. TextFormatter string `yaml:"text_formatter,omitempty"` // TextHeader and TextFooter are optional {{expr}}-interpolated strings printed From e6f67b4a56968545bb81c86291fceeedd3dfe0f9 Mon Sep 17 00:00:00 2001 From: naman-nirwan Date: Fri, 17 Jul 2026 12:48:30 +0530 Subject: [PATCH 2/2] fix: hardcoded application/json replaced by resolveContentType --- pkg/registry/endpoint.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pkg/registry/endpoint.go b/pkg/registry/endpoint.go index 776118f..d8ed012 100644 --- a/pkg/registry/endpoint.go +++ b/pkg/registry/endpoint.go @@ -90,25 +90,23 @@ func callEndpointFull(ctx *cmdctx.Ctx, ep *spec.EndpointSpec, extraQueryParams m return nil, nil, err } qp := evalQueryParams(ctx, ep.QueryParams, true, extraQueryParams) + ct := resolveContentType(ep, method) if err := runEndpointValidators(ctx, ep, cmdctx.EndpointRequest{ Method: method, Path: path, QueryParams: qp, Body: env, - ContentType: "application/json", + ContentType: ct, }); err != nil { return nil, nil, err } - if method == "PUT" || method == "PATCH" { - return c.DoRequest(client.Request{ - Method: method, - Path: path, - QueryParams: qp, - Body: env, - BodyContentType: "application/json", - }) - } - return c.Post(path, qp, env) + return c.DoRequest(client.Request{ + Method: method, + Path: path, + QueryParams: qp, + Body: env, + BodyContentType: ct, + }) } body, ct, err := cmdctx.NormalizeFileBody(rawBody, resolveFileBodyContentType(ep, method), cmdctx.GetString(ctx.FlagValues, "file"))