Compare commits

...
34 Commits
Author SHA1 Message Date
Pasha Sviderski c58108aa82 chore(.editorconfig): ignore formatting for release notes 2026-10-02 13:56:49 +10:00
Pasha Sviderski 5d7952952a docs(README): list cluster storage for certificates in features 2026-10-02 13:56:08 +10:00
Pasha Sviderski 87e99ae369 docs(caddy): new page Cluster storage for Caddy 2026-10-02 13:27:07 +10:00
Pasha Sviderski f78a8ca8b5 docs(caddy): regenerate CLI reference for 'caddy cert ls' command 2026-10-01 21:00:05 +10:00
Pasha Sviderski e4bd1ad443 feat(store): improve waiting for store replication to skip unavailable members 2026-10-01 20:59:15 +10:00
Pasha Sviderski 0e53f505c0 refactor(client): speed up service list call (N -> 1 broadcast RPC) 2026-10-01 19:52:18 +10:00
Pasha Sviderski 9c21ae6b08 feat(caddy): CLI command to list certificates stored in the Uncloud cluster storage for Caddy 2026-10-01 14:19:53 +10:00
Pasha Sviderski 70de7b1b8f feat(caddy): add healthcheck to default Caddy spec and Compose deployment in docs 2026-10-01 09:25:39 +10:00
Pasha Sviderski af277639dd chore(dns): require minimum server version 0.21.0 for 'uc dns set' command 2026-09-30 19:21:59 +10:00
Pasha Sviderski 25ca0a57e8 lint 2026-09-30 18:49:53 +10:00
Pasha Sviderski 4dae187aa5 chore(caddystorage): move storage module code to a separate unlabs-dev/caddy-uncloud repo 2026-09-30 16:43:46 +10:00
Pasha Sviderski 930e7ea637 feat(caddy): add ListCertificates method, refactor Caddy client layout (#31) 2026-09-30 16:43:46 +10:00
Pasha Sviderski e50c5fbe57 refactor(api): replace map[string]uint64 with api.StoreVersion for store version handling 2026-09-30 16:43:46 +10:00
Anton OvchinnikovandGitHub e4455e936a fix(cli): stop leaking terminal capability replies into the shell (#435)
* chore(deps): bump bubbletea to v2.0.10, drop the uncloud fork

v2.0.10 carries the upstream fix for terminal capability replies leaking into
the shell (charmbracelet/bubbletea#1801). It skips every query that expects a
reply when input is disabled: the startup probe for synchronized output and
Unicode core, clipboard reads, background, foreground and cursor colour
requests, cursor position reports, and terminal version and termcap queries.

That supersedes the unlabs-dev fork, whose patch covered only the startup
probe, so the replace directive goes away and uncloud tracks upstream again.

The bump on its own does not fix the leak. Bubble Tea skips the queries only
when a program declares that it has no input, and nothing in uncloud declares
that yet. Measured: this tree still leaks "^[[?2026;2$y^[[?2027;0$y" under a
PTY harness. The next commit is what makes the spinners declare it.

Refs: #286, charmbracelet/bubbletea#1801
2026-09-28 10:16:45 +10:00
Pasha Sviderski ebecf334d7 chore(dns): require minimum server version 0.21 2026-09-25 17:24:13 +10:00
417e402c73 feat(dns): allow setting an externally managed cluster domain (uc dns set)
* feat: add uc dns set

This allows manual setting of the cluster's domain name. I use this to
make the local cluster admin aware that this has been done.
(and then manually update the external dns)

```
% uc dns set ctrl.u.science.ru.nl
Set cluster domain: ctrl.u.science.ru.nl
Deploy the Caddy reverse proxy service ('uc caddy deploy') to enable internet access to your services via the reserved or your custom domain.

% uc dns show
ctrl.u.science.ru.nl

% uc dns set ctrl.u.science.ru.nl
Error: domain already reserved

% uc dns release
Released cluster domain: ctrl.u.science.ru.nl
```

See: #37

Signed-off-by: Miek Gieben <miek@miek.nl>

* docs

Signed-off-by: Miek Gieben <miek@miek.nl>

* check validatity

Signed-off-by: Miek Gieben <miek@miek.nl>

* No need to call out to update anything, as set doesnt use uncloud dns

Signed-off-by: Miek Gieben <miek@miek.nl>

* Ignore domain when there is no endpoint

Prevents this error, when you know what you're doing:

    Error: failed to update DNS records pointing to caddy service: create cluster domain records in Uncloud DNS: rpc error: code = Unknown desc = Post "/domains/ctrl.u.science.ru.nl/records": unsupported protocol scheme ""

Signed-off-by: Miek Gieben <miek@miek.nl>

* from main

Signed-off-by: Miek Gieben <miek@miek.nl>

* mise proto && make cli-docs

Signed-off-by: Miek Gieben <miek@miek.nl>

* Rebase and and generate protos again

Signed-off-by: Miek Gieben <miek@miek.nl>

* Rebase and fix

Signed-off-by: Miek Gieben <miek@miek.nl>

* feat(dns): allow setting an externally managed cluster domain

---------

Signed-off-by: Miek Gieben <miek@miek.nl>
Co-authored-by: Pasha Sviderski <me@psviderski.name>
2026-09-25 16:36:01 +10:00
Pasha Sviderski 9ee3ca15eb feat(dns): allow setting an externally managed cluster domain 2026-09-25 16:20:54 +10:00
Pasha Sviderski 3482eeefab chore: go mod tidy 2026-09-24 22:34:47 +10:00
Pasha Sviderski ace8cbf974 fix(caddyconfig): custom global Caddy config is preserved on regeneration,warn about last load error in 'uc caddy config' (fixes #412) 2026-09-24 22:24:42 +10:00
Pasha Sviderski 45d33d87dd docs(runtime-templates): add 'Added in v0.21.0' note 2026-09-23 19:12:58 +10:00
Pasha Sviderski 5c33e4be86 feat(runtime-templates): add support for runtime templates in bind mounts (#412) 2026-09-23 15:12:10 +10:00
Pasha Sviderski 88dda435e7 chore(gitignore): ignore /research dir 2026-09-23 09:49:37 +10:00
Pasha Sviderski ae940c3003 fix(test): flaky distributed lock tests by waiting for WG mesh to become ready 2026-09-22 15:21:18 +10:00
Pasha Sviderski e525bdeb7c chore(website): update discord members 2026-09-18 21:04:53 +10:00
Pasha Sviderski bc09954cbd feat(caddy): mount uncloud API socket for storage module (#31) 2026-09-17 16:50:26 +10:00
Miek Gieben b28c458029 Rebase and fix
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:39:48 +02:00
Miek Gieben 3fc1cddf01 Rebase and and generate protos again
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:36:28 +02:00
Miek Gieben 54a7328a34 mise proto && make cli-docs
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:36:02 +02:00
Miek Gieben 47a4a9158e from main
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:43 +02:00
Miek Gieben ad2e1a06d5 Ignore domain when there is no endpoint
Prevents this error, when you know what you're doing:

    Error: failed to update DNS records pointing to caddy service: create cluster domain records in Uncloud DNS: rpc error: code = Unknown desc = Post "/domains/ctrl.u.science.ru.nl/records": unsupported protocol scheme ""

Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:33 +02:00
Miek Gieben 81d36edc66 No need to call out to update anything, as set doesnt use uncloud dns
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:33 +02:00
Miek Gieben 5fb80e2071 check validatity
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:33 +02:00
Miek Gieben d1fe23dec0 docs
Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:33 +02:00
Miek Gieben cec3c75a10 feat: add uc dns set
This allows manual setting of the cluster's domain name. I use this to
make the local cluster admin aware that this has been done.
(and then manually update the external dns)

```
% uc dns set ctrl.u.science.ru.nl
Set cluster domain: ctrl.u.science.ru.nl
Deploy the Caddy reverse proxy service ('uc caddy deploy') to enable internet access to your services via the reserved or your custom domain.

% uc dns show
ctrl.u.science.ru.nl

% uc dns set ctrl.u.science.ru.nl
Error: domain already reserved

% uc dns release
Released cluster domain: ctrl.u.science.ru.nl
```

See: #37

Signed-off-by: Miek Gieben <miek@miek.nl>
2026-09-15 14:35:31 +02:00
107 changed files with 4132 additions and 2013 deletions
+3
View File
@@ -18,3 +18,6 @@ indent_size = 4
[*.sh] [*.sh]
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
[release-notes-*.md]
max_line_length = off
+1
View File
@@ -44,3 +44,4 @@ node_modules/
.devcontainer/ .devcontainer/
.zed/ .zed/
research/
+2
View File
@@ -44,6 +44,8 @@ complexity of Kubernetes.
[Uncloud DNS](https://github.com/psviderski/uncloud-dns) service. [Uncloud DNS](https://github.com/psviderski/uncloud-dns) service.
* **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's * **Automatic HTTPS**: Built-in Caddy reverse proxy handles TLS certificate provisioning and renewal using Let's
Encrypt. Encrypt.
* **Cluster storage for TLS certificates**: Native cluster storage for Caddy shares certificates and ACME challenge
tokens across machines and coordinates issuance.
* **Docker-like CLI**: Familiar commands for managing both infrastructure and applications. * **Docker-like CLI**: Familiar commands for managing both infrastructure and applications.
* **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster. * **Remote management**: Control your entire infrastructure through SSH access to any single machine in the cluster.
+30 -17
View File
@@ -27,10 +27,12 @@ type GetCaddyConfigResponse struct {
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
// The generated Caddyfile content. // The saved Caddyfile content.
Caddyfile string `protobuf:"bytes,1,opt,name=caddyfile,proto3" json:"caddyfile,omitempty"` Caddyfile string `protobuf:"bytes,1,opt,name=caddyfile,proto3" json:"caddyfile,omitempty"`
// Timestamp when the config was last modified. // Timestamp when the config was last modified.
ModifiedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=modified_at,json=modifiedAt,proto3" json:"modified_at,omitempty"` ModifiedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=modified_at,json=modifiedAt,proto3" json:"modified_at,omitempty"`
// Error from the latest unsuccessful reconciliation attempt.
LastReconciliationError string `protobuf:"bytes,3,opt,name=last_reconciliation_error,json=lastReconciliationError,proto3" json:"last_reconciliation_error,omitempty"`
} }
func (x *GetCaddyConfigResponse) Reset() { func (x *GetCaddyConfigResponse) Reset() {
@@ -79,6 +81,13 @@ func (x *GetCaddyConfigResponse) GetModifiedAt() *timestamppb.Timestamp {
return nil return nil
} }
func (x *GetCaddyConfigResponse) GetLastReconciliationError() string {
if x != nil {
return x.LastReconciliationError
}
return ""
}
var File_api_pb_caddy_proto protoreflect.FileDescriptor var File_api_pb_caddy_proto protoreflect.FileDescriptor
var file_api_pb_caddy_proto_rawDesc = []byte{ var file_api_pb_caddy_proto_rawDesc = []byte{
@@ -87,22 +96,26 @@ var file_api_pb_caddy_proto_rawDesc = []byte{
0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x73, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43, 0x61, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xaf, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43,
0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x18,
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x64, 0x64, 0x79, 0x66, 0x69, 0x6c, 0x65,
0x3b, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x12, 0x3b, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x74, 0x32, 0x49, 0x0a, 0x05, 0x70, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3a, 0x0a,
0x43, 0x61, 0x64, 0x64, 0x79, 0x12, 0x40, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x19, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x69, 0x61,
0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x52, 0x17, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x69, 0x61,
0x2e, 0x47, 0x65, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x32, 0x49, 0x0a, 0x05, 0x43, 0x61, 0x64,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x64, 0x79, 0x12, 0x40, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e,
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
+3 -1
View File
@@ -13,8 +13,10 @@ service Caddy {
} }
message GetCaddyConfigResponse { message GetCaddyConfigResponse {
// The generated Caddyfile content. // The saved Caddyfile content.
string caddyfile = 1; string caddyfile = 1;
// Timestamp when the config was last modified. // Timestamp when the config was last modified.
google.protobuf.Timestamp modified_at = 2; google.protobuf.Timestamp modified_at = 2;
// Error from the latest unsuccessful reconciliation attempt.
string last_reconciliation_error = 3;
} }
+205 -46
View File
@@ -446,6 +446,120 @@ func (x *StatCaddyStorageResponse) GetIsTerminal() bool {
return false return false
} }
type ListCertificatesResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Certificates []*IssuedCertificate `protobuf:"bytes,1,rep,name=certificates,proto3" json:"certificates,omitempty"`
}
func (x *ListCertificatesResponse) Reset() {
*x = ListCertificatesResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ListCertificatesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListCertificatesResponse) ProtoMessage() {}
func (x *ListCertificatesResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListCertificatesResponse.ProtoReflect.Descriptor instead.
func (*ListCertificatesResponse) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{8}
}
func (x *ListCertificatesResponse) GetCertificates() []*IssuedCertificate {
if x != nil {
return x.Certificates
}
return nil
}
type IssuedCertificate struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Subject Alternative Name (SAN) of the certificate. Caddy doesn't issue certificates with multiple SANs.
San string `protobuf:"bytes,1,opt,name=san,proto3" json:"san,omitempty"`
// The PEM-encoding of DER-encoded ASN.1 data for the cert or chain, leaf first.
Chain []byte `protobuf:"bytes,2,opt,name=chain,proto3" json:"chain,omitempty"`
// Any extra information associated with the certificate, usually provided by the issuer implementation.
// JSON-encoded, may be absent or null.
IssuerData []byte `protobuf:"bytes,3,opt,name=issuer_data,json=issuerData,proto3" json:"issuer_data,omitempty"`
}
func (x *IssuedCertificate) Reset() {
*x = IssuedCertificate{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_caddy_storage_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *IssuedCertificate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IssuedCertificate) ProtoMessage() {}
func (x *IssuedCertificate) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_caddy_storage_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IssuedCertificate.ProtoReflect.Descriptor instead.
func (*IssuedCertificate) Descriptor() ([]byte, []int) {
return file_api_pb_caddy_storage_proto_rawDescGZIP(), []int{9}
}
func (x *IssuedCertificate) GetSan() string {
if x != nil {
return x.San
}
return ""
}
func (x *IssuedCertificate) GetChain() []byte {
if x != nil {
return x.Chain
}
return nil
}
func (x *IssuedCertificate) GetIssuerData() []byte {
if x != nil {
return x.IssuerData
}
return nil
}
var File_api_pb_caddy_storage_proto protoreflect.FileDescriptor var File_api_pb_caddy_storage_proto protoreflect.FileDescriptor
var file_api_pb_caddy_storage_proto_rawDesc = []byte{ var file_api_pb_caddy_storage_proto_rawDesc = []byte{
@@ -492,32 +606,48 @@ var file_api_pb_caddy_storage_proto_rawDesc = []byte{
0x65, 0x64, 0x41, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x65, 0x64, 0x41, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01,
0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x74, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x74,
0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69,
0x73, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x32, 0xdf, 0x02, 0x0a, 0x0c, 0x43, 0x61, 0x73, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x22, 0x56, 0x0a, 0x18, 0x4c, 0x69, 0x73,
0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x3e, 0x0a, 0x05, 0x53, 0x74, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73,
0x6f, 0x72, 0x65, 0x12, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69,
0x63, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x70,
0x69, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63,
0x61, 0x74, 0x65, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65,
0x73, 0x22, 0x5c, 0x0a, 0x11, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69,
0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x61, 0x6e, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x61, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x68, 0x61, 0x69,
0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x1f,
0x0a, 0x0b, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x0a, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x32,
0xaa, 0x03, 0x0a, 0x0c, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
0x12, 0x3e, 0x0a, 0x05, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x53, 0x74, 0x6f, 0x72, 0x65, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
0x12, 0x43, 0x0a, 0x04, 0x4c, 0x6f, 0x61, 0x64, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c,
0x6f, 0x61, 0x64, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x61,
0x64, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12,
0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x64, 0x64,
0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12,
0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53,
0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f,
0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x04,
0x53, 0x74, 0x61, 0x74, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x43,
0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x43, 0x61, 0x64,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x04, 0x4c, 0x6f, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x61, 0x64, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x61, 0x64, 0x65, 0x12, 0x49, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69,
0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x61, 0x64, 0x64, 0x79, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e,
0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63,
0x40, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x1e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24,
0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x79, 0x12, 0x43, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x4c, 0x69, 0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x04, 0x53, 0x74, 0x61, 0x74, 0x12, 0x1c,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74,
0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61,
0x70, 0x69, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x43, 0x61, 0x64, 0x64, 0x79, 0x53, 0x74, 0x6f, 0x72,
0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65,
0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69,
0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@@ -532,7 +662,7 @@ func file_api_pb_caddy_storage_proto_rawDescGZIP() []byte {
return file_api_pb_caddy_storage_proto_rawDescData return file_api_pb_caddy_storage_proto_rawDescData
} }
var file_api_pb_caddy_storage_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_api_pb_caddy_storage_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_api_pb_caddy_storage_proto_goTypes = []any{ var file_api_pb_caddy_storage_proto_goTypes = []any{
(*StoreCaddyStorageRequest)(nil), // 0: api.StoreCaddyStorageRequest (*StoreCaddyStorageRequest)(nil), // 0: api.StoreCaddyStorageRequest
(*LoadCaddyStorageRequest)(nil), // 1: api.LoadCaddyStorageRequest (*LoadCaddyStorageRequest)(nil), // 1: api.LoadCaddyStorageRequest
@@ -542,27 +672,32 @@ var file_api_pb_caddy_storage_proto_goTypes = []any{
(*ListCaddyStorageResponse)(nil), // 5: api.ListCaddyStorageResponse (*ListCaddyStorageResponse)(nil), // 5: api.ListCaddyStorageResponse
(*StatCaddyStorageRequest)(nil), // 6: api.StatCaddyStorageRequest (*StatCaddyStorageRequest)(nil), // 6: api.StatCaddyStorageRequest
(*StatCaddyStorageResponse)(nil), // 7: api.StatCaddyStorageResponse (*StatCaddyStorageResponse)(nil), // 7: api.StatCaddyStorageResponse
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp (*ListCertificatesResponse)(nil), // 8: api.ListCertificatesResponse
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty (*IssuedCertificate)(nil), // 9: api.IssuedCertificate
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
(*emptypb.Empty)(nil), // 11: google.protobuf.Empty
} }
var file_api_pb_caddy_storage_proto_depIdxs = []int32{ var file_api_pb_caddy_storage_proto_depIdxs = []int32{
8, // 0: api.LoadCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp 10, // 0: api.LoadCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
8, // 1: api.StatCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp 10, // 1: api.StatCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
0, // 2: api.CaddyStorage.Store:input_type -> api.StoreCaddyStorageRequest 9, // 2: api.ListCertificatesResponse.certificates:type_name -> api.IssuedCertificate
1, // 3: api.CaddyStorage.Load:input_type -> api.LoadCaddyStorageRequest 0, // 3: api.CaddyStorage.Store:input_type -> api.StoreCaddyStorageRequest
3, // 4: api.CaddyStorage.Delete:input_type -> api.DeleteCaddyStorageRequest 1, // 4: api.CaddyStorage.Load:input_type -> api.LoadCaddyStorageRequest
4, // 5: api.CaddyStorage.List:input_type -> api.ListCaddyStorageRequest 3, // 5: api.CaddyStorage.Delete:input_type -> api.DeleteCaddyStorageRequest
6, // 6: api.CaddyStorage.Stat:input_type -> api.StatCaddyStorageRequest 4, // 6: api.CaddyStorage.List:input_type -> api.ListCaddyStorageRequest
9, // 7: api.CaddyStorage.Store:output_type -> google.protobuf.Empty 6, // 7: api.CaddyStorage.Stat:input_type -> api.StatCaddyStorageRequest
2, // 8: api.CaddyStorage.Load:output_type -> api.LoadCaddyStorageResponse 11, // 8: api.CaddyStorage.ListCertificates:input_type -> google.protobuf.Empty
9, // 9: api.CaddyStorage.Delete:output_type -> google.protobuf.Empty 11, // 9: api.CaddyStorage.Store:output_type -> google.protobuf.Empty
5, // 10: api.CaddyStorage.List:output_type -> api.ListCaddyStorageResponse 2, // 10: api.CaddyStorage.Load:output_type -> api.LoadCaddyStorageResponse
7, // 11: api.CaddyStorage.Stat:output_type -> api.StatCaddyStorageResponse 11, // 11: api.CaddyStorage.Delete:output_type -> google.protobuf.Empty
7, // [7:12] is the sub-list for method output_type 5, // 12: api.CaddyStorage.List:output_type -> api.ListCaddyStorageResponse
2, // [2:7] is the sub-list for method input_type 7, // 13: api.CaddyStorage.Stat:output_type -> api.StatCaddyStorageResponse
2, // [2:2] is the sub-list for extension type_name 8, // 14: api.CaddyStorage.ListCertificates:output_type -> api.ListCertificatesResponse
2, // [2:2] is the sub-list for extension extendee 9, // [9:15] is the sub-list for method output_type
0, // [0:2] is the sub-list for field type_name 3, // [3:9] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
} }
func init() { file_api_pb_caddy_storage_proto_init() } func init() { file_api_pb_caddy_storage_proto_init() }
@@ -667,6 +802,30 @@ func file_api_pb_caddy_storage_proto_init() {
return nil return nil
} }
} }
file_api_pb_caddy_storage_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*ListCertificatesResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_caddy_storage_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*IssuedCertificate); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
@@ -674,7 +833,7 @@ func file_api_pb_caddy_storage_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_api_pb_caddy_storage_proto_rawDesc, RawDescriptor: file_api_pb_caddy_storage_proto_rawDesc,
NumEnums: 0, NumEnums: 0,
NumMessages: 8, NumMessages: 10,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
+18
View File
@@ -18,6 +18,10 @@ service CaddyStorage {
rpc Delete(DeleteCaddyStorageRequest) returns (google.protobuf.Empty); rpc Delete(DeleteCaddyStorageRequest) returns (google.protobuf.Empty);
rpc List(ListCaddyStorageRequest) returns (ListCaddyStorageResponse); rpc List(ListCaddyStorageRequest) returns (ListCaddyStorageResponse);
rpc Stat(StatCaddyStorageRequest) returns (StatCaddyStorageResponse); rpc Stat(StatCaddyStorageRequest) returns (StatCaddyStorageResponse);
// ListCertificates lists certificates issued by Caddy and stored in the cluster store.
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
rpc ListCertificates(google.protobuf.Empty) returns (ListCertificatesResponse);
} }
message StoreCaddyStorageRequest { message StoreCaddyStorageRequest {
@@ -57,3 +61,17 @@ message StatCaddyStorageResponse {
int64 size = 3; int64 size = 3;
bool is_terminal = 4; bool is_terminal = 4;
} }
message ListCertificatesResponse {
repeated IssuedCertificate certificates = 1;
}
message IssuedCertificate {
// Subject Alternative Name (SAN) of the certificate. Caddy doesn't issue certificates with multiple SANs.
string san = 1;
// The PEM-encoding of DER-encoded ASN.1 data for the cert or chain, leaf first.
bytes chain = 2;
// Any extra information associated with the certificate, usually provided by the issuer implementation.
// JSON-encoded, may be absent or null.
bytes issuer_data = 3;
}
+42
View File
@@ -25,6 +25,7 @@ const (
CaddyStorage_Delete_FullMethodName = "/api.CaddyStorage/Delete" CaddyStorage_Delete_FullMethodName = "/api.CaddyStorage/Delete"
CaddyStorage_List_FullMethodName = "/api.CaddyStorage/List" CaddyStorage_List_FullMethodName = "/api.CaddyStorage/List"
CaddyStorage_Stat_FullMethodName = "/api.CaddyStorage/Stat" CaddyStorage_Stat_FullMethodName = "/api.CaddyStorage/Stat"
CaddyStorage_ListCertificates_FullMethodName = "/api.CaddyStorage/ListCertificates"
) )
// CaddyStorageClient is the client API for CaddyStorage service. // CaddyStorageClient is the client API for CaddyStorage service.
@@ -42,6 +43,9 @@ type CaddyStorageClient interface {
Delete(ctx context.Context, in *DeleteCaddyStorageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) Delete(ctx context.Context, in *DeleteCaddyStorageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
List(ctx context.Context, in *ListCaddyStorageRequest, opts ...grpc.CallOption) (*ListCaddyStorageResponse, error) List(ctx context.Context, in *ListCaddyStorageRequest, opts ...grpc.CallOption) (*ListCaddyStorageResponse, error)
Stat(ctx context.Context, in *StatCaddyStorageRequest, opts ...grpc.CallOption) (*StatCaddyStorageResponse, error) Stat(ctx context.Context, in *StatCaddyStorageRequest, opts ...grpc.CallOption) (*StatCaddyStorageResponse, error)
// ListCertificates lists certificates issued by Caddy and stored in the cluster store.
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
ListCertificates(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListCertificatesResponse, error)
} }
type caddyStorageClient struct { type caddyStorageClient struct {
@@ -102,6 +106,16 @@ func (c *caddyStorageClient) Stat(ctx context.Context, in *StatCaddyStorageReque
return out, nil return out, nil
} }
func (c *caddyStorageClient) ListCertificates(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListCertificatesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListCertificatesResponse)
err := c.cc.Invoke(ctx, CaddyStorage_ListCertificates_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// CaddyStorageServer is the server API for CaddyStorage service. // CaddyStorageServer is the server API for CaddyStorage service.
// All implementations must embed UnimplementedCaddyStorageServer // All implementations must embed UnimplementedCaddyStorageServer
// for forward compatibility. // for forward compatibility.
@@ -117,6 +131,9 @@ type CaddyStorageServer interface {
Delete(context.Context, *DeleteCaddyStorageRequest) (*emptypb.Empty, error) Delete(context.Context, *DeleteCaddyStorageRequest) (*emptypb.Empty, error)
List(context.Context, *ListCaddyStorageRequest) (*ListCaddyStorageResponse, error) List(context.Context, *ListCaddyStorageRequest) (*ListCaddyStorageResponse, error)
Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error) Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error)
// ListCertificates lists certificates issued by Caddy and stored in the cluster store.
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
ListCertificates(context.Context, *emptypb.Empty) (*ListCertificatesResponse, error)
mustEmbedUnimplementedCaddyStorageServer() mustEmbedUnimplementedCaddyStorageServer()
} }
@@ -142,6 +159,9 @@ func (UnimplementedCaddyStorageServer) List(context.Context, *ListCaddyStorageRe
func (UnimplementedCaddyStorageServer) Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error) { func (UnimplementedCaddyStorageServer) Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Stat not implemented") return nil, status.Errorf(codes.Unimplemented, "method Stat not implemented")
} }
func (UnimplementedCaddyStorageServer) ListCertificates(context.Context, *emptypb.Empty) (*ListCertificatesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListCertificates not implemented")
}
func (UnimplementedCaddyStorageServer) mustEmbedUnimplementedCaddyStorageServer() {} func (UnimplementedCaddyStorageServer) mustEmbedUnimplementedCaddyStorageServer() {}
func (UnimplementedCaddyStorageServer) testEmbeddedByValue() {} func (UnimplementedCaddyStorageServer) testEmbeddedByValue() {}
@@ -253,6 +273,24 @@ func _CaddyStorage_Stat_Handler(srv interface{}, ctx context.Context, dec func(i
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _CaddyStorage_ListCertificates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(CaddyStorageServer).ListCertificates(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: CaddyStorage_ListCertificates_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(CaddyStorageServer).ListCertificates(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
// CaddyStorage_ServiceDesc is the grpc.ServiceDesc for CaddyStorage service. // CaddyStorage_ServiceDesc is the grpc.ServiceDesc for CaddyStorage service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@@ -280,6 +318,10 @@ var CaddyStorage_ServiceDesc = grpc.ServiceDesc{
MethodName: "Stat", MethodName: "Stat",
Handler: _CaddyStorage_Stat_Handler, Handler: _CaddyStorage_Stat_Handler,
}, },
{
MethodName: "ListCertificates",
Handler: _CaddyStorage_ListCertificates_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "api/pb/caddy_storage.proto", Metadata: "api/pb/caddy_storage.proto",
+178 -95
View File
@@ -124,7 +124,7 @@ func (x DNSRecord_RecordType) Number() protoreflect.EnumNumber {
// Deprecated: Use DNSRecord_RecordType.Descriptor instead. // Deprecated: Use DNSRecord_RecordType.Descriptor instead.
func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) { func (DNSRecord_RecordType) EnumDescriptor() ([]byte, []int) {
return file_api_pb_cluster_proto_rawDescGZIP(), []int{9, 0} return file_api_pb_cluster_proto_rawDescGZIP(), []int{10, 0}
} }
type AddMachineRequest struct { type AddMachineRequest struct {
@@ -392,6 +392,9 @@ type Domain struct {
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// Whether the domain is reserved in Uncloud DNS. Older daemons (<0.21) omit this field
// and only support reserved domains, so clients should treat absence as true.
Reserved *bool `protobuf:"varint,2,opt,name=reserved,proto3,oneof" json:"reserved,omitempty"`
} }
func (x *Domain) Reset() { func (x *Domain) Reset() {
@@ -433,6 +436,13 @@ func (x *Domain) GetName() string {
return "" return ""
} }
func (x *Domain) GetReserved() bool {
if x != nil && x.Reserved != nil {
return *x.Reserved
}
return false
}
type ReserveDomainRequest struct { type ReserveDomainRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -480,6 +490,54 @@ func (x *ReserveDomainRequest) GetEndpoint() string {
return "" return ""
} }
type SetDomainRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// An externally managed domain name. Empty clears a manually set domain.
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *SetDomainRequest) Reset() {
*x = SetDomainRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_api_pb_cluster_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SetDomainRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetDomainRequest) ProtoMessage() {}
func (x *SetDomainRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_cluster_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetDomainRequest.ProtoReflect.Descriptor instead.
func (*SetDomainRequest) Descriptor() ([]byte, []int) {
return file_api_pb_cluster_proto_rawDescGZIP(), []int{7}
}
func (x *SetDomainRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type CreateDomainRecordsRequest struct { type CreateDomainRecordsRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -491,7 +549,7 @@ type CreateDomainRecordsRequest struct {
func (x *CreateDomainRecordsRequest) Reset() { func (x *CreateDomainRecordsRequest) Reset() {
*x = CreateDomainRecordsRequest{} *x = CreateDomainRecordsRequest{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_api_pb_cluster_proto_msgTypes[7] mi := &file_api_pb_cluster_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -504,7 +562,7 @@ func (x *CreateDomainRecordsRequest) String() string {
func (*CreateDomainRecordsRequest) ProtoMessage() {} func (*CreateDomainRecordsRequest) ProtoMessage() {}
func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message { func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_cluster_proto_msgTypes[7] mi := &file_api_pb_cluster_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -517,7 +575,7 @@ func (x *CreateDomainRecordsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead. // Deprecated: Use CreateDomainRecordsRequest.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) { func (*CreateDomainRecordsRequest) Descriptor() ([]byte, []int) {
return file_api_pb_cluster_proto_rawDescGZIP(), []int{7} return file_api_pb_cluster_proto_rawDescGZIP(), []int{8}
} }
func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord { func (x *CreateDomainRecordsRequest) GetRecords() []*DNSRecord {
@@ -538,7 +596,7 @@ type CreateDomainRecordsResponse struct {
func (x *CreateDomainRecordsResponse) Reset() { func (x *CreateDomainRecordsResponse) Reset() {
*x = CreateDomainRecordsResponse{} *x = CreateDomainRecordsResponse{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_api_pb_cluster_proto_msgTypes[8] mi := &file_api_pb_cluster_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -551,7 +609,7 @@ func (x *CreateDomainRecordsResponse) String() string {
func (*CreateDomainRecordsResponse) ProtoMessage() {} func (*CreateDomainRecordsResponse) ProtoMessage() {}
func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message { func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_cluster_proto_msgTypes[8] mi := &file_api_pb_cluster_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -564,7 +622,7 @@ func (x *CreateDomainRecordsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead. // Deprecated: Use CreateDomainRecordsResponse.ProtoReflect.Descriptor instead.
func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) { func (*CreateDomainRecordsResponse) Descriptor() ([]byte, []int) {
return file_api_pb_cluster_proto_rawDescGZIP(), []int{8} return file_api_pb_cluster_proto_rawDescGZIP(), []int{9}
} }
func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord { func (x *CreateDomainRecordsResponse) GetRecords() []*DNSRecord {
@@ -587,7 +645,7 @@ type DNSRecord struct {
func (x *DNSRecord) Reset() { func (x *DNSRecord) Reset() {
*x = DNSRecord{} *x = DNSRecord{}
if protoimpl.UnsafeEnabled { if protoimpl.UnsafeEnabled {
mi := &file_api_pb_cluster_proto_msgTypes[9] mi := &file_api_pb_cluster_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -600,7 +658,7 @@ func (x *DNSRecord) String() string {
func (*DNSRecord) ProtoMessage() {} func (*DNSRecord) ProtoMessage() {}
func (x *DNSRecord) ProtoReflect() protoreflect.Message { func (x *DNSRecord) ProtoReflect() protoreflect.Message {
mi := &file_api_pb_cluster_proto_msgTypes[9] mi := &file_api_pb_cluster_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil { if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -613,7 +671,7 @@ func (x *DNSRecord) ProtoReflect() protoreflect.Message {
// Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead. // Deprecated: Use DNSRecord.ProtoReflect.Descriptor instead.
func (*DNSRecord) Descriptor() ([]byte, []int) { func (*DNSRecord) Descriptor() ([]byte, []int) {
return file_api_pb_cluster_proto_rawDescGZIP(), []int{9} return file_api_pb_cluster_proto_rawDescGZIP(), []int{10}
} }
func (x *DNSRecord) GetName() string { func (x *DNSRecord) GetName() string {
@@ -676,63 +734,72 @@ var file_api_pb_cluster_proto_rawDesc = []byte{
0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x73, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x73, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69,
0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4a, 0x0a, 0x06, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x65, 0x72,
0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x76, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x73,
0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x65, 0x72, 0x76, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, 0x65, 0x73,
0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x65, 0x72, 0x76, 0x65, 0x64, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65,
0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a,
0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x26, 0x0a, 0x10, 0x53, 0x65, 0x74,
0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a,
0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x65, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69,
0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65,
0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73,
0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f,
0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72,
0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01,
0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f,
0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0xca, 0x03, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20,
0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53,
0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10,
0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0x86, 0x04, 0x0a, 0x07,
0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61,
0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d,
0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e,
0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61,
0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d,
0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a,
0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19,
0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61,
0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x74, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70,
0x74, 0x79, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12,
0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f,
0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44,
0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e,
0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x72,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f,
0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70,
0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e,
0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x6f, 0x74, 0x6f, 0x33,
0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d,
0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c,
0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
} }
var ( var (
@@ -748,7 +815,7 @@ func file_api_pb_cluster_proto_rawDescGZIP() []byte {
} }
var file_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_api_pb_cluster_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_api_pb_cluster_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_api_pb_cluster_proto_goTypes = []any{ var file_api_pb_cluster_proto_goTypes = []any{
(MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState (MachineMember_MembershipState)(0), // 0: api.MachineMember.MembershipState
(DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType (DNSRecord_RecordType)(0), // 1: api.DNSRecord.RecordType
@@ -759,40 +826,43 @@ var file_api_pb_cluster_proto_goTypes = []any{
(*RemoveMachineRequest)(nil), // 6: api.RemoveMachineRequest (*RemoveMachineRequest)(nil), // 6: api.RemoveMachineRequest
(*Domain)(nil), // 7: api.Domain (*Domain)(nil), // 7: api.Domain
(*ReserveDomainRequest)(nil), // 8: api.ReserveDomainRequest (*ReserveDomainRequest)(nil), // 8: api.ReserveDomainRequest
(*CreateDomainRecordsRequest)(nil), // 9: api.CreateDomainRecordsRequest (*SetDomainRequest)(nil), // 9: api.SetDomainRequest
(*CreateDomainRecordsResponse)(nil), // 10: api.CreateDomainRecordsResponse (*CreateDomainRecordsRequest)(nil), // 10: api.CreateDomainRecordsRequest
(*DNSRecord)(nil), // 11: api.DNSRecord (*CreateDomainRecordsResponse)(nil), // 11: api.CreateDomainRecordsResponse
(*NetworkConfig)(nil), // 12: api.NetworkConfig (*DNSRecord)(nil), // 12: api.DNSRecord
(*IP)(nil), // 13: api.IP (*NetworkConfig)(nil), // 13: api.NetworkConfig
(*MachineInfo)(nil), // 14: api.MachineInfo (*IP)(nil), // 14: api.IP
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty (*MachineInfo)(nil), // 15: api.MachineInfo
(*emptypb.Empty)(nil), // 16: google.protobuf.Empty
} }
var file_api_pb_cluster_proto_depIdxs = []int32{ var file_api_pb_cluster_proto_depIdxs = []int32{
12, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig 13, // 0: api.AddMachineRequest.network:type_name -> api.NetworkConfig
13, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP 14, // 1: api.AddMachineRequest.public_ip:type_name -> api.IP
14, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo 15, // 2: api.AddMachineResponse.machine:type_name -> api.MachineInfo
14, // 3: api.MachineMember.machine:type_name -> api.MachineInfo 15, // 3: api.MachineMember.machine:type_name -> api.MachineInfo
0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState 0, // 4: api.MachineMember.state:type_name -> api.MachineMember.MembershipState
4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember 4, // 5: api.ListMachinesResponse.machines:type_name -> api.MachineMember
11, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord 12, // 6: api.CreateDomainRecordsRequest.records:type_name -> api.DNSRecord
11, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord 12, // 7: api.CreateDomainRecordsResponse.records:type_name -> api.DNSRecord
1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType 1, // 8: api.DNSRecord.type:type_name -> api.DNSRecord.RecordType
2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest 2, // 9: api.Cluster.AddMachine:input_type -> api.AddMachineRequest
15, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty 16, // 10: api.Cluster.ListMachines:input_type -> google.protobuf.Empty
6, // 11: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest 6, // 11: api.Cluster.RemoveMachine:input_type -> api.RemoveMachineRequest
8, // 12: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest 8, // 12: api.Cluster.ReserveDomain:input_type -> api.ReserveDomainRequest
15, // 13: api.Cluster.GetDomain:input_type -> google.protobuf.Empty 9, // 13: api.Cluster.SetDomain:input_type -> api.SetDomainRequest
15, // 14: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty 16, // 14: api.Cluster.GetDomain:input_type -> google.protobuf.Empty
9, // 15: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest 16, // 15: api.Cluster.ReleaseDomain:input_type -> google.protobuf.Empty
3, // 16: api.Cluster.AddMachine:output_type -> api.AddMachineResponse 10, // 16: api.Cluster.CreateDomainRecords:input_type -> api.CreateDomainRecordsRequest
5, // 17: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse 3, // 17: api.Cluster.AddMachine:output_type -> api.AddMachineResponse
15, // 18: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty 5, // 18: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse
7, // 19: api.Cluster.ReserveDomain:output_type -> api.Domain 16, // 19: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty
7, // 20: api.Cluster.GetDomain:output_type -> api.Domain 7, // 20: api.Cluster.ReserveDomain:output_type -> api.Domain
7, // 21: api.Cluster.ReleaseDomain:output_type -> api.Domain 16, // 21: api.Cluster.SetDomain:output_type -> google.protobuf.Empty
10, // 22: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse 7, // 22: api.Cluster.GetDomain:output_type -> api.Domain
16, // [16:23] is the sub-list for method output_type 7, // 23: api.Cluster.ReleaseDomain:output_type -> api.Domain
9, // [9:16] is the sub-list for method input_type 11, // 24: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse
17, // [17:25] is the sub-list for method output_type
9, // [9:17] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name 9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee 9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name 0, // [0:9] is the sub-list for field type_name
@@ -891,7 +961,7 @@ func file_api_pb_cluster_proto_init() {
} }
} }
file_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[7].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsRequest); i { switch v := v.(*SetDomainRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -903,7 +973,7 @@ func file_api_pb_cluster_proto_init() {
} }
} }
file_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[8].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsResponse); i { switch v := v.(*CreateDomainRecordsRequest); i {
case 0: case 0:
return &v.state return &v.state
case 1: case 1:
@@ -915,6 +985,18 @@ func file_api_pb_cluster_proto_init() {
} }
} }
file_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any { file_api_pb_cluster_proto_msgTypes[9].Exporter = func(v any, i int) any {
switch v := v.(*CreateDomainRecordsResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_pb_cluster_proto_msgTypes[10].Exporter = func(v any, i int) any {
switch v := v.(*DNSRecord); i { switch v := v.(*DNSRecord); i {
case 0: case 0:
return &v.state return &v.state
@@ -927,13 +1009,14 @@ func file_api_pb_cluster_proto_init() {
} }
} }
} }
file_api_pb_cluster_proto_msgTypes[5].OneofWrappers = []any{}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_api_pb_cluster_proto_rawDesc, RawDescriptor: file_api_pb_cluster_proto_rawDesc,
NumEnums: 2, NumEnums: 2,
NumMessages: 10, NumMessages: 11,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
+9
View File
@@ -14,6 +14,7 @@ service Cluster {
rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty); rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty);
rpc ReserveDomain(ReserveDomainRequest) returns (Domain); rpc ReserveDomain(ReserveDomainRequest) returns (Domain);
rpc SetDomain(SetDomainRequest) returns (google.protobuf.Empty);
rpc GetDomain(google.protobuf.Empty) returns (Domain); rpc GetDomain(google.protobuf.Empty) returns (Domain);
rpc ReleaseDomain(google.protobuf.Empty) returns (Domain); rpc ReleaseDomain(google.protobuf.Empty) returns (Domain);
rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse); rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse);
@@ -56,12 +57,20 @@ message RemoveMachineRequest {
message Domain { message Domain {
string name = 1; string name = 1;
// Whether the domain is reserved in Uncloud DNS. Older daemons (<0.21) omit this field
// and only support reserved domains, so clients should treat absence as true.
optional bool reserved = 2;
} }
message ReserveDomainRequest { message ReserveDomainRequest {
string endpoint = 1; string endpoint = 1;
} }
message SetDomainRequest {
// An externally managed domain name. Empty clears a manually set domain.
string name = 1;
}
message CreateDomainRecordsRequest { message CreateDomainRecordsRequest {
repeated DNSRecord records = 1; repeated DNSRecord records = 1;
} }
+38
View File
@@ -24,6 +24,7 @@ const (
Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines" Cluster_ListMachines_FullMethodName = "/api.Cluster/ListMachines"
Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine" Cluster_RemoveMachine_FullMethodName = "/api.Cluster/RemoveMachine"
Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain" Cluster_ReserveDomain_FullMethodName = "/api.Cluster/ReserveDomain"
Cluster_SetDomain_FullMethodName = "/api.Cluster/SetDomain"
Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain" Cluster_GetDomain_FullMethodName = "/api.Cluster/GetDomain"
Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain" Cluster_ReleaseDomain_FullMethodName = "/api.Cluster/ReleaseDomain"
Cluster_CreateDomainRecords_FullMethodName = "/api.Cluster/CreateDomainRecords" Cluster_CreateDomainRecords_FullMethodName = "/api.Cluster/CreateDomainRecords"
@@ -37,6 +38,7 @@ type ClusterClient interface {
ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error) ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error)
RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error)
SetDomain(ctx context.Context, in *SetDomainRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error)
CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error) CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error)
@@ -90,6 +92,16 @@ func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequ
return out, nil return out, nil
} }
func (c *clusterClient) SetDomain(ctx context.Context, in *SetDomainRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Cluster_SetDomain_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *clusterClient) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) { func (c *clusterClient) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Domain) out := new(Domain)
@@ -128,6 +140,7 @@ type ClusterServer interface {
ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error)
RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error) RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error)
ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error)
SetDomain(context.Context, *SetDomainRequest) (*emptypb.Empty, error)
GetDomain(context.Context, *emptypb.Empty) (*Domain, error) GetDomain(context.Context, *emptypb.Empty) (*Domain, error)
ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error) ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error)
CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error) CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error)
@@ -153,6 +166,9 @@ func (UnimplementedClusterServer) RemoveMachine(context.Context, *RemoveMachineR
func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) { func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented") return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented")
} }
func (UnimplementedClusterServer) SetDomain(context.Context, *SetDomainRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetDomain not implemented")
}
func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) { func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDomain not implemented") return nil, status.Errorf(codes.Unimplemented, "method GetDomain not implemented")
} }
@@ -255,6 +271,24 @@ func _Cluster_ReserveDomain_Handler(srv interface{}, ctx context.Context, dec fu
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Cluster_SetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetDomainRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClusterServer).SetDomain(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Cluster_SetDomain_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClusterServer).SetDomain(ctx, req.(*SetDomainRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Cluster_GetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Cluster_GetDomain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty) in := new(emptypb.Empty)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -332,6 +366,10 @@ var Cluster_ServiceDesc = grpc.ServiceDesc{
MethodName: "ReserveDomain", MethodName: "ReserveDomain",
Handler: _Cluster_ReserveDomain_Handler, Handler: _Cluster_ReserveDomain_Handler,
}, },
{
MethodName: "SetDomain",
Handler: _Cluster_SetDomain_Handler,
},
{ {
MethodName: "GetDomain", MethodName: "GetDomain",
Handler: _Cluster_GetDomain_Handler, Handler: _Cluster_GetDomain_Handler,
+1 -1
View File
@@ -20,7 +20,7 @@ service Machine {
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines. // InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse); rpc InspectMachine(google.protobuf.Empty) returns (InspectMachineResponse);
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with // WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
// no known missing or pending transactions through those versions. // no known missing or pending transactions through those versions from active members.
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes // Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
// have been superseded. // have been superseded.
// //
+2 -2
View File
@@ -48,7 +48,7 @@ type MachineClient interface {
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines. // InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error) InspectMachine(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InspectMachineResponse, error)
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with // WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
// no known missing or pending transactions through those versions. // no known missing or pending transactions through those versions from active members.
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes // Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
// have been superseded. // have been superseded.
// //
@@ -224,7 +224,7 @@ type MachineServer interface {
// InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines. // InspectMachine retrieves detailed information about the machine. Supports broadcasting to multiple machines.
InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error) InspectMachine(context.Context, *emptypb.Empty) (*InspectMachineResponse, error)
// WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with // WaitForStoreVersion waits until the cluster store on this machine has reached each requested actor version, with
// no known missing or pending transactions through those versions. // no known missing or pending transactions through those versions from active members.
// Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes // Corrosion may satisfy a version by applying its surviving changes or by marking it complete because its changes
// have been superseded. // have been superseded.
// //
-165
View File
@@ -1,165 +0,0 @@
package caddystorage
import (
"context"
"errors"
"fmt"
"log/slog"
"slices"
"time"
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/distlock"
)
const (
lockPrefix = "caddy_storage:"
storeReplicationTimeout = 10 * time.Second
)
// Lock acquires an automatically renewed distributed lock and waits for the local store to catch up with versions
// observed on responding machines. Writers using the same lock can then read locally. Unavailable machines may have
// writes that this wait does not cover, and reads outside a lock remain eventually consistent.
func (s *Storage) Lock(ctx context.Context, name string) (err error) {
if name == "" {
return errors.New("lock name is empty")
}
// Count the call first. If Cleanup has already observed zero, Caddy has cancelled s.ctx and the check below rejects
// this call before it uses the client.
s.lockOps.Add(1)
// A failed Lock owns its lifecycle through any lease rollback. A successful Lock transfers that responsibility to
// Unlock, which keeps the client open until its release attempt finishes.
defer func() {
if err != nil {
s.lockOps.Add(-1)
}
}()
if s.ctx.Err() != nil {
return errors.New("storage is closed")
}
// Stop acquisition retries when Caddy unloads the module (cancels s.ctx), even if the caller's context
// is still active.
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
stopOnCleanup := context.AfterFunc(s.ctx, func() {
cancel(context.Cause(s.ctx))
})
defer stopOnCleanup()
log := s.log.With("lock", name)
started := time.Now()
log.Debug("acquiring lock", "ttl", time.Duration(s.LockTTL))
lease, err := s.locker.Acquire(ctx, lockPrefix+name)
if err != nil {
log.Debug("failed to acquire lock", "duration", time.Since(started), "error", err)
return fmt.Errorf("acquire lock '%s': %w", name, err)
}
log.Debug("lock lease acquired", "duration", time.Since(started))
// Release the lease if the lock acquisition fails after this point.
// Unlock will take care of releasing the lease on success.
defer func() {
if err == nil {
return
}
if releaseErr := s.releaseLock(ctx, name, lease, "cancelled acquisition"); releaseErr != nil {
err = errors.Join(err, releaseErr)
}
}()
// Catch up with the latest store versions observed on responding machines to increase the chance of reading
// the latest writes on them locally.
version, machines, err := s.clusterStoreVersion(ctx, log)
if err != nil {
return err
}
waitStarted := time.Now()
log.Debug("waiting for local store replication", "machine_names", machines, "store_version", version)
waitCtx, cancelWait := context.WithTimeout(ctx, storeReplicationTimeout)
err = s.client.WaitForStoreVersion(waitCtx, version)
cancelWait()
if err != nil {
return fmt.Errorf("wait for local store replication: %w", err)
}
log.Debug("local store replication complete", "duration", time.Since(waitStarted))
s.locksMu.Lock()
defer s.locksMu.Unlock()
if s.ctx.Err() != nil {
return errors.New("storage is closed")
}
if lost := context.Cause(lease.Context()); lost != nil {
return lost
}
if _, exists := s.locks[name]; exists {
return errors.New("lock is already tracked by this storage instance")
}
s.locks[name] = lease
log.Debug("lock acquired", "duration", time.Since(started))
return nil
}
// clusterStoreVersion returns the per-actor maximum store versions from responding machines and their names.
func (s *Storage) clusterStoreVersion(ctx context.Context, log *slog.Logger) (map[string]uint64, []string, error) {
ctx, cancel := context.WithTimeout(ctx, distlock.DefaultMaxNodeCallTimeout)
defer cancel()
resp, err := s.client.MachineClient.InspectMachine(client.ProxyMachinesContext(ctx, nil), nil)
if err != nil {
return nil, nil, fmt.Errorf("inspect machines for store versions: %w", err)
}
maxVersion := make(map[string]uint64)
machines := make([]string, 0, len(resp.Machines))
for _, m := range resp.Machines {
if m.Metadata.Error != "" {
log.Warn("skipping machine when collecting store versions",
"id", m.Metadata.MachineId, "name", m.Metadata.MachineName, "error", m.Metadata.Error)
continue
}
machines = append(machines, m.Metadata.MachineName)
for actor, v := range m.StoreVersion {
maxVersion[actor] = max(maxVersion[actor], v)
}
}
slices.Sort(machines)
return maxVersion, machines, nil
}
// Unlock releases a previously acquired distributed lock.
func (s *Storage) Unlock(ctx context.Context, name string) error {
s.locksMu.Lock()
lease, exists := s.locks[name]
if exists {
delete(s.locks, name)
}
s.locksMu.Unlock()
if !exists {
return fmt.Errorf("lock '%s' is not held by this storage instance", name)
}
defer s.lockOps.Add(-1)
// Release stops renewal even on error. Any nodes that cannot be reached will let the lease expire.
if err := s.releaseLock(ctx, name, lease, "unlock"); err != nil {
return fmt.Errorf("release lock '%s': %w", name, err)
}
return nil
}
// releaseLock releases the lease with a context that ignores cancellation and logs the release attempt.
func (s *Storage) releaseLock(ctx context.Context, name string, lease *distlock.Lease, reason string) error {
// Unlock and rollback must attempt node cleanup even if the caller or module has already been cancelled.
ctx = context.WithoutCancel(ctx)
log := s.log.With("lock", name, "reason", reason)
started := time.Now()
log.Debug("releasing lock")
if err := lease.Release(ctx); err != nil {
log.Debug("failed to release lock", "duration", time.Since(started), "error", err)
return err
}
log.Debug("lock released", "duration", time.Since(started))
return nil
}
-211
View File
@@ -1,211 +0,0 @@
// Package caddystorage provides Caddy storage backed by an Uncloud cluster.
package caddystorage
import (
"context"
"errors"
"fmt"
"log/slog"
"sync"
"sync/atomic"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/certmagic"
"github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/client/connector"
"github.com/psviderski/uncloud/pkg/distlock"
)
const (
// ModuleID is the Caddy module ID for Uncloud storage.
ModuleID = "caddy.storage.uncloud"
// DefaultSocketPath is the default path to the Uncloud API socket.
DefaultSocketPath = "/run/uncloud/api/uncloud.sock"
// DefaultLockTTL is the default duration of a distributed lock lease.
DefaultLockTTL = 20 * time.Second
// lockCleanupTimeout bounds how long an unloaded module waits for active lock operations when cleaning up.
lockCleanupTimeout = 5 * time.Minute
)
//nolint:gochecknoinits // Caddy modules must register during package initialisation.
func init() {
caddy.RegisterModule(new(Storage))
}
// Storage implements a Caddy storage backend that uses an Uncloud cluster to store assets such as TLS certificates.
type Storage struct {
// Socket is the path to the Uncloud API socket.
// Defaults to /run/uncloud/api/uncloud.sock when not set.
Socket string `json:"socket,omitempty"`
// LockTTL is the duration of a distributed lock after which it expires if not renewed. Locks renew automatically
// until unlocked. If an instance crashes or cannot renew, expiry allows another instance to acquire the stale lock.
// Longer durations tolerate longer interruptions but delay recovery after a crash. Normal unlocks release the lock
// immediately.
// Defaults to 20 seconds when not set.
LockTTL caddy.Duration `json:"lock_ttl,omitempty"`
client *client.Client
locker *distlock.Locker
log *slog.Logger
// ctx is the module context from Provision. Caddy cancels it when it unloads the module and calls Cleanup.
ctx context.Context
// locksMu protects locks.
locksMu sync.Mutex
// locks maps successfully acquired lock names to held leases.
locks map[string]*distlock.Lease
// lockOps counts Lock calls until they fail or their locks are released.
lockOps atomic.Int64
}
// CaddyModule returns the Caddy module information.
func (*Storage) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: ModuleID,
New: func() caddy.Module { return new(Storage) },
}
}
// Provision connects the storage to the local Uncloud API and initialises the distributed locker.
func (s *Storage) Provision(ctx caddy.Context) error {
s.ctx = ctx
s.log = ctx.Slogger()
if s.Socket == "" {
s.Socket = DefaultSocketPath
}
if s.LockTTL == 0 {
s.LockTTL = caddy.Duration(DefaultLockTTL)
}
if s.LockTTL < 0 {
return errors.New("lock_ttl must be positive")
}
cli, err := client.New(ctx, connector.NewUnixConnector(s.Socket))
if err != nil {
return fmt.Errorf("connect to Uncloud API: %w", err)
}
locker, err := cli.NewLocker(distlock.Config{
LeaseDuration: time.Duration(s.LockTTL),
})
if err != nil {
_ = cli.Close()
return fmt.Errorf("create distributed locker: %w", err)
}
s.client = cli
s.locker = locker
s.locks = make(map[string]*distlock.Lease)
s.log.Info("module provisioned", "socket", s.Socket, "lock_ttl", time.Duration(s.LockTTL))
return nil
}
// Cleanup keeps the Uncloud API connection open so acquired lock leases can renew until Caddy releases them. It closes
// the connection after all lock operations finish or the cleanup timeout expires.
func (s *Storage) Cleanup() error {
if s.client == nil {
// Provision failed before opening the storage.
return nil
}
started := time.Now()
s.log.Debug("cleaning up module", "locks", s.lockOps.Load())
go func() {
timer := time.NewTimer(lockCleanupTimeout)
defer timer.Stop()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
timedOut := false
for !timedOut && s.lockOps.Load() > 0 {
select {
case <-ticker.C:
case <-timer.C:
timedOut = true
}
}
if remaining := s.lockOps.Load(); timedOut && remaining > 0 {
s.log.Warn("timed out waiting for active locks to be unlocked",
"locks", remaining, "timeout", lockCleanupTimeout)
}
if err := s.client.Close(); err != nil {
s.log.Warn("failed to clean up module", "duration", time.Since(started), "error", err)
return
}
s.log.Debug("module cleanup complete", "duration", time.Since(started))
}()
return nil
}
// CertMagicStorage returns the provisioned CertMagic storage implementation.
func (s *Storage) CertMagicStorage() (certmagic.Storage, error) {
return s, nil
}
// UnmarshalCaddyfile configures Uncloud storage from the Caddyfile global storage block.
//
// {
// storage uncloud {
// socket /run/uncloud/api/uncloud.sock
// lock_ttl 20s
// }
// }
func (s *Storage) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // Skip the module name 'uncloud'.
// Reject inline arguments. NextArg leaves an opening brace for NextBlock.
if d.NextArg() {
return d.ArgErr()
}
// Read the optional options block, skipping its surrounding braces.
for d.NextBlock(0) {
switch d.Val() {
case "socket":
// Require a socket path on the same line as 'socket' option.
if !d.NextArg() {
return d.ArgErr()
}
s.Socket = d.Val()
// Reject extra arguments after the socket path.
if d.NextArg() {
return d.ArgErr()
}
case "lock_ttl":
if !d.NextArg() {
return d.ArgErr()
}
ttl, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("invalid lock_ttl '%s': %w", d.Val(), err)
}
if ttl <= 0 {
return d.Err("lock_ttl must be positive")
}
if d.NextArg() {
return d.ArgErr()
}
s.LockTTL = caddy.Duration(ttl)
default:
return d.Errf("unknown uncloud storage option: '%s'", d.Val())
}
}
return nil
}
var (
_ caddy.Module = (*Storage)(nil)
_ caddy.Provisioner = (*Storage)(nil)
_ caddy.CleanerUpper = (*Storage)(nil)
_ caddy.StorageConverter = (*Storage)(nil)
_ caddyfile.Unmarshaler = (*Storage)(nil)
_ certmagic.Storage = (*Storage)(nil)
)
-83
View File
@@ -1,83 +0,0 @@
package caddystorage
import (
"context"
"fmt"
"io/fs"
"github.com/caddyserver/certmagic"
"github.com/psviderski/uncloud/api/pb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Store writes a value to the cluster store.
func (s *Storage) Store(ctx context.Context, key string, value []byte) error {
if _, err := s.client.CaddyStorage.Store(ctx, &pb.StoreCaddyStorageRequest{Key: key, Value: value}); err != nil {
return storageError("store", key, err)
}
return nil
}
// Load reads a value from the cluster store.
func (s *Storage) Load(ctx context.Context, key string) ([]byte, error) {
resp, err := s.client.CaddyStorage.Load(ctx, &pb.LoadCaddyStorageRequest{Key: key})
if err != nil {
return nil, storageError("load", key, err)
}
return resp.Value, nil
}
// Delete removes a key and its descendants from the cluster store.
func (s *Storage) Delete(ctx context.Context, key string) error {
if _, err := s.client.CaddyStorage.Delete(ctx, &pb.DeleteCaddyStorageRequest{Key: key}); err != nil {
return storageError("delete", key, err)
}
return nil
}
// Exists reports whether a key exists in the cluster store.
func (s *Storage) Exists(ctx context.Context, key string) bool {
_, err := s.Stat(ctx, key)
return err == nil
}
// List returns keys under prefix from the cluster store.
func (s *Storage) List(ctx context.Context, prefix string, recursive bool) ([]string, error) {
resp, err := s.client.CaddyStorage.List(ctx, &pb.ListCaddyStorageRequest{
Prefix: prefix,
Recursive: recursive,
})
if err != nil {
return nil, storageError("list", prefix, err)
}
return resp.Keys, nil
}
// Stat returns information about a key in the cluster store.
func (s *Storage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error) {
resp, err := s.client.CaddyStorage.Stat(ctx, &pb.StatCaddyStorageRequest{Key: key})
if err != nil {
return certmagic.KeyInfo{}, storageError("stat", key, err)
}
info := certmagic.KeyInfo{
Key: resp.Key,
Size: resp.Size,
IsTerminal: resp.IsTerminal,
}
if resp.UpdatedAt != nil {
if err := resp.UpdatedAt.CheckValid(); err != nil {
return certmagic.KeyInfo{}, storageError("stat", key, fmt.Errorf("invalid updated_at timestamp: %w", err))
}
info.Modified = resp.UpdatedAt.AsTime()
}
return info, nil
}
func storageError(operation, key string, err error) error {
if status.Code(err) == codes.NotFound {
err = fs.ErrNotExist
}
return fmt.Errorf("uncloud storage: %s key %q: %w", operation, key, err)
}
+167
View File
@@ -0,0 +1,167 @@
package cert
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"slices"
"strings"
"time"
"charm.land/lipgloss/v2"
"github.com/caddyserver/certmagic"
"github.com/docker/go-units"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra"
)
type listOptions struct {
machine string
output string
}
func NewListCommand() *cobra.Command {
opts := listOptions{}
cmd := &cobra.Command{
Use: "ls",
Aliases: []string{"list"},
Short: "List certificates in cluster storage for Caddy.",
Long: `List certificates stored in the Uncloud cluster storage for Caddy.
Caddy must use the Uncloud storage module (https://github.com/unlabs-dev/caddy-uncloud)
configured with 'storage uncloud' in the global options for its certificates to appear here.
This inventory does not check whether Caddy currently serves or trusts a certificate.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return list(cmd.Context(), uncli, opts)
},
}
cmd.Flags().StringVarP(&opts.machine, "machine", "m", "",
"Name or ID of the machine to read certificate storage from. (default is connected machine)")
cmd.Flags().StringVarP(&opts.output, "output", "o", "",
"Output format: 'json' or empty for a human-readable table.")
completion.MachinesFlag(cmd)
return cmd
}
func list(ctx context.Context, uncli *cli.CLI, opts listOptions) error {
if opts.output != "" && opts.output != "json" {
return fmt.Errorf("unsupported output format '%s' (supported: json)", opts.output)
}
clusterClient, err := uncli.ConnectCluster(ctx)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
certs, listErr := clusterClient.Caddy.ListCertificates(ctx,
client.CaddyListCertificatesOptions{Machine: opts.machine})
if len(certs) > 0 || listErr == nil {
if err = printCertificates(os.Stdout, certs, opts.output); err != nil {
return fmt.Errorf("print Caddy certificates: %w", err)
}
}
if listErr != nil {
if len(certs) > 0 {
return fmt.Errorf("certificate list is partial: %w", listErr)
}
return fmt.Errorf("list Caddy certificates: %w", listErr)
}
return nil
}
func printCertificates(out io.Writer, certs []api.IssuedCertificate, output string) error {
type certificate struct {
api.IssuedCertificate
Fingerprint string
}
items := make([]certificate, 0, len(certs))
for _, cert := range certs {
fingerprint := cert.Fingerprint()
items = append(items, certificate{
IssuedCertificate: cert,
Fingerprint: hex.EncodeToString(fingerprint[:]),
})
}
slices.SortFunc(items, func(a, b certificate) int {
if cmp := strings.Compare(a.SAN, b.SAN); cmp != 0 {
return cmp
}
if cmp := a.Chain[0].NotAfter.Compare(b.Chain[0].NotAfter); cmp != 0 {
return cmp
}
return strings.Compare(a.Fingerprint, b.Fingerprint)
})
if output == "json" {
encoder := json.NewEncoder(out)
encoder.SetIndent("", " ")
if err := encoder.Encode(items); err != nil {
return fmt.Errorf("marshal certificates: %w", err)
}
return nil
}
if len(items) == 0 {
_, err := fmt.Fprintln(out, "No Caddy certificates found in cluster storage.")
return err
}
t := tui.NewTable()
t.Headers("ID", "NAME", "ISSUER", "EXPIRES")
for _, item := range items {
t.Row(
item.Fingerprint[:12],
item.SAN,
formatIssuer(item.IssuerData),
formatExpiry(item.Chain[0].NotAfter),
)
}
_, err := lipgloss.Fprintln(out, t)
return err
}
func formatIssuer(data api.CertificateIssuerData) string {
if data.ACME == nil {
return "unknown"
}
switch data.ACME.CA {
case certmagic.LetsEncryptProductionCA:
return "Let's Encrypt"
case certmagic.LetsEncryptStagingCA:
return "Let's Encrypt (staging)"
case certmagic.ZeroSSLProductionCA:
return "ZeroSSL"
case certmagic.GoogleTrustProductionCA:
return "Google Trust Services"
case certmagic.GoogleTrustStagingCA:
return "Google Trust Services (staging)"
default:
return data.ACME.CA
}
}
func formatExpiry(expires time.Time) string {
delta := expires.Sub(time.Now())
if delta <= 0 {
return fmt.Sprintf("%s (expired %s ago)", expires.UTC().Format(time.DateOnly),
strings.ToLower(units.HumanDuration(-delta)))
}
return fmt.Sprintf("%s (%s)", expires.UTC().Format(time.DateOnly),
strings.ToLower(units.HumanDuration(delta)))
}
+128
View File
@@ -0,0 +1,128 @@
package cert
import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/json"
"strings"
"testing"
"time"
"github.com/caddyserver/certmagic"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPrintCertificates(t *testing.T) {
now := time.Now().UTC().Truncate(time.Hour)
certs := []api.IssuedCertificate{
testIssuedCertificate("z.example.com", "z", now.Add(48*time.Hour), certmagic.LetsEncryptProductionCA),
testIssuedCertificate("a.example.com", "old", now.Add(-48*time.Hour), certmagic.LetsEncryptStagingCA),
testIssuedCertificate("a.example.com", "new", now.Add(72*time.Hour), "https://ca.example/directory"),
testIssuedCertificate("unknown.example.com", "unknown", now.Add(96*time.Hour), ""),
}
var table bytes.Buffer
require.NoError(t, printCertificates(&table, certs, ""))
output := table.String()
assert.Contains(t, output, "ID")
assert.Contains(t, output, "NAME")
assert.Contains(t, output, "ISSUER")
assert.Contains(t, output, "EXPIRES")
assert.Contains(t, output, now.Add(-48*time.Hour).Format(time.DateOnly)+" (expired ")
assert.Contains(t, output, now.Add(48*time.Hour).Format(time.DateOnly)+" (")
assert.Contains(t, output, "Let's Encrypt (staging)")
assert.Contains(t, output, "https://ca.example/directory")
assert.Less(t, strings.Index(output, now.Add(-48*time.Hour).Format(time.DateOnly)),
strings.Index(output, now.Add(72*time.Hour).Format(time.DateOnly)))
assert.Less(t, strings.Index(output, "a.example.com"), strings.Index(output, "z.example.com"))
assert.Contains(t, output, testFingerprint("old")[:12])
var encoded bytes.Buffer
require.NoError(t, printCertificates(&encoded, certs, "json"))
var items []struct {
api.IssuedCertificate
Fingerprint string
}
require.NoError(t, json.Unmarshal(encoded.Bytes(), &items))
require.Len(t, items, 4)
assert.Equal(t, testFingerprint("old"), items[0].Fingerprint)
assert.Equal(t, "a.example.com", items[0].SAN)
require.Len(t, items[0].Chain, 1)
assert.Equal(t, []byte("old"), items[0].Chain[0].Raw)
assert.Equal(t, now.Add(-48*time.Hour), items[0].Chain[0].NotAfter)
require.NotNil(t, items[0].IssuerData.ACME)
assert.Equal(t, certmagic.LetsEncryptStagingCA, items[0].IssuerData.ACME.CA)
assert.Equal(t, testFingerprint("new"), items[1].Fingerprint)
require.NotNil(t, items[1].IssuerData.ACME)
assert.Equal(t, "https://ca.example/directory", items[1].IssuerData.ACME.CA)
assert.Nil(t, items[2].IssuerData.ACME)
assert.Equal(t, "z.example.com", items[3].SAN)
}
func TestPrintCertificates_Empty(t *testing.T) {
for _, tt := range []struct {
output string
want string
}{
{output: "", want: "No Caddy certificates found in cluster storage.\n"},
{output: "json", want: "[]\n"},
} {
var out bytes.Buffer
require.NoError(t, printCertificates(&out, nil, tt.output))
assert.Equal(t, tt.want, out.String())
}
}
func TestFormatIssuer(t *testing.T) {
for _, tt := range []struct {
name string
ca string
want string
}{
{name: "production", ca: certmagic.LetsEncryptProductionCA, want: "Let's Encrypt"},
{name: "staging", ca: certmagic.LetsEncryptStagingCA, want: "Let's Encrypt (staging)"},
{name: "ZeroSSL", ca: certmagic.ZeroSSLProductionCA, want: "ZeroSSL"},
{name: "Google Trust Services", ca: certmagic.GoogleTrustProductionCA, want: "Google Trust Services"},
{name: "Google Trust Services staging", ca: certmagic.GoogleTrustStagingCA, want: "Google Trust Services (staging)"},
{name: "other", ca: "https://ca.example/directory", want: "https://ca.example/directory"},
} {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, formatIssuer(api.CertificateIssuerData{
ACME: &api.ACMEIssuerData{CA: tt.ca},
}))
})
}
assert.Equal(t, "unknown", formatIssuer(api.CertificateIssuerData{}))
}
func TestFormatExpiry_UTC(t *testing.T) {
now := time.Now().UTC()
brisbane := time.FixedZone("AEST", 10*60*60)
expires := time.Date(now.Year(), now.Month(), now.Day()+3, 3, 0, 0, 0, brisbane)
assert.True(t, strings.HasPrefix(formatExpiry(expires), expires.UTC().Format(time.DateOnly)+" ("))
assert.NotEqual(t, expires.Format(time.DateOnly), expires.UTC().Format(time.DateOnly))
expired := now.Add(-2 * time.Hour)
assert.True(t, strings.HasPrefix(formatExpiry(expired), expired.UTC().Format(time.DateOnly)+" (expired "))
assert.Contains(t, formatExpiry(expired), " ago)")
}
func testIssuedCertificate(name, raw string, expires time.Time, ca string) api.IssuedCertificate {
cert := api.IssuedCertificate{
SAN: name,
Chain: []*x509.Certificate{{Raw: []byte(raw), NotAfter: expires}},
}
if ca != "" {
cert.IssuerData.ACME = &api.ACMEIssuerData{CA: ca}
}
return cert
}
func testFingerprint(raw string) string {
sum := sha256.Sum256([]byte(raw))
return hex.EncodeToString(sum[:])
}
+12
View File
@@ -0,0 +1,12 @@
package cert
import "github.com/spf13/cobra"
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cert",
Short: "Inspect certificates in cluster storage for Caddy.",
}
cmd.AddCommand(NewListCommand())
return cmd
}
+8 -6
View File
@@ -8,6 +8,8 @@ import (
"github.com/alecthomas/chroma/v2/quick" "github.com/alecthomas/chroma/v2/quick"
"github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/internal/cli/completion"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -46,12 +48,7 @@ func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
} }
defer clusterClient.Close() defer clusterClient.Close()
if opts.machine != "" { config, err := clusterClient.Caddy.Config(ctx, client.CaddyConfigOptions{Machine: opts.machine})
// If a specific machine is requested, use it to get the Caddy configuration.
ctx = clusterClient.ProxySingleMachineContext(ctx, opts.machine)
}
config, err := clusterClient.Caddy.GetConfig(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf("get Caddy config: %w", err) return fmt.Errorf("get Caddy config: %w", err)
} }
@@ -66,5 +63,10 @@ func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
} }
} }
if config.LastReconciliationError != "" {
tui.PrintWarning(fmt.Sprintf("last Caddy config load failed: %s\nShowing the last saved Caddyfile.",
config.LastReconciliationError))
}
return nil return nil
} }
+8 -2
View File
@@ -110,7 +110,9 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
placement := api.Placement{ placement := api.Placement{
Machines: cli.ExpandCommaSeparatedValues(opts.machines), Machines: cli.ExpandCommaSeparatedValues(opts.machines),
} }
d, err := clusterClient.NewCaddyDeployment(opts.image, caddyfile, placement) d, err := clusterClient.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{
Image: opts.image, Config: caddyfile, Placement: placement,
})
if err != nil { if err != nil {
return fmt.Errorf("create caddy deployment: %w", err) return fmt.Errorf("create caddy deployment: %w", err)
} }
@@ -202,6 +204,10 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
} }
return fmt.Errorf("get cluster domain: %w", err) return fmt.Errorf("get cluster domain: %w", err)
} }
if !domain.Reserved {
fmt.Printf("Skipping DNS records update as cluster domain '%s' is managed externally.\n", domain.Name)
return nil
}
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...") fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...")
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation. // TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
@@ -216,7 +222,7 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
if errors.Is(err, client.ErrNoReachableMachines) { if errors.Is(err, client.ErrNoReachableMachines) {
fmt.Println() fmt.Println()
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+ fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
"machines running caddy containers.\n", domain) "machines running caddy containers.\n", domain.Name)
fmt.Println() fmt.Println()
fmt.Println("Possible solutions:") fmt.Println("Possible solutions:")
fmt.Println("- Ensure your machines have public IP addresses") fmt.Println("- Ensure your machines have public IP addresses")
+2
View File
@@ -1,6 +1,7 @@
package caddy package caddy
import ( import (
"github.com/psviderski/uncloud/cmd/uc/caddy/cert"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -10,6 +11,7 @@ func NewRootCommand() *cobra.Command {
Short: "Manage Caddy reverse proxy service.", Short: "Manage Caddy reverse proxy service.",
} }
cmd.AddCommand( cmd.AddCommand(
cert.NewRootCommand(),
NewConfigCommand(), NewConfigCommand(),
NewDeployCommand(), NewDeployCommand(),
NewLogsCommand(), NewLogsCommand(),
+1
View File
@@ -37,6 +37,7 @@ func release(ctx context.Context, uncli *cli.CLI) error {
if status.Convert(err).Code() == codes.NotFound { if status.Convert(err).Code() == codes.NotFound {
return errors.New("no domain reserved") return errors.New("no domain reserved")
} }
return fmt.Errorf("release cluster domain: %w", err)
} }
fmt.Printf("Released cluster domain: %s\n", domain.Name) fmt.Printf("Released cluster domain: %s\n", domain.Name)
+2 -1
View File
@@ -49,7 +49,8 @@ func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint}) domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
if err != nil { if err != nil {
if status.Convert(err).Code() == codes.AlreadyExists { if status.Convert(err).Code() == codes.AlreadyExists {
return errors.New("domain already reserved") return errors.New("cluster domain already configured, unset it with 'uc dns set \"\"' " +
"or release a reservation with 'uc dns release' first")
} }
return err return err
} }
+6 -3
View File
@@ -7,16 +7,19 @@ import (
func NewRootCommand() *cobra.Command { func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "dns", Use: "dns",
Short: "Manage cluster domain in Uncloud DNS.", Short: "Manage the cluster domain.",
Long: "Manage cluster domain in Uncloud DNS.\n" + Long: "Manage the cluster domain.\n" +
"DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain for your " + "DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain for your " +
"cluster. When reserved, Caddy service deployments will automatically update DNS records to route " + "cluster. When reserved, Caddy service deployments will automatically update DNS records to route " +
"traffic to the services in the cluster.", "traffic to the services in the cluster.\n\n" +
"EXPERIMENTAL: Use 'uc dns set' to configure an externally managed domain instead. " +
"Uncloud does not manage DNS records for manually set domains.",
} }
cmd.AddCommand( cmd.AddCommand(
NewReleaseCommand(), NewReleaseCommand(),
NewReserveCommand(), NewReserveCommand(),
NewShowCommand(), NewShowCommand(),
NewSetCommand(),
) )
return cmd return cmd
} }
+72
View File
@@ -0,0 +1,72 @@
package dns
import (
"context"
"errors"
"fmt"
"github.com/miekg/dns"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/grpcversion"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func NewSetCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "set DOMAIN_NAME",
Args: cobra.ExactArgs(1),
Short: "Set or unset an externally managed cluster domain (EXPERIMENTAL).",
Long: "EXPERIMENTAL: Set the cluster domain used to generate ingress hostnames for services.\n" +
"Configure wildcard DNS records for this domain with your DNS provider. " +
"Uncloud will not create, verify, update, or delete external DNS records.\n" +
"Pass an empty string to unset a manually set domain. " +
"Use 'uc dns release' to release a domain reserved in Uncloud DNS. " +
"Setting or unsetting the domain does not change existing service hostnames.",
Example: " uc dns set apps.example.com\n uc dns set \"\"",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return set(cmd.Context(), uncli, args[0])
},
}
return cmd
}
func set(ctx context.Context, uncli *cli.CLI, name string) error {
if name != "" {
if labels, ok := dns.IsDomainName(name); !ok || labels < 2 {
return fmt.Errorf("invalid cluster domain %q: must be a valid domain name with at least two labels", name)
}
}
tui.PrintWarning("Setting an externally managed cluster domain is experimental.")
clusterClient, err := uncli.ConnectCluster(ctx)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
ctx = metadata.AppendToOutgoingContext(ctx, grpcversion.MetadataKeyMinServerVersion, "0.21.0")
_, err = clusterClient.SetDomain(ctx, &pb.SetDomainRequest{Name: name})
if err != nil {
if status.Convert(err).Code() == codes.AlreadyExists {
return errors.New("cluster domain already configured, unset it with 'uc dns set \"\"' or " +
"release a reservation with 'uc dns release' first")
}
return fmt.Errorf("set cluster domain: %w", err)
}
if name == "" {
fmt.Println("Unset cluster domain. External DNS records have not been changed.")
return nil
}
fmt.Printf("Set cluster domain: %s\n", name)
fmt.Println("DNS is managed externally. " +
"Configure wildcard DNS records pointing to machines running the Caddy service.")
return nil
}
+2 -2
View File
@@ -33,11 +33,11 @@ func show(ctx context.Context, uncli *cli.CLI) error {
domain, err := clusterClient.GetDomain(ctx) domain, err := clusterClient.GetDomain(ctx)
if err != nil { if err != nil {
if errors.Is(err, api.ErrNotFound) { if errors.Is(err, api.ErrNotFound) {
return errors.New("no domain reserved") return errors.New("no cluster domain configured")
} }
return err return err
} }
fmt.Println(domain) fmt.Println(domain.Name)
return nil return nil
} }
+1 -1
View File
@@ -219,7 +219,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
fmt.Println() fmt.Println()
fmt.Println("Preparing Caddy deployment...") fmt.Println("Preparing Caddy deployment...")
d, err := clusterClient.NewCaddyDeployment(caddyImage, "", api.Placement{}) d, err := clusterClient.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{Image: caddyImage})
if err != nil { if err != nil {
return fmt.Errorf("create caddy deployment: %w", err) return fmt.Errorf("create caddy deployment: %w", err)
} }
+7 -7
View File
@@ -16,7 +16,7 @@ import (
"github.com/psviderski/uncloud/internal/cli/tui" "github.com/psviderski/uncloud/internal/cli/tui"
"github.com/psviderski/uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -221,17 +221,17 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
initOpts.WireguardEndpoints = endpoints initOpts.WireguardEndpoints = endpoints
} }
client, err := uncli.InitCluster(ctx, initOpts) clusterClient, err := uncli.InitCluster(ctx, initOpts)
if err != nil { if err != nil {
return err return err
} }
defer client.Close() defer clusterClient.Close()
// Since the cluster API needs a few moments to become ready after cluster initialisation, // Since the cluster API needs a few moments to become ready after cluster initialisation,
// we keep the user informed during this wait. We wait here even if no Caddy or DNS is requested // we keep the user informed during this wait. We wait here even if no Caddy or DNS is requested
// as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init. // as the cluster needs to be ready so that commands such as 'uc machine ls' work immediately after init.
err = tui.RunSpinner(ctx, "Waiting for the cluster to be ready...", func(ctx context.Context) error { err = tui.RunSpinner(ctx, "Waiting for the cluster to be ready...", func(ctx context.Context) error {
return client.WaitClusterReady(ctx, 1*time.Minute) return clusterClient.WaitClusterReady(ctx, 1*time.Minute)
}) })
if err != nil { if err != nil {
return fmt.Errorf("wait for cluster to be ready: %w", err) return fmt.Errorf("wait for cluster to be ready: %w", err)
@@ -245,7 +245,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
fmt.Println() fmt.Println()
if !opts.noDNS { if !opts.noDNS {
domain, err := client.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint}) domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.dnsEndpoint})
if err != nil { if err != nil {
return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err) return fmt.Errorf("reserve cluster domain in Uncloud DNS: %w", err)
} }
@@ -253,7 +253,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
} }
if !opts.noCaddy { if !opts.noCaddy {
d, err := client.NewCaddyDeployment("", "", api.Placement{}) d, err := clusterClient.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{})
if err != nil { if err != nil {
return fmt.Errorf("create caddy deployment: %w", err) return fmt.Errorf("create caddy deployment: %w", err)
} }
@@ -269,7 +269,7 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
} }
fmt.Println() fmt.Println()
return caddy.UpdateDomainRecords(ctx, client, uncli.ProgressOut()) return caddy.UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
} }
return nil return nil
+1
View File
@@ -122,6 +122,7 @@ func NewRunCommand(groupID string) *cobra.Command {
"Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+ "Mount a data volume or host path into service containers. Service containers will be scheduled on the machine(s) where\n"+
"the volume is located. Can be specified multiple times.\n"+ "the volume is located. Can be specified multiple times.\n"+
"Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+ "Format: volume_name:/container/path[:ro|volume-nocopy] or /host/path:/container/path[:ro]\n"+
"Host and container paths support runtime templates such as {{.Container.Name}}.\n"+
"Examples:\n"+ "Examples:\n"+
" -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+ " -v postgres-data:/var/lib/postgresql/data Mount volume 'postgres-data' to /var/lib/postgresql/data in container\n"+
" -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+ " -v /data/uploads:/app/uploads Bind mount /data/uploads host directory to /app/uploads in container\n"+
+22 -22
View File
@@ -1,6 +1,6 @@
module github.com/psviderski/uncloud/experiment module github.com/psviderski/uncloud/experiment
go 1.26 go 1.26.0
require ( require (
github.com/dgraph-io/badger/v3 v3.2103.5 github.com/dgraph-io/badger/v3 v3.2103.5
@@ -18,10 +18,10 @@ require (
github.com/psviderski/uncloud v0.0.0 github.com/psviderski/uncloud v0.0.0
github.com/siderolabs/discovery-api v0.1.4 github.com/siderolabs/discovery-api v0.1.4
github.com/siderolabs/discovery-client v0.1.9 github.com/siderolabs/discovery-client v0.1.9
go.uber.org/zap v1.27.0 go.uber.org/zap v1.28.0
golang.org/x/net v0.43.0 golang.org/x/net v0.59.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
google.golang.org/grpc v1.74.2 google.golang.org/grpc v1.81.0
) )
require ( require (
@@ -79,18 +79,18 @@ require (
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect github.com/jbenet/goprocess v0.1.4 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.19.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect
github.com/libp2p/go-libp2p v0.35.4 // indirect github.com/libp2p/go-libp2p v0.35.4 // indirect
github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect github.com/libp2p/go-libp2p-pubsub v0.11.0 // indirect
github.com/libp2p/go-msgio v0.3.0 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/socket v0.5.1 // indirect
github.com/miekg/dns v1.1.65 // indirect github.com/miekg/dns v1.1.73 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/cli v1.1.5 // indirect github.com/mitchellh/cli v1.1.5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
@@ -113,30 +113,30 @@ require (
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/siderolabs/gen v0.4.8 // indirect github.com/siderolabs/gen v0.4.8 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cast v1.7.0 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.41.0 // indirect golang.org/x/crypto v0.57.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/mod v0.27.0 // indirect golang.org/x/mod v0.41.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.45.0 // indirect golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.11.0 // indirect golang.org/x/time v0.16.0 // indirect
golang.org/x/tools v0.36.0 // indirect golang.org/x/tools v0.50.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.12 // indirect
gotest.tools/v3 v3.5.2 // indirect gotest.tools/v3 v3.5.2 // indirect
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect
+21
View File
@@ -302,8 +302,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
@@ -349,6 +351,7 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
@@ -368,6 +371,7 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
@@ -529,6 +533,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
@@ -581,16 +586,20 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
@@ -607,6 +616,7 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -622,6 +632,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
@@ -635,6 +646,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -654,6 +666,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -666,6 +679,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -700,6 +714,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -711,8 +726,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -727,6 +744,7 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -745,6 +763,7 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
@@ -753,6 +772,7 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -764,6 +784,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+7 -69
View File
@@ -4,7 +4,7 @@ go 1.26.0
require ( require (
charm.land/bubbles/v2 v2.0.0 charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.2 charm.land/bubbletea/v2 v2.0.10
charm.land/huh/v2 v2.0.1 charm.land/huh/v2 v2.0.1
charm.land/lipgloss/v2 v2.0.1 charm.land/lipgloss/v2 v2.0.1
github.com/BurntSushi/toml v1.6.0 github.com/BurntSushi/toml v1.6.0
@@ -34,6 +34,7 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-shellwords v1.0.12 github.com/mattn/go-shellwords v1.0.12
github.com/mholt/acmez/v3 v3.1.6
github.com/miekg/dns v1.1.73 github.com/miekg/dns v1.1.73
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/moby/term v0.5.2 github.com/moby/term v0.5.2
@@ -61,27 +62,14 @@ require (
) )
require ( require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/bigmod v0.1.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect
github.com/KimMachineGun/automemlimit v0.7.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.13.0 // indirect github.com/Microsoft/hcsshim v0.13.0 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect github.com/aws/aws-sdk-go-v2 v1.41.7 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect
@@ -101,20 +89,16 @@ require (
github.com/buger/goterm v1.0.4 // indirect github.com/buger/goterm v1.0.4 // indirect
github.com/caddyserver/zerossl v0.1.5 // indirect github.com/caddyserver/zerossl v0.1.5 // indirect
github.com/catppuccin/go v0.3.0 // indirect github.com/catppuccin/go v0.3.0 // indirect
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/containerd/cgroups/v3 v3.0.5 // indirect github.com/containerd/cgroups/v3 v3.0.5 // indirect
github.com/containerd/console v1.0.5 // indirect github.com/containerd/console v1.0.5 // indirect
github.com/containerd/containerd/api v1.9.0 // indirect github.com/containerd/containerd/api v1.9.0 // indirect
@@ -127,13 +111,8 @@ require (
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/ttrpc v1.2.7 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/distribution/v3 v3.0.0 // indirect github.com/distribution/distribution/v3 v3.0.0 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect
@@ -151,14 +130,11 @@ require (
github.com/fsnotify/fsevents v0.2.0 // indirect github.com/fsnotify/fsevents v0.2.0 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/flock v0.12.1 // indirect
@@ -166,14 +142,9 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/cel-go v0.28.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
@@ -184,15 +155,11 @@ require (
github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huandu/xstrings v1.5.0 // indirect github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.2 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect github.com/josharian/native v1.1.0 // indirect
@@ -205,7 +172,6 @@ require (
github.com/libdns/libdns v1.1.1 // indirect github.com/libdns/libdns v1.1.1 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect
@@ -213,13 +179,9 @@ require (
github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/socket v0.5.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v3 v3.1.6 // indirect
github.com/miekg/pkcs11 v1.1.2 // indirect github.com/miekg/pkcs11 v1.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/buildkit v0.25.0 // indirect github.com/moby/buildkit v0.25.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect github.com/moby/go-archive v0.1.0 // indirect
@@ -245,9 +207,7 @@ require (
github.com/opencontainers/runtime-spec v1.2.1 // indirect github.com/opencontainers/runtime-spec v1.2.1 // indirect
github.com/opencontainers/selinux v1.12.0 // indirect github.com/opencontainers/selinux v1.12.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pires/go-proxyproto v0.12.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/prometheus/client_model v0.6.3 // indirect github.com/prometheus/client_model v0.6.3 // indirect
@@ -261,28 +221,17 @@ require (
github.com/redis/go-redis/v9 v9.7.3 // indirect github.com/redis/go-redis/v9 v9.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/slackhq/nebula v1.10.3 // indirect github.com/spf13/afero v1.14.0 // indirect
github.com/smallstep/certificates v0.30.2 // indirect github.com/spf13/viper v1.12.0 // indirect
github.com/smallstep/cli-utils v0.12.2 // indirect
github.com/smallstep/linkedca v0.25.0 // indirect
github.com/smallstep/nosql v0.8.0 // indirect
github.com/smallstep/pkcs7 v0.2.1 // indirect
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect
github.com/smallstep/truststore v0.13.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect github.com/subosito/gotenv v1.4.1 // indirect
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
@@ -290,7 +239,6 @@ require (
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/urfave/cli v1.22.17 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect github.com/vbatts/tar-split v0.12.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
@@ -298,7 +246,6 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zclconf/go-cty v1.17.0 // indirect github.com/zclconf/go-cty v1.17.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect
@@ -325,29 +272,22 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.step.sm/crypto v0.81.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.6.0 // indirect go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.28.0 // indirect go.uber.org/zap v1.28.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/text v0.42.0 // indirect golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.16.0 // indirect golang.org/x/time v0.16.0 // indirect
golang.org/x/tools v0.50.0 // indirect golang.org/x/tools v0.50.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/api v0.277.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
k8s.io/api v0.32.3 // indirect k8s.io/api v0.32.3 // indirect
k8s.io/apimachinery v0.32.3 // indirect k8s.io/apimachinery v0.32.3 // indirect
k8s.io/client-go v0.32.3 // indirect k8s.io/client-go v0.32.3 // indirect
@@ -361,5 +301,3 @@ require (
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect
) )
replace charm.land/bubbletea/v2 => github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1
+4 -249
View File
@@ -1,32 +1,14 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.10 h1:oolvo20VBpI0PfqE7iFjkZ1bx0WpmXGfnKz5Yldjq5o=
charm.land/bubbletea/v2 v2.0.10/go.mod h1:QOatcnhOjYIfxzUSTz6raF7Ex4R/rIuHa3SnBdCCpMc=
charm.land/huh/v2 v2.0.1 h1:9vhBjlIDuikdPKH+qnoG++GERVxqY0Lkv14xW57lj98= charm.land/huh/v2 v2.0.1 h1:9vhBjlIDuikdPKH+qnoG++GERVxqY0Lkv14xW57lj98=
charm.land/huh/v2 v2.0.1/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= charm.land/huh/v2 v2.0.1/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys= charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE=
cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U=
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
@@ -34,30 +16,19 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
github.com/DeRuina/timberjack v1.4.2/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e h1:rd4bOvKmDIx0WeTv9Qz+hghsgyjikFiPrseXHlKepO0=
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw= github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e/go.mod h1:blbwPQh4DTlCZEfk1BLU4oMIhLda2U+A840Uag9DsZw=
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@@ -66,9 +37,6 @@ github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWS
github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ= github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ=
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
@@ -83,15 +51,10 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
@@ -112,8 +75,6 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZL
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 h1:zuSf4olLKZW8cF/W9Y5wvGT+/0raY/3kVp49KsGs0QY=
github.com/aws/aws-sdk-go-v2/service/kms v1.51.1/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
@@ -158,22 +119,18 @@ github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7l
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654 h1:FpSYhY28ucg9ZRr+2wj67FAQ0Ey5yiK0072PmRDJNek= github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 h1:3FmWoGNWK4STvqg0O0Aeav2T7rodWJAPeF0QpH+8gFw=
github.com/charmbracelet/ultraviolet v0.0.0-20260525132238-948f4557a654/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY= github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
@@ -194,15 +151,6 @@ github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw= github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y= github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
@@ -210,8 +158,6 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
@@ -247,14 +193,8 @@ github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRq
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -269,17 +209,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=
@@ -317,7 +246,6 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
@@ -332,8 +260,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c= github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c=
github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w= github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -343,8 +269,6 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -403,13 +327,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
@@ -422,33 +339,20 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.4.8 h1:V4oIYyAD3BykOycwYQzO29WefDouQMTsYZqmG3HxOfM=
github.com/google/go-tpm-tools v0.4.8/go.mod h1:4DfiOtiS1KppJjwf1+tqtW4K3PrCJjAAqFKj/TYTJKg=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -473,7 +377,6 @@ github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8z
github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -481,8 +384,6 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@@ -492,15 +393,6 @@ github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaB
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 h1:i2fYnDurfLlJH8AyyMOnkLHnHeP8Ff/DDpuZA/D3bPo= github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 h1:i2fYnDurfLlJH8AyyMOnkLHnHeP8Ff/DDpuZA/D3bPo=
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg= github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
@@ -524,7 +416,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
@@ -533,7 +424,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -559,13 +449,10 @@ github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfs
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -598,20 +485,13 @@ github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ=
github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/buildkit v0.25.0 h1:cRgh74ymzyHxS5a/lsYT4OCyVU8iC3UgkwasIEUi0og= github.com/moby/buildkit v0.25.0 h1:cRgh74ymzyHxS5a/lsYT4OCyVU8iC3UgkwasIEUi0og=
github.com/moby/buildkit v0.25.0/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc= github.com/moby/buildkit v0.25.0/go.mod h1:phM8sdqnvgK2y1dPDnbwI6veUCXHOZ6KFSl6E164tkc=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -682,18 +562,10 @@ github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0ht
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -701,8 +573,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -749,25 +619,16 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA= github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA=
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk= github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk=
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU=
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/siderolabs/grpc-proxy v0.5.1 h1:WTZYLMPTZPt43BzEJ02LT9kYA9qAfquWwCezc6NPPYE= github.com/siderolabs/grpc-proxy v0.5.1 h1:WTZYLMPTZPt43BzEJ02LT9kYA9qAfquWwCezc6NPPYE=
github.com/siderolabs/grpc-proxy v0.5.1/go.mod h1:EQwE87LiWxhiIUPBeWmpjJb9DIWxWID8R6ARtdTC+8A= github.com/siderolabs/grpc-proxy v0.5.1/go.mod h1:EQwE87LiWxhiIUPBeWmpjJb9DIWxWID8R6ARtdTC+8A=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
@@ -777,53 +638,24 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU=
github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/certificates v0.30.2 h1:1G3xBi8sJ740iA1mMPW2Svv7EIZKJ4Zf/iQtA5QlN0Y=
github.com/smallstep/certificates v0.30.2/go.mod h1:oyaE/aEYUGDr+YiCZLAxxP22bOQqcSHTeDgp8Vv2rlY=
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
github.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A=
github.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo=
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=
github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -831,7 +663,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -839,20 +670,13 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 h1:RnBbFMmodYzhC6adOjTbtUQXyzV8dcvKYbolzs6Qch0=
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747/go.mod h1:ejPAJui3kVK4u5TgMtqtXlWf5HnKh9fLy5kvpaeuas0=
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA=
@@ -867,11 +691,6 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1 h1:898LVw5QvmRX/y+pgzCo2pE/NbN9CZfdpiVnzAeAFDY=
github.com/unlabs-dev/bubbletea/v2 v2.0.8-uncloud.1/go.mod h1:DGW2q8gvzHnOpMpZTORs0aySVHCox5C+2Svk0fci1qs=
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@@ -884,7 +703,6 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -896,8 +714,6 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
@@ -952,10 +768,6 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.step.sm/crypto v0.81.0 h1:e+ouzpNt3Xm4dp7HGXhgYB5y4iFik3vh3phHKWmvugU=
go.step.sm/crypto v0.81.0/go.mod h1:fsTizqQeASjTXnbv9O00XtRlIuXRkCdoRiJNyXGQujc=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
@@ -974,7 +786,6 @@ go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -982,14 +793,8 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
@@ -999,10 +804,6 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1019,11 +820,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1036,23 +832,15 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1061,44 +849,24 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
@@ -1112,9 +880,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1127,15 +892,11 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk=
google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
@@ -1148,8 +909,6 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1166,7 +925,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -1181,7 +939,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM= gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -1195,8 +952,6 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
+1 -1
View File
@@ -458,7 +458,7 @@ func (cli *CLI) AddMachine(ctx context.Context, opts AddMachineOptions) (_ *clie
} }
// Snapshot the cluster store version so the new machine can catch up before participating. // Snapshot the cluster store version so the new machine can catch up before participating.
var storeVersion map[string]uint64 var storeVersion api.StoreVersion
inspectResp, err = c.MachineClient.InspectMachine(ctx, &emptypb.Empty{}) inspectResp, err = c.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
if err != nil { if err != nil {
// TODO(lhf): remove Unimplemented check when v0.17.0 is released. // TODO(lhf): remove Unimplemented check when v0.17.0 is released.
+11 -8
View File
@@ -45,7 +45,17 @@ func connectClusterWithProgress(ctx context.Context, conn config.MachineConnecti
} }
// Run the connection TUI model. Render to stderr so stdout stays clean for command output. // Run the connection TUI model. Render to stderr so stdout stays clean for command output.
p := tea.NewProgram(newConnectModel(ctx, conn), tea.WithOutput(os.Stderr)) p := tea.NewProgram(newConnectModel(ctx, conn),
tea.WithOutput(os.Stderr),
tea.WithInput(nil),
// Ctrl+C handling
tea.WithFilter(func(_ tea.Model, msg tea.Msg) tea.Msg {
if _, ok := msg.(tea.InterruptMsg); ok {
return connectResultMsg{err: errors.New("connection cancelled")}
}
return msg
}),
)
model, err := p.Run() model, err := p.Run()
if err != nil { if err != nil {
return nil, fmt.Errorf("run connection TUI: %w", err) return nil, fmt.Errorf("run connection TUI: %w", err)
@@ -190,13 +200,6 @@ func (m connectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg) m.spinner, cmd = m.spinner.Update(msg)
return m, cmd return m, cmd
case tea.KeyPressMsg:
if msg.String() == "ctrl+c" {
m.result.err = fmt.Errorf("connection cancelled")
m.done = true
return m, tea.Quit
}
} }
return m, nil return m, nil
+2
View File
@@ -31,6 +31,8 @@ func RunSpinner(ctx context.Context, title string, action func(ctx context.Conte
} }
})). })).
WithOutput(os.Stderr). WithOutput(os.Stderr).
// A spinner reads no keys, saying so makes Bubble Tea skip unnecessary terminal capability queries.
WithInput(nil).
Context(ctx). Context(ctx).
ActionWithErr(action). ActionWithErr(action).
Run() Run()
+8 -1
View File
@@ -13,6 +13,8 @@ import (
"time" "time"
) )
const adminCommandTimeout = 5 * time.Second
// AdminClient is a client for the Corrosion admin API. // AdminClient is a client for the Corrosion admin API.
type AdminClient struct { type AdminClient struct {
sockPath string sockPath string
@@ -32,10 +34,15 @@ type Response struct {
// The channel will be closed after sending the last or error response. The caller must read from the channel until // The channel will be closed after sending the last or error response. The caller must read from the channel until
// it is closed. // it is closed.
func (c *AdminClient) SendCommand(cmd []byte) (<-chan Response, error) { func (c *AdminClient) SendCommand(cmd []byte) (<-chan Response, error) {
conn, err := net.Dial("unix", c.sockPath) // TODO: Accept contexts in admin methods so callers can cancel requests and set their deadlines.
conn, err := net.DialTimeout("unix", c.sockPath, adminCommandTimeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("connect to admin socket: %w", err) return nil, fmt.Errorf("connect to admin socket: %w", err)
} }
if err = conn.SetDeadline(time.Now().Add(adminCommandTimeout)); err != nil {
conn.Close()
return nil, fmt.Errorf("set admin connection deadline: %w", err)
}
if _, err = conn.Write(encodeFrame(cmd)); err != nil { if _, err = conn.Write(encodeFrame(cmd)); err != nil {
conn.Close() conn.Close()
+57 -55
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"cmp" "cmp"
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"maps" "maps"
@@ -57,14 +58,12 @@ https://{{$hostname}} {
log log
}{{end}} }{{end}}
` `
caddyfileUnavailabeFooter = `# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine bootstrapMarker = "# Uncloud bootstrap for Caddy container "
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest legacyBootstrapMarker = "# NOTE: User-defined configs for services were skipped because Caddy is not running"
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
`
) )
// CaddyfileGenerator generates a Caddyfile configuration for the Caddy reverse proxy. // CaddyfileGenerator generates a Caddyfile configuration for the Caddy reverse proxy.
// It combines generated routes from published ports with user-defined Caddyfile snippets from service specs (x-caddy).
type CaddyfileGenerator struct { type CaddyfileGenerator struct {
// machineID is the unique identifier of the machine where the controller is running. // machineID is the unique identifier of the machine where the controller is running.
machineID string machineID string
@@ -74,11 +73,18 @@ type CaddyfileGenerator struct {
log *slog.Logger log *slog.Logger
} }
// CaddyfileValidator is an interface for validating Caddyfile configurations. // CaddyfileValidator checks a candidate Caddyfile without loading it. Validate returns InvalidCaddyfileError only
// when the candidate is known to be invalid. Other errors mean the check could not be completed. A successful check
// does not guarantee that the configuration will load.
type CaddyfileValidator interface { type CaddyfileValidator interface {
Validate(ctx context.Context, caddyfile string) error Validate(ctx context.Context, caddyfile string) error
} }
// InvalidCaddyfileError means validation completed and the candidate Caddyfile was rejected.
type InvalidCaddyfileError struct{ Message string }
func (e *InvalidCaddyfileError) Error() string { return e.Message }
func NewCaddyfileGenerator( func NewCaddyfileGenerator(
machineID, machineName string, validator CaddyfileValidator, log *slog.Logger, machineID, machineName string, validator CaddyfileValidator, log *slog.Logger,
) *CaddyfileGenerator { ) *CaddyfileGenerator {
@@ -93,35 +99,41 @@ func NewCaddyfileGenerator(
} }
} }
// Generate creates a Caddyfile configuration based on the provided service containers. // Generate creates a Caddyfile for the local Caddy container from the supplied healthy application containers.
// The Caddyfile is generated from the service ports of the healthy containers. // Application service ports provide the generated sites. The Caddy container's custom Caddy config (x-caddy), if
// If a 'caddy' service container is running on this machine and defines a custom Caddy config (x-caddy) in its service // defined, provides the global block even when that container is unhealthy. When application custom Caddy configs
// spec, it will be validated and prepended to the generated Caddyfile. Custom Caddy configs (x-caddy) defined in other // are included, the newest container for each service provides its config.
// service specs are validated and appended to the generated Caddyfile. Invalid configs are logged and skipped to ensure
// the generated Caddyfile remains valid.
// //
// The final Caddyfile structure includes: // The resulting Caddyfile has this structure:
// //
// [caddy x-caddy (global config)] // [caddy custom Caddy config (global)]
// [generated Caddyfile from all service ports] // [generated sites from application service ports]
// [service-a x-caddy] // [service-a custom Caddy config]
// ... // ...
// [service-z x-caddy] // [service-z custom Caddy config]
// //
// If includeCustom is false, custom Caddy configs (x-caddy) are not included in the generated Caddyfile. // Bootstrap mode keeps the global custom Caddy config and generated sites, but omits application custom Caddy configs.
// It skips validation because Caddy may not be running yet. In this case, Caddy validates the config when it starts.
// Global template errors still stop generation. In full mode, invalid globals stop generation, while invalid
// application custom Caddy configs are logged and skipped.
func (g *CaddyfileGenerator) Generate( func (g *CaddyfileGenerator) Generate(
ctx context.Context, records []store.ContainerRecord, includeCustom bool, ctx context.Context,
caddyCtr api.ServiceContainer,
records []store.ContainerRecord,
bootstrap bool,
) (string, error) { ) (string, error) {
records = slices.Clone(records)
// Sort records by local machine first, then by service name and creation time. Placing containers on the local // Sort records by local machine first, then by service name and creation time. Placing containers on the local
// machine first lets user-defined Caddy configs pair this ordering with the "first" lb_policy to always send // machine first lets user-defined Caddy configs pair this ordering with the "first" lb_policy to always send
// traffic to the same-host replica (skipping the cross-machine hop) and only fall back to remote upstreams when // traffic to the same-host replica (skipping the cross-machine hop) and only fall back to remote upstreams when
// the local one is unhealthy. // the local one is unhealthy.
// The service name and creation time tiebreakers keep the generated Caddyfile stable across regenerations. // The service name, creation time, and container ID tiebreakers keep the file stable across regenerations.
slices.SortStableFunc(records, func(a, b store.ContainerRecord) int { slices.SortStableFunc(records, func(a, b store.ContainerRecord) int {
return cmp.Or( return cmp.Or(
g.localMachineRank(a.MachineID)-g.localMachineRank(b.MachineID), g.localMachineRank(a.MachineID)-g.localMachineRank(b.MachineID),
strings.Compare(a.Container.ServiceName(), b.Container.ServiceName()), strings.Compare(a.Container.ServiceName(), b.Container.ServiceName()),
a.Container.CreatedTime().Compare(b.Container.CreatedTime()), a.Container.CreatedTime().Compare(b.Container.CreatedTime()),
strings.Compare(a.Container.ID, b.Container.ID),
) )
}) })
@@ -136,26 +148,11 @@ func (g *CaddyfileGenerator) Generate(
} }
caddyfileHeader := fmt.Sprintf(caddyfileHeaderFmt, g.machineName, time.Now().UTC().Format(time.RFC3339)) caddyfileHeader := fmt.Sprintf(caddyfileHeaderFmt, g.machineName, time.Now().UTC().Format(time.RFC3339))
if !includeCustom {
return fmt.Sprintf("%s\n%s\n%s", caddyfileHeader, caddyfile, caddyfileUnavailabeFooter), nil
}
upstreams := serviceUpstreams(containers) upstreams := serviceUpstreams(containers)
// Track validation errors for reporting. // Track validation errors for reporting.
var configErrors []string var configErrors []string
// Find the 'caddy' service container on this machine. Use the most recent one if multiple exist. if caddyCtr.ServiceSpec.CaddyConfig() != "" {
var caddyCtr *api.ServiceContainer
for _, cr := range records {
if cr.MachineID == g.machineID && cr.Container.ServiceName() == CaddyServiceName &&
(caddyCtr == nil || cr.Container.CreatedTime().Compare(caddyCtr.CreatedTime()) > 0) {
caddyCtr = &cr.Container
}
}
// If the caddy container is running on this machine and has a custom Caddy config (global),
// prepend it to the generated Caddyfile and validate it.
if caddyCtr != nil && caddyCtr.ServiceSpec.CaddyConfig() != "" {
// Render the custom global Caddy config as a Go template with the upstreams. // Render the custom global Caddy config as a Go template with the upstreams.
tmplCtx := templateContext{ tmplCtx := templateContext{
Name: caddyCtr.ServiceName(), Name: caddyCtr.ServiceName(),
@@ -163,23 +160,23 @@ func (g *CaddyfileGenerator) Generate(
} }
renderedConfig, err := renderCaddyfile(tmplCtx, caddyCtr.ServiceSpec.CaddyConfig()) renderedConfig, err := renderCaddyfile(tmplCtx, caddyCtr.ServiceSpec.CaddyConfig())
if err != nil { if err != nil {
g.log.Error("Failed to render template directives in user-defined global Caddy config, skipping it.", return "", fmt.Errorf(
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err) "render template directives in user-defined global Caddy config from Caddy container %s: %w",
configErrors = append(configErrors, caddyCtr.ShortID(), err)
fmt.Sprintf("service '%s': failed to render template: %v", caddyCtr.ServiceName(), err)) }
} else {
caddyfileCandidate := fmt.Sprintf("# User-defined global config from service '%s'.\n%s\n\n%s", caddyfileCandidate := fmt.Sprintf("# User-defined global config from service '%s'.\n%s\n\n%s",
caddyCtr.ServiceName(), renderedConfig, caddyfile) caddyCtr.ServiceName(), renderedConfig, caddyfile)
if !bootstrap && g.validator != nil {
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil { if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
g.log.Error("User-defined global Caddy config is invalid, skipping it.", return "", fmt.Errorf("validate user-defined global Caddy config from Caddy container %s: %w",
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err) caddyCtr.ShortID(), err)
configErrors = append(configErrors, }
fmt.Sprintf("service '%s': validation failed: %v", caddyCtr.ServiceName(), err)) }
} else {
caddyfile = caddyfileCandidate caddyfile = caddyfileCandidate
} }
}
if bootstrap {
return caddyfileHeader + bootstrapMarker + caddyCtr.ID + "\n\n" + caddyfile, nil
} }
// There could be multiple service containers for the same service with different custom Caddy configs, for example, // There could be multiple service containers for the same service with different custom Caddy configs, for example,
@@ -200,7 +197,7 @@ func (g *CaddyfileGenerator) Generate(
// Append a custom Caddy config for each service to the Caddyfile and validate it. If the config for a service // Append a custom Caddy config for each service to the Caddyfile and validate it. If the config for a service
// is invalid, skip it but continue processing other services to ensure the Caddyfile remains valid. // is invalid, skip it but continue processing other services to ensure the Caddyfile remains valid.
for _, serviceName := range sortedServiceNames { for _, serviceName := range sortedServiceNames {
// Skip the caddy container as we already processed it. // Skip the caddy container as we already processed it as the global config.
if serviceName == CaddyServiceName { if serviceName == CaddyServiceName {
continue continue
} }
@@ -226,16 +223,22 @@ func (g *CaddyfileGenerator) Generate(
caddyfileCandidate := fmt.Sprintf("%s\n# User-defined config for service '%s'.\n%s\n", caddyfileCandidate := fmt.Sprintf("%s\n# User-defined config for service '%s'.\n%s\n",
caddyfile, serviceName, renderedConfig) caddyfile, serviceName, renderedConfig)
if g.validator != nil {
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil { if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
if _, ok := errors.AsType[*InvalidCaddyfileError](err); !ok {
return "", fmt.Errorf("validate Caddy config for service '%s': %w", serviceName, err)
}
g.log.Error("User-defined Caddy config for service is invalid, skipping it.", g.log.Error("User-defined Caddy config for service is invalid, skipping it.",
"service", serviceName, "err", err) "service", serviceName, "err", err)
configErrors = append(configErrors, fmt.Sprintf("service '%s': validation failed: %v", serviceName, err)) configErrors = append(configErrors,
} else { fmt.Sprintf("service '%s': validation failed: %v", serviceName, err))
continue
}
}
caddyfile = caddyfileCandidate caddyfile = caddyfileCandidate
} }
}
// Append error summary as comment if there were any invalid configs. // Keep skipped-snippet errors in the saved file so an operator can see why routes are missing after a restart.
if len(configErrors) > 0 { if len(configErrors) > 0 {
var errorsComment strings.Builder var errorsComment strings.Builder
errorsComment.WriteString("# Skipped invalid user-defined configs:\n") errorsComment.WriteString("# Skipped invalid user-defined configs:\n")
@@ -249,8 +252,7 @@ func (g *CaddyfileGenerator) Generate(
return caddyfileHeader + "\n" + caddyfile, nil return caddyfileHeader + "\n" + caddyfile, nil
} }
// localMachineRank returns 0 if the given machineID matches the local machine and 1 otherwise. // localMachineRank is a sorting function for containers that puts running on the local machine first.
// Useful for sorting containers running locally first.
func (g *CaddyfileGenerator) localMachineRank(machineID string) int { func (g *CaddyfileGenerator) localMachineRank(machineID string) int {
if g.machineID == machineID { if g.machineID == machineID {
return 0 return 0
+187 -116
View File
@@ -208,10 +208,11 @@ http://app.example.com {
ctx := context.Background() ctx := context.Background()
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Validator is not expected to be called in these tests. validator := NewMockCaddyfileValidator(t)
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", nil, nil) generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
config, err := generator.Generate(ctx, tt.containers, true) caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.1", "", "", time.Now()).Container
config, err := generator.Generate(ctx, caddyCtr, tt.containers, false)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
@@ -332,7 +333,7 @@ bad.template.com {
`, `,
}, },
{ {
name: "caddy service with invalid global config is skipped", name: "caddy service with invalid global config aborts",
containers: []store.ContainerRecord{ containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig( newContainerRecordWithCaddyConfig(
"caddy", "caddy",
@@ -345,10 +346,7 @@ localhost {
time.Now(), time.Now(),
), ),
}, },
want: testCaddyfileHeader + ` wantErr: true,
# Skipped invalid user-defined configs:
# - service 'caddy': validation failed: invalid config detected
`,
}, },
{ {
name: "caddy service on different machine is ignored", name: "caddy service on different machine is ignored",
@@ -786,7 +784,7 @@ web-v2.example.com {
`, `,
}, },
{ {
name: "multiple errors: invalid global, template error, and validation error", name: "invalid global aborts before application configs",
containers: []store.ContainerRecord{ containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig( newContainerRecordWithCaddyConfig(
"caddy", "caddy",
@@ -827,17 +825,7 @@ invalid.example.com {
time.Now(), time.Now(),
), ),
}, },
want: testCaddyfileHeader + ` wantErr: true,
# User-defined config for service 'valid'.
valid.example.com {
respond "Valid config"
}
# Skipped invalid user-defined configs:
# - service 'caddy': validation failed: invalid config detected
# - service 'broken-template': failed to render template: parse config as Go template: template: Caddyfile:2: unterminated quoted string
# - service 'invalid': validation failed: invalid config detected
`,
}, },
} }
@@ -846,7 +834,7 @@ valid.example.com {
validator.EXPECT().Validate(mock.Anything, mock.Anything).RunAndReturn( validator.EXPECT().Validate(mock.Anything, mock.Anything).RunAndReturn(
func(ctx context.Context, caddyfile string) error { func(ctx context.Context, caddyfile string) error {
if strings.Contains(caddyfile, "# test:invalid") { if strings.Contains(caddyfile, "# test:invalid") {
return errors.New("invalid config detected") return &InvalidCaddyfileError{Message: "invalid config detected"}
} }
return nil return nil
}) })
@@ -855,7 +843,8 @@ valid.example.com {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil) generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
config, err := generator.Generate(ctx, tt.containers, true) caddyCtr := caddyContainerFromTestContainers(tt.containers)
config, err := generator.Generate(ctx, caddyCtr, tt.containers, false)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
@@ -868,6 +857,167 @@ valid.example.com {
} }
} }
func TestCaddyfileGeneratorBootstrap(t *testing.T) {
// Bootstrap keeps the local Caddy container's globals and generated routes, but omits application custom configs.
tests := []struct {
name string
containers []store.ContainerRecord
want string
}{
{
name: "global config retained and application custom configs skipped",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.1",
`# Global config
{
global directive
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithCaddyConfig(
"web",
"10.210.0.2",
`web.example.com {
reverse_proxy web:3000
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: strings.Replace(testCaddyfileHeader, "\n\n# Health check endpoint", `
# Uncloud bootstrap for Caddy container caddy-10.210.0.1
# User-defined global config from service 'caddy'.
# Global config
{
global directive
}
# Health check endpoint`, 1) + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
`,
},
{
name: "no containers with x-caddy configs",
containers: []store.ContainerRecord{
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: strings.Replace(testCaddyfileHeader, "\n\n# Health check endpoint", `
# Uncloud bootstrap for Caddy container caddy-10.210.0.1
# Health check endpoint`, 1) + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
`,
},
}
ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
validator := NewMockCaddyfileValidator(t)
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
caddyContainer := caddyContainerFromTestContainers(tt.containers)
config, err := generator.Generate(ctx, caddyContainer, tt.containers, true)
require.NoError(t, err)
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
})
}
}
func TestCaddyfileGenerator_ValidatorTransportErrorAbortsGeneration(t *testing.T) {
validator := NewMockCaddyfileValidator(t)
validator.EXPECT().Validate(mock.Anything, mock.Anything).Return(errors.New("socket disappeared"))
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", validator, nil)
app := newContainerRecordWithCaddyConfig(
"web",
"10.210.0.2",
"web.example.com { respond ok }",
"test-machine-id",
time.Now(),
)
_, err := generator.Generate(context.Background(), caddyContainerFromTestContainers(nil),
[]store.ContainerRecord{app}, false)
require.ErrorContains(t, err, "socket disappeared")
}
func newContainer(ip string, ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
},
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
docker.NetworkName: {
IPAddress: ip,
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
},
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
"other-network": {
IPAddress: "172.17.0.2",
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
func newContainerRecord(ctr api.ServiceContainer, machineID string) store.ContainerRecord { func newContainerRecord(ctr api.ServiceContainer, machineID string) store.ContainerRecord {
return store.ContainerRecord{ return store.ContainerRecord{
Container: ctr, Container: ctr,
@@ -911,100 +1061,6 @@ func newContainerRecordWithCaddyConfig(serviceName, ip, caddyConfig, machineID s
} }
} }
func TestCaddyfileGeneratorWithoutCustomConfigs(t *testing.T) {
// Test that when includeCustom is false (Caddy not available), x-caddy configs are skipped.
tests := []struct {
name string
containers []store.ContainerRecord
want string
}{
{
name: "x-caddy configs are skipped",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.1",
`# Global config
{
global directive
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithCaddyConfig(
"web",
"10.210.0.2",
`web.example.com {
reverse_proxy web:3000
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
`,
},
{
name: "no containers with x-caddy configs",
containers: []store.ContainerRecord{
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
`,
},
}
ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Validator is not expected to be called in these tests.
generator := NewCaddyfileGenerator("test-machine-id", "test-machine", nil, nil)
config, err := generator.Generate(ctx, tt.containers, false)
require.NoError(t, err)
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
})
}
}
func newContainerRecordWithPorts(serviceName, ip string, ports []string, machineID string) store.ContainerRecord { func newContainerRecordWithPorts(serviceName, ip string, ports []string, machineID string) store.ContainerRecord {
portsLabel := strings.Join(ports, ",") portsLabel := strings.Join(ports, ",")
return store.ContainerRecord{ return store.ContainerRecord{
@@ -1037,3 +1093,18 @@ func newContainerRecordWithPorts(serviceName, ip string, ports []string, machine
MachineID: machineID, MachineID: machineID,
} }
} }
// caddyContainerFromTestContainers returns the local Caddy container from the provided test containers,
// or creates a default one if not found.
func caddyContainerFromTestContainers(records []store.ContainerRecord) api.ServiceContainer {
if caddyCtr := selectLocalCaddyContainer(records, "test-machine-id"); caddyCtr != nil {
return *caddyCtr
}
return newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.1",
"",
"test-machine-id",
time.Now(),
).Container
}
+12 -8
View File
@@ -3,7 +3,6 @@ package caddyconfig
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@@ -26,15 +25,15 @@ func NewCaddyAdminClient(socketPath string) *CaddyAdminClient {
client: &http.Client{ client: &http.Client{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath) return (&net.Dialer{}).DialContext(ctx, "unix", socketPath)
}, },
}, },
}, },
} }
} }
// IsAvailable checks if the local Caddy instance is listening on the admin socket. // IsAvailable checks whether a Caddy process accepts connections at the admin socket.
func (c *CaddyAdminClient) IsAvailable() bool { func (c *CaddyAdminClient) IsAvailable() bool {
conn, err := net.DialTimeout("unix", c.socketPath, 1*time.Second) conn, err := net.DialTimeout("unix", c.socketPath, 1*time.Second)
// A stale socket file left over from a crashed Caddy container returns ECONNREFUSED so this is correctly handled // A stale socket file left over from a crashed Caddy container returns ECONNREFUSED so this is correctly handled
@@ -80,15 +79,16 @@ func (c *CaddyAdminClient) Adapt(ctx context.Context, caddyfile string) (string,
// If the response is a 400 Bad Request, try to parse the error message from it. // If the response is a 400 Bad Request, try to parse the error message from it.
if resp.StatusCode == http.StatusBadRequest { if resp.StatusCode == http.StatusBadRequest {
var apiError caddy.APIError var apiError caddy.APIError
if err = json.Unmarshal(body, &apiError); err == nil { if err = json.Unmarshal(body, &apiError); err == nil && apiError.Message != "" {
return "", errors.New(apiError.Message) return "", &InvalidCaddyfileError{Message: apiError.Message}
} }
return "", &InvalidCaddyfileError{Message: string(body)}
} }
return "", errors.New(string(body)) return "", fmt.Errorf("adapt request failed: HTTP %d: %s", resp.StatusCode, string(body))
} }
// Load loads a Caddyfile configuration into the Caddy instance running on the machine. // Load loads a Caddyfile configuration into the Caddy instance.
// Due to a Caddy bug (https://github.com/caddyserver/caddy/issues/7246), we first adapt the Caddyfile to JSON // Due to a Caddy bug (https://github.com/caddyserver/caddy/issues/7246), we first adapt the Caddyfile to JSON
// and then load the JSON config to get proper error handling. // and then load the JSON config to get proper error handling.
func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error { func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error {
@@ -96,7 +96,11 @@ func (c *CaddyAdminClient) Load(ctx context.Context, caddyfile string) error {
if err != nil { if err != nil {
return fmt.Errorf("adapt Caddyfile to JSON config: %w", err) return fmt.Errorf("adapt Caddyfile to JSON config: %w", err)
} }
return c.LoadJSON(ctx, jsonConfig)
}
// LoadJSON loads a JSON configuration into the Caddy instance.
func (c *CaddyAdminClient) LoadJSON(ctx context.Context, jsonConfig string) error {
req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost/load", strings.NewReader(jsonConfig)) req, err := http.NewRequestWithContext(ctx, "POST", "http://localhost/load", strings.NewReader(jsonConfig))
if err != nil { if err != nil {
return fmt.Errorf("create load request: %w", err) return fmt.Errorf("create load request: %w", err)
@@ -0,0 +1,54 @@
package caddyconfig
import (
"context"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testAdminServer(t *testing.T, handler http.HandlerFunc) string {
t.Helper()
// macOS limits Unix socket paths to 104 bytes. t.TempDir can exceed that.
dir, err := os.MkdirTemp("/tmp", "uc-caddy-")
require.NoError(t, err)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
socketPath := filepath.Join(dir, "admin.sock")
listener, err := net.Listen("unix", socketPath)
require.NoError(t, err)
server := httptest.NewUnstartedServer(handler)
_ = server.Listener.Close()
server.Listener = listener
server.Start()
t.Cleanup(server.Close)
return socketPath
}
func TestCaddyAdminClient_Validate(t *testing.T) {
var status atomic.Int64
status.Store(http.StatusBadRequest)
socket := testAdminServer(t, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(int(status.Load()))
_, _ = w.Write([]byte(`{"message":"invalid config"}`))
})
client := NewCaddyAdminClient(socket)
err := client.Validate(context.Background(), "invalid")
var invalid *InvalidCaddyfileError
assert.ErrorAs(t, err, &invalid)
status.Store(http.StatusInternalServerError)
err = client.Validate(context.Background(), "invalid")
assert.Error(t, err)
assert.NotErrorAs(t, err, &invalid)
}
+197 -106
View File
@@ -2,7 +2,7 @@ package caddyconfig
import ( import (
"context" "context"
"encoding/json" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net/netip" "net/netip"
@@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"time"
"github.com/psviderski/uncloud/internal/fs" "github.com/psviderski/uncloud/internal/fs"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
@@ -20,27 +21,40 @@ const (
CaddyServiceName = "caddy" CaddyServiceName = "caddy"
CaddyGroup = "uncloud" CaddyGroup = "uncloud"
VerifyPath = "/.uncloud-verify" VerifyPath = "/.uncloud-verify"
// periodicReconciliationInterval is the interval at which the controller reconciles the Caddyfile
// even if no container changes are observed.
periodicReconciliationInterval = 30 * time.Second
) )
// Controller monitors container changes in the cluster store and generates a configuration file for Caddy reverse // Controller keeps the local Caddy reverse proxy in sync with container state from the cluster store. It writes a
// proxy. The generated configuration allows Caddy to route external traffic to service containers across the internal // Caddyfile that routes external traffic to healthy service containers across the internal network.
// network. //
// Current Caddy deployments share one Caddyfile and one admin socket per machine. The controller must preserve a
// saved full Caddyfile when Caddy is unavailable because that file may be needed to restart or roll back a container.
// It writes a reduced bootstrap config only when no full file exists, including when an older offline file must be
// replaced. The shared layout cannot give overlapping Caddy revisions separate configurations.
type Controller struct { type Controller struct {
machineID string machineID string
caddyfilePath string caddyfilePath string
service *Service
generator *CaddyfileGenerator generator *CaddyfileGenerator
client *CaddyAdminClient client *CaddyAdminClient
store *store.Store store *store.Store
log *slog.Logger log *slog.Logger
// lastFingerprint caches the fingerprint of the containers used to generate the latest successfully loaded // Tests use their own group so they can exercise real file publication without the daemon's uncloud group.
// Caddyfile. nil means it hasn't been loaded yet or the last load failed. fileGroup string
// lastFingerprint records the healthy application-container inputs used for the last full Caddyfile that was
// successfully loaded into Caddy and saved to disk. It excludes the Caddy container, whose identity, start time,
// and global config are tracked separately below.
lastFingerprint []containerFingerprint lastFingerprint []containerFingerprint
// lastCaddyfile caches the last generated Caddyfile. lastCaddyCtrID string
lastCaddyfile string lastStartedAt string
lastGlobal string
lastSavedBody string
} }
// containerFingerprint is the subset of container data that the Caddyfile generator depends on. // containerFingerprint contains container identity and routing inputs that can change the generated Caddyfile.
// Comparing fingerprints lets the controller skip no-op regenerations. // It excludes incidental Docker state so unrelated container updates do not cause a Caddy reload.
type containerFingerprint struct { type containerFingerprint struct {
ID string ID string
IP netip.Addr IP netip.Addr
@@ -56,7 +70,8 @@ func (f containerFingerprint) Equal(other containerFingerprint) bool {
f.CaddyConfig == other.CaddyConfig f.CaddyConfig == other.CaddyConfig
} }
func NewController(machineID, configDir, adminSock string, store *store.Store) (*Controller, error) { func NewController(machineID string, service *Service, adminSock string, store *store.Store) (*Controller, error) {
configDir := service.configDir
if err := os.MkdirAll(configDir, 0o750); err != nil { if err := os.MkdirAll(configDir, 0o750); err != nil {
return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err) return nil, fmt.Errorf("create directory for Caddy configuration '%s': %w", configDir, err)
} }
@@ -67,16 +82,21 @@ func NewController(machineID, configDir, adminSock string, store *store.Store) (
log := slog.With("component", "caddy-controller") log := slog.With("component", "caddy-controller")
client := NewCaddyAdminClient(adminSock) client := NewCaddyAdminClient(adminSock)
// generator is initialised by Run() once the machine name is resolved from the store. // The generator is initialised by Run() after resolving the machine name from the store.
return &Controller{ return &Controller{
machineID: machineID, machineID: machineID,
caddyfilePath: filepath.Join(configDir, "Caddyfile"), caddyfilePath: filepath.Join(configDir, "Caddyfile"),
service: service,
client: client, client: client,
store: store, store: store,
log: log, log: log,
fileGroup: CaddyGroup,
}, nil }, nil
} }
// Run reconciles on container changes and every periodicReconciliationInterval. Failed work is retried every second,
// but repeated retry failures are logged only during the periodic check.
// The periodic check also covers missed events and outstanding failures after the daemon restarts.
func (c *Controller) Run(ctx context.Context) error { func (c *Controller) Run(ctx context.Context) error {
// Default the machine name to the machine ID so the Caddyfile header still carries a stable identifier if // Default the machine name to the machine ID so the Caddyfile header still carries a stable identifier if
// the store lookup fails. // the store lookup fails.
@@ -95,13 +115,34 @@ func (c *Controller) Run(ctx context.Context) error {
} }
c.log.Info("Subscribed to container changes in the cluster to generate Caddy configuration.") c.log.Info("Subscribed to container changes in the cluster to generate Caddy configuration.")
containers = filterHealthyContainers(containers) var retryC <-chan time.Time
c.generateAndLoadCaddyfile(ctx, containers) periodic := time.NewTicker(periodicReconciliationInterval)
defer periodic.Stop()
// TODO: left for backward compatibility, remove later. handleResult := func(err error, logFailure bool) {
if err = c.generateJSONConfig(containers); err != nil { c.service.setReconciliationResult(err)
c.log.Error("Failed to generate Caddy JSON configuration to disk.", "err", err) if err == nil {
retryC = nil
return
} }
if logFailure {
c.log.Error("Failed to reconcile Caddy configuration, will retry.", "err", err)
}
retryC = time.After(time.Second)
}
reconcile := func(logFailure bool) {
ctrs, err := c.store.ListContainers(ctx, store.ListOptions{})
if err != nil {
err = fmt.Errorf("list containers: %w", err)
} else {
err = c.generateAndLoadCaddyfile(ctx, ctrs)
}
handleResult(err, logFailure)
}
// The subscription supplies an initial snapshot, so the first attempt need not wait for a change event.
handleResult(c.generateAndLoadCaddyfile(ctx, containers), true)
for { for {
select { select {
@@ -111,25 +152,18 @@ func (c *Controller) Run(ctx context.Context) error {
} }
c.log.Debug("Cluster containers changed, regenerating Caddy configuration.") c.log.Debug("Cluster containers changed, regenerating Caddy configuration.")
containers, err = c.store.ListContainers(ctx, store.ListOptions{}) reconcile(true)
if err != nil { case <-periodic.C:
c.log.Error("Failed to list containers.", "err", err) reconcile(true)
continue case <-retryC:
} reconcile(false)
containers = filterHealthyContainers(containers)
c.generateAndLoadCaddyfile(ctx, containers)
// TODO: left for backward compatibility, remove later.
if err = c.generateJSONConfig(containers); err != nil {
c.log.Error("Failed to generate Caddy JSON configuration to disk.", "err", err)
}
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
} }
} }
} }
// filterHealthyContainers filters out unhealthy and hook containers. // filterHealthyContainers filters out unhealthy, hook, and caddy containers.
// TODO: Filters out containers from this machine that are likely unavailable. The availability can be determined // TODO: Filters out containers from this machine that are likely unavailable. The availability can be determined
// by the cluster membership state of the machine that the container is running on. Implement machine membership // by the cluster membership state of the machine that the container is running on. Implement machine membership
// check using Corrossion Admin client. // check using Corrossion Admin client.
@@ -139,6 +173,9 @@ func filterHealthyContainers(containers []store.ContainerRecord) []store.Contain
if cr.Container.IsHook() { if cr.Container.IsHook() {
continue continue
} }
if cr.Container.ServiceName() == CaddyServiceName {
continue
}
if cr.Container.Healthy() { if cr.Container.Healthy() {
healthy = append(healthy, cr) healthy = append(healthy, cr)
} }
@@ -146,65 +183,96 @@ func filterHealthyContainers(containers []store.ContainerRecord) []store.Contain
return healthy return healthy
} }
// generateAndLoadCaddyfile regenerates the Caddyfile from the given containers and loads it into the local Caddy // generateAndLoadCaddyfile reconciles the shared Caddyfile for the selected local Caddy container. A full candidate
// if available. // reaches disk only after Caddy accepts it. Without an admin endpoint, the controller keeps a saved full file intact
func (c *Controller) generateAndLoadCaddyfile(ctx context.Context, containers []store.ContainerRecord) { // and writes a bootstrap config only when the file is absent or already a bootstrap.
// Check if Caddy is available before attempting to generate and load config. func (c *Controller) generateAndLoadCaddyfile(ctx context.Context, containers []store.ContainerRecord) error {
caddyAvailable := c.client.IsAvailable() caddyCtr := selectLocalCaddyContainer(containers, c.machineID)
if caddyCtr == nil {
// Skip regeneration when Caddy is available and the containers since the last successful load haven't changed. // Caddy is not running locally which means there is no reliable source for the global config, skipping.
// When Caddy is unavailable we still regenerate to keep the Caddyfile on disk updated. return nil
fingerprint := fingerprintContainers(containers)
if caddyAvailable && slices.EqualFunc(fingerprint, c.lastFingerprint, containerFingerprint.Equal) {
c.log.Debug("Caddy configuration is unchanged.", "path", c.caddyfilePath)
return
} }
caddyfile, err := c.generator.Generate(ctx, containers, caddyAvailable) saved, readErr := os.ReadFile(c.caddyfilePath)
if readErr != nil && !errors.Is(readErr, os.ErrNotExist) {
return fmt.Errorf("read saved Caddyfile: %w", readErr)
}
haveSaved := readErr == nil
healthyCtrs := filterHealthyContainers(containers)
fingerprint := fingerprintContainers(healthyCtrs)
if !caddyCtr.State.Running || !c.client.IsAvailable() {
if haveSaved && !isBootstrapCaddyfile(string(saved)) {
// Caddy may need this file to restart or roll back. A hosts-only replacement could drop
// storage or other global settings while the admin API is unavailable.
if caddyCtr.State.Running {
return fmt.Errorf("caddy admin socket unavailable, preserving saved Caddyfile")
}
return nil
}
bootstrap, err := c.generator.Generate(ctx, *caddyCtr, healthyCtrs, true)
if err != nil { if err != nil {
c.log.Error("Failed to generate Caddyfile configuration.", "err", err) return fmt.Errorf("generate Caddy bootstrap config: %w", err)
return }
if err = c.writeCaddyfileIfChanged(bootstrap); err != nil {
return fmt.Errorf("save Caddy bootstrap config: %w", err)
}
if caddyCtr.State.Running {
return fmt.Errorf("caddy admin socket unavailable, bootstrap config saved")
}
return nil
} }
if !caddyAvailable { if haveSaved &&
// Caddy is not running so the generated Caddyfile should not include user-defined configs thus must be valid. caddyfileBody(string(saved)) == c.lastSavedBody &&
// It's safe to write the config to disk so that when Caddy is deployed on this machine, it can pick it up. !isBootstrapCaddyfile(string(saved)) &&
if err = c.writeCaddyfileIfChanged(caddyfile); err != nil { c.lastCaddyCtrID == caddyCtr.ID &&
c.log.Error("Failed to write Caddyfile to disk.", "err", err) c.lastStartedAt == caddyCtr.State.StartedAt &&
return c.lastGlobal == caddyCtr.ServiceSpec.CaddyConfig() &&
} slices.EqualFunc(fingerprint, c.lastFingerprint, containerFingerprint.Equal) {
c.log.Debug("Caddy is not running on this machine, skipping configuration load.", "path", c.caddyfilePath) // No changes to the inputs that affect the generated Caddyfile, so no reload is needed.
return return nil
} }
// Caddy is available, try to load the config which may fail if the config is invalid. Generally, a config can caddyfile, err := c.generator.Generate(ctx, *caddyCtr, healthyCtrs, false)
// pass the adaptation/validation step but still fail to load, for example, if it references resources that are if err != nil {
// not available. return fmt.Errorf("generate Caddyfile: %w", err)
}
// Try to load the config which may fail if the config is invalid. Generally, a config can pass
// the adaptation/validation step but still fail to load, for example, if it references resources
// that are not available.
if err = c.client.Load(ctx, caddyfile); err != nil { if err = c.client.Load(ctx, caddyfile); err != nil {
c.log.Error("Failed to load new Caddy configuration into local Caddy instance.", return fmt.Errorf("load Caddyfile: %w", err)
"err", err, "path", c.caddyfilePath) }
// Mark the cache stale so the next container change retries the load even if the container set is unchanged. if err = c.writeCaddyfileIfChanged(caddyfile); err != nil {
c.lastFingerprint = nil return fmt.Errorf("save loaded Caddyfile: %w", err)
// Don't write invalid config to disk.
return
} }
c.lastFingerprint = fingerprint c.lastFingerprint = fingerprint
c.lastCaddyCtrID = caddyCtr.ID
c.lastStartedAt = caddyCtr.State.StartedAt
c.lastGlobal = caddyCtr.ServiceSpec.CaddyConfig()
c.lastSavedBody = caddyfileBody(caddyfile)
c.log.Info("New Caddy configuration loaded into local Caddy instance.", "path", c.caddyfilePath)
// Config loaded successfully, now write it to disk. return nil
if err = c.writeCaddyfileIfChanged(caddyfile); err != nil {
c.log.Error("Failed to write Caddyfile to disk after successful load.", "err", err)
// Config is already loaded in Caddy, so this is not critical. The next regeneration retries the disk write.
return
} }
c.log.Info("New Caddy configuration loaded into local Caddy instance.", "path", c.caddyfilePath) // isBootstrapCaddyfile distinguishes a reduced startup file from a full file that must survive Caddy outage.
// It also recognizes the older daemon's offline file. That file omitted every x-caddy block, including globals,
// so treating it as a protected full file would preserve the configuration-loss bug:
// https://github.com/psviderski/uncloud/issues/412
func isBootstrapCaddyfile(caddyfile string) bool {
return strings.Contains(caddyfile, bootstrapMarker) || strings.Contains(caddyfile, legacyBootstrapMarker)
} }
// fingerprintContainers returns a fingerprint of containers that the Caddyfile generator depends on. // fingerprintContainers returns a fingerprint of containers that the Caddyfile generator depends on.
func fingerprintContainers(containers []store.ContainerRecord) []containerFingerprint { func fingerprintContainers(containers []store.ContainerRecord) []containerFingerprint {
fingerprints := make([]containerFingerprint, len(containers)) fingerprints := make([]containerFingerprint, len(containers))
for i, cr := range containers { for i, cr := range containers {
// Ignore ports parsing error as not much we can do about it. The generator just logs them and continues. // Ignore ports parsing error as not much we can do about it. The generator just logs them and skips
// the container.
ports, _ := cr.Container.ServicePorts() ports, _ := cr.Container.ServicePorts()
fingerprints[i] = containerFingerprint{ fingerprints[i] = containerFingerprint{
ID: cr.Container.ID, ID: cr.Container.ID,
@@ -220,21 +288,71 @@ func fingerprintContainers(containers []store.ContainerRecord) []containerFinger
return fingerprints return fingerprints
} }
// writeCaddyfileIfChanged writes the Caddyfile content to disk with proper permissions only if its body differs // selectLocalCaddyContainer chooses which local Caddy container supplies global Caddy config. A running container
// from the last successfully written content. The first line of the Caddyfile carries a generation timestamp that // takes precedence over a newer stopped replacement, even if the running container is unhealthy. If two revisions
// changes on every regeneration, so it's excluded from the comparison to avoid redundant writes. // run at once, the shared admin socket does not identify its owner, so this choice remains best effort.
func selectLocalCaddyContainer(records []store.ContainerRecord, machineID string) *api.ServiceContainer {
var selected *api.ServiceContainer
for _, cr := range records {
ctr := cr.Container
if cr.MachineID != machineID || ctr.ServiceName() != CaddyServiceName || ctr.IsHook() {
continue
}
if selected == nil {
selected = &ctr
continue
}
if ctr.State.Running != selected.State.Running {
if ctr.State.Running {
selected = &ctr
}
continue
}
if ctr.CreatedTime().Compare(selected.CreatedTime()) > 0 {
selected = &ctr
}
}
return selected
}
// writeCaddyfileIfChanged atomically replaces the saved Caddyfile only if its body differs from the new content.
func (c *Controller) writeCaddyfileIfChanged(caddyfile string) error { func (c *Controller) writeCaddyfileIfChanged(caddyfile string) error {
if caddyfileBody(caddyfile) == caddyfileBody(c.lastCaddyfile) { saved, err := os.ReadFile(c.caddyfilePath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("read Caddyfile '%s': %w", c.caddyfilePath, err)
}
if err == nil && caddyfileBody(caddyfile) == caddyfileBody(string(saved)) {
return nil return nil
} }
if err := os.WriteFile(c.caddyfilePath, []byte(caddyfile), 0o640); err != nil { dir := filepath.Dir(c.caddyfilePath)
return fmt.Errorf("write Caddyfile to file '%s': %w", c.caddyfilePath, err) tmp, err := os.CreateTemp(dir, ".Caddyfile-*")
if err != nil {
return fmt.Errorf("create temporary Caddyfile: %w", err)
} }
if err := fs.Chown(c.caddyfilePath, "", CaddyGroup); err != nil { defer os.Remove(tmp.Name())
return fmt.Errorf("change owner of Caddyfile '%s': %w", c.caddyfilePath, err) defer tmp.Close()
if err = tmp.Chmod(0o640); err != nil {
return fmt.Errorf("set temporary Caddyfile permissions: %w", err)
}
if err = fs.Chown(tmp.Name(), "", c.fileGroup); err != nil {
return fmt.Errorf("change owner of temporary Caddyfile: %w", err)
}
if _, err = tmp.WriteString(caddyfile); err != nil {
return fmt.Errorf("write temporary Caddyfile: %w", err)
}
if err = tmp.Sync(); err != nil {
return fmt.Errorf("sync temporary Caddyfile: %w", err)
}
if err = tmp.Close(); err != nil {
return fmt.Errorf("close temporary Caddyfile: %w", err)
}
if err = os.Rename(tmp.Name(), c.caddyfilePath); err != nil {
return fmt.Errorf("replace Caddyfile '%s': %w", c.caddyfilePath, err)
} }
c.lastCaddyfile = caddyfile
return nil return nil
} }
@@ -247,30 +365,3 @@ func caddyfileBody(caddyfile string) string {
} }
return caddyfile return caddyfile
} }
func (c *Controller) generateJSONConfig(containers []store.ContainerRecord) error {
serviceContainers := make([]api.ServiceContainer, len(containers))
for i, cr := range containers {
serviceContainers[i] = cr.Container
}
config, err := GenerateJSONConfig(serviceContainers, c.machineID)
if err != nil {
return err
}
configBytes, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("marshal Caddy configuration: %w", err)
}
configPath := filepath.Join(filepath.Dir(c.caddyfilePath), "caddy.json")
if err = os.WriteFile(configPath, configBytes, 0o640); err != nil {
return fmt.Errorf("write Caddy configuration to file '%s': %w", configPath, err)
}
if err = fs.Chown(configPath, "", CaddyGroup); err != nil {
return fmt.Errorf("change owner of Caddy configuration file '%s': %w", configPath, err)
}
return nil
}
@@ -1,14 +1,488 @@
package caddyconfig package caddyconfig
import ( import (
"context"
"io"
"log/slog"
"net/http"
"net/netip" "net/netip"
"os"
"os/user"
"path/filepath"
"reflect" "reflect"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing" "testing"
"time"
"github.com/docker/docker/api/types/container"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func testController(t *testing.T, socketPath string) *Controller {
t.Helper()
group, err := user.LookupGroupId(strconv.Itoa(os.Getegid()))
require.NoError(t, err)
path := filepath.Join(t.TempDir(), "Caddyfile")
client := NewCaddyAdminClient(socketPath)
return &Controller{
machineID: "test-machine-id",
caddyfilePath: path,
generator: NewCaddyfileGenerator("test-machine-id", "test-machine", client, nil),
client: client,
log: slog.Default(),
fileGroup: group.Name,
}
}
type testCaddyAdmin struct {
mu sync.Mutex
adaptRequests []string
loadCount int
rejectAdaptContaining string
rejectLoad bool
}
func (a *testCaddyAdmin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/adapt":
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
a.mu.Lock()
a.adaptRequests = append(a.adaptRequests, string(body))
reject := a.rejectAdaptContaining != "" && strings.Contains(string(body), a.rejectAdaptContaining)
a.mu.Unlock()
if reject {
http.Error(w, "invalid Caddyfile", http.StatusBadRequest)
return
}
_, _ = io.WriteString(w, `{"result":{}}`)
case "/load":
a.mu.Lock()
a.loadCount++
reject := a.rejectLoad
a.mu.Unlock()
if reject {
http.Error(w, "load rejected", http.StatusBadRequest)
}
default:
http.NotFound(w, r)
}
}
func (a *testCaddyAdmin) snapshot() ([]string, int) {
a.mu.Lock()
defer a.mu.Unlock()
return append([]string(nil), a.adaptRequests...), a.loadCount
}
func (a *testCaddyAdmin) setRejectLoad(reject bool) {
a.mu.Lock()
a.rejectLoad = reject
a.mu.Unlock()
}
func TestController_GenerateAndLoadCaddyfile(t *testing.T) {
t.Run("keeps the saved file when there is no local Caddy container", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
saved := "# previous full Caddyfile\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
remote := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ remote }",
"other-machine", time.Now())
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{remote}))
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
})
t.Run("does not publish a bootstrap with an invalid global template", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{{upstreams",
"test-machine-id", time.Now())
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(),
[]store.ContainerRecord{caddyCtr}), "render template")
_, err := os.Stat(controller.caddyfilePath)
assert.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("bootstraps unhealthy Caddy with its globals and healthy routes", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
caddyCtr.Container.State.Health = &container.Health{Status: "unhealthy"}
newerStopped := newContainerRecordWithCaddyConfig("caddy", "10.210.0.6", "{\n\tstorage replacement\n}",
"test-machine-id", time.Now().Add(time.Minute))
newerStopped.Container.State.Running = false
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"app.example.com:8080/http"},
"test-machine-id")
app.Container.ServiceSpec.Caddy = &api.CaddySpec{Config: "custom.example.com { respond app }"}
unhealthy := newContainerRecordWithPorts("unhealthy", "10.210.0.4",
[]string{"unhealthy.example.com:8080/http"}, "test-machine-id")
unhealthy.Container.State.Health = &container.Health{Status: "unhealthy"}
hook := newContainerRecordWithPorts("hook", "10.210.0.5",
[]string{"hook.example.com:8080/http"}, "test-machine-id")
hook.Container.Config.Labels[api.LabelHook] = "pre-deploy"
err := controller.generateAndLoadCaddyfile(context.Background(),
[]store.ContainerRecord{newerStopped, caddyCtr, app, unhealthy, hook})
require.ErrorContains(t, err, "admin socket unavailable")
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
assert.Contains(t, string(saved), "storage uncloud")
assert.NotContains(t, string(saved), "storage replacement")
assert.Contains(t, string(saved), "app.example.com")
assert.NotContains(t, string(saved), "custom.example.com")
assert.NotContains(t, string(saved), "unhealthy.example.com")
assert.NotContains(t, string(saved), "hook.example.com")
})
t.Run("replaces a legacy offline file with a bootstrap containing globals", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
legacy := "# generated by older daemon\nhttp:// { respond ok }\n" + legacyBootstrapMarker + "\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(legacy), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr})
require.ErrorContains(t, err, "admin socket unavailable")
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), "storage uncloud")
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
assert.NotContains(t, string(saved), legacyBootstrapMarker)
})
t.Run("saves a bootstrap for a stopped Caddy container with no file", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
caddyCtr.Container.State.Running = false
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr}))
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), bootstrapMarker+caddyCtr.Container.ID)
assert.Contains(t, string(saved), "storage uncloud")
})
for _, tc := range []struct {
name string
running bool
}{
{name: "preserves a full file when the admin socket is unavailable", running: true},
{name: "preserves a stopped container's full file", running: false},
} {
t.Run(tc.name, func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
saved := "{\n\tstorage uncloud\n}\n\nold.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage new\n}",
"test-machine-id", time.Now())
caddyCtr.Container.State.Running = tc.running
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"new.example.com:8080/http"},
"test-machine-id")
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr, app})
if tc.running {
require.ErrorContains(t, err, "admin socket unavailable")
} else {
require.NoError(t, err)
}
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
})
}
t.Run("loads a full file and reloads after Caddy restarts", func(t *testing.T) {
admin := &testCaddyAdmin{}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
caddyCtr.Container.State.Health = &container.Health{Status: "unhealthy"}
app := newContainerRecordWithCaddyConfig("web", "10.210.0.3", "app.example.com { respond app }",
"test-machine-id", time.Now())
records := []store.ContainerRecord{caddyCtr, app}
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), "storage uncloud")
assert.Contains(t, string(saved), "app.example.com")
assert.NotContains(t, string(saved), bootstrapMarker)
adapted, loads := admin.snapshot()
require.NotEmpty(t, adapted)
assert.Contains(t, adapted[0], "storage uncloud")
assert.Equal(t, 1, loads)
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
_, loads = admin.snapshot()
assert.Equal(t, 1, loads, "unchanged inputs should not reload Caddy")
records[0].Container.State.StartedAt = time.Now().UTC().Format(time.RFC3339Nano)
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
_, loads = admin.snapshot()
assert.Equal(t, 2, loads, "a restarted Caddy process needs the full configuration")
})
t.Run("reloads when an application route changes", func(t *testing.T) {
admin := &testCaddyAdmin{}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
app := newContainerRecordWithPorts("web", "10.210.0.3", []string{"app.example.com:8080/http"},
"test-machine-id")
records := []store.ContainerRecord{caddyCtr, app}
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
_, loads := admin.snapshot()
assert.Equal(t, 1, loads)
records[1] = newContainerRecordWithPorts("web", "10.210.0.4",
[]string{"app.example.com:8080/http"}, "test-machine-id")
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), "10.210.0.4:8080")
assert.NotContains(t, string(saved), "10.210.0.3:8080")
_, loads = admin.snapshot()
assert.Equal(t, 2, loads)
})
t.Run("rejects invalid globals without loading or replacing the saved file", func(t *testing.T) {
admin := &testCaddyAdmin{rejectAdaptContaining: "storage rejected"}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
saved := "{\n\tstorage uncloud\n}\n\nold.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage rejected\n}",
"test-machine-id", time.Now())
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr})
require.ErrorContains(t, err, "validate user-defined global Caddy config")
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
_, loads := admin.snapshot()
assert.Zero(t, loads)
})
t.Run("skips invalid application custom config", func(t *testing.T) {
admin := &testCaddyAdmin{rejectAdaptContaining: "invalid.example.com"}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
invalid := newContainerRecordWithCaddyConfig("invalid", "10.210.0.3",
"invalid.example.com { respond bad }", "test-machine-id", time.Now())
valid := newContainerRecordWithCaddyConfig("valid", "10.210.0.4",
"valid.example.com { respond good }", "test-machine-id", time.Now())
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(),
[]store.ContainerRecord{caddyCtr, invalid, valid}))
saved, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(saved), "storage uncloud")
assert.Contains(t, string(saved), "valid.example.com")
assert.NotContains(t, string(saved), "invalid.example.com")
assert.Contains(t, string(saved), "Skipped invalid user-defined configs")
_, loads := admin.snapshot()
assert.Equal(t, 1, loads)
})
t.Run("keeps the saved file when application validation cannot complete", func(t *testing.T) {
var loads atomic.Int64
socket := testAdminServer(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/adapt":
http.Error(w, "adapter unavailable", http.StatusInternalServerError)
case "/load":
loads.Add(1)
default:
http.NotFound(w, r)
}
})
controller := testController(t, socket)
saved := "old.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "",
"test-machine-id", time.Now())
app := newContainerRecordWithCaddyConfig("web", "10.210.0.3", "new.example.com { respond new }",
"test-machine-id", time.Now())
err := controller.generateAndLoadCaddyfile(context.Background(), []store.ContainerRecord{caddyCtr, app})
require.ErrorContains(t, err, "validate Caddy config for service")
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
assert.Zero(t, loads.Load())
})
t.Run("retries a rejected load without changing inputs", func(t *testing.T) {
admin := &testCaddyAdmin{rejectLoad: true}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
saved := "{\n\tstorage old\n}\n\nold.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
records := []store.ContainerRecord{caddyCtr}
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(), records), "load Caddyfile")
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
admin.setRejectLoad(false)
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
got, err = os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(got), "storage uncloud")
_, loads := admin.snapshot()
assert.Equal(t, 2, loads)
})
t.Run("retries saving after a successful load", func(t *testing.T) {
admin := &testCaddyAdmin{}
controller := testController(t, testAdminServer(t, admin.ServeHTTP))
group := controller.fileGroup
controller.fileGroup = "uncloud-test-group-that-does-not-exist"
saved := "{\n\tstorage old\n}\n\nold.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
records := []store.ContainerRecord{caddyCtr}
require.ErrorContains(t, controller.generateAndLoadCaddyfile(context.Background(), records), "save loaded Caddyfile")
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
controller.fileGroup = group
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
got, err = os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(got), "storage uncloud")
_, loads := admin.snapshot()
assert.Equal(t, 2, loads)
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
_, loads = admin.snapshot()
assert.Equal(t, 2, loads, "a loaded and saved revision should not reload")
})
t.Run("loads a saved bootstrap when the admin socket becomes available", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
caddyCtr := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{\n\tstorage uncloud\n}",
"test-machine-id", time.Now())
records := []store.ContainerRecord{caddyCtr}
require.Error(t, controller.generateAndLoadCaddyfile(context.Background(), records))
bootstrap, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Contains(t, string(bootstrap), bootstrapMarker)
admin := &testCaddyAdmin{}
socket := testAdminServer(t, admin.ServeHTTP)
controller.client = NewCaddyAdminClient(socket)
controller.generator = NewCaddyfileGenerator("test-machine-id", "test-machine", controller.client, nil)
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
full, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.NotContains(t, string(full), bootstrapMarker)
_, loads := admin.snapshot()
assert.Equal(t, 1, loads)
require.NoError(t, os.WriteFile(controller.caddyfilePath, bootstrap, 0o640))
require.NoError(t, controller.generateAndLoadCaddyfile(context.Background(), records))
_, loads = admin.snapshot()
assert.Equal(t, 2, loads, "a saved bootstrap is not an applied full configuration")
})
}
func TestController_WriteCaddyfileIfChanged(t *testing.T) {
t.Run("publishes a complete file with the configured group", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
caddyfile := "# Generated now\n{\n\tstorage uncloud\n}\n"
require.NoError(t, controller.writeCaddyfileIfChanged(caddyfile))
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, caddyfile, string(got))
info, err := os.Stat(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o640), info.Mode().Perm())
})
t.Run("does not replace a file when only the timestamp changes", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
saved := "# Generated yesterday\nsite.example.com { respond ok }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
before, err := os.Stat(controller.caddyfilePath)
require.NoError(t, err)
require.NoError(t, controller.writeCaddyfileIfChanged(
"# Generated today\nsite.example.com { respond ok }\n"))
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
after, err := os.Stat(controller.caddyfilePath)
require.NoError(t, err)
assert.True(t, os.SameFile(before, after), "an unchanged body should not replace the file")
})
t.Run("preserves the previous file when publication fails", func(t *testing.T) {
controller := testController(t, filepath.Join(t.TempDir(), "missing.sock"))
controller.fileGroup = "uncloud-test-group-that-does-not-exist"
saved := "# previous\nsite.example.com { respond old }\n"
require.NoError(t, os.WriteFile(controller.caddyfilePath, []byte(saved), 0o640))
err := controller.writeCaddyfileIfChanged("# next\nsite.example.com { respond new }\n")
require.ErrorContains(t, err, "change owner of temporary Caddyfile")
got, err := os.ReadFile(controller.caddyfilePath)
require.NoError(t, err)
assert.Equal(t, saved, string(got))
tmpFiles, err := filepath.Glob(filepath.Join(filepath.Dir(controller.caddyfilePath), ".Caddyfile-*"))
require.NoError(t, err)
assert.Empty(t, tmpFiles)
})
}
func TestSelectLocalCaddyContainer(t *testing.T) {
t.Run("prefers a running unhealthy container over a newer stopped one", func(t *testing.T) {
older := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ older }",
"test-machine-id", time.Now().Add(-time.Hour))
older.Container.State.Health = &container.Health{Status: "unhealthy"}
newer := newContainerRecordWithCaddyConfig("caddy", "10.210.0.3", "{ newer }",
"test-machine-id", time.Now())
newer.Container.State.Running = false
selected := selectLocalCaddyContainer([]store.ContainerRecord{newer, older}, "test-machine-id")
require.NotNil(t, selected)
assert.Equal(t, older.Container.ID, selected.ID)
})
t.Run("ignores remote and hook containers", func(t *testing.T) {
local := newContainerRecordWithCaddyConfig("caddy", "10.210.0.2", "{ local }",
"test-machine-id", time.Now().Add(-time.Hour))
remote := newContainerRecordWithCaddyConfig("caddy", "10.210.0.3", "{ remote }",
"other-machine", time.Now())
hook := newContainerRecordWithCaddyConfig("caddy", "10.210.0.4", "{ hook }",
"test-machine-id", time.Now())
hook.Container.Config.Labels[api.LabelHook] = "pre-deploy"
selected := selectLocalCaddyContainer([]store.ContainerRecord{remote, hook, local}, "test-machine-id")
require.NotNil(t, selected)
assert.Equal(t, local.Container.ID, selected.ID)
})
}
// TestContainerFingerprint_EqualCoversAllFields is a guard: when a field is added to containerFingerprint, // TestContainerFingerprint_EqualCoversAllFields is a guard: when a field is added to containerFingerprint,
// Equal must also compare it. A mutation of any single field should flip equality to false. If this test fails // Equal must also compare it. A mutation of any single field should flip equality to false. If this test fails
// after adding a field, update Equal to include it. // after adding a field, update Equal to include it.
-141
View File
@@ -1,141 +0,0 @@
package caddyconfig
import (
"encoding/json"
"errors"
"fmt"
"maps"
"net/http"
"slices"
"strconv"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
"github.com/psviderski/uncloud/pkg/api"
)
func GenerateJSONConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromPorts(containers)
var warnings []caddyconfig.Warning
servers := make(map[string]*caddyhttp.Server)
servers["http"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPPort)},
// All http requests to this server are logged to the default logger.
Logs: &caddyhttp.ServerLogConfig{},
Routes: append(
hostUpstreamsToRoutes(httpHostUpstreams, &warnings),
// Add a route to respond with a static verification response at the /.uncloud-verify path.
verificationRoute(verifyResponse, &warnings),
),
}
servers["https"] = &caddyhttp.Server{
Listen: []string{fmt.Sprintf(":%d", caddyhttp.DefaultHTTPSPort)},
// All https requests to this server are logged to the default logger.
Logs: &caddyhttp.ServerLogConfig{},
Routes: hostUpstreamsToRoutes(httpsHostUpstreams, &warnings),
}
httpApp := caddyhttp.App{
Servers: servers,
}
config := &caddy.Config{
AppsRaw: caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, &warnings),
},
}
var err error
if len(warnings) > 0 {
// warnings only contains errors from JSON marshaling, which are highly unlikely with correct code.
for _, w := range warnings {
err = errors.Join(err, errors.New(w.Message))
}
return nil, fmt.Errorf("marshal Caddy configuration: %w", err)
}
return config, nil
}
// hostUpstreamsToRoutes converts a map of hostnames to upstreams to a list of Caddy routes.
func hostUpstreamsToRoutes(hostUpstreams map[string][]string, warnings *[]caddyconfig.Warning) []caddyhttp.Route {
// Sort hostnames for deterministic output.
hostnames := slices.Collect(maps.Keys(hostUpstreams))
slices.Sort(hostnames)
routes := make([]caddyhttp.Route, 0, len(hostUpstreams))
for _, hostname := range hostnames {
upstreams := hostUpstreams[hostname]
upstreamPool := make([]*reverseproxy.Upstream, len(upstreams))
for i, upstream := range upstreams {
upstreamPool[i] = &reverseproxy.Upstream{
Dial: upstream,
}
}
handler := &reverseproxy.Handler{
HealthChecks: &reverseproxy.HealthChecks{
// Enable passive health checks to automatically detect unhealthy upstreams.
Passive: &reverseproxy.PassiveHealthChecks{
FailDuration: caddy.Duration(30 * time.Second),
},
},
LoadBalancing: &reverseproxy.LoadBalancing{
// Retry failed requests to skip over temporarily unavailable upstreams.
Retries: 3,
},
Upstreams: upstreamPool,
}
routes = append(routes, caddyhttp.Route{
MatcherSetsRaw: caddyhttp.RawMatcherSets{
{
"host": caddyconfig.JSON(caddyhttp.MatchHost{hostname}, warnings),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", warnings),
},
})
}
return routes
}
// verificationRoute returns a Caddy route that responds with the given static response at the /.uncloud-verify path.
func verificationRoute(response string, warnings *[]caddyconfig.Warning) caddyhttp.Route {
// Return the following route:
// {
// "match": [
// {
// "path": [
// "/.uncloud-verify"
// ]
// }
// ],
// "handle": [
// {
// "handler": "static_response",
// "body": "<response>",
// "status_code": 200
// }
// ]
// }
staticResponse := caddyhttp.StaticResponse{
StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusOK)),
Body: response,
}
return caddyhttp.Route{
MatcherSetsRaw: caddyhttp.RawMatcherSets{
{
"path": caddyconfig.JSON(caddyhttp.MatchPath{VerifyPath}, warnings),
},
},
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(staticResponse, "handler", "static_response", warnings),
},
}
}
@@ -1,378 +0,0 @@
package caddyconfig
import (
"strings"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateJSONConfig(t *testing.T) {
configWithoutServices := `{
"servers": {
"http": {
"listen": [":80"],
"routes": [{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}],
"logs": {}
},
"https": {
"listen": [":443"],
"logs": {}
}
}
}`
tests := []struct {
name string
containers []api.ServiceContainer
want string
wantErr bool
}{
{
name: "empty containers",
containers: []api.ServiceContainer{},
want: configWithoutServices,
wantErr: false,
},
{
name: "HTTP container",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "app.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [{"dial": "10.210.0.2:8080"}]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "load balancing multiple containers",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "app.example.com:8080/http"),
newContainer("10.210.0.3", "app.example.com:8080/http"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.2:8080"},
{"dial": "10.210.0.3:8080"}
]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "HTTPS container",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "secure.example.com:8000/https"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"routes": [
{
"match": [{"host": ["secure.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [{"dial": "10.210.0.2:8000"}]
}]
}
],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "mixed HTTP and HTTPS",
containers: []api.ServiceContainer{
newContainer("10.210.0.2",
"app.example.com:8080/http",
"web.example.com:8000/http"),
newContainer("10.210.0.3",
"app.example.com:8080/http",
"secure.example.com:8888/https"),
newContainer("10.210.0.4",
"web.example.com:8000/http",
"secure.example.com:8888/https"),
newContainer("10.210.0.5",
"app.example.com:8080/http",
"web.example.com:8000/http",
"secure.example.com:8888/https"),
},
want: `{
"servers": {
"http": {
"listen": [":80"],
"routes": [
{
"match": [{"host": ["app.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.2:8080"},
{"dial": "10.210.0.3:8080"},
{"dial": "10.210.0.5:8080"}
]
}]
},
{
"match": [{"host": ["web.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.2:8000"},
{"dial": "10.210.0.4:8000"},
{"dial": "10.210.0.5:8000"}
]
}]
},
{
"match": [{"path": ["/.uncloud-verify"]}],
"handle": [{
"body": "verification-response-body",
"handler": "static_response",
"status_code": 200
}]
}
],
"logs": {}
},
"https": {
"listen": [":443"],
"routes": [
{
"match": [{"host": ["secure.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"health_checks": {
"passive": {
"fail_duration": 30000000000
}
},
"load_balancing": {
"retries": 3
},
"upstreams": [
{"dial": "10.210.0.3:8888"},
{"dial": "10.210.0.4:8888"},
{"dial": "10.210.0.5:8888"}
]
}]
}
],
"logs": {}
}
}
}`,
wantErr: false,
},
{
name: "container without uncloud network ignored",
containers: []api.ServiceContainer{
newContainerWithoutNetwork("ignored.example.com:8080/http"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "container with invalid port ignored",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "invalid-port"),
},
want: configWithoutServices,
wantErr: false,
},
{
name: "containers with unsupported protocols and host mode ignored",
containers: []api.ServiceContainer{
newContainer("10.210.0.2", "5000/tcp"),
newContainer("10.210.0.3", "5000/udp"),
newContainer("10.210.0.4", "80:8080/tcp@host"),
},
want: configWithoutServices,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config, err := GenerateJSONConfig(tt.containers, "verification-response-body")
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
require.Len(t, config.AppsRaw, 1, "Expected one http app")
require.Contains(t, config.AppsRaw, "http", "Expected http app")
assert.JSONEq(t, tt.want, string(config.AppsRaw["http"]), "Generated Caddy app config doesn't match")
})
}
}
func newContainer(ip string, ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
},
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
docker.NetworkName: {
IPAddress: ip,
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
portsLabel := strings.Join(ports, ",")
return api.ServiceContainer{Container: api.Container{InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
State: &container.State{
Running: true,
},
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
"other-network": {
IPAddress: "172.17.0.2",
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServicePorts: portsLabel,
},
},
}}}
}
+11 -2
View File
@@ -2,6 +2,7 @@ package caddyconfig
import ( import (
"context" "context"
"errors"
"os" "os"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -22,18 +23,26 @@ func NewServer(service *Service) *Server {
return &Server{service: service} return &Server{service: service}
} }
// GetConfig retrieves the current Caddy configuration from the machine. // GetConfig retrieves the saved Caddy configuration and the latest reconciliation error from the machine.
func (s *Server) GetConfig(ctx context.Context, _ *emptypb.Empty) (*pb.GetCaddyConfigResponse, error) { func (s *Server) GetConfig(ctx context.Context, _ *emptypb.Empty) (*pb.GetCaddyConfigResponse, error) {
caddyfile, modifiedAt, err := s.service.Caddyfile() caddyfile, modifiedAt, err := s.service.Caddyfile()
if err != nil { if err != nil {
if os.IsNotExist(err) { if errors.Is(err, os.ErrNotExist) {
if lastErr := s.service.LastReconciliationError(); lastErr != nil {
return nil, status.Errorf(codes.NotFound, "%v; last Caddy config load failed: %v", err, lastErr)
}
return nil, status.Error(codes.NotFound, err.Error()) return nil, status.Error(codes.NotFound, err.Error())
} }
return nil, status.Error(codes.Internal, err.Error()) return nil, status.Error(codes.Internal, err.Error())
} }
recErr := ""
if lastErr := s.service.LastReconciliationError(); lastErr != nil {
recErr = lastErr.Error()
}
return &pb.GetCaddyConfigResponse{ return &pb.GetCaddyConfigResponse{
Caddyfile: caddyfile, Caddyfile: caddyfile,
ModifiedAt: timestamppb.New(modifiedAt), ModifiedAt: timestamppb.New(modifiedAt),
LastReconciliationError: recErr,
}, nil }, nil
} }
@@ -0,0 +1,62 @@
package caddyconfig
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
func TestServer_GetConfig(t *testing.T) {
t.Run("returns saved Caddyfile and reconciliation error", func(t *testing.T) {
dir := t.TempDir()
caddyfile := "example.com { respond ok }\n"
path := filepath.Join(dir, "Caddyfile")
require.NoError(t, os.WriteFile(path, []byte(caddyfile), 0o640))
info, err := os.Stat(path)
require.NoError(t, err)
service := NewService(dir)
service.setReconciliationResult(errors.New("module not registered: caddy.storage.uncloud"))
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
require.NoError(t, err)
assert.Equal(t, caddyfile, config.GetCaddyfile())
assert.True(t, info.ModTime().Equal(config.GetModifiedAt().AsTime()))
assert.Equal(t, "module not registered: caddy.storage.uncloud", config.GetLastReconciliationError())
service.setReconciliationResult(nil)
config, err = NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
require.NoError(t, err)
assert.Empty(t, config.GetLastReconciliationError())
})
t.Run("returns NotFound with reconciliation error when Caddyfile is absent", func(t *testing.T) {
service := NewService(t.TempDir())
service.setReconciliationResult(errors.New("module not registered: caddy.storage.uncloud"))
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
assert.Nil(t, config)
assert.Equal(t, codes.NotFound, status.Code(err))
assert.ErrorContains(t, err, "Caddyfile")
assert.ErrorContains(t, err, "last Caddy config load failed: module not registered: caddy.storage.uncloud")
})
t.Run("returns NotFound without reconciliation error when Caddyfile is absent", func(t *testing.T) {
service := NewService(t.TempDir())
config, err := NewServer(service).GetConfig(context.Background(), &emptypb.Empty{})
assert.Nil(t, config)
assert.Equal(t, codes.NotFound, status.Code(err))
assert.NotContains(t, err.Error(), "last Caddy config load failed")
})
}
+19 -1
View File
@@ -4,12 +4,15 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"time" "time"
) )
// Service provides methods to interact with the Caddy configuration on the machine. // Service provides methods to interact with the Caddy configuration on the machine.
type Service struct { type Service struct {
configDir string configDir string
mu sync.RWMutex
lastReconciliationError error
} }
// NewService creates a new Service instance with the specified Caddy configuration directory. // NewService creates a new Service instance with the specified Caddy configuration directory.
@@ -17,7 +20,8 @@ func NewService(configDir string) *Service {
return &Service{configDir: configDir} return &Service{configDir: configDir}
} }
// Caddyfile retrieves the current Caddy configuration (Caddyfile) from the machine's config directory. // Caddyfile retrieves the saved Caddyfile from the machine's config directory. The saved file may differ from the
// running configuration if Caddy accepted a load but the subsequent write failed.
func (s *Service) Caddyfile() (string, time.Time, error) { func (s *Service) Caddyfile() (string, time.Time, error) {
path := filepath.Join(s.configDir, "Caddyfile") path := filepath.Join(s.configDir, "Caddyfile")
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
@@ -33,3 +37,17 @@ func (s *Service) Caddyfile() (string, time.Time, error) {
return string(content), fileInfo.ModTime(), nil return string(content), fileInfo.ModTime(), nil
} }
// LastReconciliationError reports the most recent unsuccessful controller attempt, if any. A successful attempt
// clears it. An empty value does not prove that Caddy has loaded the saved Caddyfile.
func (s *Service) LastReconciliationError() error {
s.mu.RLock()
defer s.mu.RUnlock()
return s.lastReconciliationError
}
func (s *Service) setReconciliationResult(err error) {
s.mu.Lock()
defer s.mu.Unlock()
s.lastReconciliationError = err
}
@@ -0,0 +1,42 @@
package caddyconfig
import (
"errors"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestService_LastReconciliationError(t *testing.T) {
service := NewService(t.TempDir())
assert.NoError(t, service.LastReconciliationError())
service.setReconciliationResult(errors.New("global Caddy config rejected"))
assert.EqualError(t, service.LastReconciliationError(), "global Caddy config rejected")
service.setReconciliationResult(nil)
assert.NoError(t, service.LastReconciliationError())
}
func TestService_LastReconciliationErrorConcurrent(t *testing.T) {
service := NewService(t.TempDir())
var wg sync.WaitGroup
for range 10 {
wg.Add(2)
go func() {
defer wg.Done()
for range 100 {
service.setReconciliationResult(errors.New("retry"))
service.setReconciliationResult(nil)
}
}()
go func() {
defer wg.Done()
for range 100 {
_ = service.LastReconciliationError()
}
}()
}
wg.Wait()
}
@@ -0,0 +1,79 @@
package caddystorage
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strings"
"github.com/caddyserver/certmagic"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/machine/store"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// ListCertificates lists issued certificates from this machine's Caddy storage replica.
// Unreadable entries and invalid resource metadata are skipped. Chains and issuer data are returned as stored.
func (s *Server) ListCertificates(ctx context.Context, _ *emptypb.Empty) (*pb.ListCertificatesResponse, error) {
records, err := s.store.List(ctx, "certificates/", store.KeyspaceListOptions{KeysOnly: true})
if err != nil {
if ctx.Err() != nil {
return nil, status.FromContextError(ctx.Err()).Err()
}
return nil, status.Errorf(codes.Internal, "list Caddy certificates: %v", err)
}
resp := &pb.ListCertificatesResponse{}
for _, record := range records {
if err = ctx.Err(); err != nil {
return nil, status.FromContextError(err).Err()
}
if !isCertificateKey(record.Key) {
continue
}
cert, err := s.loadIssuedCertificate(ctx, record.Key)
if err != nil {
slog.Warn("Failed to load issued certificate from Caddy storage.", "key", record.Key, "err", err)
continue
}
resp.Certificates = append(resp.Certificates, cert)
}
return resp, nil
}
// isCertificateKey recognizes CertMagic's certificates/<issuer>/<name>/<name>.crt layout.
func isCertificateKey(key string) bool {
parts := strings.Split(key, "/")
return len(parts) == 4 && parts[0] == "certificates" && parts[3] == parts[2]+".crt"
}
func (s *Server) loadIssuedCertificate(ctx context.Context, key string) (*pb.IssuedCertificate, error) {
chain, err := s.store.Get(ctx, key)
if err != nil {
return nil, err
}
metaKey := strings.TrimSuffix(key, ".crt") + ".json"
meta, err := s.store.Get(ctx, metaKey)
if err != nil {
return nil, err
}
var resource certmagic.CertificateResource
if err = json.Unmarshal(meta.Value, &resource); err != nil {
return nil, err
}
if len(resource.SANs) == 0 || strings.TrimSpace(resource.SANs[0]) == "" {
return nil, fmt.Errorf("certificate metadata has no SAN")
}
return &pb.IssuedCertificate{
San: resource.SANs[0],
Chain: chain.Value,
IssuerData: resource.IssuerData,
}, nil
}
@@ -0,0 +1,233 @@
package caddystorage
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"errors"
"math/big"
"net"
"slices"
"strings"
"testing"
"time"
"github.com/caddyserver/certmagic"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
func TestServerListCertificates(t *testing.T) {
for _, sans := range [][]string{
{"app.example.com"}, {"*.example.com"}, {"192.0.2.1"}, {"www.example.com", "example.com"},
} {
t.Run(sans[0], func(t *testing.T) {
bundle, _, _ := certificateFixture(t, &x509.Certificate{
DNSNames: []string{"example.com", "www.example.com"},
IPAddresses: []net.IP{net.ParseIP("192.0.2.1")},
NotAfter: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
})
key := certmagic.StorageKeys.SiteCert("local", sans[0])
storage := newCertificateStorage(t)
raw := json.RawMessage(`{"unknown_issuer_field":true}`)
storage.add(t, key, bundle, sans, raw)
storage.keys = append(storage.keys, "certificates", "certificates/local", "certificates/local/app.crt",
"certificates/local/app/wrong.crt", "certificates/local/app/app.crt/extra.crt",
"pki/authorities/local/root.crt", "ocsp/certificate")
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
require.NoError(t, err)
require.Len(t, resp.Certificates, 1)
cert := resp.Certificates[0]
assert.Equal(t, sans[0], cert.San)
assert.Equal(t, bundle, cert.Chain, "PEM must be returned unchanged")
assert.Equal(t, []byte(raw), cert.IssuerData)
assert.Equal(t, []string{key, strings.TrimSuffix(key, ".crt") + ".json"}, storage.loads)
})
}
}
func TestServerListCertificates_PreservesIssuerData(t *testing.T) {
bundle, _, _ := certificateFixture(t, &x509.Certificate{})
for _, raw := range []string{
"", "null", `{"url":"https://ca/cert/1","ca":"https://ca/directory"}`,
`{"url":"https://ca/cert/1","ca":"https://ca/directory","renewal_info":{"_selectedTime":"invalid"}}`,
`"provider-record"`, "[1,2,3]",
} {
t.Run(raw, func(t *testing.T) {
storage := newCertificateStorage(t)
storage.add(t, "certificates/custom/app/app.crt", bundle, []string{"app"}, json.RawMessage(raw))
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
require.NoError(t, err)
require.Len(t, resp.Certificates, 1)
assert.Equal(t, raw, string(resp.Certificates[0].IssuerData))
})
}
}
func TestServerListCertificates_OrderingAndDuplicates(t *testing.T) {
bundle, _, _ := certificateFixture(t, &x509.Certificate{})
storage := newCertificateStorage(t)
storage.add(t, certmagic.StorageKeys.SiteCert("z-issuer", "a.example.com"), bundle, []string{"a.example.com"}, nil)
storage.add(t, certmagic.StorageKeys.SiteCert("a-issuer", "z.example.com"), bundle, []string{"z.example.com"}, nil)
storage.add(t, certmagic.StorageKeys.SiteCert("b-issuer", "z.example.com"), bundle, []string{"z.example.com"}, nil)
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
require.NoError(t, err)
require.Len(t, resp.Certificates, 3)
certs := resp.Certificates
assert.Equal(t, []string{"z.example.com", "z.example.com", "a.example.com"},
[]string{certs[0].San, certs[1].San, certs[2].San})
for _, cert := range certs {
assert.Equal(t, bundle, cert.Chain)
}
}
func TestServerListCertificates_SkipsUnreadableEntries(t *testing.T) {
bundle, _, _ := certificateFixture(t, &x509.Certificate{})
const badKey = "certificates/local/bad/bad.crt"
const metaKey = "certificates/local/bad/bad.json"
for _, tt := range []struct {
name string
change func(*certificateStorageStub)
}{
{"missing chain", func(s *certificateStorageStub) { delete(s.values, badKey) }},
{"chain read failure", func(s *certificateStorageStub) { s.loadErrors[badKey] = errors.New("offline") }},
{"missing metadata", func(s *certificateStorageStub) { delete(s.values, metaKey) }},
{"metadata read failure", func(s *certificateStorageStub) { s.loadErrors[metaKey] = errors.New("offline") }},
{"invalid JSON", func(s *certificateStorageStub) { s.values[metaKey] = []byte("{") }},
{"missing SAN", func(s *certificateStorageStub) { s.values[metaKey] = []byte(`{}`) }},
{"empty first SAN", func(s *certificateStorageStub) { s.values[metaKey] = []byte(`{"sans":["","app"]}`) }},
{"blank first SAN", func(s *certificateStorageStub) { s.values[metaKey] = []byte(`{"sans":[" "]}`) }},
} {
t.Run(tt.name, func(t *testing.T) {
storage := newCertificateStorage(t)
storage.add(t, badKey, bundle, []string{"bad"}, nil)
storage.add(t, "certificates/local/good/good.crt", bundle, []string{"good"}, nil)
tt.change(storage)
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
require.NoError(t, err)
require.Len(t, resp.Certificates, 1)
assert.Equal(t, "good", resp.Certificates[0].San)
})
}
t.Run("all entries unreadable", func(t *testing.T) {
storage := newCertificateStorage(t)
storage.keys = []string{badKey}
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
require.NoError(t, err)
assert.Empty(t, resp.Certificates)
})
}
func TestServerListCertificates_ListErrors(t *testing.T) {
for _, fail := range []bool{false, true} {
storage := newCertificateStorage(t)
if fail {
storage.listError = errors.New("offline")
}
resp, err := NewServer(storage).ListCertificates(t.Context(), &emptypb.Empty{})
assert.Empty(t, storage.loads)
if fail {
assert.Nil(t, resp)
assert.Equal(t, codes.Internal, status.Code(err))
} else {
require.NoError(t, err)
assert.Empty(t, resp.Certificates)
}
}
}
// Only reads are implemented. Unexpected writes panic through the nil embedded Keyspace.
type certificateStorageStub struct {
*store.Keyspace
t *testing.T
keys []string
values map[string][]byte
loads []string
listCalls int
listError error
loadErrors map[string]error
onLoad func(string)
}
func newCertificateStorage(t *testing.T) *certificateStorageStub {
return &certificateStorageStub{t: t, values: make(map[string][]byte), loadErrors: make(map[string]error)}
}
func (s *certificateStorageStub) add(t *testing.T, key string, bundle []byte, sans []string, issuerData json.RawMessage) {
t.Helper()
metaKey := strings.TrimSuffix(key, ".crt") + ".json"
keyKey := strings.TrimSuffix(key, ".crt") + ".key"
meta, err := json.Marshal(certmagic.CertificateResource{SANs: sans, IssuerData: issuerData})
require.NoError(t, err)
s.keys = append(s.keys, key, metaKey, keyKey)
s.values[key], s.values[metaKey], s.values[keyKey] = bundle, meta, []byte("must not read private keys")
}
func (s *certificateStorageStub) List(ctx context.Context, prefix string, opts store.KeyspaceListOptions) ([]store.Record, error) {
s.listCalls++
assert.Equal(s.t, "certificates/", prefix)
assert.True(s.t, opts.KeysOnly, "listing must not fetch private key values")
var records []store.Record
for _, key := range slices.Sorted(slices.Values(s.keys)) {
records = append(records, store.Record{Key: key})
}
return records, s.listError
}
func (s *certificateStorageStub) Get(ctx context.Context, key string) (store.Record, error) {
require.False(s.t, strings.HasSuffix(key, ".key"), "private keys must never be loaded")
s.loads = append(s.loads, key)
if s.onLoad != nil {
s.onLoad(key)
}
if err := s.loadErrors[key]; err != nil {
return store.Record{}, err
}
value, ok := s.values[key]
if !ok {
return store.Record{}, store.ErrKeyNotFound
}
return store.Record{Key: key, Value: value}, nil
}
func certificateFixture(t *testing.T, template *x509.Certificate) ([]byte, *x509.Certificate, *x509.Certificate) {
t.Helper()
publicKey, key, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
root := &x509.Certificate{
SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "Test CA"}, IsCA: true,
BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign,
NotBefore: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
NotAfter: time.Date(2040, 1, 1, 0, 0, 0, 0, time.UTC),
}
rootDER, err := x509.CreateCertificate(rand.Reader, root, root, publicKey, key)
require.NoError(t, err)
root, err = x509.ParseCertificate(rootDER)
require.NoError(t, err)
template.SerialNumber = big.NewInt(2)
template.NotBefore = root.NotBefore
if template.NotAfter.IsZero() {
template.NotAfter = root.NotAfter
}
leafDER, err := x509.CreateCertificate(rand.Reader, template, root, publicKey, key)
require.NoError(t, err)
leaf, err := x509.ParseCertificate(leafDER)
require.NoError(t, err)
bundle := append(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafDER}),
pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootDER})...)
return bundle, leaf, root
}
+9 -2
View File
@@ -25,10 +25,17 @@ const Namespace = "caddy_storage"
// Server implements the machine-local CaddyStorage gRPC service. // Server implements the machine-local CaddyStorage gRPC service.
type Server struct { type Server struct {
pb.UnimplementedCaddyStorageServer pb.UnimplementedCaddyStorageServer
store *store.Keyspace store keyspace
} }
func NewServer(store *store.Keyspace) *Server { type keyspace interface {
Get(context.Context, string) (store.Record, error)
Put(context.Context, string, []byte) error
Delete(context.Context, string, store.KeyspaceDeleteOptions) error
List(context.Context, string, store.KeyspaceListOptions) ([]store.Record, error)
}
func NewServer(store keyspace) *Server {
return &Server{store: store} return &Server{store: store}
} }
+71 -13
View File
@@ -4,20 +4,24 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"strings"
"github.com/miekg/dns"
"github.com/psviderski/uncloud/api/pb" "github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/dns" undns "github.com/psviderski/uncloud/internal/dns"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
) )
// uncloudDNSKey is the key used to store the details of the reserved domain in the store. // uncloudDNSKey stores the cluster domain and, when reserved, its Uncloud DNS credentials.
const uncloudDNSKey = "uncloud_dns" const uncloudDNSKey = "uncloud_dns"
type uncloudDNSDomain struct { type uncloudDNSDomain struct {
// Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved. // Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved.
// An empty endpoint means the domain is managed externally.
Endpoint string Endpoint string
Name string Name string
// TODO: encrypt the token in the store. // TODO: encrypt the token in the store.
@@ -34,14 +38,14 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
} }
if _, err := c.storedDomain(ctx); err == nil { if _, err := c.storedDomain(ctx); err == nil {
return nil, status.Errorf(codes.AlreadyExists, "domain already reserved") return nil, status.Error(codes.AlreadyExists, "cluster domain already configured")
} else { } else {
if s := status.Convert(err); s.Code() != codes.NotFound { if s := status.Convert(err); s.Code() != codes.NotFound {
return nil, err return nil, err
} }
} }
dnsClient := dns.NewClient() dnsClient := undns.NewClient()
name, token, err := dnsClient.ReserveDomain(req.Endpoint) name, token, err := dnsClient.ReserveDomain(req.Endpoint)
if err != nil { if err != nil {
return nil, status.Error(codes.Internal, err.Error()) return nil, status.Error(codes.Internal, err.Error())
@@ -60,7 +64,7 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err) return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err)
} }
return &pb.Domain{Name: name}, nil return &pb.Domain{Name: name, Reserved: proto.Bool(true)}, nil
} }
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) { func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
@@ -73,7 +77,7 @@ func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain,
return nil, err return nil, err
} }
return &pb.Domain{Name: domain.Name}, nil return &pb.Domain{Name: domain.Name, Reserved: proto.Bool(domain.Endpoint != "")}, nil
} }
func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) { func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
@@ -103,13 +107,63 @@ func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Doma
if err != nil { if err != nil {
return nil, err return nil, err
} }
if domain.Endpoint == "" {
return nil, status.Error(codes.FailedPrecondition,
"cluster domain is set manually, use 'uc dns set \"\"' to unset it")
}
if err = c.store.Delete(ctx, uncloudDNSKey); err != nil { if err = c.store.Delete(ctx, uncloudDNSKey); err != nil {
return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err) return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err)
} }
// TODO: implement and call Uncloud DNS endpoint to release/delete the domain. // TODO: implement and call Uncloud DNS endpoint to release/delete the domain.
return &pb.Domain{Name: domain.Name}, nil return &pb.Domain{Name: domain.Name, Reserved: proto.Bool(true)}, nil
}
func (c *Cluster) SetDomain(ctx context.Context, req *pb.SetDomainRequest) (*emptypb.Empty, error) {
if err := c.checkReady(); err != nil {
return nil, err
}
name := req.GetName()
if name != "" {
if labels, ok := dns.IsDomainName(name); !ok || labels < 2 {
return nil, status.Errorf(codes.InvalidArgument,
"invalid cluster domain '%s': must be a valid domain name with at least two labels", name)
}
}
name = strings.ToLower(strings.TrimSuffix(name, "."))
existing, err := c.storedDomain(ctx)
if err != nil && status.Code(err) != codes.NotFound {
return nil, err
}
if name == "" {
if err == nil {
if existing.Endpoint != "" {
return nil, status.Error(codes.FailedPrecondition,
"cluster domain is reserved in Uncloud DNS, use 'uc dns release' to release it")
}
if err := c.store.Delete(ctx, uncloudDNSKey); err != nil {
return nil, status.Errorf(codes.Internal, "unset cluster domain: %v", err)
}
}
return &emptypb.Empty{}, nil
}
if err == nil {
return nil, status.Error(codes.AlreadyExists, "cluster domain already configured")
}
domain := uncloudDNSDomain{Name: name}
domainJSON, err := json.Marshal(domain)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal set domain for store: %v", err)
}
if err = c.store.Put(ctx, uncloudDNSKey, domainJSON); err != nil {
return nil, status.Errorf(codes.Internal, "store set domain: %v", err)
}
return &emptypb.Empty{}, nil
} }
func (c *Cluster) CreateDomainRecords( func (c *Cluster) CreateDomainRecords(
@@ -124,12 +178,16 @@ func (c *Cluster) CreateDomainRecords(
return nil, err return nil, err
} }
dnsClient := dns.NewClient() if domain.Endpoint == "" {
recordsReq := make([]dns.RecordRequest, len(req.Records)) return nil, status.Error(codes.FailedPrecondition, "cluster domain is not reserved in Uncloud DNS")
}
dnsClient := undns.NewClient()
recordsReq := make([]undns.RecordRequest, len(req.Records))
for i, r := range req.Records { for i, r := range req.Records {
recordsReq[i] = dns.RecordRequest{ recordsReq[i] = undns.RecordRequest{
Name: r.Name, Name: r.Name,
Type: dns.RecordType(r.Type.String()), Type: undns.RecordType(r.Type.String()),
Values: r.Values, Values: r.Values,
} }
} }
@@ -149,9 +207,9 @@ func (c *Cluster) CreateDomainRecords(
} }
switch r.Type { switch r.Type {
case dns.RecordTypeA: case undns.RecordTypeA:
resp.Records[i].Type = pb.DNSRecord_A resp.Records[i].Type = pb.DNSRecord_A
case dns.RecordTypeAAAA: case undns.RecordTypeAAAA:
resp.Records[i].Type = pb.DNSRecord_AAAA resp.Records[i].Type = pb.DNSRecord_AAAA
} }
} }
+10 -1
View File
@@ -538,6 +538,15 @@ func (s *Server) CreateServiceContainer(
containerName = fmt.Sprintf("%s-%s", spec.Name, suffix) containerName = fmt.Sprintf("%s-%s", spec.Name, suffix)
} }
desiredSpec := spec
renderedSpec, renderErr := desiredSpec.RenderRuntimeTemplates(api.RuntimeTemplateContext{
Container: api.RuntimeTemplateContainerContext{Name: containerName},
})
if renderErr != nil {
return nil, status.Errorf(codes.InvalidArgument, "render runtime templates: %v", renderErr)
}
spec = renderedSpec
envVars := maps.Clone(spec.Container.Env) envVars := maps.Clone(spec.Container.Env)
if envVars == nil { if envVars == nil {
envVars = make(api.EnvVars) envVars = make(api.EnvVars)
@@ -745,7 +754,7 @@ func (s *Server) CreateServiceContainer(
_ = s.client.ContainerRemove(ctx, resp.ID, container.RemoveOptions{RemoveVolumes: true}) _ = s.client.ContainerRemove(ctx, resp.ID, container.RemoveOptions{RemoveVolumes: true})
} }
specBytes, err := json.Marshal(spec) specBytes, err := json.Marshal(desiredSpec)
if err != nil { if err != nil {
removeContainer() removeContainer()
return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err) return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
@@ -0,0 +1,53 @@
package docker
import (
"context"
"encoding/json"
"strings"
"testing"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
_ "modernc.org/sqlite"
)
func TestCreateServiceContainer_RejectsRuntimeTemplateError(t *testing.T) {
t.Parallel()
desiredSpec := api.ServiceSpec{
Name: "web",
Container: api.ContainerSpec{
Image: "busybox:latest",
VolumeMounts: []api.VolumeMount{
{VolumeName: "data", ContainerPath: "/data"},
},
},
Volumes: []api.VolumeSpec{
{
Name: "data",
Type: api.VolumeTypeBind,
BindOptions: &api.BindOptions{
// Early validation takes the valid branch. Rendering with the real container name takes the invalid one.
HostPath: `/host/{{if eq .Container.Name "web-a1b2"}}{{.Container.ID}}{{else}}valid{{end}}`,
},
},
},
}
specJSON, err := json.Marshal(desiredSpec)
require.NoError(t, err)
server := &Server{}
_, err = server.CreateServiceContainer(context.Background(), &pb.CreateServiceContainerRequest{
ServiceId: strings.Repeat("a", 32),
ServiceSpec: specJSON,
ContainerName: "web-a1b2",
})
require.Error(t, err)
assert.Equal(t, codes.InvalidArgument, status.Code(err))
assert.ErrorContains(t, err, "render runtime template in bind volume 'data' host path")
}
+6 -3
View File
@@ -198,6 +198,8 @@ type Machine struct {
dockerServer *machinedocker.Server dockerServer *machinedocker.Server
// machineAPIServer handles API requests directly on this machine. // machineAPIServer handles API requests directly on this machine.
machineAPIServer *grpc.Server machineAPIServer *grpc.Server
// caddyService provides methods to interact with the Caddy configuration on the machine.
caddyService *caddyconfig.Service
// proxyDirector routes API requests to local or remote machines. // proxyDirector routes API requests to local or remote machines.
proxyDirector *apiproxy.Director proxyDirector *apiproxy.Director
@@ -260,11 +262,11 @@ func NewMachine(config *Config) (*Machine, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("create corrosion API client: %w", err) return nil, fmt.Errorf("create corrosion API client: %w", err)
} }
corroStore := store.New(corro)
corroAdmin, err := corrosion.NewAdminClient(config.CorrosionAdminSockPath) corroAdmin, err := corrosion.NewAdminClient(config.CorrosionAdminSockPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("create corrosion admin client: %w", err) return nil, fmt.Errorf("create corrosion admin client: %w", err)
} }
corroStore := store.New(corro, corroAdmin)
initialised := make(chan struct{}) initialised := make(chan struct{})
clusterReady := make(chan struct{}) clusterReady := make(chan struct{})
@@ -302,6 +304,7 @@ func NewMachine(config *Config) (*Machine, error) {
dockerService: dockerService, dockerService: dockerService,
clusterAPIServer: clusterAPIServer, clusterAPIServer: clusterAPIServer,
proxyDirector: proxyDirector, proxyDirector: proxyDirector,
caddyService: caddyconfig.NewService(config.CaddyConfigDir),
} }
// Machine IP will only be available after the machine is initialised as a cluster member so wrap it in a function. // Machine IP will only be available after the machine is initialised as a cluster member so wrap it in a function.
@@ -316,7 +319,7 @@ func NewMachine(config *Config) (*Machine, error) {
NetworkReady: m.IsNetworkReady, NetworkReady: m.IsNetworkReady,
WaitForNetworkReady: m.WaitForNetworkReady, WaitForNetworkReady: m.WaitForNetworkReady,
}) })
caddyServer := caddyconfig.NewServer(caddyconfig.NewService(config.CaddyConfigDir)) caddyServer := caddyconfig.NewServer(m.caddyService)
caddyStore, err := corroStore.Keyspace(caddystorage.Namespace) caddyStore, err := corroStore.Keyspace(caddystorage.Namespace)
if err != nil { if err != nil {
@@ -535,7 +538,7 @@ func (m *Machine) Run(ctx context.Context) error {
// It will also serve the current machine ID at /.uncloud-verify to verify Caddy reachability. // It will also serve the current machine ID at /.uncloud-verify to verify Caddy reachability.
caddyconfigCtrl, err := caddyconfig.NewController( caddyconfigCtrl, err := caddyconfig.NewController(
m.state.ID, m.state.ID,
m.config.CaddyConfigDir, m.caddyService,
DefaultCaddyAdminSockPath, DefaultCaddyAdminSockPath,
m.store, m.store,
) )
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
"github.com/psviderski/uncloud/pkg/api"
) )
const ( const (
@@ -31,7 +32,7 @@ type State struct {
// MinStoreVersion is the cluster store version this machine must reach before participating. // MinStoreVersion is the cluster store version this machine must reach before participating.
// Per-actor vector (Corrosion actor UUID → max processed db_version) captured from an existing // Per-actor vector (Corrosion actor UUID → max processed db_version) captured from an existing
// member at join time. Cleared once reached. // member at join time. Cleared once reached.
MinStoreVersion map[string]uint64 `json:",omitempty"` MinStoreVersion api.StoreVersion `json:",omitempty"`
// CorrosionAPIToken authenticates requests to the local Corrosion API. // CorrosionAPIToken authenticates requests to the local Corrosion API.
CorrosionAPIToken secret.Secret `json:",omitempty"` CorrosionAPIToken secret.Secret `json:",omitempty"`
+57 -14
View File
@@ -8,14 +8,16 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/psviderski/uncloud/internal/corrosion"
"github.com/psviderski/uncloud/pkg/api"
) )
// ErrInvalidStoreVersion indicates an invalid actor UUID in a store version vector. // ErrInvalidStoreVersion indicates an invalid actor UUID in a store version vector.
var ErrInvalidStoreVersion = errors.New("invalid store version") var ErrInvalidStoreVersion = errors.New("invalid store version")
// WaitForVersion waits until the local store has reached each actor's minimum version in minVersion, with no known // WaitForVersion waits until the local store has reached each actor's minimum version in minVersion, with no known
// missing or pending transactions through those versions. Corrosion may satisfy a version by applying its surviving // missing or pending transactions through those versions from active members. Corrosion may satisfy a version
// changes or by marking it complete because its changes have been superseded. // by applying its surviving changes or by marking it complete because its changes have been superseded.
// //
// Waiting normally makes the captured data available locally. However, if another write replaces some of that data // Waiting normally makes the captured data available locally. However, if another write replaces some of that data
// before it arrives, Corrosion can complete the older version without transferring the replaced data. If the // before it arrives, Corrosion can complete the older version without transferring the replaced data. If the
@@ -27,7 +29,7 @@ var ErrInvalidStoreVersion = errors.New("invalid store version")
// //
// The method observes native replication without initiating synchronisation. // The method observes native replication without initiating synchronisation.
// An empty minVersion requires no replication. The context controls cancellation and the deadline. // An empty minVersion requires no replication. The context controls cancellation and the deadline.
func (s *Store) WaitForVersion(ctx context.Context, minVersion map[string]uint64) error { func (s *Store) WaitForVersion(ctx context.Context, minVersion api.StoreVersion) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
@@ -52,14 +54,50 @@ func (s *Store) WaitForVersion(ctx context.Context, minVersion map[string]uint64
return nil return nil
} }
// Check all bookkeeping in one SQLite snapshot. Raw buffered rows can remain after application, queryPrefix := `WITH min_version(actor_id, version) AS (VALUES ` + strings.Join(placeholders, ", ") + `)
// but sequence bookkeeping is removed in the same transaction that applies or clears a version.
query := `WITH min_version(actor_id, version) AS (VALUES ` + strings.Join(placeholders, ", ") + `)
SELECT NOT EXISTS ( SELECT NOT EXISTS (
SELECT 1 FROM min_version SELECT 1 FROM min_version
LEFT JOIN crsql_db_versions AS current ON current.site_id = min_version.actor_id LEFT JOIN crsql_db_versions AS current ON current.site_id = min_version.actor_id
WHERE COALESCE(current.db_version, 0) < min_version.version WHERE COALESCE(current.db_version, 0) < min_version.version`
OR EXISTS (
versionsReached := false
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
query := queryPrefix
queryArgs := args
// Check for gaps and pending transactions only after the requested versions are reached.
if versionsReached {
states, err := s.corroAdmin.ClusterMembershipStates(true)
if err != nil {
return fmt.Errorf("get cluster membership for store replication: %w", err)
}
activePlaceholders := make([]string, 0, len(states))
queryArgs = make([]any, len(args), len(args)+len(states))
copy(queryArgs, args)
for _, state := range states {
if state.State != corrosion.MembershipStateAlive && state.State != corrosion.MembershipStateSuspect {
continue
}
actorID, err := uuid.Parse(state.ID)
if err != nil {
return fmt.Errorf("parse cluster member actor '%s': %w", state.ID, err)
}
activePlaceholders = append(activePlaceholders, "?")
queryArgs = append(queryArgs, [16]byte(actorID))
}
if len(activePlaceholders) > 0 {
// Check active members' gaps and pending sequences in the same query for simplicity.
// Buffered rows can remain in __corro_buffered_changes after application, but sequence bookkeeping
// is removed when a version completes.
query += `
OR (
min_version.actor_id IN (` + strings.Join(activePlaceholders, ", ") + `)
AND (
EXISTS (
SELECT 1 FROM __corro_bookkeeping_gaps SELECT 1 FROM __corro_bookkeeping_gaps
WHERE actor_id = min_version.actor_id AND start <= min_version.version WHERE actor_id = min_version.actor_id AND start <= min_version.version
) )
@@ -67,13 +105,13 @@ func (s *Store) WaitForVersion(ctx context.Context, minVersion map[string]uint64
SELECT 1 FROM __corro_seq_bookkeeping SELECT 1 FROM __corro_seq_bookkeeping
WHERE site_id = min_version.actor_id AND db_version <= min_version.version WHERE site_id = min_version.actor_id AND db_version <= min_version.version
) )
)
)` )`
}
}
query += ")"
ticker := time.NewTicker(100 * time.Millisecond) rows, err := s.corro.QueryContext(ctx, query, queryArgs...)
defer ticker.Stop()
for {
rows, err := s.corro.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return fmt.Errorf("check store replication: %w", err) return fmt.Errorf("check store replication: %w", err)
} }
@@ -83,6 +121,7 @@ func (s *Store) WaitForVersion(ctx context.Context, minVersion map[string]uint64
} }
return errors.New("check store replication: store version query returned no rows") return errors.New("check store replication: store version query returned no rows")
} }
var reached int var reached int
if err = rows.Scan(&reached); err != nil { if err = rows.Scan(&reached); err != nil {
rows.Close() rows.Close()
@@ -94,7 +133,11 @@ func (s *Store) WaitForVersion(ctx context.Context, minVersion map[string]uint64
return fmt.Errorf("check store replication: %w", err) return fmt.Errorf("check store replication: %w", err)
} }
if reached == 1 { if reached == 1 {
return ctx.Err() if versionsReached {
return nil
}
versionsReached = true
continue
} }
select { select {
+6 -4
View File
@@ -10,6 +10,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/psviderski/uncloud/api/pb" "github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/corrosion" "github.com/psviderski/uncloud/internal/corrosion"
"github.com/psviderski/uncloud/pkg/api"
"google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/protojson"
) )
@@ -24,10 +25,11 @@ var (
// Store is a cluster store backed by a distributed Corrosion database. // Store is a cluster store backed by a distributed Corrosion database.
type Store struct { type Store struct {
corro *corrosion.APIClient corro *corrosion.APIClient
corroAdmin *corrosion.AdminClient
} }
func New(corro *corrosion.APIClient) *Store { func New(corro *corrosion.APIClient, corroAdmin *corrosion.AdminClient) *Store {
return &Store{corro: corro} return &Store{corro: corro, corroAdmin: corroAdmin}
} }
// Get retrieves an unnamespaced legacy value. // Get retrieves an unnamespaced legacy value.
@@ -81,14 +83,14 @@ func (s *Store) Delete(ctx context.Context, key string) error {
// limitations documented there. // limitations documented there.
// //
// Capturing a vector does not wait for replication or prevent further writes. // Capturing a vector does not wait for replication or prevent further writes.
func (s *Store) Version(ctx context.Context) (map[string]uint64, error) { func (s *Store) Version(ctx context.Context) (api.StoreVersion, error) {
rows, err := s.corro.QueryContext(ctx, "SELECT site_id, db_version FROM crsql_db_versions") rows, err := s.corro.QueryContext(ctx, "SELECT site_id, db_version FROM crsql_db_versions")
if err != nil { if err != nil {
return nil, fmt.Errorf("query crsql_db_versions: %w", err) return nil, fmt.Errorf("query crsql_db_versions: %w", err)
} }
defer rows.Close() defer rows.Close()
versions := make(map[string]uint64) versions := make(api.StoreVersion)
for rows.Next() { for rows.Next() {
var ( var (
siteID []byte siteID []byte
+74
View File
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
"sync"
"time" "time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
@@ -16,6 +17,7 @@ import (
"github.com/psviderski/uncloud/api/pb" "github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/machine" "github.com/psviderski/uncloud/internal/machine"
"github.com/psviderski/uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/pkg/client"
"google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/emptypb"
) )
@@ -322,6 +324,78 @@ func (p *Provisioner) WaitClusterReady(ctx context.Context, c Cluster, timeout t
return nil return nil
} }
// WaitClusterMeshReady waits until every machine can reach every other machine through the WireGuard mesh network.
func (p *Provisioner) WaitClusterMeshReady(ctx context.Context, c Cluster, timeout time.Duration) error {
if len(c.Machines) < 2 {
return nil
}
clients := make([]*client.Client, len(c.Machines))
for i := range c.Machines {
cli, err := c.Machines[i].Connect(ctx)
if err != nil {
return fmt.Errorf("connect to machine '%s' over TCP '%s': %w",
c.Machines[i].Name, c.Machines[i].APIAddress, err)
}
clients[i] = cli
defer cli.Close()
}
type machinePair struct {
source int
target int
}
pairs := make([]machinePair, 0, len(c.Machines)*(len(c.Machines)-1))
for source := range c.Machines {
for target := range c.Machines {
if source != target {
pairs = append(pairs, machinePair{source: source, target: target})
}
}
}
boff := backoff.WithContext(backoff.NewExponentialBackOff(
backoff.WithInitialInterval(100*time.Millisecond),
backoff.WithMaxInterval(time.Second),
backoff.WithMaxElapsedTime(timeout),
), ctx)
checkMeshReady := func() error {
errs := make([]error, len(pairs))
var wg sync.WaitGroup
for i, pair := range pairs {
wg.Go(func() {
source := c.Machines[pair.source]
target := c.Machines[pair.target]
probeCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
resp, err := clients[pair.source].MachineClient.InspectMachine(
clients[pair.source].ProxySingleMachineContext(probeCtx, target.ID),
nil,
)
if err != nil {
errs[i] = fmt.Errorf("reach machine '%s' from '%s': %w", target.Name, source.Name, err)
return
}
if len(resp.Machines) != 1 || resp.Machines[0].Machine == nil ||
resp.Machines[0].Machine.Id != target.ID {
errs[i] = fmt.Errorf("machine '%s' returned an unexpected response when reached from '%s'",
target.Name, source.Name)
}
})
}
wg.Wait()
return errors.Join(errs...)
}
if err := backoff.Retry(checkMeshReady, boff); err != nil {
return fmt.Errorf("wait for cluster WireGuard mesh to be ready: %w", err)
}
return nil
}
func (p *Provisioner) ListClusters(ctx context.Context) ([]Cluster, error) { func (p *Provisioner) ListClusters(ctx context.Context) ([]Cluster, error) {
nets, err := p.dockerCli.NetworkList(ctx, network.ListOptions{ nets, err := p.dockerCli.NetworkList(ctx, network.ListOptions{
Filters: filters.NewArgs( Filters: filters.NewArgs(
+13 -1
View File
@@ -1,6 +1,18 @@
package api package api
import "strings" import (
"strings"
"time"
)
// CaddyConfig is the saved Caddy configuration on a machine.
type CaddyConfig struct {
Caddyfile string
// ModifiedAt is zero when the server does not supply a modification timestamp.
ModifiedAt time.Time
// LastReconciliationError describes the latest unsuccessful configuration reconciliation.
LastReconciliationError string
}
// CaddySpec is the Caddy reverse proxy configuration for a service. // CaddySpec is the Caddy reverse proxy configuration for a service.
type CaddySpec struct { type CaddySpec struct {
+111
View File
@@ -0,0 +1,111 @@
package api
import (
"crypto/sha256"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"strings"
"github.com/mholt/acmez/v3/acme"
"github.com/psviderski/uncloud/api/pb"
)
// IssuedCertificate describes a certificate in Caddy's managed certificate storage.
// It does not indicate whether Caddy currently serves the certificate or whether it is trusted.
type IssuedCertificate struct {
// SAN is the Subject Alternative Name (SAN) of the certificate.
// Caddy doesn't issue certificates with multiple SANs.
SAN string
// Chain contains the parsed certificates in stored order, with the leaf first.
Chain []*x509.Certificate
// IssuerData is extra information associated with the certificate, usually provided by the issuer implementation.
IssuerData CertificateIssuerData
}
// Fingerprint returns the SHA-256 fingerprint of the leaf certificate.
func (c IssuedCertificate) Fingerprint() [sha256.Size]byte {
return sha256.Sum256(c.Chain[0].Raw)
}
// CertificateIssuerData preserves issuer-specific metadata and provides a best-effort typed view of ACME records.
type CertificateIssuerData struct {
// Raw is the original issuer_data JSON, including unrecognized formats and fields.
Raw json.RawMessage
// ACME is populated when the metadata can be decoded as an ACME record with CA and certificate URLs.
// It is nil for absent, unrecognized, or malformed metadata.
ACME *ACMEIssuerData
}
// ACMEIssuerData identifies the ACME resources used to issue a certificate.
type ACMEIssuerData struct {
// URL is the certificate resource URL as provisioned by the ACME server.
URL string
// CA is the directory URL of the ACME CA that issued this certificate.
CA string
// Account is the URL of the account that obtained the certificate.
Account string
// RenewalInfo is the stored renewal guidance, not a guarantee of when renewal will run.
RenewalInfo *acme.RenewalInfo
}
// IssuedCertificateFromProto parses a stored certificate chain without verifying trust or expiry.
// Issuer metadata is decoded as ACME on a best-effort basis and is always preserved in its raw form.
func IssuedCertificateFromProto(p *pb.IssuedCertificate) (IssuedCertificate, error) {
if p == nil || strings.TrimSpace(p.San) == "" {
return IssuedCertificate{}, fmt.Errorf("invalid certificate: missing SAN")
}
chain, err := parseCertificateChain(p.Chain)
if err != nil {
return IssuedCertificate{}, fmt.Errorf("parse certificate '%s': %w", p.San, err)
}
return IssuedCertificate{
SAN: p.San,
Chain: chain,
IssuerData: parseCertificateIssuerData(p.IssuerData),
}, nil
}
func parseCertificateChain(data []byte) ([]*x509.Certificate, error) {
var chain []*x509.Certificate
for len(data) > 0 {
var block *pem.Block
block, data = pem.Decode(data)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("unexpected PEM block type '%s'", block.Type)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse X.509 certificate: %w", err)
}
chain = append(chain, cert)
}
if len(chain) == 0 {
return nil, fmt.Errorf("empty certificate chain")
}
return chain, nil
}
func parseCertificateIssuerData(raw json.RawMessage) CertificateIssuerData {
data := CertificateIssuerData{Raw: raw}
// Recognize ACME by its resource URLs and decodable metadata.
// Unknown formats and malformed records remain raw-only.
var cert acme.Certificate
if err := json.Unmarshal(raw, &cert); err != nil || cert.CA == "" || cert.URL == "" {
return data
}
data.ACME = &ACMEIssuerData{
URL: cert.URL,
CA: cert.CA,
Account: cert.Account,
RenewalInfo: cert.RenewalInfo,
}
return data
}
+182
View File
@@ -0,0 +1,182 @@
package api
import (
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"math/big"
"testing"
"time"
"github.com/mholt/acmez/v3/acme"
"github.com/psviderski/uncloud/api/pb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIssuedCertificateFromProto(t *testing.T) {
leaf := testCertificatePEM(t, 1)
issuer := testCertificatePEM(t, 2)
for _, tt := range []struct {
name string
chain []byte
serials []int64
}{
{name: "single certificate", chain: leaf, serials: []int64{1}},
{name: "certificate chain", chain: append(append([]byte(nil), leaf...), issuer...), serials: []int64{1, 2}},
} {
t.Run(tt.name, func(t *testing.T) {
cert, err := IssuedCertificateFromProto(&pb.IssuedCertificate{
San: "app.example.com", Chain: tt.chain,
})
require.NoError(t, err)
assert.Equal(t, "app.example.com", cert.SAN)
require.Len(t, cert.Chain, len(tt.serials))
for i, serial := range tt.serials {
assert.Equal(t, big.NewInt(serial), cert.Chain[i].SerialNumber, "chain order must be preserved")
}
assert.Equal(t, []string{"app.example.com"}, cert.Chain[0].DNSNames)
// Expired, self-signed certificates are parsed without verifying trust or validity.
assert.Equal(t, time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), cert.Chain[0].NotAfter)
assert.Empty(t, cert.IssuerData)
})
}
}
func TestIssuedCertificateFingerprint(t *testing.T) {
cert := IssuedCertificate{Chain: []*x509.Certificate{
{Raw: []byte("abc")},
{Raw: []byte("issuer")},
}}
want, err := hex.DecodeString("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")
require.NoError(t, err)
fingerprint := cert.Fingerprint()
assert.Equal(t, want, fingerprint[:])
}
func TestIssuedCertificateFromProto_InvalidCertificate(t *testing.T) {
valid := testCertificatePEM(t, 1)
invalidDER := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: []byte("invalid")})
for _, tt := range []struct {
name string
input *pb.IssuedCertificate
wantErr string
}{
{name: "nil input", wantErr: "missing SAN"},
{name: "missing SAN", input: &pb.IssuedCertificate{Chain: valid}, wantErr: "missing SAN"},
{name: "blank SAN", input: &pb.IssuedCertificate{San: " \t", Chain: valid}, wantErr: "missing SAN"},
{name: "empty chain", input: &pb.IssuedCertificate{San: "app.example.com"}, wantErr: "empty certificate chain"},
{
name: "invalid PEM", input: &pb.IssuedCertificate{San: "app.example.com", Chain: []byte("not PEM")},
wantErr: "empty certificate chain",
},
{
name: "wrong PEM block type", input: &pb.IssuedCertificate{
San: "app.example.com",
Chain: pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: []byte("key")}),
},
wantErr: "unexpected PEM block type",
},
{
name: "invalid DER", input: &pb.IssuedCertificate{San: "app.example.com", Chain: invalidDER},
wantErr: "parse X.509 certificate",
},
{name: "invalid certificate after valid leaf", input: &pb.IssuedCertificate{
San: "app.example.com",
Chain: append(append([]byte(nil), valid...), invalidDER...),
}, wantErr: "parse X.509 certificate"},
} {
t.Run(tt.name, func(t *testing.T) {
cert, err := IssuedCertificateFromProto(tt.input)
require.ErrorContains(t, err, tt.wantErr)
assert.Empty(t, cert)
})
}
}
func TestIssuedCertificateFromProto_IssuerData(t *testing.T) {
chain := testCertificatePEM(t, 1)
retryAfter := time.Date(2029, 12, 1, 0, 0, 0, 0, time.UTC)
renewalInfo := &acme.RenewalInfo{
ExplanationURL: "https://ca.example/why",
UniqueIdentifier: "aki.serial",
RetryAfter: &retryAfter,
SelectedTime: time.Date(2030, 1, 2, 0, 0, 0, 0, time.UTC),
}
renewalInfo.SuggestedWindow.Start = time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
renewalInfo.SuggestedWindow.End = time.Date(2030, 1, 3, 0, 0, 0, 0, time.UTC)
for _, tt := range []struct {
name string
raw string
want *ACMEIssuerData
}{
{
name: "ACME",
raw: `{"url":"https://ca.example/cert/1","ca":"https://ca.example/directory","account":"https://ca.example/acct/1","future_field":true}`,
want: &ACMEIssuerData{
URL: "https://ca.example/cert/1", CA: "https://ca.example/directory", Account: "https://ca.example/acct/1",
},
},
{
name: "ACME with renewal information",
raw: `{
"url": "https://ca.example/cert/1",
"ca": "https://ca.example/directory",
"account": "https://ca.example/acct/1",
"renewal_info": {
"suggestedWindow": {"start": "2030-01-01T00:00:00Z", "end": "2030-01-03T00:00:00Z"},
"explanationURL": "https://ca.example/why",
"_uniqueIdentifier": "aki.serial",
"_retryAfter": "2029-12-01T00:00:00Z",
"_selectedTime": "2030-01-02T00:00:00Z"
}
}`,
want: &ACMEIssuerData{
URL: "https://ca.example/cert/1", CA: "https://ca.example/directory",
Account: "https://ca.example/acct/1", RenewalInfo: renewalInfo,
},
},
{name: "absent"},
{name: "null", raw: "null"},
{name: "unknown issuer", raw: `{"id":"provider-id","status":"issued"}`},
{name: "non-object JSON", raw: `"provider-record"`},
{name: "missing CA", raw: `{"url":"https://ca.example/cert/1"}`},
{name: "missing certificate URL", raw: `{"ca":"https://ca.example/directory"}`},
{name: "invalid JSON", raw: "{"},
{name: "invalid field types", raw: `{"url":123,"ca":false}`},
{
name: "malformed renewal information",
raw: `{"url":"https://ca.example/cert/1","ca":"https://ca.example/directory","renewal_info":{"_selectedTime":"invalid"}}`,
},
} {
t.Run(tt.name, func(t *testing.T) {
cert, err := IssuedCertificateFromProto(&pb.IssuedCertificate{
San: "app.example.com", Chain: chain, IssuerData: []byte(tt.raw),
})
require.NoError(t, err, "issuer metadata must not prevent certificate parsing")
require.Len(t, cert.Chain, 1)
assert.Equal(t, tt.raw, string(cert.IssuerData.Raw), "preserve original JSON including unknown fields")
assert.Equal(t, tt.want, cert.IssuerData.ACME)
})
}
}
// testCertificatePEM creates an expired, self-signed certificate for parsing tests.
func testCertificatePEM(t *testing.T, serial int64) []byte {
t.Helper()
public, private, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{
SerialNumber: big.NewInt(serial),
DNSNames: []string{"app.example.com"},
NotBefore: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
NotAfter: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
}
der, err := x509.CreateCertificate(rand.Reader, template, template, public, private)
require.NoError(t, err)
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
}
+1 -1
View File
@@ -36,7 +36,7 @@ type ContainerClient interface {
} }
type DNSClient interface { type DNSClient interface {
GetDomain(ctx context.Context) (string, error) GetDomain(ctx context.Context) (ClusterDomain, error)
} }
type ImageClient interface { type ImageClient interface {
+8
View File
@@ -0,0 +1,8 @@
package api
// ClusterDomain is the domain name assigned to the cluster.
type ClusterDomain struct {
Name string
// Reserved reports whether the domain is reserved in Uncloud DNS or set and managed externally.
Reserved bool
}
+90
View File
@@ -0,0 +1,90 @@
package api
import (
"bytes"
"fmt"
"text/template"
)
// RuntimeTemplateContext contains the data available to runtime templates in a service spec.
type RuntimeTemplateContext struct {
Container RuntimeTemplateContainerContext
}
// RuntimeTemplateContainerContext contains container data available to runtime templates in a service spec.
type RuntimeTemplateContainerContext struct {
Name string
}
// RenderRuntimeTemplates returns a copy of the service spec with supported runtime template expressions rendered
// using metadata from ctx. Runtime templates use Go template expressions such as {{.Container.Name}} and are supported
// in bind volume host paths and volume mount container paths. The original service spec is not modified.
func (s *ServiceSpec) RenderRuntimeTemplates(ctx RuntimeTemplateContext) (ServiceSpec, error) {
if ctx.Container.Name == "" {
return ServiceSpec{}, fmt.Errorf("container name must not be empty")
}
spec := s.Clone()
for i := range spec.Volumes {
volume := &spec.Volumes[i]
if volume.Type != VolumeTypeBind {
continue
}
if err := volume.Validate(); err != nil {
return ServiceSpec{}, fmt.Errorf("invalid bind volume '%s': %w", volume.Name, err)
}
hostPath, err := renderRuntimeTemplate(volume.BindOptions.HostPath, ctx)
if err != nil {
return ServiceSpec{}, fmt.Errorf("render runtime template in bind volume '%s' host path: %w",
volume.Name, err)
}
volume.BindOptions.HostPath = hostPath
if err = volume.Validate(); err != nil {
return ServiceSpec{}, fmt.Errorf("invalid rendered bind volume '%s': %w", volume.Name, err)
}
}
for i := range spec.Container.VolumeMounts {
volumeMount := &spec.Container.VolumeMounts[i]
if err := volumeMount.Validate(); err != nil {
return ServiceSpec{}, fmt.Errorf("invalid volume mount '%s': %w", volumeMount.VolumeName, err)
}
containerPath, err := renderRuntimeTemplate(volumeMount.ContainerPath, ctx)
if err != nil {
return ServiceSpec{}, fmt.Errorf(
"render runtime template in volume '%s' container path: %w", volumeMount.VolumeName, err)
}
volumeMount.ContainerPath = containerPath
if err = volumeMount.Validate(); err != nil {
return ServiceSpec{}, fmt.Errorf("invalid rendered volume mount '%s': %w", volumeMount.VolumeName, err)
}
}
return spec, nil
}
func renderRuntimeTemplate(value string, ctx RuntimeTemplateContext) (string, error) {
tmpl, err := template.New("runtime template").Option("missingkey=error").Parse(value)
if err != nil {
return "", fmt.Errorf("parse runtime template: %w", err)
}
var rendered bytes.Buffer
if err = tmpl.Execute(&rendered, ctx); err != nil {
return "", fmt.Errorf("execute runtime template: %w", err)
}
return rendered.String(), nil
}
func validateRuntimeTemplates(spec *ServiceSpec) error {
_, err := spec.RenderRuntimeTemplates(RuntimeTemplateContext{
Container: RuntimeTemplateContainerContext{Name: "validation-container-name"},
})
return err
}
+225
View File
@@ -0,0 +1,225 @@
package api
import (
"testing"
"github.com/docker/docker/api/types/mount"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestServiceSpec_RenderRuntimeTemplates(t *testing.T) {
t.Parallel()
spec := ServiceSpec{
Container: ContainerSpec{
Image: "busybox:latest",
Env: EnvVars{
"UNCHANGED": "{{.Container.Name}}",
},
VolumeMounts: []VolumeMount{
{VolumeName: "config", ContainerPath: "/etc/app/{{.Container.Name}}", ReadOnly: true},
{VolumeName: "data", ContainerPath: "/{{.Container.Name}}"},
{VolumeName: "scratch", ContainerPath: "/tmp/{{.Container.Name}}"},
},
Volumes: []string{"/legacy/{{.Container.Name}}:/legacy"},
},
Volumes: []VolumeSpec{
{
Name: "config",
Type: VolumeTypeBind,
BindOptions: &BindOptions{
HostPath: "/var/lib/uncloud/{{.Container.Name}}",
CreateHostPath: true,
Propagation: mount.PropagationRShared,
},
},
{
Name: "data",
Type: VolumeTypeVolume,
VolumeOptions: &VolumeOptions{
Name: "data-{{.Container.Name}}",
},
},
{Name: "scratch", Type: VolumeTypeTmpfs},
},
}
original := spec.Clone()
rendered, err := spec.RenderRuntimeTemplates(RuntimeTemplateContext{
Container: RuntimeTemplateContainerContext{Name: "web-a1b2"},
})
require.NoError(t, err)
assert.Equal(t, "/var/lib/uncloud/web-a1b2", rendered.Volumes[0].BindOptions.HostPath)
assert.True(t, rendered.Volumes[0].BindOptions.CreateHostPath)
assert.Equal(t, mount.PropagationRShared, rendered.Volumes[0].BindOptions.Propagation)
assert.Equal(t, "data-{{.Container.Name}}", rendered.Volumes[1].VolumeOptions.Name)
assert.Equal(t, "/etc/app/web-a1b2", rendered.Container.VolumeMounts[0].ContainerPath)
assert.Equal(t, "/web-a1b2", rendered.Container.VolumeMounts[1].ContainerPath)
assert.Equal(t, "/tmp/web-a1b2", rendered.Container.VolumeMounts[2].ContainerPath)
assert.Equal(t, "{{.Container.Name}}", rendered.Container.Env["UNCHANGED"])
assert.Equal(t, "/legacy/{{.Container.Name}}:/legacy", rendered.Container.Volumes[0])
assert.Equal(t, original, spec)
}
func TestServiceSpec_RenderRuntimeTemplates_GoTemplateLanguage(t *testing.T) {
t.Parallel()
tests := []struct {
name string
runtimeTemplate string
containerName string
want string
}{
{
name: "condition variables and pipeline",
runtimeTemplate: `/data/{{if .Container.Name}}{{$name := .Container.Name}}{{$name | printf "%s"}}{{end}}`,
containerName: "web-a1b2",
want: "/data/web-a1b2",
},
{
name: "literal delimiters",
runtimeTemplate: `/data/{{"{{"}}.Container.Name{{"}}"}}`,
containerName: "web-a1b2",
want: "/data/{{.Container.Name}}",
},
{
name: "one pass",
runtimeTemplate: "/data/{{.Container.Name}}",
containerName: "{{.Container.Name}}",
want: "/data/{{.Container.Name}}",
},
{
name: "static path",
runtimeTemplate: "/data/static",
containerName: "web-a1b2",
want: "/data/static",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
spec := serviceSpecWithBindRuntimeTemplate(tt.runtimeTemplate)
rendered, err := spec.RenderRuntimeTemplates(RuntimeTemplateContext{
Container: RuntimeTemplateContainerContext{Name: tt.containerName},
})
require.NoError(t, err)
assert.Equal(t, tt.want, rendered.Volumes[0].BindOptions.HostPath)
})
}
}
func TestServiceSpec_RenderRuntimeTemplates_Errors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
runtimeTemplate string
containerName string
wantErr string
}{
{
name: "empty container name",
runtimeTemplate: "/data/{{.Container.Name}}",
containerName: "",
wantErr: "container name must not be empty",
},
{
name: "malformed runtime template",
runtimeTemplate: "/data/{{.Container.Name",
containerName: "web-a1b2",
wantErr: "parse runtime template",
},
{
name: "unknown field",
runtimeTemplate: "/data/{{.Container.ID}}",
containerName: "web-a1b2",
wantErr: "can't evaluate field ID",
},
{
name: "unregistered function",
runtimeTemplate: `/data/{{env "HOME"}}`,
containerName: "web-a1b2",
wantErr: `function "env" not defined`,
},
{
name: "relative rendered path",
runtimeTemplate: "{{.Container.Name}}",
containerName: "web-a1b2",
wantErr: "must be an absolute path",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
spec := serviceSpecWithBindRuntimeTemplate(tt.runtimeTemplate)
_, err := spec.RenderRuntimeTemplates(RuntimeTemplateContext{
Container: RuntimeTemplateContainerContext{Name: tt.containerName},
})
require.ErrorContains(t, err, tt.wantErr)
})
}
}
func TestServiceSpec_Validate_RuntimeTemplates(t *testing.T) {
t.Parallel()
tests := []struct {
name string
runtimeTemplate string
wantErr string
}{
{
name: "valid",
runtimeTemplate: "/data/{{.Container.Name}}",
},
{
name: "malformed",
runtimeTemplate: "/data/{{.Container.Name",
wantErr: "validate runtime templates",
},
{
name: "unknown field",
runtimeTemplate: "/data/{{.Container.ID}}",
wantErr: "can't evaluate field ID",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
spec := serviceSpecWithBindRuntimeTemplate(tt.runtimeTemplate)
err := spec.Validate()
if tt.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tt.wantErr)
}
})
}
}
func serviceSpecWithBindRuntimeTemplate(hostPath string) ServiceSpec {
return ServiceSpec{
Name: "web",
Container: ContainerSpec{
Image: "busybox:latest",
VolumeMounts: []VolumeMount{
{VolumeName: "data", ContainerPath: "/data"},
},
},
Volumes: []VolumeSpec{
{
Name: "data",
Type: VolumeTypeBind,
BindOptions: &BindOptions{HostPath: hostPath},
},
},
}
}
+4
View File
@@ -214,6 +214,10 @@ func (s *ServiceSpec) Validate() error {
} }
} }
if err := validateRuntimeTemplates(s); err != nil {
return fmt.Errorf("validate runtime templates: %w", err)
}
return nil return nil
} }
+14
View File
@@ -0,0 +1,14 @@
package api
// StoreVersion records the highest processed database version for each Corrosion actor UUID.
// A version can include changes that were superseded, so it does not guarantee that all data
// through that version is available locally.
type StoreVersion map[string]uint64
// MergeMax keeps the highest observed version for each actor in other.
// The receiver must be initialised before calling MergeMax.
func (v StoreVersion) MergeMax(other StoreVersion) {
for actor, version := range other {
v[actor] = max(v[actor], version)
}
}
+10 -6
View File
@@ -3,10 +3,10 @@ package api
import ( import (
"fmt" "fmt"
"maps" "maps"
"path"
"reflect" "reflect"
"slices" "slices"
"sort" "sort"
"strings"
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/volume" "github.com/docker/docker/api/types/volume"
@@ -39,7 +39,8 @@ type VolumeSpec struct {
// BindOptions represents options for a bind volume. // BindOptions represents options for a bind volume.
type BindOptions struct { type BindOptions struct {
// HostPath is the absolute path on the host filesystem. // HostPath is the absolute path on the host filesystem. It may contain runtime templates such as
// {{.Container.Name}}, which the destination daemon renders before creating the container.
HostPath string HostPath string
// CreateHostPath indicates whether the host path should be created if it doesn't exist. // CreateHostPath indicates whether the host path should be created if it doesn't exist.
// If false, deployment will fail if the path doesn't exist. // If false, deployment will fail if the path doesn't exist.
@@ -103,6 +104,9 @@ func (v *VolumeSpec) Validate() error {
if v.BindOptions == nil { if v.BindOptions == nil {
return fmt.Errorf("bind volume must have bind options") return fmt.Errorf("bind volume must have bind options")
} }
if !path.IsAbs(v.BindOptions.HostPath) {
return fmt.Errorf("invalid host path: %q must be an absolute path", v.BindOptions.HostPath)
}
case VolumeTypeVolume, VolumeTypeTmpfs: case VolumeTypeVolume, VolumeTypeTmpfs:
default: default:
return fmt.Errorf("invalid volume type: '%s', must be one of '%s', '%s', '%s')", return fmt.Errorf("invalid volume type: '%s', must be one of '%s', '%s', '%s')",
@@ -196,7 +200,8 @@ func (v *VolumeSpec) Clone() VolumeSpec {
type VolumeMount struct { type VolumeMount struct {
// VolumeName references a volume defined in ServiceSpec.Volumes by its Name field. // VolumeName references a volume defined in ServiceSpec.Volumes by its Name field.
VolumeName string VolumeName string
// ContainerPath is the absolute path where the volume is mounted in the container. // ContainerPath is the absolute path where the volume is mounted in the container. It may contain runtime templates
// such as {{.Container.Name}}, which the destination daemon renders before creating the container.
ContainerPath string ContainerPath string
// ReadOnly indicates whether the volume should be mounted read-only. // ReadOnly indicates whether the volume should be mounted read-only.
// If false (default), the volume is mounted read-write. // If false (default), the volume is mounted read-write.
@@ -207,9 +212,8 @@ func (m *VolumeMount) Validate() error {
if m.VolumeName == "" { if m.VolumeName == "" {
return fmt.Errorf("volume name must not be empty") return fmt.Errorf("volume name must not be empty")
} }
if !path.IsAbs(m.ContainerPath) {
if !strings.HasPrefix(m.ContainerPath, "/") { return fmt.Errorf("invalid container path: %q must be an absolute path", m.ContainerPath)
return fmt.Errorf("invalid container path: '%s', must be an absolute path in the container", m.ContainerPath)
} }
return nil return nil
+118 -23
View File
@@ -1,15 +1,19 @@
package client package client
import ( import (
"context"
"fmt" "fmt"
"regexp" "regexp"
"time"
"github.com/Masterminds/semver" "github.com/Masterminds/semver"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client/deploy" "github.com/psviderski/uncloud/pkg/client/deploy"
"google.golang.org/protobuf/types/known/emptypb"
) )
const ( const (
@@ -20,12 +24,66 @@ const (
var caddyImageTagRegex = regexp.MustCompile(`^2\.\d+\.\d+$`) var caddyImageTagRegex = regexp.MustCompile(`^2\.\d+\.\d+$`)
// NewCaddyDeployment creates a new deployment for a Caddy reverse proxy service. // CaddyClient provides Caddy operations over its parent Client's connection.
// The parent client owns the connection and must be used to close it.
type CaddyClient struct {
// Storage provides low-level access to Caddy's cluster-backed storage.
Storage pb.CaddyStorageClient
grpc pb.CaddyClient
client *Client
}
// CaddyConfigOptions controls which machine's saved Caddy configuration is retrieved.
type CaddyConfigOptions struct {
// Machine is the machine name or ID. If empty, the configuration is retrieved from the machine the client
// is connected to.
Machine string
}
// Config retrieves the saved Caddy configuration from the machine selected by opts.
func (c *CaddyClient) Config(ctx context.Context, opts CaddyConfigOptions) (api.CaddyConfig, error) {
if opts.Machine != "" {
ctx = ProxySingleMachineContext(ctx, opts.Machine)
}
resp, err := c.grpc.GetConfig(ctx, &emptypb.Empty{})
if err != nil {
return api.CaddyConfig{}, fmt.Errorf("get Caddy config: %w", err)
}
config := api.CaddyConfig{
Caddyfile: resp.Caddyfile,
LastReconciliationError: resp.LastReconciliationError,
}
if resp.ModifiedAt != nil {
if err := resp.ModifiedAt.CheckValid(); err != nil {
return api.CaddyConfig{}, fmt.Errorf("invalid Caddy config modification timestamp: %w", err)
}
config.ModifiedAt = resp.ModifiedAt.AsTime()
}
return config, nil
}
// CaddyDeploymentOptions configures a Caddy reverse proxy deployment.
type CaddyDeploymentOptions struct {
// Image defaults to the latest stable 2.x.x official Caddy image.
// Custom images must include curl and start Caddy with /etc/caddy/Caddyfile.
Image string
// Config contains an optional global Caddyfile.
Config string
Placement api.Placement
}
// NewDeployment creates a new deployment for a Caddy reverse proxy service.
// The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest // The service is deployed in global mode to all machines in the cluster. If the image is not provided, the latest
// version of the official Caddy Docker image is used. // version of the official Caddy Docker image is used.
func (cli *Client) NewCaddyDeployment(image, config string, placement api.Placement) (*deploy.Deployment, error) { func (c *CaddyClient) NewDeployment(ctx context.Context, opts CaddyDeploymentOptions) (*deploy.Deployment, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
image := opts.Image
if image == "" { if image == "" {
latest, err := LatestCaddyImage() latest, err := latestCaddyImage(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("look up latest Caddy image: %w", err) return nil, fmt.Errorf("look up latest Caddy image: %w", err)
} }
@@ -35,17 +93,32 @@ func (cli *Client) NewCaddyDeployment(image, config string, placement api.Placem
spec := api.ServiceSpec{ spec := api.ServiceSpec{
Container: api.ContainerSpec{ Container: api.ContainerSpec{
Command: []string{"caddy", "run", "-c", "/config/Caddyfile"},
Env: map[string]string{ Env: map[string]string{
"CADDY_ADMIN": "unix//run/caddy/admin.sock", "CADDY_ADMIN": "unix//run/caddy/admin.sock",
}, },
Healthcheck: &api.HealthcheckSpec{
Test: []string{
"CMD",
"curl", "-fsS",
"-o", "/dev/null",
"--unix-socket", "/run/caddy/admin.sock",
"http://localhost/config/",
},
Interval: 30 * time.Second,
Timeout: 5 * time.Second,
Retries: 3,
StartPeriod: 10 * time.Second,
StartInterval: 1 * time.Second,
},
Image: image, Image: image,
VolumeMounts: []api.VolumeMount{ VolumeMounts: []api.VolumeMount{
{ {
VolumeName: "data", VolumeName: "data",
ContainerPath: "/config", ContainerPath: "/etc/caddy",
ReadOnly: true,
}, },
{ {
// Keep local TLS assets persistent while Caddy uses the default file system storage.
VolumeName: "data", VolumeName: "data",
ContainerPath: "/data", ContainerPath: "/data",
}, },
@@ -53,11 +126,16 @@ func (cli *Client) NewCaddyDeployment(image, config string, placement api.Placem
VolumeName: "run", VolumeName: "run",
ContainerPath: "/run/caddy", ContainerPath: "/run/caddy",
}, },
{
VolumeName: "uncloud-api",
ContainerPath: "/run/uncloud/api",
ReadOnly: true,
},
}, },
}, },
Mode: api.ServiceModeGlobal, Mode: api.ServiceModeGlobal,
Name: CaddyServiceName, Name: CaddyServiceName,
Placement: placement, Placement: opts.Placement,
Ports: []api.PortSpec{ Ports: []api.PortSpec{
{ {
PublishedPort: 80, PublishedPort: 80,
@@ -95,31 +173,57 @@ func (cli *Client) NewCaddyDeployment(image, config string, placement api.Placem
CreateHostPath: true, CreateHostPath: true,
}, },
}, },
// Bind the Uncloud API socket so caddy.storage.uncloud module can use it to store assets in the cluster.
{
Name: "uncloud-api",
Type: api.VolumeTypeBind,
BindOptions: &api.BindOptions{
// Mount the parent directory that contains the socket so it lets the container see a replacement
// socket after the Uncloud daemon restarts (in case it doesn't use a systemd-activated socket).
HostPath: "/run/uncloud/api",
},
},
}, },
} }
if config != "" { if opts.Config != "" {
spec.Caddy = &api.CaddySpec{ spec.Caddy = &api.CaddySpec{
Config: config, Config: opts.Config,
} }
} }
return cli.NewDeployment(spec, nil), nil return c.client.NewDeployment(spec, nil), nil
} }
// LatestCaddyImage returns the latest image of the official Caddy Docker image on Docker Hub. // latestCaddyImage returns the latest image of the official Caddy Docker image on Docker Hub.
// The latest image is determined by the latest version tag 2.x.x. // The latest image is determined by the latest version tag 2.x.x.
func LatestCaddyImage() (reference.NamedTagged, error) { func latestCaddyImage(ctx context.Context) (reference.NamedTagged, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
repo, err := name.NewRepository(CaddyImage) repo, err := name.NewRepository(CaddyImage)
if err != nil { if err != nil {
return nil, fmt.Errorf("parse image: %w", err) return nil, fmt.Errorf("parse image: %w", err)
} }
tags, err := remote.List(repo) tags, err := remote.List(repo, remote.WithContext(ctx))
if err != nil { if err != nil {
return nil, fmt.Errorf("list image tags: %w", err) return nil, fmt.Errorf("list image tags: %w", err)
} }
// Default to the 'latest' tag but try to find the latest version tag 2.x.x. image, err := reference.ParseDockerRef(CaddyImage)
if err != nil {
return nil, fmt.Errorf("parse image: %w", err)
}
imageWithTag, err := reference.WithTag(image, latestCaddyTag(tags))
if err != nil {
return nil, fmt.Errorf("set image tag: %w", err)
}
return imageWithTag, nil
}
// latestCaddyTag selects the newest stable 2.x.x tag, falling back to latest.
func latestCaddyTag(tags []string) string {
latestTag := "latest" latestTag := "latest"
var latestVersion *semver.Version var latestVersion *semver.Version
for _, t := range tags { for _, t := range tags {
@@ -137,14 +241,5 @@ func LatestCaddyImage() (reference.NamedTagged, error) {
} }
} }
image, err := reference.ParseDockerRef(CaddyImage) return latestTag
if err != nil {
return nil, fmt.Errorf("parse image: %w", err)
}
imageWithTag, err := reference.WithTag(image, latestTag)
if err != nil {
return nil, fmt.Errorf("set image tag: %w", err)
}
return imageWithTag, nil
} }
+44
View File
@@ -0,0 +1,44 @@
package client
import (
"context"
"errors"
"github.com/psviderski/uncloud/pkg/api"
"google.golang.org/protobuf/types/known/emptypb"
)
// CaddyListCertificatesOptions controls which machine's certificate storage replica is queried.
type CaddyListCertificatesOptions struct {
// Machine is the machine name or ID. If empty, the machine the client is connected to is used.
Machine string
}
// ListCertificates lists issued certificates from the Caddy's managed certificate storage backed by the distributed
// cluster store. It doesn't verify trust or whether Caddy serves them and may include expired certificates.
// Private key assets are never read or returned.
//
// This is a non-atomic inventory of one store replica. It does not wait for replication.
// It returns parsing errors as a combined error, but still returns successfully read certificates regardless
// of errors.
func (c *CaddyClient) ListCertificates(ctx context.Context, opts CaddyListCertificatesOptions) ([]api.IssuedCertificate, error) {
if opts.Machine != "" {
ctx = ProxySingleMachineContext(ctx, opts.Machine)
}
resp, err := c.Storage.ListCertificates(ctx, &emptypb.Empty{})
if err != nil {
return nil, err
}
var certs []api.IssuedCertificate
var errs []error
for _, p := range resp.Certificates {
if cert, err := api.IssuedCertificateFromProto(p); err != nil {
errs = append(errs, err)
} else {
certs = append(certs, cert)
}
}
return certs, errors.Join(errs...)
}
+23 -1
View File
@@ -1,6 +1,7 @@
package client package client
import ( import (
"context"
"testing" "testing"
"github.com/distribution/reference" "github.com/distribution/reference"
@@ -11,8 +12,29 @@ import (
func TestLatestCaddyImage(t *testing.T) { func TestLatestCaddyImage(t *testing.T) {
t.Parallel() t.Parallel()
image, err := LatestCaddyImage() image, err := latestCaddyImage(context.Background())
require.NoError(t, err) require.NoError(t, err)
assert.Regexp(t, `^caddy:2\.\d+\.\d+$`, reference.FamiliarString(image)) assert.Regexp(t, `^caddy:2\.\d+\.\d+$`, reference.FamiliarString(image))
} }
func TestLatestCaddyTag(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
name string
tags []string
want string
}{
{name: "empty", want: "latest"},
{name: "semantic ordering", tags: []string{"2.9.9", "2.11.4", "2.10.0", "2.11.3"}, want: "2.11.4"},
{name: "ignore other versions and variants", tags: []string{
"latest", "1.0.0", "3.0.0", "2.12.0-rc.1", "2.12.0-alpine", "2.12", "v2.12.0", "2.11.4",
}, want: "2.11.4"},
{name: "no stable version", tags: []string{"builder", "2.12.0-beta.1"}, want: "latest"},
} {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, latestCaddyTag(tt.tags))
})
}
}
+7 -4
View File
@@ -25,8 +25,8 @@ type Client struct {
// Methods such as Reset or Inspect are ambiguous in the context of a machine+cluster client. // Methods such as Reset or Inspect are ambiguous in the context of a machine+cluster client.
pb.MachineClient pb.MachineClient
pb.ClusterClient pb.ClusterClient
Caddy pb.CaddyClient // Caddy provides Caddy configuration, deployment, and certificate storage operations.
CaddyStorage pb.CaddyStorageClient Caddy *CaddyClient
// Docker is a namespaced client for the Docker service to distinguish Uncloud-specific service container operations // Docker is a namespaced client for the Docker service to distinguish Uncloud-specific service container operations
// from generic Docker operations. // from generic Docker operations.
Docker *docker.Client Docker *docker.Client
@@ -58,8 +58,11 @@ func New(ctx context.Context, connector Connector) (*Client, error) {
c.MachineClient = pb.NewMachineClient(c.conn) c.MachineClient = pb.NewMachineClient(c.conn)
c.ClusterClient = pb.NewClusterClient(c.conn) c.ClusterClient = pb.NewClusterClient(c.conn)
c.Caddy = pb.NewCaddyClient(c.conn) c.Caddy = &CaddyClient{
c.CaddyStorage = pb.NewCaddyStorageClient(c.conn) Storage: pb.NewCaddyStorageClient(c.conn),
grpc: pb.NewCaddyClient(c.conn),
client: c,
}
c.Docker = docker.NewClient(c.conn) c.Docker = docker.NewClient(c.conn)
c.leases = distlockgrpc.NewLeaseClient(c.conn) c.leases = distlockgrpc.NewLeaseClient(c.conn)
+2 -2
View File
@@ -46,8 +46,8 @@ func NewDeploymentWithStrategy(ctx context.Context, cli Client, project *types.P
return nil, fmt.Errorf("get cluster domain: %w", err) return nil, fmt.Errorf("get cluster domain: %w", err)
} }
resolver := &deploy.ServiceSpecResolver{ resolver := &deploy.ServiceSpecResolver{
// If the domain is not found (not reserved), an empty domain is used for the resolver. // If no domain is configured, an empty domain is used for the resolver.
ClusterDomain: domain, ClusterDomain: domain.Name,
} }
return &Deployment{ return &Deployment{
+50
View File
@@ -160,6 +160,10 @@ func TestServiceSpecFromCompose(t *testing.T) {
ContainerPath: "/host/etc/passwd", ContainerPath: "/host/etc/passwd",
ReadOnly: true, ReadOnly: true,
}, },
{
VolumeName: "bind-cd9bef32904c6831a0cd0d49cf503078a46a0989acea375fdd3d3b43b2bfec4f",
ContainerPath: "/unique/host/bind",
},
{ {
VolumeName: "data1", VolumeName: "data1",
ContainerPath: "/data1", ContainerPath: "/data1",
@@ -240,6 +244,14 @@ func TestServiceSpecFromCompose(t *testing.T) {
CreateHostPath: true, CreateHostPath: true,
}, },
}, },
{
Name: "bind-cd9bef32904c6831a0cd0d49cf503078a46a0989acea375fdd3d3b43b2bfec4f",
Type: "bind",
BindOptions: &api.BindOptions{
HostPath: "/runtime/template/{{.Container.Name}}",
CreateHostPath: true,
},
},
{ {
Name: "data-external", Name: "data-external",
Type: api.VolumeTypeVolume, Type: api.VolumeTypeVolume,
@@ -1293,3 +1305,41 @@ services:
}) })
} }
} }
func TestServiceSpecFromCompose_RuntimeTemplates(t *testing.T) {
t.Setenv("UNCLOUD_TEST_TEMPLATE_ROOT", "uncloud-test")
project, err := LoadProjectFromContent(context.Background(), `
services:
short:
image: busybox:latest
volumes:
- "/var/lib/${UNCLOUD_TEST_TEMPLATE_ROOT}/{{.Container.Name}}:/data/{{.Container.Name}}:ro"
long:
image: busybox:latest
volumes:
- type: bind
source: "/var/lib/${UNCLOUD_TEST_TEMPLATE_ROOT}/{{.Container.Name}}"
target: "/data/{{.Container.Name}}"
read_only: true
bind:
create_host_path: true
`)
require.NoError(t, err)
shortSpec, err := ServiceSpecFromCompose(project, "short")
require.NoError(t, err)
longSpec, err := ServiceSpecFromCompose(project, "long")
require.NoError(t, err)
for _, spec := range []api.ServiceSpec{shortSpec, longSpec} {
require.Len(t, spec.Volumes, 1)
require.NotNil(t, spec.Volumes[0].BindOptions)
assert.Equal(t, "/var/lib/uncloud-test/{{.Container.Name}}", spec.Volumes[0].BindOptions.HostPath)
assert.True(t, spec.Volumes[0].BindOptions.CreateHostPath)
require.Len(t, spec.Container.VolumeMounts, 1)
assert.Equal(t, "/data/{{.Container.Name}}", spec.Container.VolumeMounts[0].ContainerPath)
assert.True(t, spec.Container.VolumeMounts[0].ReadOnly)
require.NoError(t, spec.Validate())
}
}
+1
View File
@@ -52,6 +52,7 @@ services:
user: nginx:nginx user: nginx:nginx
volumes: volumes:
- /etc/passwd:/host/etc/passwd:ro - /etc/passwd:/host/etc/passwd:ro
- /runtime/template/{{.Container.Name}}:/unique/host/bind
- data1:/data1 - data1:/data1
- type: bind - type: bind
source: /path/on/host source: /path/on/host
+2 -2
View File
@@ -306,8 +306,8 @@ func (d *Deployment) Plan(ctx context.Context) (ServicePlan, error) {
return ServicePlan{}, fmt.Errorf("get cluster domain: %w", err) return ServicePlan{}, fmt.Errorf("get cluster domain: %w", err)
} }
specResolver := &ServiceSpecResolver{ specResolver := &ServiceSpecResolver{
// If the domain is not found (not reserved), an empty domain is used for the resolver. // If no domain is configured, an empty domain is used for the resolver.
ClusterDomain: clusterDomain, ClusterDomain: clusterDomain.Name,
} }
resolvedSpec, err := specResolver.Resolve(d.Spec) resolvedSpec, err := specResolver.Resolve(d.Spec)
+20 -7
View File
@@ -21,17 +21,21 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )
// GetDomain returns the cluster domain name or ErrNotFound if it hasn't been reserved yet. // GetDomain returns the cluster domain or api.ErrNotFound if none is configured.
func (cli *Client) GetDomain(ctx context.Context) (string, error) { func (cli *Client) GetDomain(ctx context.Context) (api.ClusterDomain, error) {
domain, err := cli.ClusterClient.GetDomain(ctx, nil) domain, err := cli.ClusterClient.GetDomain(ctx, nil)
if err != nil { if err != nil {
if status.Convert(err).Code() == codes.NotFound { if status.Code(err) == codes.NotFound {
return "", api.ErrNotFound return api.ClusterDomain{}, api.ErrNotFound
} }
return "", err return api.ClusterDomain{}, err
} }
return domain.Name, nil return api.ClusterDomain{
Name: domain.Name,
// Older daemons (<0.21) omit Reserved and only support reserved domains.
Reserved: domain.Reserved == nil || domain.GetReserved(),
}, nil
} }
var ErrNoReachableMachines = errors.New("no internet-reachable machines running service containers") var ErrNoReachableMachines = errors.New("no internet-reachable machines running service containers")
@@ -39,8 +43,17 @@ var ErrNoReachableMachines = errors.New("no internet-reachable machines running
// CreateIngressRecords verifies which machines running the specified service (typically Caddy) are reachable from // CreateIngressRecords verifies which machines running the specified service (typically Caddy) are reachable from
// the internet, then creates DNS records for the cluster domain pointing to those machines. It tests each machine // the internet, then creates DNS records for the cluster domain pointing to those machines. It tests each machine
// by sending HTTP requests to their public IPs. Only machines that respond correctly with their machine ID are included // by sending HTTP requests to their public IPs. Only machines that respond correctly with their machine ID are included
// in the resulting DNS configuration. Returns the created DNS records or an error. // in the resulting DNS configuration. The domain must be reserved in Uncloud DNS.
// Returns the created DNS records or an error.
func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) ([]*pb.DNSRecord, error) { func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) ([]*pb.DNSRecord, error) {
domain, err := cli.GetDomain(ctx)
if err != nil {
return nil, fmt.Errorf("get cluster domain: %w", err)
}
if !domain.Reserved {
return nil, status.Error(codes.FailedPrecondition, "cluster domain is not reserved in Uncloud DNS")
}
svc, err := cli.InspectService(ctx, serviceID) svc, err := cli.InspectService(ctx, serviceID)
if err != nil { if err != nil {
return nil, fmt.Errorf("inspect service '%s': %w", serviceID, err) return nil, fmt.Errorf("inspect service '%s': %w", serviceID, err)
+2 -2
View File
@@ -143,7 +143,7 @@ func (cli *Client) WaitClusterReady(ctx context.Context, timeout time.Duration)
} }
// WaitForStoreVersion waits until the cluster store on the target machine has reached each requested actor version // WaitForStoreVersion waits until the cluster store on the target machine has reached each requested actor version
// in minVersion, with no known missing or pending transactions through those versions. // in minVersion, with no known missing or pending transactions through those versions from active members.
// The context controls cancellation and the deadline. An empty minVersion requires no replication. // The context controls cancellation and the deadline. An empty minVersion requires no replication.
// This method observes replication without initiating synchronisation. // This method observes replication without initiating synchronisation.
// //
@@ -157,7 +157,7 @@ func (cli *Client) WaitClusterReady(ctx context.Context, timeout time.Duration)
// //
// Success does not guarantee an exact snapshot or delivery of every historical value. // Success does not guarantee an exact snapshot or delivery of every historical value.
// Callers that require a specific record or condition should verify it after waiting. // Callers that require a specific record or condition should verify it after waiting.
func (cli *Client) WaitForStoreVersion(ctx context.Context, minVersion map[string]uint64) error { func (cli *Client) WaitForStoreVersion(ctx context.Context, minVersion api.StoreVersion) error {
_, err := cli.MachineClient.WaitForStoreVersion(ctx, &pb.WaitForStoreVersionRequest{MinVersion: minVersion}) _, err := cli.MachineClient.WaitForStoreVersion(ctx, &pb.WaitForStoreVersionRequest{MinVersion: minVersion})
return err return err
} }
+27 -43
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"maps"
"slices" "slices"
"sync" "sync"
@@ -320,40 +321,18 @@ func (cli *Client) StartService(ctx context.Context, id string) error {
// ListServices returns a list of all services and their containers. // ListServices returns a list of all services and their containers.
func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) { func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
machines, err := cli.ListMachines(ctx, nil) // Broadcast the container list request to all machines in the cluster.
if err != nil { listCtx := ProxyMachinesContext(ctx, nil)
return nil, fmt.Errorf("list machines: %w", err) // List all containers including stopped ones and deployment hooks.
}
// Broadcast the container list request to all available machines.
md := metadata.New(nil)
for _, m := range machines {
if m.State == pb.MachineMember_UP || m.State == pb.MachineMember_SUSPECT {
md.Append("machines", m.Machine.Id)
} else {
tui.PrintWarning(fmt.Sprintf("failed to list service containers on machine '%s' (state is %s). "+
"The results may be incomplete.", m.Machine.Name, m.State.String()))
}
}
listCtx := metadata.NewOutgoingContext(ctx, md)
// List all containers including stopped ones.
opts := container.ListOptions{All: true} opts := container.ListOptions{All: true}
machineContainers, err := cli.Docker.ListServiceContainers(listCtx, "", opts) machineContainers, err := cli.Docker.ListServiceContainers(listCtx, "", opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("list containers: %w", err) return nil, fmt.Errorf("list containers: %w", err)
} }
// TODO: optimise by extracting services from the list of all containers instead of inspecting each service. // Group containers by service ID using the broadcast results.
// Most of the code can be reused in both InspectService and ListServices.
servicesByID := make(map[string]api.Service) servicesByID := make(map[string]api.Service)
for _, mc := range machineContainers { for _, mc := range machineContainers {
// NOTE: Metadata should never be nil in practice. This is legacy fallback that will be removed.
if mc.Metadata == nil {
tui.PrintWarning("metadata is missing in response from unknown server")
continue
}
if mc.Metadata.Error != "" { if mc.Metadata.Error != "" {
// TODO: return failed machines in the response. // TODO: return failed machines in the response.
tui.PrintWarning(fmt.Sprintf("failed to list containers on machine '%s': %s", tui.PrintWarning(fmt.Sprintf("failed to list containers on machine '%s': %s",
@@ -362,25 +341,30 @@ func (cli *Client) ListServices(ctx context.Context) ([]api.Service, error) {
} }
for _, ctr := range append(mc.Containers, mc.HookContainers...) { for _, ctr := range append(mc.Containers, mc.HookContainers...) {
if _, ok := servicesByID[ctr.ServiceID()]; ok { serviceID := ctr.ServiceID()
continue svc, ok := servicesByID[serviceID]
} if !ok {
svc = api.Service{
svc, err := cli.InspectService(ctx, ctr.ServiceID()) ID: serviceID,
if err != nil { Name: ctr.ServiceName(),
if errors.Is(err, api.ErrNotFound) { Mode: ctr.ServiceMode(),
continue
}
return nil, fmt.Errorf("inspect service: %w", err)
}
servicesByID[ctr.ServiceID()] = svc
} }
} }
services := make([]api.Service, 0, len(servicesByID)) machineContainer := api.MachineServiceContainer{
for _, svc := range servicesByID { MachineID: mc.Metadata.MachineId,
services = append(services, svc) MachineName: mc.Metadata.MachineName,
Container: ctr,
} }
return services, nil if ctr.IsHook() {
svc.HookContainers = append(svc.HookContainers, machineContainer)
} else {
svc.Containers = append(svc.Containers, machineContainer)
}
servicesByID[serviceID] = svc
}
}
return slices.Collect(maps.Values(servicesByID)), nil
} }
+18 -2
View File
@@ -38,9 +38,10 @@ func assertServiceMatchesSpec(t *testing.T, svc api.Service, spec api.ServiceSpe
} }
func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api.ServiceSpec) { func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api.ServiceSpec) {
spec = spec.SetDefaults() desiredSpec := spec.SetDefaults()
status := deploy.EvalContainerSpecChange(ctr.ServiceSpec, spec) status := deploy.EvalContainerSpecChange(ctr.ServiceSpec, desiredSpec)
assert.Equal(t, deploy.ContainerUpToDate, status) assert.Equal(t, deploy.ContainerUpToDate, status)
spec = renderRuntimeTemplatesForContainer(t, desiredSpec, ctr.Name)
// Verify labels. // Verify labels.
assert.True(t, api.ValidateServiceID(ctr.Config.Labels[api.LabelServiceID])) assert.True(t, api.ValidateServiceID(ctr.Config.Labels[api.LabelServiceID]))
@@ -148,8 +149,10 @@ func assertHookContainersMatchSpec(t *testing.T, svc api.Service, spec api.Servi
t.Helper() t.Helper()
require.NotEmpty(t, svc.HookContainers, "Expected at least one hook container") require.NotEmpty(t, svc.HookContainers, "Expected at least one hook container")
desiredSpec := spec
for _, mc := range svc.HookContainers { for _, mc := range svc.HookContainers {
ctr := mc.Container ctr := mc.Container
spec := renderRuntimeTemplatesForContainer(t, desiredSpec, ctr.Name)
// Verify labels. // Verify labels.
assert.True(t, api.ValidateServiceID(ctr.Config.Labels[api.LabelServiceID])) assert.True(t, api.ValidateServiceID(ctr.Config.Labels[api.LabelServiceID]))
@@ -215,6 +218,19 @@ func assertHookContainersMatchSpec(t *testing.T, svc api.Service, spec api.Servi
} }
} }
func renderRuntimeTemplatesForContainer(
t *testing.T, spec api.ServiceSpec, containerName string,
) api.ServiceSpec {
t.Helper()
renderedSpec, err := spec.RenderRuntimeTemplates(api.RuntimeTemplateContext{
Container: api.RuntimeTemplateContainerContext{Name: containerName},
})
require.NoError(t, err)
return renderedSpec
}
func assertContainerMountsMatchSpec(t *testing.T, mounts []mount.Mount, spec api.ServiceSpec) { func assertContainerMountsMatchSpec(t *testing.T, mounts []mount.Mount, spec api.ServiceSpec) {
expectedMounts, err := machinedocker.ToDockerMounts(spec.Volumes, spec.Container.VolumeMounts) expectedMounts, err := machinedocker.ToDockerMounts(spec.Volumes, spec.Container.VolumeMounts)
require.NoError(t, err) require.NoError(t, err)
+31 -26
View File
@@ -13,6 +13,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/psviderski/uncloud/api/pb" "github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/ucind" "github.com/psviderski/uncloud/internal/ucind"
"github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client" "github.com/psviderski/uncloud/pkg/client"
"github.com/psviderski/uncloud/pkg/distlock" "github.com/psviderski/uncloud/pkg/distlock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -36,6 +37,10 @@ func createTestCluster(
if envName != "" { if envName != "" {
c, err := p.InspectCluster(ctx, envName) c, err := p.InspectCluster(ctx, envName)
if err == nil { if err == nil {
if waitReady {
require.NoError(t, p.WaitClusterReady(ctx, c, 90*time.Second))
require.NoError(t, p.WaitClusterMeshReady(ctx, c, 90*time.Second))
}
return c, p return c, p
} }
if !errors.Is(err, ucind.ErrNotFound) { if !errors.Is(err, ucind.ErrNotFound) {
@@ -60,6 +65,7 @@ func createTestCluster(
if waitReady { if waitReady {
require.NoError(t, p.WaitClusterReady(ctx, c, 90*time.Second)) require.NoError(t, p.WaitClusterReady(ctx, c, 90*time.Second))
require.NoError(t, p.WaitClusterMeshReady(ctx, c, 90*time.Second))
} }
return c, p return c, p
@@ -111,6 +117,7 @@ func TestClusterLifecycle(t *testing.T) {
}, 30*time.Second, 50*time.Millisecond, "cluster store not reconciled on machine #%d", i+1) }, 30*time.Second, 50*time.Millisecond, "cluster store not reconciled on machine #%d", i+1)
} }
}) })
require.NoError(t, p.WaitClusterMeshReady(ctx, c, 90*time.Second))
t.Run("inspect", func(t *testing.T) { t.Run("inspect", func(t *testing.T) {
cluster, err := p.InspectCluster(ctx, name) cluster, err := p.InspectCluster(ctx, name)
@@ -189,25 +196,23 @@ func TestClusterLifecycle(t *testing.T) {
clients[i] = cli clients[i] = cli
} }
storeVersion := func(clis ...*client.Client) map[string]uint64 { storeVersion := func(clis ...*client.Client) api.StoreVersion {
t.Helper() t.Helper()
callCtx, cancel := context.WithTimeout(ctx, 5*time.Second) callCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() defer cancel()
version := make(map[string]uint64) version := make(api.StoreVersion)
for _, cli := range clis { for _, cli := range clis {
resp, err := cli.MachineClient.InspectMachine(callCtx, &emptypb.Empty{}) resp, err := cli.MachineClient.InspectMachine(callCtx, nil)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, resp.Machines, 1) require.Len(t, resp.Machines, 1)
m := resp.Machines[0] m := resp.Machines[0]
require.Len(t, m.StoreVersion, 3) require.Len(t, m.StoreVersion, 3)
for actor, v := range m.StoreVersion { version.MergeMax(m.StoreVersion)
version[actor] = max(version[actor], v)
}
} }
return version return version
} }
waitForStoreVersion := func(cli *client.Client, version map[string]uint64) { waitForStoreVersion := func(cli *client.Client, version api.StoreVersion) {
t.Helper() t.Helper()
waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second) waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel() defer cancel()
@@ -223,19 +228,19 @@ func TestClusterLifecycle(t *testing.T) {
t.Cleanup(func() { t.Cleanup(func() {
cleanupCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) cleanupCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()
_, _ = clients[0].CaddyStorage.Delete(cleanupCtx, &pb.DeleteCaddyStorageRequest{Key: prefix}) _, _ = clients[0].Caddy.Storage.Delete(cleanupCtx, &pb.DeleteCaddyStorageRequest{Key: prefix})
_, _ = clients[1].CaddyStorage.Delete(cleanupCtx, &pb.DeleteCaddyStorageRequest{Key: prefix}) _, _ = clients[1].Caddy.Storage.Delete(cleanupCtx, &pb.DeleteCaddyStorageRequest{Key: prefix})
}) })
// Create and overwrite a key on the first machine before waiting for replication. // Create and overwrite a key on the first machine before waiting for replication.
var updatedAt time.Time var updatedAt time.Time
for _, value := range [][]byte{[]byte("test-value"), []byte("replacement-value")} { for _, value := range [][]byte{[]byte("test-value"), []byte("replacement-value")} {
_, err := clients[0].CaddyStorage.Store(ctx, &pb.StoreCaddyStorageRequest{Key: key, Value: value}) _, err := clients[0].Caddy.Storage.Store(ctx, &pb.StoreCaddyStorageRequest{Key: key, Value: value})
require.NoError(t, err) require.NoError(t, err)
// A Load through another machine must find the value on the machine that accepted the local write, // A Load through another machine must find the value on the machine that accepted the local write,
// regardless of whether Corrosion has replicated it to the other machines yet. // regardless of whether Corrosion has replicated it to the other machines yet.
loadResp, err := clients[1].CaddyStorage.Load(client.ProxySingleMachineContext(ctx, c.Machines[0].ID), loadResp, err := clients[1].Caddy.Storage.Load(client.ProxySingleMachineContext(ctx, c.Machines[0].ID),
&pb.LoadCaddyStorageRequest{Key: key}) &pb.LoadCaddyStorageRequest{Key: key})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, value, loadResp.Value) require.Equal(t, value, loadResp.Value)
@@ -251,7 +256,7 @@ func TestClusterLifecycle(t *testing.T) {
// Write a distinct key on the second machine, then capture the combined store version from both machines. // Write a distinct key on the second machine, then capture the combined store version from both machines.
otherValue := []byte("second-value") otherValue := []byte("second-value")
_, err := clients[1].CaddyStorage.Store(ctx, &pb.StoreCaddyStorageRequest{Key: otherKey, Value: otherValue}) _, err := clients[1].Caddy.Storage.Store(ctx, &pb.StoreCaddyStorageRequest{Key: otherKey, Value: otherValue})
require.NoError(t, err) require.NoError(t, err)
version := storeVersion(clients[0], clients[1]) version := storeVersion(clients[0], clients[1])
values := map[string][]byte{key: []byte("replacement-value"), otherKey: otherValue} values := map[string][]byte{key: []byte("replacement-value"), otherKey: otherValue}
@@ -263,7 +268,7 @@ func TestClusterLifecycle(t *testing.T) {
// Both final values must be readable locally as soon as the wait returns. // Both final values must be readable locally as soon as the wait returns.
for k, v := range values { for k, v := range values {
resp, err := cli.CaddyStorage.Load(ctx, &pb.LoadCaddyStorageRequest{Key: k}) resp, err := cli.Caddy.Storage.Load(ctx, &pb.LoadCaddyStorageRequest{Key: k})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, v, resp.Value) assert.Equal(t, v, resp.Value)
require.NoError(t, resp.UpdatedAt.CheckValid()) require.NoError(t, resp.UpdatedAt.CheckValid())
@@ -271,7 +276,7 @@ func TestClusterLifecycle(t *testing.T) {
assert.True(t, resp.UpdatedAt.AsTime().Equal(updatedAt)) assert.True(t, resp.UpdatedAt.AsTime().Equal(updatedAt))
} }
statResp, err := cli.CaddyStorage.Stat(ctx, &pb.StatCaddyStorageRequest{Key: k}) statResp, err := cli.Caddy.Storage.Stat(ctx, &pb.StatCaddyStorageRequest{Key: k})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, k, statResp.Key) assert.Equal(t, k, statResp.Key)
assert.True(t, statResp.UpdatedAt.AsTime().Equal(resp.UpdatedAt.AsTime())) assert.True(t, statResp.UpdatedAt.AsTime().Equal(resp.UpdatedAt.AsTime()))
@@ -280,25 +285,25 @@ func TestClusterLifecycle(t *testing.T) {
} }
// A path with descendants should exist as a directory even though no value is stored at that key. // A path with descendants should exist as a directory even though no value is stored at that key.
statResp, err := cli.CaddyStorage.Stat(ctx, &pb.StatCaddyStorageRequest{Key: prefix + "/key"}) statResp, err := cli.Caddy.Storage.Stat(ctx, &pb.StatCaddyStorageRequest{Key: prefix + "/key"})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, prefix+"/key", statResp.Key) assert.Equal(t, prefix+"/key", statResp.Key)
assert.Nil(t, statResp.UpdatedAt) assert.Nil(t, statResp.UpdatedAt)
assert.EqualValues(t, 0, statResp.Size) assert.EqualValues(t, 0, statResp.Size)
assert.False(t, statResp.IsTerminal) assert.False(t, statResp.IsTerminal)
listResp, err := cli.CaddyStorage.List(ctx, &pb.ListCaddyStorageRequest{Prefix: prefix, Recursive: true}) listResp, err := cli.Caddy.Storage.List(ctx, &pb.ListCaddyStorageRequest{Prefix: prefix, Recursive: true})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []string{prefix + "/key", key, otherKey}, listResp.Keys) assert.Equal(t, []string{prefix + "/key", key, otherKey}, listResp.Keys)
// A non-recursive list should only return the immediate child keys. // A non-recursive list should only return the immediate child keys.
listResp, err = cli.CaddyStorage.List(ctx, &pb.ListCaddyStorageRequest{Prefix: prefix, Recursive: false}) listResp, err = cli.Caddy.Storage.List(ctx, &pb.ListCaddyStorageRequest{Prefix: prefix, Recursive: false})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []string{prefix + "/key", otherKey}, listResp.Keys) assert.Equal(t, []string{prefix + "/key", otherKey}, listResp.Keys)
} }
// Delete through one machine. The deletion must reach the other replicas through Corrosion. // Delete through one machine. The deletion must reach the other replicas through Corrosion.
_, err = clients[2].CaddyStorage.Delete(ctx, &pb.DeleteCaddyStorageRequest{Key: prefix}) _, err = clients[2].Caddy.Storage.Delete(ctx, &pb.DeleteCaddyStorageRequest{Key: prefix})
require.NoError(t, err) require.NoError(t, err)
version = storeVersion(clients[2]) version = storeVersion(clients[2])
@@ -306,16 +311,16 @@ func TestClusterLifecycle(t *testing.T) {
waitForStoreVersion(cli, version) waitForStoreVersion(cli, version)
for k := range values { for k := range values {
_, err = cli.CaddyStorage.Load(ctx, &pb.LoadCaddyStorageRequest{Key: k}) _, err = cli.Caddy.Storage.Load(ctx, &pb.LoadCaddyStorageRequest{Key: k})
assert.Equal(t, codes.NotFound, status.Code(err)) assert.Equal(t, codes.NotFound, status.Code(err))
_, err = cli.CaddyStorage.Stat(ctx, &pb.StatCaddyStorageRequest{Key: k}) _, err = cli.Caddy.Storage.Stat(ctx, &pb.StatCaddyStorageRequest{Key: k})
assert.Equal(t, codes.NotFound, status.Code(err)) assert.Equal(t, codes.NotFound, status.Code(err))
} }
_, err = cli.CaddyStorage.List(ctx, &pb.ListCaddyStorageRequest{Prefix: prefix, Recursive: true}) _, err = cli.Caddy.Storage.List(ctx, &pb.ListCaddyStorageRequest{Prefix: prefix, Recursive: true})
assert.Equal(t, codes.NotFound, status.Code(err)) assert.Equal(t, codes.NotFound, status.Code(err))
// Delete is idempotent on each machine. // Delete is idempotent on each machine.
_, err = cli.CaddyStorage.Delete(ctx, &pb.DeleteCaddyStorageRequest{Key: prefix}) _, err = cli.Caddy.Storage.Delete(ctx, &pb.DeleteCaddyStorageRequest{Key: prefix})
require.NoError(t, err) require.NoError(t, err)
} }
}) })
@@ -346,12 +351,12 @@ func TestClusterLifecycle(t *testing.T) {
}) })
t.Run("zero version for unknown actor", func(t *testing.T) { t.Run("zero version for unknown actor", func(t *testing.T) {
err := cli.WaitForStoreVersion(ctx, map[string]uint64{uuid.NewString(): 0}) err := cli.WaitForStoreVersion(ctx, api.StoreVersion{uuid.NewString(): 0})
require.NoError(t, err) require.NoError(t, err)
}) })
t.Run("invalid actor UUID", func(t *testing.T) { t.Run("invalid actor UUID", func(t *testing.T) {
err := cli.WaitForStoreVersion(ctx, map[string]uint64{"not-a-uuid": 1}) err := cli.WaitForStoreVersion(ctx, api.StoreVersion{"not-a-uuid": 1})
require.Equal(t, codes.InvalidArgument, status.Code(err)) require.Equal(t, codes.InvalidArgument, status.Code(err))
}) })
@@ -360,7 +365,7 @@ func TestClusterLifecycle(t *testing.T) {
defer cancel() defer cancel()
// Background writes cannot satisfy a target for an actor that does not exist. // Background writes cannot satisfy a target for an actor that does not exist.
err := cli.WaitForStoreVersion(waitCtx, map[string]uint64{uuid.NewString(): 1}) err := cli.WaitForStoreVersion(waitCtx, api.StoreVersion{uuid.NewString(): 1})
require.Equal(t, codes.DeadlineExceeded, status.Code(err)) require.Equal(t, codes.DeadlineExceeded, status.Code(err))
}) })
@@ -380,7 +385,7 @@ func TestClusterLifecycle(t *testing.T) {
timer := time.AfterFunc(500*time.Millisecond, cancel) timer := time.AfterFunc(500*time.Millisecond, cancel)
defer timer.Stop() defer timer.Stop()
err := cli.WaitForStoreVersion(waitCtx, map[string]uint64{uuid.NewString(): 1}) err := cli.WaitForStoreVersion(waitCtx, api.StoreVersion{uuid.NewString(): 1})
require.Equal(t, codes.Canceled, status.Code(err)) require.Equal(t, codes.Canceled, status.Code(err))
}) })
}) })
+13
View File
@@ -282,6 +282,11 @@ func TestComposeDeployment(t *testing.T) {
ContainerPath: "/host/etc/passwd", ContainerPath: "/host/etc/passwd",
ReadOnly: true, ReadOnly: true,
}, },
{
VolumeName: "bind-b78f4a1e9255dca2997ee1006f32b0fc168bac785cf2440f00aa84c61e58f6fb",
ContainerPath: "/host/{{.Container.Name}}",
ReadOnly: true,
},
}, },
}, },
Volumes: []api.VolumeSpec{ Volumes: []api.VolumeSpec{
@@ -297,6 +302,14 @@ func TestComposeDeployment(t *testing.T) {
CreateHostPath: true, CreateHostPath: true,
}, },
}, },
{
Name: "bind-b78f4a1e9255dca2997ee1006f32b0fc168bac785cf2440f00aa84c61e58f6fb",
Type: api.VolumeTypeBind,
BindOptions: &api.BindOptions{
HostPath: "/tmp/uncloud-e2e/{{.Container.Name}}",
CreateHostPath: true,
},
},
}, },
Replicas: 3, Replicas: 3,
} }
+1
View File
@@ -5,6 +5,7 @@ services:
volumes: volumes:
- test-compose-volumes-data1:/data1 - test-compose-volumes-data1:/data1
- /etc/passwd:/host/etc/passwd:ro - /etc/passwd:/host/etc/passwd:ro
- "/tmp/uncloud-e2e/{{.Container.Name}}:/host/{{.Container.Name}}:ro"
deploy: deploy:
replicas: 3 replicas: 3
test-compose-volumes-service2: test-compose-volumes-service2:
+20 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/psviderski/uncloud/api/pb" "github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/ucind" "github.com/psviderski/uncloud/internal/ucind"
"github.com/psviderski/uncloud/pkg/api" "github.com/psviderski/uncloud/pkg/api"
"github.com/psviderski/uncloud/pkg/client"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -456,6 +457,23 @@ func TestMachineOperations(t *testing.T) {
}) })
t.Run("remove machine clears container records from cluster store", func(t *testing.T) { t.Run("remove machine clears container records from cluster store", func(t *testing.T) {
// The Caddy controller needs a local Caddy container to supply the global config before it can generate
// routes. Place it on the connected machine by ID because earlier tests rename that machine.
caddyDeployment, err := cli.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{
Placement: api.Placement{Machines: []string{c.Machines[0].ID}},
})
require.NoError(t, err)
monitorPeriod := 5 * time.Second
caddyDeployment.Spec.UpdateConfig.MonitorPeriod = &monitorPeriod
_, err = caddyDeployment.Run(ctx)
require.NoError(t, err)
t.Cleanup(func() {
err := cli.RemoveService(ctx, client.CaddyServiceName)
if err != nil && !errors.Is(err, api.ErrNotFound) {
assert.NoError(t, err)
}
})
// Deploy a global service with an HTTP ingress port so the auto-generated Caddyfile lists // Deploy a global service with an HTTP ingress port so the auto-generated Caddyfile lists
// each container's IP as an upstream. // each container's IP as an upstream.
serviceName := "test-machine-rm-cleanup" serviceName := "test-machine-rm-cleanup"
@@ -495,7 +513,7 @@ func TestMachineOperations(t *testing.T) {
// The Caddyfile contains both upstream IPs before the machine removal. // The Caddyfile contains both upstream IPs before the machine removal.
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
cfg, err := cli.Caddy.GetConfig(ctx, nil) cfg, err := cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
if err != nil { if err != nil {
return false return false
} }
@@ -510,7 +528,7 @@ func TestMachineOperations(t *testing.T) {
// so the Caddy controller regenerates a Caddyfile without that upstream while keeping the // so the Caddy controller regenerates a Caddyfile without that upstream while keeping the
// upstreams for the still-running containers. // upstreams for the still-running containers.
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
cfg, err := cli.Caddy.GetConfig(ctx, nil) cfg, err := cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
if err != nil { if err != nil {
return false return false
} }
+31 -19
View File
@@ -17,7 +17,6 @@ import (
"github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/volume" "github.com/docker/docker/api/types/volume"
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/machine/metrics" "github.com/psviderski/uncloud/internal/machine/metrics"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/secret" "github.com/psviderski/uncloud/internal/secret"
@@ -27,6 +26,8 @@ import (
"github.com/psviderski/uncloud/pkg/client/deploy" "github.com/psviderski/uncloud/pkg/client/deploy"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
func newServiceID() string { func newServiceID() string {
@@ -42,6 +43,9 @@ func TestDeployment(t *testing.T) {
clusterName := "ucind-test.deployment" clusterName := "ucind-test.deployment"
ctx := context.Background() ctx := context.Background()
// Caddy may restart once while the controller writes its bootstrap file. Use the normal deployment monitor period
// instead of the zero-duration override used by most e2e tests.
caddyMonitorPeriod := 5 * time.Second
c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 3}, true) c, _ := createTestCluster(t, clusterName, ucind.CreateClusterOptions{Machines: 3}, true)
cli, cErr := c.Machines[0].Connect(ctx) cli, cErr := c.Machines[0].Connect(ctx)
@@ -311,8 +315,9 @@ func TestDeployment(t *testing.T) {
} }
}) })
deployment, err := cli.NewCaddyDeployment("", "", api.Placement{}) deployment, err := cli.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{})
require.NoError(t, err) require.NoError(t, err)
deployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
_, err = deployment.Run(ctx) _, err = deployment.Run(ctx)
require.NoError(t, err) require.NoError(t, err)
@@ -325,7 +330,7 @@ func TestDeployment(t *testing.T) {
ctr := svc.Containers[0].Container ctr := svc.Containers[0].Container
assert.Regexp(t, `^caddy:2\.\d+\.\d+$`, ctr.Config.Image) assert.Regexp(t, `^caddy:2\.\d+\.\d+$`, ctr.Config.Image)
config, err := cli.Caddy.GetConfig(ctx, nil) config, err := cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
require.NoError(t, err) require.NoError(t, err)
assert.Contains(t, config.Caddyfile, "# Caddyfile autogenerated by Uncloud") assert.Contains(t, config.Caddyfile, "# Caddyfile autogenerated by Uncloud")
@@ -341,10 +346,11 @@ func TestDeployment(t *testing.T) {
}) })
// Deploy to machine #0. // Deploy to machine #0.
deployment, err := cli.NewCaddyDeployment("", "", api.Placement{ deployment, err := cli.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{
Machines: []string{c.Machines[0].Name}, Placement: api.Placement{Machines: []string{c.Machines[0].Name}},
}) })
require.NoError(t, err) require.NoError(t, err)
deployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
image := deployment.Spec.Container.Image image := deployment.Spec.Container.Image
_, err = deployment.Run(ctx) _, err = deployment.Run(ctx)
@@ -359,8 +365,9 @@ func TestDeployment(t *testing.T) {
// initialContainerID := svc.Containers[0].Container.ID // initialContainerID := svc.Containers[0].Container.ID
// Deploy to all machines without a placement constraint. // Deploy to all machines without a placement constraint.
deployment, err = cli.NewCaddyDeployment(image, "", api.Placement{}) deployment, err = cli.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{Image: image})
require.NoError(t, err) require.NoError(t, err)
deployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
_, err = deployment.Run(ctx) _, err = deployment.Run(ctx)
require.NoError(t, err) require.NoError(t, err)
@@ -391,6 +398,13 @@ func TestDeployment(t *testing.T) {
} }
}) })
// Without a Caddy container, the controller has no global config to pair with application configs. Keep any
// previously saved Caddyfile unchanged rather than publishing a new one without Caddy's global settings.
savedBefore, savedBeforeErr := cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
if savedBeforeErr != nil {
require.Equal(t, codes.NotFound, status.Code(savedBeforeErr))
}
// First deploy a service with custom caddy config before caddy is deployed. // First deploy a service with custom caddy config before caddy is deployed.
serviceCaddyfile := `test-custom-caddy-config.example.com { serviceCaddyfile := `test-custom-caddy-config.example.com {
reverse_proxy {{upstreams}} { reverse_proxy {{upstreams}} {
@@ -416,17 +430,13 @@ func TestDeployment(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assertServiceMatchesSpec(t, svc, spec) assertServiceMatchesSpec(t, svc, spec)
// Check that the generated Caddyfile contains a comment that user-define configs were skipped. require.Never(t, func() bool {
var config *pb.GetCaddyConfigResponse current, currentErr := cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
require.Eventually(t, func() bool { if savedBeforeErr != nil {
config, err = cli.Caddy.GetConfig(ctx, nil) return currentErr == nil
if err != nil {
return false
} }
return strings.Contains(config.Caddyfile, "# NOTE: User-defined configs for services were skipped") return currentErr == nil && current.Caddyfile != savedBefore.Caddyfile
}, 5*time.Second, 100*time.Millisecond) }, 2*time.Second, 100*time.Millisecond)
assert.NotContains(t, config.Caddyfile, "test-custom-caddy-config.example.com {")
// Now deploy caddy with custom config. // Now deploy caddy with custom config.
caddyCaddyfile := `{ caddyCaddyfile := `{
@@ -436,8 +446,9 @@ func TestDeployment(t *testing.T) {
myapp.example.com { myapp.example.com {
reverse_proxy 1.2.3.4:8000 reverse_proxy 1.2.3.4:8000
}` }`
caddyDeployment, err := cli.NewCaddyDeployment("", caddyCaddyfile, api.Placement{}) caddyDeployment, err := cli.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{Config: caddyCaddyfile})
require.NoError(t, err) require.NoError(t, err)
caddyDeployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
_, err = caddyDeployment.Run(ctx) _, err = caddyDeployment.Run(ctx)
require.NoError(t, err) require.NoError(t, err)
@@ -447,8 +458,9 @@ myapp.example.com {
assertServiceMatchesSpec(t, caddySvc, caddyDeployment.Spec) assertServiceMatchesSpec(t, caddySvc, caddyDeployment.Spec)
// Wait for the Caddyfile to be regenerated with both custom configs. // Wait for the Caddyfile to be regenerated with both custom configs.
var config api.CaddyConfig
require.Eventually(t, func() bool { require.Eventually(t, func() bool {
config, err = cli.Caddy.GetConfig(ctx, nil) config, err = cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
if err != nil { if err != nil {
return false return false
} }
@@ -513,7 +525,7 @@ myapp.example.com {
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// Check that the Caddy config hasn't changed. // Check that the Caddy config hasn't changed.
newConfig, err := cli.Caddy.GetConfig(ctx, nil) newConfig, err := cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
require.NoError(t, err) require.NoError(t, err)
// Compare stable parts of the Caddyfile only (skip autogenerated comment with timestamp). // Compare stable parts of the Caddyfile only (skip autogenerated comment with timestamp).
+1 -1
View File
@@ -20,7 +20,7 @@ traditional container orchestrators like Kubernetes or Swarm:
* Scaling services across multiple machines * Scaling services across multiple machines
* Cross-machine service communication without exposing ports to the internet * Cross-machine service communication without exposing ports to the internet
* DNS-based service discovery * DNS-based service discovery
* Automatic HTTPS and reverse proxy configuration * Automatic HTTPS and reverse proxy configuration with clustered storage for certificates
* Load balancing * Load balancing
* Persistent storage * Persistent storage
@@ -24,3 +24,11 @@ When you [publish a service port](2-publishing-services.md), Uncloud automatical
For advanced use cases, Uncloud allows to customise the Caddy config using the `x-caddy` extension in Compose files. For advanced use cases, Uncloud allows to customise the Caddy config using the `x-caddy` extension in Compose files.
See [Custom Caddy configuration](2-publishing-services.md#custom-caddy-configuration) for details. See [Custom Caddy configuration](2-publishing-services.md#custom-caddy-configuration) for details.
:::tip Shared TLS certificates
Uncloud also provides native cluster storage for Caddy. It shares TLS certificates and ACME challenge tokens across
machines and coordinates certificate issuance. See [Cluster storage for Caddy](4-cluster-storage-for-caddy.md)
to enable it.
:::
@@ -3,6 +3,10 @@
Caddy is automatically deployed as a global service `caddy` when you initialise a cluster with `uc machine init`. By Caddy is automatically deployed as a global service `caddy` when you initialise a cluster with `uc machine init`. By
default, it runs on every machine to handle incoming HTTP/HTTPS traffic and route it to your services. default, it runs on every machine to handle incoming HTTP/HTTPS traffic and route it to your services.
Since [v0.21.0](https://github.com/psviderski/uncloud/releases/tag/v0.21.0), Uncloud provides native
[cluster storage for Caddy](4-cluster-storage-for-caddy.md) (opt-in) to share TLS certificates and coordinate
certificate issuance across machines.
## Checking status ## Checking status
View the `caddy` service status and which machines it's running on: View the `caddy` service status and which machines it's running on:
@@ -34,7 +38,7 @@ uc caddy deploy
Deploy a specific version or custom image: Deploy a specific version or custom image:
```shell ```shell
uc caddy deploy --image caddybuilds/caddy-cloudflare:2.10.2 uc caddy deploy --image caddybuilds/caddy-cloudflare:2.11.4
``` ```
Deploy only to a specific machine or a subset of machines (comma-separated list): Deploy only to a specific machine or a subset of machines (comma-separated list):
@@ -80,17 +84,29 @@ config that uses the DNS challenge with Cloudflare to obtain a wildcard TLS cert
```yaml ```yaml
services: services:
caddy: caddy:
image: caddybuilds/caddy-cloudflare:2.10.2 image: caddybuilds/caddy-cloudflare:2.11.4
command: caddy run -c /config/Caddyfile
environment: environment:
# unix// is not a typo. Caddy uses network/address format, not a unix:// URL.
CADDY_ADMIN: unix//run/caddy/admin.sock CADDY_ADMIN: unix//run/caddy/admin.sock
env_file: env_file:
# Contains CLOUDFLARE_API_TOKEN=xxxxx # Contains CLOUDFLARE_API_TOKEN=xxxxx
- .env.secrets - .env.secrets
healthcheck:
test: curl -fsS -o /dev/null --unix-socket /run/caddy/admin.sock http://localhost/config/
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
start_interval: 1s
volumes: volumes:
- /var/lib/uncloud/caddy:/etc/caddy:ro
# Persists TLS certificates and other assets when using Caddy's default local storage.
- /var/lib/uncloud/caddy:/data - /var/lib/uncloud/caddy:/data
- /var/lib/uncloud/caddy:/config
- /run/uncloud/caddy:/run/caddy - /run/uncloud/caddy:/run/caddy
# Required by the Uncloud cluster storage module (https://github.com/unlabs-dev/caddy-uncloud).
# Remove this mount if cluster storage is not used for Caddy.
# Mount the directory, not the socket file, so Caddy sees a replacement socket after the daemon restarts.
- /run/uncloud/api:/run/uncloud/api:ro
x-ports: x-ports:
- 80:80@host - 80:80@host
- 443:443@host - 443:443@host
@@ -138,9 +154,15 @@ internal.example.com {
:::info note :::info note
The specified `command`, `environment`, `volumes`, and `x-ports` properties are essential for Caddy to function The specified `environment`, `volumes`, and `x-ports` properties are essential for Caddy to function correctly in the
correctly in the Uncloud cluster. Do not change the source paths of the volume mounts as the Uncloud daemon relies on Uncloud cluster. Do not change the source paths of the volume mounts as the Uncloud daemon relies on them to communicate
them to communicate with Caddy and update its configuration. with Caddy and update its configuration.
The image must include `curl` for the healthcheck and start Caddy with `/etc/caddy/Caddyfile`. Images based on the
official [Caddy image](https://hub.docker.com/_/caddy) do both by default.
Keep the `/data` mount while using Caddy's default local storage so TLS certificates survive container updates. You can
remove this mount when using [cluster storage for Caddy](4-cluster-storage-for-caddy.md).
::: :::
@@ -0,0 +1,58 @@
# Cluster storage for Caddy
The [Uncloud storage module](https://github.com/unlabs-dev/caddy-uncloud) lets Caddy instances share TLS certificates,
private keys, and ACME challenge tokens through Uncloud's cluster store. It uses distributed locks to coordinate
certificate issuance. Once one instance obtains a certificate, the others can use it too.
## Why use cluster storage?
By default, each Caddy instance stores its certificates locally. When DNS points to multiple machines or a load balancer
distributes traffic between them, an ACME challenge can reach a different instance from the one requesting the
certificate. That instance may not have the challenge token, which can delay or prevent certificate issuance.
Cluster storage is optional. The default Caddy image does not include the module, so you need to deploy an image that
includes it and configure Caddy to use it.
## Enabling cluster storage
:::info Requirements
Cluster storage requires Uncloud **v0.21.0 or newer** for both the `uc` CLI and the daemon on every cluster machine.
Check your CLI version with `uc version` and daemon versions with `uc machine ls`. Upgrade older versions before
enabling cluster storage.
:::
Create a global Caddyfile, or add `storage uncloud` to the global options in your existing one:
```caddyfile title="global.Caddyfile"
{
storage uncloud
# Uncomment to enable debug logs useful for troubleshooting storage operations:
# debug
}
```
Deploy Caddy with the pre-built module image and your global config:
```shell
uc caddy deploy --image ghcr.io/unlabs-dev/caddy-uncloud:0.1.1 --caddyfile global.Caddyfile
```
See the [module README](https://github.com/unlabs-dev/caddy-uncloud#usage) for custom image builds, Compose deployment,
and additional storage options.
## Verifying storage
Check that `storage uncloud` appears in the Caddy config:
```shell
uc caddy config
```
List issued certificates in cluster storage with [`uc caddy cert ls`](../../9-cli-reference/uc_caddy_cert_ls.md):
```shell
uc caddy cert ls
```
@@ -0,0 +1,93 @@
# Runtime templates
> Added in [v0.21.0](https://github.com/psviderski/uncloud/releases/tag/v0.21.0)
Runtime templates let service configuration use metadata that is only known when Uncloud creates a container. They use
[Go template](https://pkg.go.dev/text/template) syntax.
Uncloud renders runtime templates on the destination machine before it creates each container. This means that every
replica can receive a value based on its own container metadata.
## Available metadata
Runtime templates currently expose the following fields:
| Field | Description | Example |
|-------------------|--------------------------------|------------|
| `.Container.Name` | Name assigned to the container | `app-c1zd` |
## Supported template locations
You can currently use runtime templates in these locations of a service definition:
| Location | Compose attribute |
|---------------------------------|-----------------------------------------------------------------------------|
| Host path of a bind mount | `volumes[].source` or `volumes` short-syntax `- /host/path:/container/path` |
| Mount path inside the container | `volumes[].target` or `volumes` short-syntax `- /host/path:/container/path` |
:::tip Want other locations or metadata fields?
If you want to use runtime templates in other locations, such as environment variable, or use other metadata fields, add
a 👍 reaction or comment with your use case on the issue [#431](https://github.com/psviderski/uncloud/issues/431).
:::
## Use runtime templates in a Compose file
The following service gets a separate host path for every replica:
```yaml title="compose.yaml"
services:
app:
image: app:latest
volumes:
- "/var/lib/app/{{.Container.Name}}:/data"
scale: 2
```
If Uncloud names the containers `app-c1zd` and `app-f7kx`, their host paths are `/var/lib/app/app-c1zd` and
`/var/lib/app/app-f7kx`.
You can combine runtime templates with
[Compose environment interpolation](https://github.com/compose-spec/compose-spec/blob/main/12-interpolation.md):
```yaml
volumes:
- "${DATA_ROOT:-/var/lib/app}/{{.Container.Name}}:/data"
```
Uncloud expands the `DATA_ROOT` environment variable on the local machine first and renders `.Container.Name` later on
the destination machine.
## Use runtime templates with `uc run`
Quote the volume argument so your shell passes the template through unchanged:
```shell
uc run --replicas 2 \
--volume "/var/lib/app/{{.Container.Name}}:/data" \
app:latest
```
:::warning Clean up templated host directories
Uncloud does not remove host directories created for bind mounts when it replaces a container or removes a service. If a
runtime template gives each container a unique host directory, you are responsible for cleaning up directories that are
no longer needed.
If you only need per-container ephemeral storage, consider a `tmpfs` mount. An anonymous volume declared by the Docker
image can also provide Docker-managed disk storage for each container. Use a named volume or a stable bind mount path
for data that must remain available across container replacements.
:::
## Runtime templates and image tag templates
Runtime templates are separate from [image tag templates](../../8-compose-file-reference/3-image-tag-template.md). Image
tag templates run locally when Uncloud builds an image. Runtime templates run on a cluster machine when Uncloud deploys
a container. Each template type exposes different metadata.
## See also
- [Compose support matrix](../../8-compose-file-reference/1-support-matrix.md)
- [`uc run` reference](../../9-cli-reference/uc_run.md)
@@ -64,7 +64,7 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea
| `update_config` | ⚠️ Limited | `order` and `monitor` supported. See [rolling deployments](../4-guides/1-deployments/4-rolling-deployments.md) | | `update_config` | ⚠️ Limited | `order` and `monitor` supported. See [rolling deployments](../4-guides/1-deployments/4-rolling-deployments.md) |
| **Volumes** | | | | **Volumes** | | |
| Named volumes | ✅ Supported | Docker volumes | | Named volumes | ✅ Supported | Docker volumes |
| Bind mounts | ✅ Supported | Host path binding | | Bind mounts | ✅ Supported | Host path binding with optional [runtime templates](../3-concepts/6-services/3-runtime-templates.md) |
| Tmpfs mounts | ✅ Supported | In-memory filesystems | | Tmpfs mounts | ✅ Supported | In-memory filesystems |
| Volume labels | ✅ Supported | Custom labels | | Volume labels | ✅ Supported | Custom labels |
| External volumes | ✅ Supported | Must exist before deployment | | External volumes | ✅ Supported | Must exist before deployment |
@@ -143,6 +143,7 @@ image: myapp:{{gitsha 7}}.${GITHUB_RUN_ID:-local} # GITHUB_RUN_ID not set →
## See also ## See also
- [Runtime templates](../3-concepts/6-services/3-runtime-templates.md): Use per-container metadata in service configuration
- [Deploy an app](../4-guides/1-deployments/1-deploy-app.md): Deploy from source code or pre-built images - [Deploy an app](../4-guides/1-deployments/1-deploy-app.md): Deploy from source code or pre-built images
- [Compose Build Specification](https://github.com/compose-spec/compose-spec/blob/main/build.md) - [Compose Build Specification](https://github.com/compose-spec/compose-spec/blob/main/build.md)
- [Compose Specification: image](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image) - [Compose Specification: image](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image)
+1 -1
View File
@@ -18,7 +18,7 @@ A CLI tool for managing Uncloud resources such as machines, services, and volume
* [uc caddy](uc_caddy.md) - Manage Caddy reverse proxy service. * [uc caddy](uc_caddy.md) - Manage Caddy reverse proxy service.
* [uc ctx](uc_ctx.md) - Switch between different cluster contexts. Contains subcommands to manage contexts. * [uc ctx](uc_ctx.md) - Switch between different cluster contexts. Contains subcommands to manage contexts.
* [uc deploy](uc_deploy.md) - Deploy services from a Compose file. * [uc deploy](uc_deploy.md) - Deploy services from a Compose file.
* [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS. * [uc dns](uc_dns.md) - Manage the cluster domain.
* [uc exec](uc_exec.md) - Execute a command in a running service container. * [uc exec](uc_exec.md) - Execute a command in a running service container.
* [uc image](uc_image.md) - Manage images on machines in the cluster. * [uc image](uc_image.md) - Manage images on machines in the cluster.
* [uc images](uc_images.md) - List images on machines in the cluster. * [uc images](uc_images.md) - List images on machines in the cluster.
+1
View File
@@ -20,6 +20,7 @@ Manage Caddy reverse proxy service.
## See also ## See also
* [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes. * [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes.
* [uc caddy cert](uc_caddy_cert.md) - Inspect certificates in cluster storage for Caddy.
* [uc caddy config](uc_caddy_config.md) - Show the current Caddy configuration (Caddyfile). * [uc caddy config](uc_caddy_config.md) - Show the current Caddy configuration (Caddyfile).
* [uc caddy deploy](uc_caddy_deploy.md) - Deploy or upgrade Caddy reverse proxy across all machines in the cluster. * [uc caddy deploy](uc_caddy_deploy.md) - Deploy or upgrade Caddy reverse proxy across all machines in the cluster.
* [uc caddy logs](uc_caddy_logs.md) - View caddy logs. * [uc caddy logs](uc_caddy_logs.md) - View caddy logs.
@@ -0,0 +1,24 @@
# uc caddy cert
Inspect certificates in cluster storage for Caddy.
## Options
```
-h, --help help for cert
```
## Options inherited from parent commands
```
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
Format: [ssh://]user@host[:port], ssh+go://user@host[:port], tcp://host:port, or unix:///path/to/uncloud.sock
-c, --context string Name of the cluster context to use (default is the current context). [$UNCLOUD_CONTEXT]
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
```
## See also
* [uc caddy](uc_caddy.md) - Manage Caddy reverse proxy service.
* [uc caddy cert ls](uc_caddy_cert_ls.md) - List certificates in cluster storage for Caddy.
@@ -0,0 +1,38 @@
# uc caddy cert ls
List certificates in cluster storage for Caddy.
## Synopsis
List certificates stored in the Uncloud cluster storage for Caddy.
Caddy must use the Uncloud storage module (https://github.com/unlabs-dev/caddy-uncloud)
configured with 'storage uncloud' in the global options for its certificates to appear here.
This inventory does not check whether Caddy currently serves or trusts a certificate.
```
uc caddy cert ls [flags]
```
## Options
```
-h, --help help for ls
-m, --machine string Name or ID of the machine to read certificate storage from. (default is connected machine)
-o, --output string Output format: 'json' or empty for a human-readable table.
```
## Options inherited from parent commands
```
--connect string Connect to a remote cluster machine without using the Uncloud configuration file. [$UNCLOUD_CONNECT]
Format: [ssh://]user@host[:port], ssh+go://user@host[:port], tcp://host:port, or unix:///path/to/uncloud.sock
-c, --context string Name of the cluster context to use (default is the current context). [$UNCLOUD_CONTEXT]
--uncloud-config string Path to the Uncloud configuration file. [$UNCLOUD_CONFIG] (default "~/.config/uncloud/config.yaml")
```
## See also
* [uc caddy cert](uc_caddy_cert.md) - Inspect certificates in cluster storage for Caddy.
+5 -2
View File
@@ -1,12 +1,14 @@
# uc dns # uc dns
Manage cluster domain in Uncloud DNS. Manage the cluster domain.
## Synopsis ## Synopsis
Manage cluster domain in Uncloud DNS. Manage the cluster domain.
DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain for your cluster. When reserved, Caddy service deployments will automatically update DNS records to route traffic to the services in the cluster. DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain for your cluster. When reserved, Caddy service deployments will automatically update DNS records to route traffic to the services in the cluster.
EXPERIMENTAL: Use 'uc dns set' to configure an externally managed domain instead. Uncloud does not manage DNS records for manually set domains.
## Options ## Options
``` ```
@@ -27,5 +29,6 @@ DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain
* [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes. * [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes.
* [uc dns release](uc_dns_release.md) - Release the reserved cluster domain. * [uc dns release](uc_dns_release.md) - Release the reserved cluster domain.
* [uc dns reserve](uc_dns_reserve.md) - Reserve a cluster domain in Uncloud DNS. * [uc dns reserve](uc_dns_reserve.md) - Reserve a cluster domain in Uncloud DNS.
* [uc dns set](uc_dns_set.md) - Set or unset an externally managed cluster domain (EXPERIMENTAL).
* [uc dns show](uc_dns_show.md) - Print the cluster domain name. * [uc dns show](uc_dns_show.md) - Print the cluster domain name.

Some files were not shown because too many files have changed in this diff Show More