Files
server-deploy/internal/cli/run.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
}