mirror of
https://github.com/psviderski/uncloud.git
synced 2026-10-06 13:18:58 +00:00
feat(caddy): add ListCertificates method, refactor Caddy client layout (#31)
This commit is contained in:
1 parent
e50c5fbe57
commit
930e7ea637
23 files changed
+1112
-149
No files matched your search
+205
-46
@@ -446,6 +446,120 @@ func (x *StatCaddyStorageResponse) GetIsTerminal() bool {
|
||||
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_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,
|
||||
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,
|
||||
0x73, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x32, 0xdf, 0x02, 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,
|
||||
0x73, 0x54, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x22, 0x56, 0x0a, 0x18, 0x4c, 0x69, 0x73,
|
||||
0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73,
|
||||
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,
|
||||
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, 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,
|
||||
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, 0x12, 0x49, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69,
|
||||
0x63, 0x61, 0x74, 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, 0x1d, 0x2e,
|
||||
0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63,
|
||||
0x61, 0x74, 0x65, 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 (
|
||||
@@ -532,7 +662,7 @@ func file_api_pb_caddy_storage_proto_rawDescGZIP() []byte {
|
||||
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{
|
||||
(*StoreCaddyStorageRequest)(nil), // 0: api.StoreCaddyStorageRequest
|
||||
(*LoadCaddyStorageRequest)(nil), // 1: api.LoadCaddyStorageRequest
|
||||
@@ -542,27 +672,32 @@ var file_api_pb_caddy_storage_proto_goTypes = []any{
|
||||
(*ListCaddyStorageResponse)(nil), // 5: api.ListCaddyStorageResponse
|
||||
(*StatCaddyStorageRequest)(nil), // 6: api.StatCaddyStorageRequest
|
||||
(*StatCaddyStorageResponse)(nil), // 7: api.StatCaddyStorageResponse
|
||||
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
|
||||
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
|
||||
(*ListCertificatesResponse)(nil), // 8: api.ListCertificatesResponse
|
||||
(*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{
|
||||
8, // 0: api.LoadCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||
8, // 1: api.StatCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||
0, // 2: api.CaddyStorage.Store:input_type -> api.StoreCaddyStorageRequest
|
||||
1, // 3: api.CaddyStorage.Load:input_type -> api.LoadCaddyStorageRequest
|
||||
3, // 4: api.CaddyStorage.Delete:input_type -> api.DeleteCaddyStorageRequest
|
||||
4, // 5: api.CaddyStorage.List:input_type -> api.ListCaddyStorageRequest
|
||||
6, // 6: api.CaddyStorage.Stat:input_type -> api.StatCaddyStorageRequest
|
||||
9, // 7: api.CaddyStorage.Store:output_type -> google.protobuf.Empty
|
||||
2, // 8: api.CaddyStorage.Load:output_type -> api.LoadCaddyStorageResponse
|
||||
9, // 9: api.CaddyStorage.Delete:output_type -> google.protobuf.Empty
|
||||
5, // 10: api.CaddyStorage.List:output_type -> api.ListCaddyStorageResponse
|
||||
7, // 11: api.CaddyStorage.Stat:output_type -> api.StatCaddyStorageResponse
|
||||
7, // [7:12] is the sub-list for method output_type
|
||||
2, // [2:7] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
10, // 0: api.LoadCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||
10, // 1: api.StatCaddyStorageResponse.updated_at:type_name -> google.protobuf.Timestamp
|
||||
9, // 2: api.ListCertificatesResponse.certificates:type_name -> api.IssuedCertificate
|
||||
0, // 3: api.CaddyStorage.Store:input_type -> api.StoreCaddyStorageRequest
|
||||
1, // 4: api.CaddyStorage.Load:input_type -> api.LoadCaddyStorageRequest
|
||||
3, // 5: api.CaddyStorage.Delete:input_type -> api.DeleteCaddyStorageRequest
|
||||
4, // 6: api.CaddyStorage.List:input_type -> api.ListCaddyStorageRequest
|
||||
6, // 7: api.CaddyStorage.Stat:input_type -> api.StatCaddyStorageRequest
|
||||
11, // 8: api.CaddyStorage.ListCertificates:input_type -> google.protobuf.Empty
|
||||
11, // 9: api.CaddyStorage.Store:output_type -> google.protobuf.Empty
|
||||
2, // 10: api.CaddyStorage.Load:output_type -> api.LoadCaddyStorageResponse
|
||||
11, // 11: api.CaddyStorage.Delete:output_type -> google.protobuf.Empty
|
||||
5, // 12: api.CaddyStorage.List:output_type -> api.ListCaddyStorageResponse
|
||||
7, // 13: api.CaddyStorage.Stat:output_type -> api.StatCaddyStorageResponse
|
||||
8, // 14: api.CaddyStorage.ListCertificates:output_type -> api.ListCertificatesResponse
|
||||
9, // [9:15] is the sub-list for method output_type
|
||||
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() }
|
||||
@@ -667,6 +802,30 @@ func file_api_pb_caddy_storage_proto_init() {
|
||||
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{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
@@ -674,7 +833,7 @@ func file_api_pb_caddy_storage_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_api_pb_caddy_storage_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 8,
|
||||
NumMessages: 10,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -18,6 +18,10 @@ service CaddyStorage {
|
||||
rpc Delete(DeleteCaddyStorageRequest) returns (google.protobuf.Empty);
|
||||
rpc List(ListCaddyStorageRequest) returns (ListCaddyStorageResponse);
|
||||
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 {
|
||||
@@ -57,3 +61,17 @@ message StatCaddyStorageResponse {
|
||||
int64 size = 3;
|
||||
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;
|
||||
}
|
||||
@@ -20,11 +20,12 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
CaddyStorage_Store_FullMethodName = "/api.CaddyStorage/Store"
|
||||
CaddyStorage_Load_FullMethodName = "/api.CaddyStorage/Load"
|
||||
CaddyStorage_Delete_FullMethodName = "/api.CaddyStorage/Delete"
|
||||
CaddyStorage_List_FullMethodName = "/api.CaddyStorage/List"
|
||||
CaddyStorage_Stat_FullMethodName = "/api.CaddyStorage/Stat"
|
||||
CaddyStorage_Store_FullMethodName = "/api.CaddyStorage/Store"
|
||||
CaddyStorage_Load_FullMethodName = "/api.CaddyStorage/Load"
|
||||
CaddyStorage_Delete_FullMethodName = "/api.CaddyStorage/Delete"
|
||||
CaddyStorage_List_FullMethodName = "/api.CaddyStorage/List"
|
||||
CaddyStorage_Stat_FullMethodName = "/api.CaddyStorage/Stat"
|
||||
CaddyStorage_ListCertificates_FullMethodName = "/api.CaddyStorage/ListCertificates"
|
||||
)
|
||||
|
||||
// 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)
|
||||
List(ctx context.Context, in *ListCaddyStorageRequest, opts ...grpc.CallOption) (*ListCaddyStorageResponse, 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 {
|
||||
@@ -102,6 +106,16 @@ func (c *caddyStorageClient) Stat(ctx context.Context, in *StatCaddyStorageReque
|
||||
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.
|
||||
// All implementations must embed UnimplementedCaddyStorageServer
|
||||
// for forward compatibility.
|
||||
@@ -117,6 +131,9 @@ type CaddyStorageServer interface {
|
||||
Delete(context.Context, *DeleteCaddyStorageRequest) (*emptypb.Empty, error)
|
||||
List(context.Context, *ListCaddyStorageRequest) (*ListCaddyStorageResponse, 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()
|
||||
}
|
||||
|
||||
@@ -142,6 +159,9 @@ func (UnimplementedCaddyStorageServer) List(context.Context, *ListCaddyStorageRe
|
||||
func (UnimplementedCaddyStorageServer) Stat(context.Context, *StatCaddyStorageRequest) (*StatCaddyStorageResponse, error) {
|
||||
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) testEmbeddedByValue() {}
|
||||
|
||||
@@ -253,6 +273,24 @@ func _CaddyStorage_Stat_Handler(srv interface{}, ctx context.Context, dec func(i
|
||||
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.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -280,6 +318,10 @@ var CaddyStorage_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Stat",
|
||||
Handler: _CaddyStorage_Stat_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListCertificates",
|
||||
Handler: _CaddyStorage_ListCertificates_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "api/pb/caddy_storage.proto",
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -47,12 +48,7 @@ func runConfig(ctx context.Context, uncli *cli.CLI, opts configOptions) error {
|
||||
}
|
||||
defer clusterClient.Close()
|
||||
|
||||
if 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)
|
||||
config, err := clusterClient.Caddy.Config(ctx, client.CaddyConfigOptions{Machine: opts.machine})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get Caddy config: %w", err)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,9 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
||||
placement := api.Placement{
|
||||
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 {
|
||||
return fmt.Errorf("create caddy deployment: %w", err)
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
||||
|
||||
fmt.Println()
|
||||
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 {
|
||||
return fmt.Errorf("create caddy deployment: %w", err)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/psviderski/uncloud/internal/cli/tui"
|
||||
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||
"github.com/psviderski/uncloud/internal/machine/network"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -221,17 +221,17 @@ func initCluster(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteM
|
||||
initOpts.WireguardEndpoints = endpoints
|
||||
}
|
||||
|
||||
client, err := uncli.InitCluster(ctx, initOpts)
|
||||
clusterClient, err := uncli.InitCluster(ctx, initOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
defer clusterClient.Close()
|
||||
|
||||
// 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
|
||||
// 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 {
|
||||
return client.WaitClusterReady(ctx, 1*time.Minute)
|
||||
return clusterClient.WaitClusterReady(ctx, 1*time.Minute)
|
||||
})
|
||||
if err != nil {
|
||||
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()
|
||||
|
||||
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 {
|
||||
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 {
|
||||
d, err := client.NewCaddyDeployment("", "", api.Placement{})
|
||||
d, err := clusterClient.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{})
|
||||
if err != nil {
|
||||
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()
|
||||
return caddy.UpdateDomainRecords(ctx, client, uncli.ProgressOut())
|
||||
return caddy.UpdateDomainRecords(ctx, clusterClient, uncli.ProgressOut())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+22
-22
@@ -1,6 +1,6 @@
|
||||
module github.com/psviderski/uncloud/experiment
|
||||
|
||||
go 1.26
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5
|
||||
@@ -18,10 +18,10 @@ require (
|
||||
github.com/psviderski/uncloud v0.0.0
|
||||
github.com/siderolabs/discovery-api v0.1.4
|
||||
github.com/siderolabs/discovery-client v0.1.9
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/net v0.43.0
|
||||
go.uber.org/zap v1.28.0
|
||||
golang.org/x/net v0.59.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
google.golang.org/grpc v1.74.2
|
||||
google.golang.org/grpc v1.81.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -79,18 +79,18 @@ require (
|
||||
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2 // indirect
|
||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/klauspost/compress v1.19.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.4.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-pubsub v0.11.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/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // 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/mitchellh/cli v1.1.5 // 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/shopspring/decimal v1.4.0 // 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/spf13/cast v1.7.0 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // 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/mod v0.27.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
golang.org/x/tools v0.50.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // 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/protobuf v1.36.9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
|
||||
@@ -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.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.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/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/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
||||
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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
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.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc=
|
||||
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/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
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.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
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/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
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.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.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/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/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/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/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
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.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
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/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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
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/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
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-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
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-20180826012351-8a410e7b638d/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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
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/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=
|
||||
@@ -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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
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-20180830151530-49385e6e1522/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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
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/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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
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-20191011141410-1b5146add898/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/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-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
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.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.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
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.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
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/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=
|
||||
|
||||
@@ -34,6 +34,7 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
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/mitchellh/mapstructure v1.5.0
|
||||
github.com/moby/term v0.5.2
|
||||
@@ -178,7 +179,6 @@ require (
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // 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/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -25,10 +25,17 @@ const Namespace = "caddy_storage"
|
||||
// Server implements the machine-local CaddyStorage gRPC service.
|
||||
type Server struct {
|
||||
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}
|
||||
}
|
||||
|
||||
|
||||
+13
-1
@@ -1,6 +1,18 @@
|
||||
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.
|
||||
type CaddySpec struct {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"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 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})
|
||||
}
|
||||
+84
-21
@@ -1,6 +1,7 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
@@ -8,8 +9,10 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"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/client/deploy"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,12 +23,65 @@ const (
|
||||
|
||||
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.
|
||||
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
|
||||
// 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 == "" {
|
||||
latest, err := LatestCaddyImage()
|
||||
latest, err := latestCaddyImage(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("look up latest Caddy image: %w", err)
|
||||
}
|
||||
@@ -62,7 +118,7 @@ func (cli *Client) NewCaddyDeployment(image, config string, placement api.Placem
|
||||
},
|
||||
Mode: api.ServiceModeGlobal,
|
||||
Name: CaddyServiceName,
|
||||
Placement: placement,
|
||||
Placement: opts.Placement,
|
||||
Ports: []api.PortSpec{
|
||||
{
|
||||
PublishedPort: 80,
|
||||
@@ -113,28 +169,44 @@ func (cli *Client) NewCaddyDeployment(image, config string, placement api.Placem
|
||||
},
|
||||
}
|
||||
|
||||
if config != "" {
|
||||
if opts.Config != "" {
|
||||
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.
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse image: %w", err)
|
||||
}
|
||||
tags, err := remote.List(repo)
|
||||
tags, err := remote.List(repo, remote.WithContext(ctx))
|
||||
if err != nil {
|
||||
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"
|
||||
var latestVersion *semver.Version
|
||||
for _, t := range tags {
|
||||
@@ -152,14 +224,5 @@ func LatestCaddyImage() (reference.NamedTagged, error) {
|
||||
}
|
||||
}
|
||||
|
||||
image, err := reference.ParseDockerRef(CaddyImage)
|
||||
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
|
||||
return latestTag
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
@@ -11,8 +12,29 @@ import (
|
||||
func TestLatestCaddyImage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
image, err := LatestCaddyImage()
|
||||
image, err := latestCaddyImage(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,8 @@ type Client struct {
|
||||
// Methods such as Reset or Inspect are ambiguous in the context of a machine+cluster client.
|
||||
pb.MachineClient
|
||||
pb.ClusterClient
|
||||
Caddy pb.CaddyClient
|
||||
CaddyStorage pb.CaddyStorageClient
|
||||
// Caddy provides Caddy configuration, deployment, and certificate storage operations.
|
||||
Caddy *CaddyClient
|
||||
// Docker is a namespaced client for the Docker service to distinguish Uncloud-specific service container operations
|
||||
// from generic Docker operations.
|
||||
Docker *docker.Client
|
||||
@@ -58,8 +58,11 @@ func New(ctx context.Context, connector Connector) (*Client, error) {
|
||||
|
||||
c.MachineClient = pb.NewMachineClient(c.conn)
|
||||
c.ClusterClient = pb.NewClusterClient(c.conn)
|
||||
c.Caddy = pb.NewCaddyClient(c.conn)
|
||||
c.CaddyStorage = pb.NewCaddyStorageClient(c.conn)
|
||||
c.Caddy = &CaddyClient{
|
||||
Storage: pb.NewCaddyStorageClient(c.conn),
|
||||
grpc: pb.NewCaddyClient(c.conn),
|
||||
client: c,
|
||||
}
|
||||
c.Docker = docker.NewClient(c.conn)
|
||||
c.leases = distlockgrpc.NewLeaseClient(c.conn)
|
||||
|
||||
|
||||
+15
-15
@@ -228,19 +228,19 @@ func TestClusterLifecycle(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
cleanupCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_, _ = clients[0].CaddyStorage.Delete(cleanupCtx, &pb.DeleteCaddyStorageRequest{Key: prefix})
|
||||
_, _ = clients[1].CaddyStorage.Delete(cleanupCtx, &pb.DeleteCaddyStorageRequest{Key: prefix})
|
||||
_, _ = clients[0].Caddy.Storage.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.
|
||||
var updatedAt time.Time
|
||||
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)
|
||||
|
||||
// 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.
|
||||
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})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, value, loadResp.Value)
|
||||
@@ -256,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.
|
||||
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)
|
||||
version := storeVersion(clients[0], clients[1])
|
||||
values := map[string][]byte{key: []byte("replacement-value"), otherKey: otherValue}
|
||||
@@ -268,7 +268,7 @@ func TestClusterLifecycle(t *testing.T) {
|
||||
|
||||
// Both final values must be readable locally as soon as the wait returns.
|
||||
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)
|
||||
assert.Equal(t, v, resp.Value)
|
||||
require.NoError(t, resp.UpdatedAt.CheckValid())
|
||||
@@ -276,7 +276,7 @@ func TestClusterLifecycle(t *testing.T) {
|
||||
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)
|
||||
assert.Equal(t, k, statResp.Key)
|
||||
assert.True(t, statResp.UpdatedAt.AsTime().Equal(resp.UpdatedAt.AsTime()))
|
||||
@@ -285,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.
|
||||
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)
|
||||
assert.Equal(t, prefix+"/key", statResp.Key)
|
||||
assert.Nil(t, statResp.UpdatedAt)
|
||||
assert.EqualValues(t, 0, statResp.Size)
|
||||
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)
|
||||
assert.Equal(t, []string{prefix + "/key", key, otherKey}, listResp.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)
|
||||
assert.Equal(t, []string{prefix + "/key", otherKey}, listResp.Keys)
|
||||
}
|
||||
|
||||
// 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)
|
||||
version = storeVersion(clients[2])
|
||||
|
||||
@@ -311,16 +311,16 @@ func TestClusterLifecycle(t *testing.T) {
|
||||
waitForStoreVersion(cli, version)
|
||||
|
||||
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))
|
||||
_, 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))
|
||||
}
|
||||
_, 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))
|
||||
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -459,8 +459,8 @@ func TestMachineOperations(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.NewCaddyDeployment("", "", api.Placement{
|
||||
Machines: []string{c.Machines[0].ID},
|
||||
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
|
||||
@@ -513,7 +513,7 @@ func TestMachineOperations(t *testing.T) {
|
||||
|
||||
// The Caddyfile contains both upstream IPs before the machine removal.
|
||||
require.Eventually(t, func() bool {
|
||||
cfg, err := cli.Caddy.GetConfig(ctx, nil)
|
||||
cfg, err := cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -528,7 +528,7 @@ func TestMachineOperations(t *testing.T) {
|
||||
// so the Caddy controller regenerates a Caddyfile without that upstream while keeping the
|
||||
// upstreams for the still-running containers.
|
||||
require.Eventually(t, func() bool {
|
||||
cfg, err := cli.Caddy.GetConfig(ctx, nil)
|
||||
cfg, err := cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
+11
-12
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/psviderski/uncloud/api/pb"
|
||||
"github.com/psviderski/uncloud/internal/machine/metrics"
|
||||
"github.com/psviderski/uncloud/internal/machine/network"
|
||||
"github.com/psviderski/uncloud/internal/secret"
|
||||
@@ -316,7 +315,7 @@ func TestDeployment(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
deployment, err := cli.NewCaddyDeployment("", "", api.Placement{})
|
||||
deployment, err := cli.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{})
|
||||
require.NoError(t, err)
|
||||
deployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
|
||||
|
||||
@@ -331,7 +330,7 @@ func TestDeployment(t *testing.T) {
|
||||
ctr := svc.Containers[0].Container
|
||||
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)
|
||||
|
||||
assert.Contains(t, config.Caddyfile, "# Caddyfile autogenerated by Uncloud")
|
||||
@@ -347,8 +346,8 @@ func TestDeployment(t *testing.T) {
|
||||
})
|
||||
|
||||
// Deploy to machine #0.
|
||||
deployment, err := cli.NewCaddyDeployment("", "", api.Placement{
|
||||
Machines: []string{c.Machines[0].Name},
|
||||
deployment, err := cli.Caddy.NewDeployment(ctx, client.CaddyDeploymentOptions{
|
||||
Placement: api.Placement{Machines: []string{c.Machines[0].Name}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
deployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
|
||||
@@ -366,7 +365,7 @@ func TestDeployment(t *testing.T) {
|
||||
// initialContainerID := svc.Containers[0].Container.ID
|
||||
|
||||
// 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)
|
||||
deployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
|
||||
|
||||
@@ -401,7 +400,7 @@ 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.GetConfig(ctx, nil)
|
||||
savedBefore, savedBeforeErr := cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
|
||||
if savedBeforeErr != nil {
|
||||
require.Equal(t, codes.NotFound, status.Code(savedBeforeErr))
|
||||
}
|
||||
@@ -432,7 +431,7 @@ func TestDeployment(t *testing.T) {
|
||||
assertServiceMatchesSpec(t, svc, spec)
|
||||
|
||||
require.Never(t, func() bool {
|
||||
current, currentErr := cli.Caddy.GetConfig(ctx, nil)
|
||||
current, currentErr := cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
|
||||
if savedBeforeErr != nil {
|
||||
return currentErr == nil
|
||||
}
|
||||
@@ -447,7 +446,7 @@ func TestDeployment(t *testing.T) {
|
||||
myapp.example.com {
|
||||
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)
|
||||
caddyDeployment.Spec.UpdateConfig.MonitorPeriod = &caddyMonitorPeriod
|
||||
|
||||
@@ -459,9 +458,9 @@ myapp.example.com {
|
||||
assertServiceMatchesSpec(t, caddySvc, caddyDeployment.Spec)
|
||||
|
||||
// Wait for the Caddyfile to be regenerated with both custom configs.
|
||||
var config *pb.GetCaddyConfigResponse
|
||||
var config api.CaddyConfig
|
||||
require.Eventually(t, func() bool {
|
||||
config, err = cli.Caddy.GetConfig(ctx, nil)
|
||||
config, err = cli.Caddy.Config(ctx, client.CaddyConfigOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -526,7 +525,7 @@ myapp.example.com {
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 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)
|
||||
|
||||
// Compare stable parts of the Caddyfile only (skip autogenerated comment with timestamp).
|
||||
|
||||
Reference in new issue
Block a user