168 lines
6.5 KiB
Go
168 lines
6.5 KiB
Go
// Package cli exposes read-only protocol endpoints, not a shell wrapper.
|
|
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"server-deploy/internal/appbundle"
|
|
"server-deploy/internal/aptrepo"
|
|
"server-deploy/internal/composepolicy"
|
|
"server-deploy/internal/inspect"
|
|
"server-deploy/internal/installplan"
|
|
"server-deploy/internal/planner"
|
|
"server-deploy/internal/preflight"
|
|
)
|
|
|
|
func Run(args []string, in io.Reader, out, diagnostics io.Writer, now func() time.Time) int {
|
|
fail := func(message string) int { fmt.Fprintln(diagnostics, message); return 1 }
|
|
if len(args) != 1 {
|
|
return fail("usage: deployctl version | inspect | preflight | plan-environment | verify-repository | verify-artifacts | plan | verify-plan | verify-package | check-package")
|
|
}
|
|
var response any
|
|
switch args[0] {
|
|
case "verify-artifacts":
|
|
var request struct {
|
|
Directory string `json:"directory"`
|
|
ArtifactDirectory string `json:"artifactDirectory"`
|
|
Suite string `json:"suite"`
|
|
Architecture string `json:"architecture"`
|
|
Versions map[string]string `json:"versions"`
|
|
}
|
|
if decodeStrict(in, &request) != nil {
|
|
return fail("invalid artifact verification request")
|
|
}
|
|
verified, err := aptrepo.VerifyArtifacts(request.Directory, request.ArtifactDirectory, request.Suite, request.Architecture, request.Versions, now())
|
|
if err != nil {
|
|
return fail("artifact verification failed")
|
|
}
|
|
response = verified
|
|
case "verify-repository":
|
|
var request struct {
|
|
Directory string `json:"directory"`
|
|
Suite string `json:"suite"`
|
|
Architecture string `json:"architecture"`
|
|
Versions map[string]string `json:"versions"`
|
|
}
|
|
if decodeStrict(in, &request) != nil {
|
|
return fail("invalid repository verification request")
|
|
}
|
|
verified, err := aptrepo.Verify(request.Directory, request.Suite, request.Architecture, request.Versions, now())
|
|
if err != nil {
|
|
return fail("repository verification failed")
|
|
}
|
|
response = verified
|
|
case "plan-environment":
|
|
var request struct {
|
|
Lock installplan.Lock `json:"lock"`
|
|
}
|
|
if decodeStrict(in, &request) != nil {
|
|
return fail("invalid environment lock request")
|
|
}
|
|
report := preflight.Collect()
|
|
draft, err := installplan.Build(report, request.Lock)
|
|
if err != nil {
|
|
return fail("environment lock rejected")
|
|
}
|
|
response = struct {
|
|
ProtocolVersion int `json:"protocolVersion"`
|
|
Mode string `json:"mode"`
|
|
ObservedAt time.Time `json:"observedAt"`
|
|
Report preflight.Report `json:"report"`
|
|
Draft installplan.Draft `json:"draft"`
|
|
}{1, "local-environment-draft", now().UTC().Truncate(time.Second), report, draft}
|
|
case "preflight":
|
|
report := preflight.Collect()
|
|
response = struct {
|
|
ProtocolVersion int `json:"protocolVersion"`
|
|
Mode string `json:"mode"`
|
|
ObservedAt time.Time `json:"observedAt"`
|
|
Report preflight.Report `json:"report"`
|
|
Proposal preflight.Proposal `json:"proposal"`
|
|
}{1, "local-environment-proposal", now().UTC().Truncate(time.Second), report, preflight.Plan(report)}
|
|
case "verify-package", "check-package":
|
|
var request struct {
|
|
Directory string `json:"directory"`
|
|
ExpectedDigest string `json:"expectedDigest"`
|
|
}
|
|
if err := decodeStrict(in, &request); err != nil {
|
|
return fail("invalid package verification request")
|
|
}
|
|
verified, err := appbundle.Verify(request.Directory, request.ExpectedDigest)
|
|
if err != nil {
|
|
return fail("package verification failed: invalid manifest, inventory or digest")
|
|
}
|
|
if args[0] == "check-package" {
|
|
if composepolicy.Check(verified.Manifest, verified.Files[verified.Manifest.Entrypoint]) != nil {
|
|
return fail("package rejected by restricted Compose policy")
|
|
}
|
|
response = struct {
|
|
ProtocolVersion int `json:"protocolVersion"`
|
|
PolicyPassed bool `json:"policyPassed"`
|
|
Profile string `json:"profile"`
|
|
Digest string `json:"digest"`
|
|
Executable bool `json:"executable"`
|
|
PublisherAuthenticated bool `json:"publisherAuthenticated"`
|
|
}{1, true, composepolicy.Profile, verified.Digest, false, false}
|
|
break
|
|
}
|
|
response = struct {
|
|
ProtocolVersion int `json:"protocolVersion"`
|
|
Verified bool `json:"verified"`
|
|
Executable bool `json:"executable"`
|
|
PublisherAuthenticated bool `json:"publisherAuthenticated"`
|
|
Digest string `json:"digest"`
|
|
FileCount int `json:"fileCount"`
|
|
Manifest appbundle.Manifest `json:"manifest"`
|
|
}{1, true, false, false, verified.Digest, len(verified.Files), verified.Manifest}
|
|
case "inspect":
|
|
response = inspect.Collect()
|
|
case "version":
|
|
response = struct {
|
|
Version string `json:"version"`
|
|
ProtocolVersion int `json:"protocolVersion"`
|
|
WritesEnabled bool `json:"writesEnabled"`
|
|
}{"0.1.0-dev", planner.ProtocolVersion, false}
|
|
case "plan":
|
|
var intent planner.Intent
|
|
if err := decodeStrict(in, &intent); err != nil {
|
|
return fail("invalid request: expected strict protocol JSON (maximum 1 MiB)")
|
|
}
|
|
plan, err := planner.Build(intent, now())
|
|
if err != nil {
|
|
return fail("invalid deployment intent")
|
|
}
|
|
response = struct {
|
|
ProtocolVersion int `json:"protocolVersion"`
|
|
Mode string `json:"mode"`
|
|
Executable bool `json:"executable"`
|
|
Plan planner.Plan `json:"plan"`
|
|
}{planner.ProtocolVersion, "offline-preview", false, plan}
|
|
case "verify-plan":
|
|
var request struct {
|
|
Plan planner.Plan `json:"plan"`
|
|
Current planner.Intent `json:"current"`
|
|
}
|
|
if err := decodeStrict(in, &request); err != nil {
|
|
return fail("invalid verification request")
|
|
}
|
|
if err := request.Plan.Verify(request.Current, now()); err != nil {
|
|
return fail("plan rejected: expired, changed or invalid")
|
|
}
|
|
response = struct {
|
|
ProtocolVersion int `json:"protocolVersion"`
|
|
Mode string `json:"mode"`
|
|
Valid bool `json:"valid"`
|
|
Executable bool `json:"executable"`
|
|
}{planner.ProtocolVersion, "offline-preview", true, false}
|
|
default:
|
|
return fail("unsupported command; deployment writes are not enabled")
|
|
}
|
|
if err := json.NewEncoder(out).Encode(response); err != nil {
|
|
return fail("cannot write response")
|
|
}
|
|
return 0
|
|
}
|