2023-02-28 15:50:35 +01:00
|
|
|
package spec
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"os"
|
|
|
|
|
|
|
|
"forge.cadoles.com/Cadoles/emissary/internal/client"
|
|
|
|
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/agent/flag"
|
|
|
|
"forge.cadoles.com/Cadoles/emissary/internal/command/client/apierr"
|
|
|
|
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/client/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"
|
2023-03-03 20:37:09 +01:00
|
|
|
|
|
|
|
// Import specs
|
|
|
|
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
|
|
|
|
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/gateway"
|
|
|
|
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
|
2023-02-28 15:50:35 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
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 spec name",
|
|
|
|
},
|
|
|
|
&cli.StringFlag{
|
|
|
|
Name: "spec-data",
|
2023-03-02 13:05:24 +01:00
|
|
|
Usage: "use `DATA` as spec data, '-' to read from STDIN",
|
2023-02-28 15:50:35 +01:00
|
|
|
},
|
|
|
|
&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")
|
|
|
|
|
|
|
|
client := client.New(baseFlags.ServerURL)
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-03-02 13:05:24 +01:00
|
|
|
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)
|
2023-02-28 15:50:35 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-03-02 13:05:24 +01:00
|
|
|
func assertSpecData(ctx *cli.Context) (map[string]any, error) {
|
2023-02-28 15:50:35 +01:00
|
|
|
rawSpecData := ctx.String("spec-data")
|
|
|
|
|
|
|
|
if rawSpecData == "" {
|
|
|
|
return nil, errors.New("flag 'spec-data' is required")
|
|
|
|
}
|
|
|
|
|
2023-03-02 13:05:24 +01:00
|
|
|
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)
|
|
|
|
}
|
2023-02-28 15:50:35 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return specData, nil
|
|
|
|
}
|
|
|
|
|
2023-03-02 13:05:24 +01:00
|
|
|
func applyPatch(origin any, patch any) (map[string]any, error) {
|
2023-02-28 15:50:35 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-03-02 13:05:24 +01:00
|
|
|
var specData map[string]any
|
2023-02-28 15:50:35 +01:00
|
|
|
|
|
|
|
if err := json.Unmarshal(result, &specData); err != nil {
|
|
|
|
}
|
|
|
|
|
|
|
|
return specData, nil
|
|
|
|
}
|