feat: add deployment foundation and cross-device handoff
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user