package spec import ( "encoding/json" "os" "forge.cadoles.com/Cadoles/emissary/internal/client" agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag" "forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr" clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag" "forge.cadoles.com/Cadoles/emissary/internal/format" "forge.cadoles.com/Cadoles/emissary/internal/spec" jsonpatch "github.com/evanphx/json-patch/v5" "github.com/pkg/errors" "github.com/urfave/cli/v2" // Import specs _ "forge.cadoles.com/Cadoles/emissary/internal/spec/app" _ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy" _ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci" ) func UpdateCommand() *cli.Command { return &cli.Command{ Name: "update", Usage: "Update agent specification", Flags: agentFlag.WithAgentFlags( &cli.StringFlag{ Name: "spec-name", Usage: "use `NAME` as specification's name", }, &cli.StringFlag{ Name: "spec-data", Usage: "use `DATA` as specification's data, '-' to read from STDIN", }, &cli.BoolFlag{ Name: "no-patch", Usage: "Dont use spec-data as a patch to existing specification", }, &cli.IntFlag{ Name: "revision", Usage: "Use `REVISION` as specification revision number", }, ), Action: func(ctx *cli.Context) error { baseFlags := clientFlag.GetBaseFlags(ctx) agentID, err := agentFlag.AssertAgentID(ctx) if err != nil { return errors.WithStack(err) } specName, err := assertSpecName(ctx) if err != nil { return errors.WithStack(err) } specData, err := assertSpecData(ctx) if err != nil { return errors.WithStack(err) } noPatch := ctx.Bool("no-patch") token, err := clientFlag.GetToken(baseFlags) if err != nil { return errors.WithStack(apierr.Wrap(err)) } client := client.New(baseFlags.ServerURL, client.WithToken(token)) specs, err := client.GetAgentSpecs(ctx.Context, agentID) if err != nil { return errors.WithStack(apierr.Wrap(err)) } var existingSpec spec.Spec for _, s := range specs { if s.SpecName() != specName { continue } existingSpec = s break } revision := 0 if existingSpec != nil { originSpecData := existingSpec.SpecData() if !noPatch { specData, err = applyPatch(originSpecData, specData) if err != nil { return errors.WithStack(err) } } revision = existingSpec.SpecRevision() } if specificRevision := ctx.Int("revision"); specificRevision != 0 { revision = specificRevision } rawSpec := &spec.RawSpec{ Name: specName, Revision: revision, Data: specData, } if err := spec.Validate(ctx.Context, rawSpec); err != nil { return errors.WithStack(apierr.Wrap(err)) } spec, err := client.UpdateAgentSpec(ctx.Context, agentID, rawSpec) if err != nil { return errors.WithStack(apierr.Wrap(err)) } hints := format.Hints{ OutputMode: baseFlags.OutputMode, } if err := format.Write(baseFlags.Format, os.Stdout, hints, spec); err != nil { return errors.WithStack(err) } return nil }, } } func assertSpecName(ctx *cli.Context) (spec.Name, error) { specName := ctx.String("spec-name") if specName == "" { return "", errors.New("flag 'spec-name' is required") } return spec.Name(specName), nil } func assertSpecData(ctx *cli.Context) (map[string]any, error) { rawSpecData := ctx.String("spec-data") if rawSpecData == "" { return nil, errors.New("flag 'spec-data' is required") } var specData map[string]any if rawSpecData == "-" { decoder := json.NewDecoder(os.Stdin) if err := decoder.Decode(&specData); err != nil { return nil, errors.WithStack(err) } } else { if err := json.Unmarshal([]byte(rawSpecData), &specData); err != nil { return nil, errors.WithStack(err) } } return specData, nil } func applyPatch(origin any, patch any) (map[string]any, error) { originJSON, err := json.Marshal(origin) if err != nil { return nil, errors.WithStack(err) } patchJSON, err := json.Marshal(patch) if err != nil { return nil, errors.WithStack(err) } result, err := jsonpatch.MergePatch(originJSON, patchJSON) if err != nil { return nil, errors.WithStack(err) } var specData map[string]any if err := json.Unmarshal(result, &specData); err != nil { } return specData, nil }