Compare commits

..
Author SHA1 Message Date
mrhid6 189a8fa963 feat: Move grpc pbs to shared
Chart Release / chart (push) Successful in 21s
Agent Release / build (push) Successful in 3m16s
Server Deploy / deploy (push) Successful in 5m32s
Agent Release / msi (push) Successful in 3m25s
2026-09-07 15:50:25 +00:00
mrhid6 7b13944c24 fix: Build the vantagectl image on its release tag, not on every push to main
Chart Release / chart (push) Successful in 29s
vantagectl Release / build (push) Successful in 1m28s
vantagectl Release / image (push) Successful in 1m52s
Server Deploy / deploy (push) Successful in 3m46s
vantagectl is a released tool, not a running service. An operator restoring a
database should be able to name the version they ran, and ":latest, rebuilt
whenever main moved" cannot be named after the fact.

The image now builds in vantagectl-release.yml on a vantagectl/v* tag, tagged
with that version as well as latest, with VERSION passed through so the binary
inside reports the tag rather than "dev". server-deploy.yml no longer builds
it and is back to seven images.

The cost is that a shared/ fix reaches the image only at the next release
rather than the next push to main. That is the intended trade and is written
down in CLAUDE.md next to the trigger table.
2026-09-07 15:11:39 +00:00
33 changed files with 139 additions and 636 deletions
+10 -12
View File
@@ -71,16 +71,22 @@ jobs:
fi
}
# The four Go images build from the repo root and COPY
# shared/ plus their own directory, so shared/ rebuilds all
# four. proto/ is in server's list as insurance: the
# The three Go images here build from the repo root and
# COPY shared/ plus their own directory, so shared/ rebuilds
# all three. vantagectl also depends on shared/ but is NOT
# built here: it is a released tool, so its image is built and
# version-tagged by vantagectl-release.yml on a vantagectl/v*
# tag. A shared/ change therefore reaches it at the next
# release rather than on the next push to main, which is the
# point — an operator restoring a database should be running a
# version they can name, not whatever main built last night.
# proto/ is in server's list as insurance: the
# generated pb is committed under server/, but a proto change
# that someone regenerates in the same push should not depend
# on that ordering.
flag server '^(server/|shared/|proto/|default_steps/|go\.work)'
flag sitesvc '^(sitesvc/|shared/|go\.work)'
flag admin '^(admin/|shared/|go\.work)'
flag vantagectl '^(vantagectl/|shared/|go\.work)'
# The three Next images and the docs site use their own
# directory as the build context, so nothing outside it can
@@ -160,14 +166,6 @@ jobs:
docker build -t "$IMAGE" -f admin/Dockerfile .
docker push "$IMAGE"
- name: Build and push vantagectl image
if: steps.changed.outputs.vantagectl == 'true'
run: |
IMAGE="${{ vars.DOCKER_HOST }}/${{ github.repository_owner }}/vantage/vantagectl:latest"
# Root context: vantagectl depends on the shared module.
docker build -t "$IMAGE" -f vantagectl/Dockerfile .
docker push "$IMAGE"
- name: Build and push adminsite image
if: steps.changed.outputs.adminsite == 'true'
run: |
+51
View File
@@ -58,3 +58,54 @@ jobs:
vantagectl/dist/vantagectl-darwin-arm64
vantagectl/dist/vantagectl-windows-amd64.exe
vantagectl/dist/checksums.txt
# The image is built here rather than in server-deploy.yml on every push to
# main, because vantagectl is a released tool rather than a running service.
# An operator restoring a database should be able to name the version they
# ran; ":latest, rebuilt whenever main moved" cannot be named after the
# fact. It is a separate job from the binaries because it needs a
# docker-capable runner rather than a Go one, and it does not need the
# binaries — the image builds from source in its own stage.
image:
runs-on: ubuntu-docker
container: docker:dind
steps:
- name: Setup
run: apk add --update nodejs npm git
- name: Checkout
uses: actions/checkout@v4
- name: Extract version
id: version
run: |
# v0.1.0 for the binary stamp, 0.1.0 for the image tag: a
# leading v is conventional on a git tag and unconventional on
# a container tag.
VERSION="${GITHUB_REF_NAME#vantagectl/}"
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
echo "IMAGE_TAG=${VERSION#v}" >> $GITHUB_OUTPUT
- name: Log in to registry
run: |
echo "${{ secrets.RELEASE_TOKEN }}" | \
docker login ${{ vars.DOCKER_HOST }} \
-u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build and push image
env:
VERSION: ${{ steps.version.outputs.VERSION }}
IMAGE_TAG: ${{ steps.version.outputs.IMAGE_TAG }}
run: |
REPO="${{ vars.DOCKER_HOST }}/${{ github.repository_owner }}/vantage/vantagectl"
# Root context: vantagectl depends on the shared module through
# a replace directive, so the build needs shared/ alongside it.
# VERSION is passed through so `vantagectl --version` inside the
# image reports the tag it was built from rather than "dev".
docker build \
--build-arg VERSION="${VERSION}" \
-t "${REPO}:${IMAGE_TAG}" \
-t "${REPO}:latest" \
-f vantagectl/Dockerfile .
docker push "${REPO}:${IMAGE_TAG}"
docker push "${REPO}:latest"
+32 -12
View File
@@ -451,10 +451,21 @@ is in `ScopedCollections` (which `scopedCollectionsForPurge` derives from). Ther
is no history: a workload list is state, not a record.
**`proto/vantage/v1/vantage.proto` is documentation, not a generator input.**
Both `pb` packages are hand-written JSON-tagged structs over a custom codec, and
there are two copies — `agent/internal/grpc/pb` and `server/internal/grpc/pb`.
A message added to one must be added to the other and to the `.proto`, in the
same commit.
`shared/grpc/pb` is hand-written JSON-tagged structs over the custom codec in
`shared/grpc/codec`, and it is **one** package shared by both sides — a message
added to it must be added to the `.proto` in the same commit, but there is no
longer a second Go copy to keep in step. There used to be two
(`agent/internal/grpc/pb` and `server/internal/grpc/pb`) and they had already
drifted: the agent's `UnimplementedVantageServer` was three methods stale and
carried no `ReportWorkloads` at all. The agent links the server half as dead
code, which the linker drops.
This makes `agent` the **fifth** consumer of `shared/`, and the second one CI
does not rebuild on a push to main: like `vantagectl`, the agent image is cut by
`agent-release.yml` on an `agent/v*` tag, so a wire change reaches the server at
the next push and the fleet at the next agent release. That gap existed before
too — it is just now a compile error in the same tree rather than a silent
mismatch between two copies that both compiled.
### Status pages
@@ -657,8 +668,11 @@ temporary directory before verifying its checksums, and a scratch image has no
`server`, but here it would break every restore rather than only vulnerability
scanning.
**`shared/` now fans out to four Go images** in `server-deploy.yml`:
`server`, `sitesvc`, `admin` and `vantagectl` — see the CI section below.
**`shared/` reaches four Go images, but only three of them from
`server-deploy.yml`** (`server`, `sitesvc`, `admin`). The `vantagectl` image is
built by `vantagectl-release.yml` on a `vantagectl/v*` tag instead, so a
`shared/` change reaches it at the next release rather than the next push to
main — see the CI section below.
### API tokens and OpenAPI
@@ -1318,7 +1332,7 @@ GOOS=linux GOARCH=amd64 go build \
### `server-deploy.yml` — triggered on every push to `main`
Builds and pushes eight images to the Gitea container registry: `server`, `web`, `site`, `sitesvc`, `admin`, `adminsite`, `docsite` and `vantagectl`.
Builds and pushes seven images to the Gitea container registry: `server`, `web`, `site`, `sitesvc`, `admin`, `adminsite` and `docsite`. **`vantagectl` is deliberately not among them** — it is a released tool rather than a running service, and its image is version-tagged by `vantagectl-release.yml`.
Note that despite the name, **this workflow does not deploy** — it only builds and pushes. There is no SSH step. Rolling images out is a separate manual step on the host:
@@ -1334,13 +1348,19 @@ cd /opt/vantage && docker compose -f docker-compose.yml -f docker-compose.site.y
| `server` | `server/`, `shared/`, `proto/`, `go.work` |
| `admin` | `admin/`, `shared/`, `go.work` |
| `sitesvc` | `sitesvc/`, `shared/`, `go.work` |
| `vantagectl` | `vantagectl/`, `shared/`, `go.work` |
| `web` · `site` · `adminsite` · `docsite` | their own directory only |
`shared/` fans out to **four** Go images (`server`, `sitesvc`, `admin`,
`vantagectl`) because each of their Dockerfiles copies `shared/` from a root
context — **if a fifth service ever imports `shared/`, add it to that list or
it will ship stale**. A change to the workflow file rebuilds everything, since
`shared/` fans out to **three** images here (`server`, `sitesvc`, `admin`)
because each of their Dockerfiles copies `shared/` from a root context — **if a
fourth service ever imports `shared/`, add it to that list or it will ship
stale**. `vantagectl` also imports `shared/` and is the exception: it is built
by `vantagectl-release.yml`, so a `shared/` fix reaches it only when someone
cuts a `vantagectl/v*` tag. That is deliberate — an operator restoring a
database should be running a version they can name — but it does mean a
`shared/backup` fix is not live until it is released. `agent` is the same shape
of exception since `shared/grpc/pb` moved there: it is built by
`agent-release.yml` on an `agent/v*` tag, so a wire change lands on the server
at the next push to main and on the fleet only at the next agent release. A change to the workflow file rebuilds everything, since
a build arg is baked into the image. So does anything that leaves no
trustworthy base commit: a manual `workflow_dispatch`, a new branch, or a
force-push whose old head is gone.
+4 -1
View File
@@ -9,8 +9,11 @@ require (
)
require (
gitea.hostxtra.co.uk/mrhid6/vantage/shared v0.0.0
golang.org/x/net v0.25.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/text v0.40.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect
google.golang.org/protobuf v1.34.1 // indirect
)
replace gitea.hostxtra.co.uk/mrhid6/vantage/shared => ../shared
+2 -2
View File
@@ -4,8 +4,8 @@ golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"sync"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
type streamWriter struct {
+3 -2
View File
@@ -6,7 +6,8 @@ import (
"strings"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/codec"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
@@ -15,7 +16,7 @@ import (
)
func init() {
encoding.RegisterCodec(JSONCodec{})
encoding.RegisterCodec(codec.JSONCodec{})
}
type Client struct {
-17
View File
@@ -1,17 +0,0 @@
package grpcclient
import "encoding/json"
type JSONCodec struct{}
func (JSONCodec) Marshal(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
func (JSONCodec) Unmarshal(data []byte, v interface{}) error {
return json.Unmarshal(data, v)
}
func (JSONCodec) Name() string {
return "proto"
}
-480
View File
@@ -1,480 +0,0 @@
package pb
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type RegisterRequest struct {
ServerId string `json:"server_id"`
PreRegToken string `json:"pre_reg_token"`
Hostname string `json:"hostname"`
IpAddress string `json:"ip_address"`
OsInfo string `json:"os_info"`
}
type RegisterResponse struct {
AgentToken string `json:"agent_token"`
}
type SyncRequest struct {
ServerId string `json:"server_id"`
AgentToken string `json:"agent_token"`
AgentVersion string `json:"agent_version,omitempty"`
}
type SyncResponse struct {
PublicKeys []string `json:"public_keys"`
// CollectPackages tells the agent whether this instance's licence grants
// vulnerability scanning. Absent decodes as false, which is the safe
// direction: an older server leaves agents collecting nothing.
CollectPackages bool `json:"collect_packages,omitempty"`
}
type OSRelease struct {
Family string `json:"family"`
// VersionId is not optional: Ubuntu 22.04 and 24.04 publish different fixed
// versions for the same CVE, so a scan without it is guesswork.
VersionId string `json:"version_id"`
Arch string `json:"arch,omitempty"`
}
type InstalledPackage struct {
Name string `json:"name"`
Version string `json:"version"`
Epoch int32 `json:"epoch,omitempty"`
Arch string `json:"arch,omitempty"`
// SourceName is what the Debian and Ubuntu feeds are keyed on: one advisory
// against "openssl" covers libssl3, openssl and libssl-dev.
SourceName string `json:"source_name,omitempty"`
}
// ReportPackagesRequest carries a server's installed package set.
//
// The agent calls twice at most: first with Packages empty, offering only the
// hash. If the server already holds it, NeedFull is false and the ~150KB body
// is never sent.
type ReportPackagesRequest struct {
ServerId string `json:"server_id"`
AgentToken string `json:"agent_token"`
Hash string `json:"hash"`
Os OSRelease `json:"os"`
Packages []InstalledPackage `json:"packages,omitempty"`
}
type ReportPackagesResponse struct {
NeedFull bool `json:"need_full"`
}
type UploadKeyRequest struct {
ServerId string `json:"server_id"`
AgentToken string `json:"agent_token"`
PublicKey string `json:"public_key"`
Label string `json:"label"`
PrivateKey string `json:"private_key,omitempty"`
}
type UploadKeyResponse struct {
KeyId string `json:"key_id"`
}
type PackageUpdate struct {
Name string `json:"name"`
CurrentVersion string `json:"current_version,omitempty"`
NewVersion string `json:"new_version"`
}
type ReportUpdatesRequest struct {
ServerId string `json:"server_id"`
AgentToken string `json:"agent_token"`
Updates []PackageUpdate `json:"updates"`
}
type ReportUpdatesResponse struct{}
type CPUReport struct {
Model string `json:"model,omitempty"`
Cores int `json:"cores,omitempty"`
UsagePct float64 `json:"usage_pct"`
Load1 float64 `json:"load1,omitempty"`
}
type MemReport struct {
TotalBytes uint64 `json:"total_bytes"`
UsedBytes uint64 `json:"used_bytes"`
}
type PartitionReport struct {
Device string `json:"device"`
Mountpoint string `json:"mountpoint"`
Fstype string `json:"fstype,omitempty"`
TotalBytes uint64 `json:"total_bytes"`
UsedBytes uint64 `json:"used_bytes"`
}
type InventoryReport struct {
ServerId string `json:"server_id"`
AgentToken string `json:"agent_token"`
IncludeStatic bool `json:"include_static"`
CPU *CPUReport `json:"cpu,omitempty"`
Memory *MemReport `json:"memory,omitempty"`
SwapTotal uint64 `json:"swap_total"`
SwapUsed uint64 `json:"swap_used"`
Partitions []PartitionReport `json:"partitions,omitempty"`
Kernel string `json:"kernel,omitempty"`
RebootRequired bool `json:"reboot_required,omitempty"`
}
type InventoryReportResponse struct{}
type MonitorSpec struct {
MonitorId string `json:"monitor_id"`
Type string `json:"type"`
URL string `json:"url,omitempty"`
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
Method string `json:"method,omitempty"`
ExpectedStatus int `json:"expected_status,omitempty"`
Keyword string `json:"keyword,omitempty"`
TLSWarnDays int `json:"tls_warn_days,omitempty"`
Insecure bool `json:"insecure,omitempty"`
IntervalSec int `json:"interval_sec"`
Retries int `json:"retries"`
}
type SyncMonitorsRequest struct {
ServerId string `json:"server_id"`
AgentToken string `json:"agent_token"`
}
type SyncMonitorsResponse struct {
Monitors []MonitorSpec `json:"monitors,omitempty"`
}
type CheckResult struct {
MonitorId string `json:"monitor_id"`
Up bool `json:"up"`
LatencyMs int `json:"latency_ms"`
Message string `json:"message,omitempty"`
CertExpiryUnix int64 `json:"cert_expiry_unix,omitempty"`
}
type ReportChecksRequest struct {
ServerId string `json:"server_id"`
AgentToken string `json:"agent_token"`
Results []CheckResult `json:"results,omitempty"`
}
type ReportChecksResponse struct{}
type ApplyUpdatesCmd struct{}
type OpenProxyCmd struct {
ProxyId string `json:"proxy_id"`
Port uint32 `json:"port"`
}
type ProxyOpen struct {
ServerId string `json:"server_id"`
AgentToken string `json:"agent_token"`
ProxyId string `json:"proxy_id"`
}
type ProxyClose struct {
Reason string `json:"reason,omitempty"`
}
type ProxyClientMsg struct {
Open *ProxyOpen `json:"open,omitempty"`
Data []byte `json:"data,omitempty"`
Close *ProxyClose `json:"close,omitempty"`
}
type ProxyServerMsg struct {
Data []byte `json:"data,omitempty"`
Close *ProxyClose `json:"close,omitempty"`
}
type ServerCommand struct {
CommandId string `json:"command_id"`
GenerateKey *GenerateKeyCmd `json:"generate_key,omitempty"`
DeleteKey *DeleteKeyCmd `json:"delete_key,omitempty"`
UpdateAgent *UpdateAgentCmd `json:"update_agent,omitempty"`
ApplyUpdates *ApplyUpdatesCmd `json:"apply_updates,omitempty"`
RunStep *RunStepCmd `json:"run_step,omitempty"`
CleanupWorkspace *CleanupWorkspaceCmd `json:"cleanup_workspace,omitempty"`
OpenProxy *OpenProxyCmd `json:"open_proxy,omitempty"`
Ping *PingCmd `json:"ping,omitempty"`
RefreshWorkloads *RefreshWorkloadsCmd `json:"refresh_workloads,omitempty"`
ControlWorkload *ControlWorkloadCmd `json:"control_workload,omitempty"`
WorkloadLogs *WorkloadLogsCmd `json:"workload_logs,omitempty"`
}
// PingCmd is a server-originated liveness beat. It carries nothing and expects
// no reply: its arrival is the entire message. See the .proto for why gRPC
// keepalive is not sufficient on its own.
type PingCmd struct{}
type CleanupWorkspaceCmd struct {
WorkspaceId string `json:"workspace_id"`
}
type DeleteKeyCmd struct {
Label string `json:"label"`
}
type UpdateAgentCmd struct {
Version string `json:"version"`
GiteaBaseURL string `json:"gitea_base_url"`
}
type GenerateKeyCmd struct {
Label string `json:"label"`
KeyType string `json:"key_type,omitempty"`
KeySize int `json:"key_size,omitempty"`
Passphrase string `json:"passphrase,omitempty"`
Comment string `json:"comment,omitempty"`
}
type AgentMessage struct {
ServerId string `json:"server_id"`
AgentToken string `json:"agent_token"`
Ready *AgentReady `json:"ready,omitempty"`
Result *CommandResult `json:"result,omitempty"`
StepResult *StepResult `json:"step_result,omitempty"`
StepOutput *StepOutputChunk `json:"step_output,omitempty"`
WorkloadLogsResult *WorkloadLogsResult `json:"workload_logs_result,omitempty"`
}
type AgentReady struct{}
type CommandResult struct {
CommandId string `json:"command_id"`
Success bool `json:"success"`
Message string `json:"message"`
}
type RunStepCmd struct {
Interpreter string `json:"interpreter"`
Script string `json:"script"`
Env map[string]string `json:"env,omitempty"`
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
WorkspaceId string `json:"workspace_id,omitempty"`
}
type StepResult struct {
CommandId string `json:"command_id"`
ExitCode int `json:"exit_code"`
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
OutputEnv map[string]string `json:"output_env,omitempty"`
}
type StepOutputChunk struct {
CommandId string `json:"command_id"`
Seq uint64 `json:"seq"`
Data []byte `json:"data,omitempty"`
Eof bool `json:"eof,omitempty"`
}
type Vantage_CommandStreamClient interface {
Send(*AgentMessage) error
Recv() (*ServerCommand, error)
grpc.ClientStream
}
type vantageCommandStreamClient struct {
grpc.ClientStream
}
func (c *vantageCommandStreamClient) Send(m *AgentMessage) error {
return c.ClientStream.SendMsg(m)
}
func (c *vantageCommandStreamClient) Recv() (*ServerCommand, error) {
m := new(ServerCommand)
if err := c.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
type Vantage_CommandStreamServer interface {
Send(*ServerCommand) error
Recv() (*AgentMessage, error)
grpc.ServerStream
}
type keyManagerCommandStreamServer struct {
grpc.ServerStream
}
func (s *keyManagerCommandStreamServer) Send(m *ServerCommand) error {
return s.ServerStream.SendMsg(m)
}
func (s *keyManagerCommandStreamServer) Recv() (*AgentMessage, error) {
m := new(AgentMessage)
if err := s.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
type Vantage_ProxyStreamServer interface {
Send(*ProxyServerMsg) error
Recv() (*ProxyClientMsg, error)
grpc.ServerStream
}
type vantageProxyStreamServer struct {
grpc.ServerStream
}
func (s *vantageProxyStreamServer) Send(m *ProxyServerMsg) error {
return s.ServerStream.SendMsg(m)
}
func (s *vantageProxyStreamServer) Recv() (*ProxyClientMsg, error) {
m := new(ProxyClientMsg)
if err := s.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
type Vantage_ProxyStreamClient interface {
Send(*ProxyClientMsg) error
Recv() (*ProxyServerMsg, error)
CloseSend() error
grpc.ClientStream
}
type vantageProxyStreamClient struct {
grpc.ClientStream
}
func (c *vantageProxyStreamClient) Send(m *ProxyClientMsg) error {
return c.ClientStream.SendMsg(m)
}
func (c *vantageProxyStreamClient) Recv() (*ProxyServerMsg, error) {
m := new(ProxyServerMsg)
if err := c.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
type VantageClient interface {
Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error)
SyncKeys(ctx context.Context, in *SyncRequest, opts ...grpc.CallOption) (*SyncResponse, error)
UploadGeneratedKey(ctx context.Context, in *UploadKeyRequest, opts ...grpc.CallOption) (*UploadKeyResponse, error)
ReportUpdates(ctx context.Context, in *ReportUpdatesRequest, opts ...grpc.CallOption) (*ReportUpdatesResponse, error)
ReportPackages(ctx context.Context, in *ReportPackagesRequest, opts ...grpc.CallOption) (*ReportPackagesResponse, error)
ReportWorkloads(ctx context.Context, in *ReportWorkloadsRequest, opts ...grpc.CallOption) (*ReportWorkloadsResponse, error)
ReportInventory(ctx context.Context, in *InventoryReport, opts ...grpc.CallOption) (*InventoryReportResponse, error)
SyncMonitors(ctx context.Context, in *SyncMonitorsRequest, opts ...grpc.CallOption) (*SyncMonitorsResponse, error)
ReportChecks(ctx context.Context, in *ReportChecksRequest, opts ...grpc.CallOption) (*ReportChecksResponse, error)
CommandStream(ctx context.Context, opts ...grpc.CallOption) (Vantage_CommandStreamClient, error)
ProxyStream(ctx context.Context, opts ...grpc.CallOption) (Vantage_ProxyStreamClient, error)
}
type UnimplementedVantageServer struct{}
func (UnimplementedVantageServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "not implemented")
}
func (UnimplementedVantageServer) SyncKeys(context.Context, *SyncRequest) (*SyncResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "not implemented")
}
func (UnimplementedVantageServer) UploadGeneratedKey(context.Context, *UploadKeyRequest) (*UploadKeyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "not implemented")
}
type keyManagerClient struct {
cc grpc.ClientConnInterface
}
func NewVantageClient(cc grpc.ClientConnInterface) VantageClient {
return &keyManagerClient{cc}
}
func (c *keyManagerClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) {
out := new(RegisterResponse)
if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/Register", in, out, opts...); err != nil {
return nil, err
}
return out, nil
}
func (c *keyManagerClient) SyncKeys(ctx context.Context, in *SyncRequest, opts ...grpc.CallOption) (*SyncResponse, error) {
out := new(SyncResponse)
if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/SyncKeys", in, out, opts...); err != nil {
return nil, err
}
return out, nil
}
func (c *keyManagerClient) UploadGeneratedKey(ctx context.Context, in *UploadKeyRequest, opts ...grpc.CallOption) (*UploadKeyResponse, error) {
out := new(UploadKeyResponse)
if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/UploadGeneratedKey", in, out, opts...); err != nil {
return nil, err
}
return out, nil
}
func (c *keyManagerClient) ReportUpdates(ctx context.Context, in *ReportUpdatesRequest, opts ...grpc.CallOption) (*ReportUpdatesResponse, error) {
out := new(ReportUpdatesResponse)
if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/ReportUpdates", in, out, opts...); err != nil {
return nil, err
}
return out, nil
}
func (c *keyManagerClient) ReportPackages(ctx context.Context, in *ReportPackagesRequest, opts ...grpc.CallOption) (*ReportPackagesResponse, error) {
out := new(ReportPackagesResponse)
if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/ReportPackages", in, out, opts...); err != nil {
return nil, err
}
return out, nil
}
func (c *keyManagerClient) ReportInventory(ctx context.Context, in *InventoryReport, opts ...grpc.CallOption) (*InventoryReportResponse, error) {
out := new(InventoryReportResponse)
if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/ReportInventory", in, out, opts...); err != nil {
return nil, err
}
return out, nil
}
func (c *keyManagerClient) SyncMonitors(ctx context.Context, in *SyncMonitorsRequest, opts ...grpc.CallOption) (*SyncMonitorsResponse, error) {
out := new(SyncMonitorsResponse)
if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/SyncMonitors", in, out, opts...); err != nil {
return nil, err
}
return out, nil
}
func (c *keyManagerClient) ReportChecks(ctx context.Context, in *ReportChecksRequest, opts ...grpc.CallOption) (*ReportChecksResponse, error) {
out := new(ReportChecksResponse)
if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/ReportChecks", in, out, opts...); err != nil {
return nil, err
}
return out, nil
}
func (c *keyManagerClient) CommandStream(ctx context.Context, opts ...grpc.CallOption) (Vantage_CommandStreamClient, error) {
desc := &grpc.StreamDesc{StreamName: "CommandStream", ServerStreams: true, ClientStreams: true}
stream, err := c.cc.NewStream(ctx, desc, "/vantage.v1.Vantage/CommandStream", opts...)
if err != nil {
return nil, err
}
return &vantageCommandStreamClient{stream}, nil
}
func (c *keyManagerClient) ProxyStream(ctx context.Context, opts ...grpc.CallOption) (Vantage_ProxyStreamClient, error) {
desc := &grpc.StreamDesc{StreamName: "ProxyStream", ServerStreams: true, ClientStreams: true}
stream, err := c.cc.NewStream(ctx, desc, "/vantage.v1.Vantage/ProxyStream", opts...)
if err != nil {
return nil, err
}
return &vantageProxyStreamClient{stream}, nil
}
-80
View File
@@ -1,80 +0,0 @@
package pb
import (
"context"
"google.golang.org/grpc"
)
// Workload registry messages. Hand-written like the rest of this package: the
// .proto is the contract, this file is the Go side of it, and the two must be
// changed together.
// Workload is one container or one systemd unit.
type Workload struct {
Kind string `json:"kind"`
Id string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
Health string `json:"health,omitempty"`
Image string `json:"image,omitempty"`
Stack string `json:"stack,omitempty"`
Ports []string `json:"ports,omitempty"`
Restarts int32 `json:"restarts,omitempty"`
StartedAt string `json:"started_at,omitempty"` // RFC3339, empty when not running
Protected bool `json:"protected,omitempty"`
}
// ReportWorkloadsRequest carries what a server is running.
//
// Offer-then-send, the same handshake as ReportPackages: the agent calls once
// with Workloads empty, and resends with the body only if NeedFull is set.
type ReportWorkloadsRequest struct {
ServerId string `json:"server_id"`
AgentToken string `json:"agent_token"`
Hash string `json:"hash"`
DockerOk bool `json:"docker_ok"`
DockerError string `json:"docker_error,omitempty"`
SystemdOk bool `json:"systemd_ok"`
SystemdError string `json:"systemd_error,omitempty"`
Workloads []Workload `json:"workloads,omitempty"` // empty on the offer call
// Full marks the second call. It is not inferred from an empty Workloads
// slice: a host running nothing sends an empty list as its full report.
Full bool `json:"full,omitempty"`
}
type ReportWorkloadsResponse struct {
NeedFull bool `json:"need_full"`
}
// RefreshWorkloadsCmd carries no payload back. It makes the agent report
// immediately through ReportWorkloads, so there is exactly one writer for the
// server_workloads collection rather than two arriving by different routes.
type RefreshWorkloadsCmd struct{}
type ControlWorkloadCmd struct {
Kind string `json:"kind"`
Id string `json:"id"`
Action string `json:"action"` // start | stop | restart
}
type WorkloadLogsCmd struct {
Kind string `json:"kind"`
Id string `json:"id"`
Tail int32 `json:"tail,omitempty"`
}
type WorkloadLogsResult struct {
CommandId string `json:"command_id"`
Text string `json:"text,omitempty"`
Truncated bool `json:"truncated,omitempty"`
Error string `json:"error,omitempty"`
}
func (c *keyManagerClient) ReportWorkloads(ctx context.Context, in *ReportWorkloadsRequest, opts ...grpc.CallOption) (*ReportWorkloadsResponse, error) {
out := new(ReportWorkloadsResponse)
if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/ReportWorkloads", in, out, opts...); err != nil {
return nil, err
}
return out, nil
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"syscall"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
func collect(r *pb.InventoryReport, includeStatic bool) {
+1 -2
View File
@@ -7,7 +7,6 @@
// without it this file compiles on Linux too and collides with collect_linux.go.
package inventory
import "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
import "gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
func collect(r *pb.InventoryReport, includeStatic bool) {}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
var (
+1 -1
View File
@@ -1,6 +1,6 @@
package inventory
import "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
import "gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
func Collect(includeStatic bool) *pb.InventoryReport {
r := &pb.InventoryReport{IncludeStatic: includeStatic, CPU: &pb.CPUReport{}, Memory: &pb.MemReport{}}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/checker"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/config"
grpcclient "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
const syncInterval = 30 * time.Second
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"strconv"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
const (
+1 -1
View File
@@ -10,8 +10,8 @@ import (
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/config"
grpcclient "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/packages"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
// collectPackagesFlag is written by the 30s key poll and read by the hourly
+1 -1
View File
@@ -20,12 +20,12 @@ import (
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/config"
agentexec "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/exec"
grpcclient "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/inventory"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/keys"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/monitors"
agentproxy "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/proxy"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/updates"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
func Run(ctx context.Context, cfg *config.Config, version string) error {
+1 -1
View File
@@ -7,8 +7,8 @@ import (
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/config"
grpcclient "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/workloads"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
// workloadInterval is the report cadence. Sixty seconds is affordable because
@@ -40,6 +40,13 @@ nobody can read.
reconnect on their own, because `servers.agent_token_hash` — the thing an
agent authenticates with — is itself in the backup.
:::note Pin the version
The image is published on each `vantagectl/v*` release and tagged with that
version; `:latest` also moves. Pin a version in anything scheduled. A restore
is easier to reason about when you can say which build produced the archive and
which one read it back.
:::
## Taking a backup
The loose binary:
@@ -59,7 +66,7 @@ docker run --rm \
-e MONGO_DB=vantage \
-e KEY_ENCRYPTION_KEY=<your 64-char hex key> \
-v /backups:/backups \
gitea.hostxtra.co.uk/mrhid6/vantage/vantagectl:latest backup --out /backups
gitea.hostxtra.co.uk/mrhid6/vantage/vantagectl:0.1.0 backup --out /backups
```
Kubernetes, as a scheduled `CronJob` the Helm chart can render for you:
@@ -68,7 +75,7 @@ Kubernetes, as a scheduled `CronJob` the Helm chart can render for you:
backup:
enabled: true
schedule: "0 2 * * *"
image: "gitea.hostxtra.co.uk/mrhid6/vantage/vantagectl:latest"
image: "gitea.hostxtra.co.uk/mrhid6/vantage/vantagectl:0.1.0"
pvcName: "vantage-backups"
```
+1 -1
View File
@@ -3,9 +3,9 @@ package grpcserver
import (
"log"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/proxy"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/services"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
+3 -2
View File
@@ -8,9 +8,10 @@ import (
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/checker"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/services"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/codec"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/encoding"
@@ -19,7 +20,7 @@ import (
)
func init() {
encoding.RegisterCodec(JSONCodec{})
encoding.RegisterCodec(codec.JSONCodec{})
}
type vantageServer struct {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"sync"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
const (
+1 -1
View File
@@ -12,8 +12,8 @@ import (
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/bus"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/proxy"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
// ErrAgentOffline means the console cannot be opened because the target's agent
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/bus"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
"github.com/google/uuid"
)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
"go.mongodb.org/mongo-driver/v2/bson"
)
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"log"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/bus"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
// Step results travel back over the bus for the same reason commands travel out
+1 -1
View File
@@ -7,8 +7,8 @@ import (
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"log"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/bus"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
)
// Workload results travel back over the bus for the same reason commands travel
+1 -1
View File
@@ -7,8 +7,8 @@ import (
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/grpc/pb"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
@@ -1,4 +1,4 @@
package grpcserver
package codec
import (
"encoding/json"
@@ -280,15 +280,15 @@ type Vantage_CommandStreamServer interface {
grpc.ServerStream
}
type keyManagerCommandStreamServer struct {
type vantageCommandStreamServer struct {
grpc.ServerStream
}
func (s *keyManagerCommandStreamServer) Send(m *ServerCommand) error {
func (s *vantageCommandStreamServer) Send(m *ServerCommand) error {
return s.ServerStream.SendMsg(m)
}
func (s *keyManagerCommandStreamServer) Recv() (*AgentMessage, error) {
func (s *vantageCommandStreamServer) Recv() (*AgentMessage, error) {
m := new(AgentMessage)
if err := s.ServerStream.RecvMsg(m); err != nil {
return nil, err
@@ -681,5 +681,5 @@ func _Vantage_ReportChecks_Handler(srv interface{}, ctx context.Context, dec fun
}
func _Vantage_CommandStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(VantageServer).CommandStream(&keyManagerCommandStreamServer{stream})
return srv.(VantageServer).CommandStream(&vantageCommandStreamServer{stream})
}
@@ -10,7 +10,7 @@ import (
// Workload registry messages. Hand-written like the rest of this package: the
// .proto is the contract, this file is the Go side of it, and the two must be
// changed together. The agent module carries the same declarations.
// changed together.
// Workload is one container or one systemd unit.
type Workload struct {