Skip to content
Merged
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
118 changes: 111 additions & 7 deletions pkg/registry/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,40 @@ 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)
ct := resolveContentType(ep, method)
if err := runEndpointValidators(ctx, ep, cmdctx.EndpointRequest{
Method: method,
Path: path,
QueryParams: qp,
Body: env,
ContentType: ct,
}); err != nil {
return nil, nil, err
}
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"))
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -113,7 +142,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,
})
}
Expand All @@ -133,20 +162,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)
}
Expand Down Expand Up @@ -561,6 +596,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: "<full resource 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 {
Expand Down
18 changes: 12 additions & 6 deletions pkg/spec/cd.spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,26 +143,28 @@ commands:
- command: create service
verb: create
noun: service
short: "Create a service: harness create service <id> -f service.json"
short: "Create a service: harness create service <id> -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"

- 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
del: true
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
Expand Down Expand Up @@ -230,26 +232,28 @@ commands:
- command: create environment
verb: create
noun: environment
short: "Create an environment: harness create environment <id> -f environment.json"
short: "Create an environment: harness create environment <id> -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"

- 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
del: true
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
Expand Down Expand Up @@ -326,26 +330,28 @@ commands:
- command: create infrastructure
verb: create
noun: infrastructure
short: "Create an infrastructure: harness create infrastructure <id> -f infrastructure.json"
short: "Create an infrastructure: harness create infrastructure <id> -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"

- 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
del: true
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
Expand Down
4 changes: 2 additions & 2 deletions pkg/spec/platform.spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
9 changes: 9 additions & 0 deletions pkg/spec/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,15 @@ type EndpointSpec struct {
// FileBodyContentType. Used by APIs that expect { "<key>": "<file contents as a
// string>" } (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
Expand Down
Loading